Partner MCP Spec v1

A bilateral authentication contract between AgentPress and a registered partner's MCP (Model Context Protocol) server. AgentPress signs short-lived JWTs that delegate a specific end-user's context to the partner; the partner verifies the JWT against AgentPress's public JWKS and operates on behalf of that user.

Targets MCP 2025-11-25 Streamable HTTP transport. Distinct from MCP's public-server OAuth 2.1 flow (which addresses unknown-client scenarios); this spec addresses registered-partner integrations where AgentPress and the partner have an explicit prior relationship.

Roles

  • Issuer (AgentPress): holds signing keys, issues JWTs, emits rotation webhooks.
  • Verifier (Partner MCP): verifies JWTs against the issuer's JWKS, resolves sub to a local user, enforces scopes, runs tool calls.

Cryptography

  • Signing algorithm: EdDSA over Ed25519, per RFC 8037.
  • Algorithm pinning: verifiers MUST hardcode algorithms: ["EdDSA"]; MUST NOT trust the token's alg header.
  • Key distribution: JWKS (RFC 7517) at <issuer>/.well-known/jwks.json, derived per RFC 8414 convention.
  • Rotation: overlap-window model. Both retiring and new keys published simultaneously until retirement.
  • Per org AND per environment: each AgentPress org has its own signing keypair, scoped per environment (staging / production). Compromise of one org's keys affects only that org. The iss claim embeds the org slug so partners can discover the correct JWKS URL from the token.

Environments and issuers

Signing keys are per-org. The iss claim always has the shape:

https://<api-host>/orgs/<org-slug>

Where <api-host> is api.agent.press (production) or stg-api.agent.press (staging), and <org-slug> identifies which AgentPress org registered the partner. The JWKS URL is always <iss>/.well-known/jwks.json.

Enviss patternJWKS URL pattern
Staginghttps://stg-api.agent.press/orgs/<org-slug>https://stg-api.agent.press/orgs/<org-slug>/.well-known/jwks.json
Productionhttps://api.agent.press/orgs/<org-slug>https://api.agent.press/orgs/<org-slug>/.well-known/jwks.json

Secret material (signing private keys, webhook HMAC secrets) is isolated per (org, environment) pair. A staging JWT presented to a partner configured for prod, or an org-A token presented to a client configured for org-B, MUST fail on issuer_mismatch at minimum (and typically also kid_missing_or_unknown, since keypairs are fully isolated).

Partners that integrate with multiple AgentPress orgs MUST run a separate verifier client per org. Do NOT accept arbitrary iss values — maintain an explicit whitelist of (orgSlug, environment) pairs you trust.

JWT claim set

Header:

{ "alg": "EdDSA", "kid": "<current kid>", "typ": "JWT" }

Payload:

ClaimRequiredTypeSemantics
issYesstring (URL)Exact per-org AgentPress issuer URL (e.g. https://api.agent.press/orgs/<org-slug>). Verifier MUST match against a whitelisted value; the JWKS URL is derived as <iss>/.well-known/jwks.json.
audYesstring (URL)Partner's stable MCP URL, bound at partner registration
subYesnon-empty stringVerbatim externalAuthId returned by the bound IExternalAuthProvider
ext_providerYesstringMatches IExternalAuthProvider.name (e.g. acme)
scopeYesstringSpace-separated scope list, per RFC 8693 conventions
jtiYesnon-empty stringUnique per token; enables verifier-side replay caches
iatYesnumberUnix seconds at signing
expYesnumberiat + 60 — 60-second TTL is non-negotiable
org_idNostringAgentPress org identifier, for partner audit logs
thread_idNostringAgentPress thread identifier, for trace correlation
settings_idNostringReserved for multi-tenant partner flows

Verifier obligations

  • Pin algorithms: ["EdDSA"].
  • Validate iss, aud, ext_provider, exp, iat (with clock skew ±30s).
  • Require kid header; look up against JWKS; refetch JWKS on miss.
  • Reject missing/empty sub, jti, scope, ext_provider.
  • Maintain a jti replay cache sized to exp - now per entry.
  • Resolve sub against the partner's internal user record (partner-owned).
  • Enforce scopes per tool (partner-defined scope vocabulary).
  • Return RFC 6750 errors: 401 invalid_token, 403 insufficient_scope, 403 user_not_provisioned with WWW-Authenticate: Bearer error="..." headers.

Issuer obligations

  • Serve JWKS at <iss>/.well-known/jwks.json with Cache-Control: public, max-age=3600, CORS Access-Control-Allow-Origin: *, no auth required.
  • Rotate keys on a ≤90-day cadence (or emergency on suspected compromise) with a ≥24h overlap window.
  • Emit signing_key_rotation webhooks to registered partners at their keyRotationWebhookUrl, HMAC-SHA256 signed with the partner's webhookSecret. Rotation is per-org: only partners registered with the rotating org receive the webhook.
  • Maintain separate keypairs per (org, environment) pair.

Rotation webhook

POST {partner.keyRotationWebhookUrl}
Content-Type: application/json
X-AgentPress-Signature: sha256=<hex HMAC-SHA256 of raw body>
X-AgentPress-Timestamp: <unix seconds>

Body:

{
  "event": "signing_key_rotation",
  "type": "scheduled" | "emergency",
  "retiredKid": "<ULID>",
  "newCurrentKid": "<ULID>",
  "retiredAt": "<ISO8601>",
  "effectiveAt": "<ISO8601>",
  "jwksUrl": "https://{api-host}/orgs/{org-slug}/.well-known/jwks.json",
  "reason": "<free text, present only when type is emergency>"
}

Retry on non-2xx with exponential backoff, max 5 attempts. The webhook is an optional fast-path — partners with kid-miss JWKS refetch (the default jose behavior) still catch rotations without it, just slower.

The HMAC secret used to sign this webhook is distinct from the JWT signing key. Compromise of the HMAC secret cannot forge JWTs because the HMAC secret cannot sign EdDSA tokens.

Partner registration

Minimum fields captured per partner at registration time:

FieldPurpose
name (identifier)e.g. acme; matches ext_provider claim
audiencePartner's stable MCP URL, baked into aud claim
webhookSecretShared OOB for HMAC-signed rotation webhooks
keyRotationWebhookUrlWhere to POST rotation events
allowedScopesThe scope vocabulary this partner accepts

Error taxonomy

  • invalid_token — signature/claims validation failed
  • insufficient_scope — token valid, missing required scope for the called tool
  • user_not_provisioned — valid token, but sub does not resolve to a provisioned user on the partner side
  • idempotency_key_required — partner requires Idempotency-Key on mutating tool calls; missing
  • idempotency_conflict — concurrent duplicate in-flight request

Scope vocabulary

Per-partner. The AgentPress org admin configures an allowlist at registration; AgentPress issues tokens with scopes that match that allowlist. Three supported allowlist forms:

  • Exactsettings:read matches only settings:read.
  • Namespace globsettings:* matches any scope whose prefix is settings: (including settings:read, settings:write, and deeper paths like settings:admin:users). Does NOT match the bare namespace settings.
  • Full wildcard* matches any scope the partner requests. Use for trusted partners where the admin vouches for the integration as a whole.

Each signed JWT still carries a single concrete scope in the scope claim — drawn from the partner's _meta.requiredScope tool annotation — so verifiers do not need to parse or expand globs. Allowlist matching is performed exclusively on AgentPress's signing side.

Admins opting into * or settings:* are accepting that future scopes the partner adds under that prefix will be auto-approved without re-review.

Transport

MCP 2025-11-25 Streamable HTTP; both stateless and session modes supported. Stateless is recommended — matches the short-lived JWT model.

Out of scope

  • User resolution, tool dispatch, idempotency cache, rate limiting, tool-level audit logs — all partner-domain.
  • Refresh tokens — intentionally omitted; 60s TTL + fresh sign-per-request is the model.
  • Authorization server discovery — partners are pre-registered, not discovered.

SDK helpers

@agentpress/sdk ships matching helpers so partners don't hand-roll verification:

import { AgentPress } from "@agentpress/sdk";

const client = new AgentPress({
  webhookSecret: process.env.AGENTPRESS_WEBHOOK_SECRET,
  partnerMcp: {
    jwksUrl: "https://api.agent.press/orgs/<org-slug>/.well-known/jwks.json",
    issuer: "https://api.agent.press/orgs/<org-slug>",
    audience: "https://mcp.partner.example/v1",
    expectedExtProvider: "acme",
  },
});

// Inbound JWT verification:
const claims = await client.partners.verifyToken(bearerToken);

// Key rotation webhook:
const event = client.webhooks.verifyKeyRotation({ payload, headers });
await client.partners.refreshJwks();

See the SDK README for the full API surface and error hierarchy.

On this page