Partner MCP Integration
This guide walks through wiring the @agentpress/sdk verification helpers into a Partner MCP server so that AgentPress-issued bearer tokens are authenticated correctly, and optional signing-key rotations are handled without a failed-verify penalty.
Read the Partner MCP Spec v1 first for the underlying wire protocol (JWT claim set, JWKS discovery, rotation webhook shape). This page is about turning that protocol into a running integration.
Prerequisites
Before you start, the AgentPress org that registered you as a partner will have shared the following out-of-band:
| Field | What it is |
|---|---|
name (identifier) | Stable partner identifier, e.g. acme. Matches the ext_provider claim on every JWT. |
audience | Your MCP server's stable URL, baked into the aud claim on every JWT. |
webhookSecret | Shared secret used to HMAC-sign the optional signing_key_rotation webhook. Distinct from the JWT signing key — compromise of one does not compromise the other. |
allowedScopes | The scope vocabulary this partner accepts. AgentPress only issues tokens with scopes drawn from this set. |
orgSlug | The AgentPress org that registered you. Signing keys are per-org — your issuer and jwksUrl both embed this slug. If multiple AgentPress orgs integrate with your MCP, each org is a separate trust relationship with its own signing keypair and iss value. |
You will also need a working MCP server that:
- Accepts HTTP POSTs on a stable URL (matching the
audienceabove). - Can read an
Authorization: Bearer <jwt>header from the incoming request. - Runs on Node 22+ (or Bun, Deno, or Cloudflare Workers — the SDK uses only
node:cryptoand Web Crypto APIs, plusjosefor EdDSA verification).
Install the SDK
bun add @agentpress/sdk@agentpress/sdk has exactly one runtime dependency: jose, used for EdDSA JWT verification and remote JWKS fetching. It ships both ESM and CJS builds and runs wherever fetch is available.
Initialise the client
Create a single AgentPress instance per deployment environment and reuse it across requests. The JWKS cache lives on the instance, so constructing a new client per request would make every verification call cold-fetch the JWKS.
import { AgentPress } from "@agentpress/sdk";
// Replace `<org-slug>` with the slug of the AgentPress org that registered
// you as a partner. Signing keys are per-org — this slug is baked into
// both the `iss` claim and the JWKS URL.
export const client = new AgentPress({
webhookSecret: process.env.AGENTPRESS_WEBHOOK_SECRET, // for verifyKeyRotation
partnerMcp: {
jwksUrl: "https://api.agent.press/orgs/<org-slug>/.well-known/jwks.json",
issuer: "https://api.agent.press/orgs/<org-slug>",
audience: "https://mcp.example.com",
expectedExtProvider: "acme",
},
});Field reference:
| Field | Required | Default | Description |
|---|---|---|---|
jwksUrl | Yes | — | Per-org JWKS endpoint: <issuer>/.well-known/jwks.json. Always matches the pattern https://<api-host>/orgs/<org-slug>/.well-known/jwks.json. |
issuer | Yes | — | Exact match for the iss claim on every JWT. Per-org: https://api.agent.press/orgs/<org-slug> in prod, https://stg-api.agent.press/orgs/<org-slug> in staging. |
audience | Yes | — | Exact match for the aud claim — your MCP server's stable URL, registered with AgentPress. |
expectedExtProvider | Yes | — | Required value for the ext_provider claim, e.g. "acme". Matches the name you were assigned at partner registration. |
clockTolerance | No | "30s" | Skew tolerance for iat / exp validation. Accepts a jose duration string ("30s", "1m") or a number of seconds. |
jwksCacheMaxAgeMs | No | 3_600_000 | JWKS cache max age in milliseconds. Cache is per-instance, not module-global. |
Multi-org integrations
If more than one AgentPress org integrates with your MCP, each is a separate trust relationship with its own signing keypair, iss value, and JWKS URL. Spin up one AgentPress client per org and route inbound requests to the correct instance based on the JWT's iss claim (peek the unverified payload or the URL path the request arrived on).
const clientsByIssuer = new Map<string, AgentPress>();
function getClient(issuer: string): AgentPress {
let c = clientsByIssuer.get(issuer);
if (!c) {
c = new AgentPress({
partnerMcp: {
jwksUrl: `${issuer}/.well-known/jwks.json`,
issuer,
audience: "https://mcp.example.com",
expectedExtProvider: "acme",
},
});
clientsByIssuer.set(issuer, c);
}
return c;
}Whitelist the set of issuers you trust — do NOT accept arbitrary iss values from inbound tokens. A reasonable pattern is to keep an explicit list of (orgSlug, environment) pairs that have registered with your MCP and derive each issuer from that list.
Staging and production side-by-side
Each environment MUST be a separate AgentPress instance with its own partnerMcp block. JWKS cache state is per-instance, so two clients can run side-by-side in the same process without cross-talk. A staging JWT presented to the production client will fail on issuer_mismatch (and typically also kid_missing_or_unknown, since the keypairs are isolated per environment AND per org).
export const staging = new AgentPress({
webhookSecret: process.env.AGENTPRESS_WEBHOOK_SECRET_STAGING,
partnerMcp: {
jwksUrl:
"https://stg-api.agent.press/orgs/<org-slug>/.well-known/jwks.json",
issuer: "https://stg-api.agent.press/orgs/<org-slug>",
audience: "https://mcp-staging.example.com",
expectedExtProvider: "acme",
},
});
export const production = new AgentPress({
webhookSecret: process.env.AGENTPRESS_WEBHOOK_SECRET_PROD,
partnerMcp: {
jwksUrl: "https://api.agent.press/orgs/<org-slug>/.well-known/jwks.json",
issuer: "https://api.agent.press/orgs/<org-slug>",
audience: "https://mcp.example.com",
expectedExtProvider: "acme",
},
});Route inbound requests to the correct client by inspecting the JWT's iss claim (decode the header without verifying to peek), or by a request-scoped environment flag upstream of the verifier.
Verify inbound JWTs
Every MCP tool call from AgentPress arrives with an Authorization: Bearer <jwt> header. Extract the token, hand it to client.partners.verifyToken, and map typed PartnerTokenError failures onto RFC 6750 WWW-Authenticate: Bearer error="invalid_token" responses.
The example below uses Hono, but the pattern is identical in Express, Fastify, a Cloudflare Worker, or a raw fetch handler.
import { AgentPress, PartnerTokenError } from "@agentpress/sdk";
import { Hono } from "hono";
const app = new Hono();
app.post("/mcp", async (c) => {
const auth = c.req.header("authorization") ?? "";
const token = auth.startsWith("Bearer ") ? auth.slice(7) : "";
if (!token) {
return c.text("Unauthorized", 401, {
"WWW-Authenticate":
'Bearer error="invalid_token", error_description="Missing bearer token"',
});
}
try {
const claims = await client.partners.verifyToken(token);
// claims.sub is the external user id; resolve it to your local user record.
const user = await resolveExternalUser(claims.ext_provider, claims.sub);
c.set("user", user);
c.set("claims", claims);
return handleMcpRequest(c);
} catch (err) {
if (err instanceof PartnerTokenError) {
return c.text("Unauthorized", 401, {
"WWW-Authenticate": `Bearer error="invalid_token", error_description="${err.reason}"`,
});
}
throw err;
}
});verifyToken enforces the full spec: alg pinned to EdDSA, iss and aud exact-match, ext_provider exact-match against expectedExtProvider, sub / jti / scope required and non-empty, kid header required, and exp / iat validated with the configured skew. You do not need any additional checks on the returned claims beyond your own scope enforcement.
Resolving sub to a local user
PartnerTokenClaims.sub contains the verbatim externalAuthId returned by AgentPress's bound IExternalAuthProvider for your integration. Mapping that external id to a local user record is partner-owned logic — AgentPress does not make assumptions about how your user system works.
A common pattern is a users table with an external_auth_id column that stores the value AgentPress sends. On first contact, provision the user from the claim set (sub, optionally org_id); on subsequent calls, look them up by (ext_provider, sub) pair.
If you cannot resolve sub to a provisioned user, return 403 user_not_provisioned per the spec's error taxonomy so AgentPress can surface a clear message in the admin console.
Scope enforcement
PartnerTokenClaims.scope is a space-separated string per RFC 8693. Split it on whitespace, compare against the scope vocabulary your tool requires, and return 403 insufficient_scope when a required scope is missing. The spec has no implicit hierarchy — settings:* does not imply settings:read. Enforce strict flat-set membership.
For the full list of error reasons verifyToken can throw and how to map each to an HTTP response, see the Error Reference.
Handle the rotation webhook (optional)
AgentPress rotates signing keys on a ≤90-day cadence with a ≥24h overlap window where both retiring and new keys are valid. The rotation webhook is an optional fast-path: partners who skip it still pick up new keys automatically on the next kid miss (with a one-failed-verify latency penalty). Implementing the handler makes rotations seamless.
import { AgentPress, KeyRotationVerifyError } from "@agentpress/sdk";
import { Hono } from "hono";
const app = new Hono();
app.post("/webhooks/agentpress/key-rotation", async (c) => {
// IMPORTANT: read the raw body, not c.req.json(). HMAC is computed over the
// exact bytes AgentPress sent, not a re-serialized JSON object.
const rawBody = await c.req.text();
const headers: Record<string, string | undefined> = {};
c.req.raw.headers.forEach((value, key) => {
headers[key] = value;
});
try {
const event = client.webhooks.verifyKeyRotation({
payload: rawBody,
headers,
});
console.log(
`Rotation (${event.type}): ${event.retiredKid} -> ${event.newCurrentKid}`,
);
// Force-refetch the JWKS so the next verifyToken() picks up the new key.
await client.partners.refreshJwks();
return c.json({ received: true });
} catch (err) {
if (err instanceof KeyRotationVerifyError) {
// Map err.reason to the right HTTP status — see the error reference.
return c.text(err.message, 401);
}
throw err;
}
});verifyKeyRotation validates the X-AgentPress-Signature HMAC (using the webhookSecret you configured on the client), enforces a ±5-minute timestamp window via X-AgentPress-Timestamp, caps the payload at 8 KB, and parses the body into a typed KeyRotationEvent. Retries from AgentPress use exponential backoff up to 5 attempts on non-2xx responses, so transient failures are tolerated.
The HMAC secret used to sign this webhook is distinct from the JWT signing key. Compromise of the webhook secret cannot forge JWTs because the secret cannot sign EdDSA tokens.
For the full list of rotation-verification errors and their recommended HTTP responses, see the Error Reference.
Testing your integration locally
The SDK verifies against a real remote JWKS, so local testing typically means standing up a local mock of both pieces:
- A local JWKS endpoint — serve a static
jwks.jsonover HTTP (e.g. fromhttp://localhost:4000/.well-known/jwks.json) containing an Ed25519 public key you generated. - A local JWT signer — a small script that signs test tokens against the matching private key, with claims that mirror what AgentPress would issue (correct
iss,aud,ext_provider,sub,jti,scope,iat,exp, plus thekidheader matching the JWKS entry). - Point the SDK at the mock — in your local environment, set
jwksUrl,issuer, andaudienceonpartnerMcpto match the mock, then callverifyTokenwith tokens from the signer.
Because the SDK pins algorithms: ["EdDSA"] and requires a kid header, your mock signer must produce Ed25519 signatures with a kid that matches the JWKS entry. Any deviation (wrong algorithm, missing kid, mismatched kid, expired token) will surface as a typed PartnerTokenError.reason so you can verify your error-mapping code path.
For CI, checking in a pre-generated Ed25519 keypair (public in jwks.json, private encrypted or just used for tests) is usually sufficient.
Next steps
- Partner MCP Spec v1 — the authoritative protocol spec (JWT claim set, JWKS discovery, rotation semantics).
- Partner MCP Error Reference — typed error reasons and recommended HTTP responses.
- MCP Servers — how AgentPress admins connect MCP servers to agents, including partner MCPs like yours.
Partner MCP Spec v1
Bilateral delegation contract between AgentPress and a registered partner's MCP server using Ed25519-signed JWTs.
Partner MCP Error Reference
Typed error reasons thrown by the AgentPress SDK during Partner MCP JWT verification and key-rotation webhook handling, plus recommended HTTP responses.