Support per-user identity via request headers

Add multi-user auth: _config reads X-Freshservice-Domain/X-Freshservice-Key
from the per-request headers (via the SDK request_ctx, set inside the tool's
task) and prefers them over the container env. One shared server can now act as
each caller's own Freshservice identity — actions attributed to them, their
permissions, their rate limit — while the env vars remain a single-user
fallback. Verified end-to-end that concurrent clients don't cross-wire and that
a client with no creds errors instead of falling back silently.

Docs/.env.example updated with the multi-user client config and the guidance to
omit env FS creds on a shared deployment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 08:23:52 -06:00
parent ff627b5620
commit 4f9a862c25
3 changed files with 99 additions and 10 deletions

View File

@@ -5,11 +5,22 @@ Runs as a container on the LAN box and is reached from Claude Desktop / Cowork
via mcp-remote, exactly like the sibling sharepoint-lists server. Credentials
live here (in the container), outside the Cowork VM sandbox.
Identity / credentials, in precedence order:
1. Per-request headers (multi-user): each client sends its OWN
X-Freshservice-Domain and X-Freshservice-Key on every call, so the
shared server acts as that caller's identity and actions are attributed
to them. Use this when more than one person shares the server.
2. Env vars (single-user): FRESHSERVICE_DOMAIN / FRESHSERVICE_API_KEY.
A fallback used only when the headers are absent. OMIT these on a
multi-user deployment so a client that forgets its headers errors out
rather than silently acting as whoever owns the env key.
Env:
FRESHSERVICE_DOMAIN "acme" or "acme.freshservice.com"
FRESHSERVICE_API_KEY profile API key
MCP_AUTH_TOKEN shared bearer token the client must send (optional but
strongly recommended; if unset, auth is open)
FRESHSERVICE_DOMAIN "acme" or "acme.freshservice.com" (single-user only)
FRESHSERVICE_API_KEY profile API key (single-user only)
MCP_AUTH_TOKEN shared bearer token every client must send to reach
the server (gate); separate from per-user identity.
Strongly recommended; if unset, the server is open.
MCP_TRANSPORT "http" (default here) or "stdio"
HOST / PORT bind address (default 0.0.0.0:3838)
@@ -28,6 +39,7 @@ import urllib.request
from typing import Any
from mcp.server.fastmcp import FastMCP
from mcp.server.lowlevel.server import request_ctx
from mcp.server.transport_security import TransportSecuritySettings
# The streamable-HTTP transport enables DNS-rebinding protection by default,
@@ -45,12 +57,38 @@ TICKET_STATUS = {2: "Open", 3: "Pending", 4: "Resolved", 5: "Closed"}
TICKET_PRIORITY = {1: "Low", 2: "Medium", 3: "High", 4: "Urgent"}
def _header_creds() -> tuple[str, str]:
"""Per-request Freshservice creds from headers the client sends on every
call — X-Freshservice-Domain / X-Freshservice-Key — so one shared server
acts as each caller's own identity. Returns ("", "") when unavailable
(stdio transport, or headers absent). Read via the SDK's request_ctx, which
is set inside the tool's own task, so it reflects THIS call's request."""
try:
rc = request_ctx.get()
except LookupError:
return "", ""
req = getattr(rc, "request", None)
if req is None:
return "", ""
h = req.headers
return (h.get("x-freshservice-domain", "").strip(),
h.get("x-freshservice-key", "").strip())
def _config() -> tuple[str, str]:
domain = os.environ.get("FRESHSERVICE_DOMAIN", "").strip()
api_key = os.environ.get("FRESHSERVICE_API_KEY", "").strip()
# Per-request header creds take precedence over the container env, so a
# shared server acts as each caller's identity. Env is the single-user
# fallback — OMIT FRESHSERVICE_* from a multi-user deployment so a client
# that forgets its headers errors out instead of silently acting as whoever
# owns the env key.
h_domain, h_key = _header_creds()
domain = (h_domain or os.environ.get("FRESHSERVICE_DOMAIN", "")).strip()
api_key = (h_key or os.environ.get("FRESHSERVICE_API_KEY", "")).strip()
if not domain or not api_key:
raise RuntimeError(
"Missing credentials. Set FRESHSERVICE_DOMAIN and FRESHSERVICE_API_KEY."
"Missing credentials. Send X-Freshservice-Domain and "
"X-Freshservice-Key headers (per-user), or set FRESHSERVICE_DOMAIN "
"and FRESHSERVICE_API_KEY in the environment (single-user)."
)
if "." not in domain:
domain = f"{domain}.freshservice.com"