Skip to content

Authenticated remote MCP

The remote MCP service uses Streamable HTTP at /mcp. Local stdio remains available through python -m pycharter.mcp. Both use the same PyCharter tool catalog and dispatch service. Remote calls are checked against a fresh token introspection result on every HTTP request.

Deploy the resource server

Configure the existing Optophi token introspection endpoint and PyCharter database. The introspection service must be reachable by this process. Install pycharter[ai,server] for the MCP SDK and HTTP server dependencies.

export PYCHARTER_AUTH_SERVICE_URL=https://auth.example.test
export PYCHARTER_AUTH_SERVICE_INTROSPECT_PATH=/oauth/introspect
export PYCHARTER_DATABASE_URL=postgresql://…
# Also inject PYCHARTER_AGENT_VERIFICATION_URL, PYCHARTER_AGENT_RECEIPT_KEYS,
# and PYCHARTER_AGENT_RECEIPT_ACTIVE_KEY; migrate the dedicated ledger first.
python -m pycharter.mcp.http \
  --issuer-url https://auth.example.test \
  --resource-url https://mcp.example.test/mcp \
  --host 127.0.0.1 --port 8000 \
  --config agent-tools.yaml

Terminate TLS at a trusted proxy and preserve the public Host header. The resource URL must be the URL clients use, including /mcp; it is the token audience and the identifier in protected-resource metadata. Plain HTTP is accepted only for localhost development. Add --allowed-origin for a known browser origin when cross-origin clients are required. The service checks Host and any Origin header before dispatch.

The metadata endpoint is https://mcp.example.test/.well-known/oauth-protected-resource/mcp. Unauthenticated MCP requests return 401 with a WWW-Authenticate link to that endpoint. A valid token missing pycharter:read returns 403.

Token contract and access modes

PyCharter is an OAuth resource server and does not issue remote tokens or maintain another user database. The configured Optophi introspection response must contain active, sub, client_id, role, workspace_id, namespace, scope, exp, iss, and aud. aud must include the exact resource URL; iss must match --issuer-url. Inactive, expired, wrong-audience, malformed, or unknown-role tokens are rejected. Local API JWTs without an audience are not remote MCP credentials.

For service-to-service access, issue a token to the service client with its service principal in sub, an assigned role, one workspace and contract namespace, and pycharter:read. A pycharter:write scope is accepted only when the operator policy explicitly enables it; the current remote catalog does not yet expose contract writes. For user-delegated access, the identity provider must put the current user in sub and their current role, workspace, namespace, and scopes in each introspection result. Role changes and revocation then take effect on the next request. Do not use a shared service subject for user-delegated approvals.

The default operator policy is read-only. pycharter:write narrows rather than overrides it. Remote contract writes remain withheld until policy-safe diffs and the existing distinct-approval lifecycle are proven together. A superuser may access all vocabulary workspaces only when the operator policy sets role: superuser and the token also has pycharter:all-workspaces. Every token remains bound to one contract namespace. The contract-list tool injects that namespace and rejects an explicit mismatch.

Remote discovery is sorted by tool name and reevaluated for each request. Legacy contract-schema, name-only validation, diff, proposal, commit, and binding tools are withheld remotely because their current paths do not yet apply every field-classification and imported-guidance review rule. Exact hosted validation is described in the next section. Operations without a safe workspace or namespace filter, including global conformance and pipeline operations, are also withheld. They remain available to local stdio under its operator policy. New catalog tools must pass a scope and privacy review before joining remote discovery.

Remote tool errors set MCP isError and return the same safe JSON in text and structuredContent: error, stable code, boolean retryable, and message. A refused request is not retryable. Temporary unavailability and timeouts are retryable; private backend and argument text is never echoed.

A tool that publishes an outputSchema never returns a success that contradicts it. As on stdio, the server checks each success against that schema and replaces a mismatch with invalid_tool_output, logging only the tool name.

Hosted preparation and exact verification

The python -m pycharter.mcp.http command requires the dedicated ledger URL and receipt key settings, and enables durable R1.7/R3 preparation and exact verification. Missing or invalid settings stop startup with a generic error. See Environment provisioning for migrations, key encoding, rotation, and retention cleanup.

A programmatic host enables the same path by passing a RemoteHostedVerification to create_app. It wraps a verification store opened with its own HMAC key ring, as described in Agent verification metadata store:

from pycharter.mcp.http import RemoteHostedVerification, create_app
from pycharter.verification_store import open_verification_store

ledger = open_verification_store(
    ledger_url, digest_keys=key_ring, active_digest_key_id=active_key_id
)
app = create_app(
    resource_url="https://mcp.example.test/mcp",
    issuer_url="https://auth.example.test",
    hosted_verification=RemoteHostedVerification(ledger),
)

Programmatic create_app callers may still omit that argument for a read-only, non-durable host: preparation then returns recorded: false and validation is withheld. With a store configured:

  • prepare_contract_context stores a preparation event for the token's principal, role, workspace, and namespace. It then returns a preparation_id and recorded: true. The ID expires after one hour.
  • validate_record appears for tokens with a workspace. It accepts only exact hosted verification: contract_ref as name@version, record, preparation_id, and context_fingerprint are required. Pass the same profile and budget_tokens used for preparation. Identity, namespace, workspace, and receipt mode never come from tool arguments.
  • The server loads the preparation for the same principal and workspace, rebuilds the current governed projection, validates the record, and stores a metadata-only receipt before it returns a verdict. The result carries value-free diagnostics and receipt: {id, recorded, reason}.
  • In the default required receipt mode, a failed receipt write returns receipt_unavailable and no verdict. receipt_mode="optional" may instead return the verdict with receipt.recorded: false.
  • When a call supplies both correlation_id and attempt_id, an identical retry returns the original receipt. Reusing that pair for a different request returns receipt_conflict.

A receipt documents a check. It does not authorize an action. Hosted refusals use these stable codes, each with a fixed message that names the next step:

Code Meaning Retryable
preparation_required The preparation is missing, expired, or belongs to another principal or workspace. No
scope_or_context_mismatch The preparation's namespace, contract, fingerprint, or role differs from this request. No
stale_context The contract or governed projection changed; prepare again. No
invalid_contract_ref, unresolved_version contract_ref is malformed or lacks the prepared version. No
contract_unavailable The contract version is missing or no longer active. No
authenticated_workspace_required, scope_mismatch The token lacks a workspace or does not cover the contract namespace. No
receipt_conflict The correlation and attempt ID pair was used for another request. No
preparation_unavailable, verification_unavailable, receipt_unavailable Storage or verification is temporarily unavailable. Yes

Operational limits

The HTTP boundary caps each request at 1 MiB, each response at 2 MiB, and total handling time at 30 seconds. It permits 120 requests per minute per source IP and 32 concurrent requests per process. A deployment with multiple replicas should apply a shared upstream rate limit as well. Logs contain a generated request ID, method, safe operation class, and status; they do not contain bearer tokens or request/response payloads.