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_contextstores a preparation event for the token's principal, role, workspace, and namespace. It then returns apreparation_idandrecorded: true. The ID expires after one hour.validate_recordappears for tokens with a workspace. It accepts only exact hosted verification:contract_refasname@version,record,preparation_id, andcontext_fingerprintare required. Pass the sameprofileandbudget_tokensused 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
requiredreceipt mode, a failed receipt write returnsreceipt_unavailableand no verdict.receipt_mode="optional"may instead return the verdict withreceipt.recorded: false. - When a call supplies both
correlation_idandattempt_id, an identical retry returns the original receipt. Reusing that pair for a different request returnsreceipt_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.