Signed context for MCP requests
What context is for
Senler adds the X-Senler-MCP-Context header to requests for connections in the project's MCP servers section: custom servers and servers installed from templates. The header contains a signed JWT. No additional Cabinet setting is required.
The signature lets your MCP server verify that Senler issued the identifiers in the context and that they have not changed since signing. The HTTP request itself may be sent by Senler, MCP Router, or the AI provider with a direct connection. The signature confirms the origin of the context, rather than the sender's network address.
Verification by the MCP is optional: the server can ignore the header and continue using its authorization. If the server uses signed identifiers to make decisions, it must verify the entire JWT and reject missing, forged, or expired context.
This is a Senler integration convention. It does not add a mandatory requirement to the MCP authorization standard.
Context and authorization mode
The header is sent with any selected authorization mode, including No authorization. It does not change the selected account or replace an access token, OAuth, or a lead authorization JWT.
| What the server verifies | Where the data is |
|---|---|
| Who issued the context and which connection it belongs to | X-Senler-MCP-Context, JWT type senler-mcp-context+jwt |
| Shared project account access | Token, OAuth, or secret headers from the selected authorization method |
| The lead's external identity with signed authorization | The configured lead authorization header, X-Senler-Identity by default, JWT type senler-lead+jwt |
Shared project account and Separate lead account remain mutually exclusive modes. The presence of service context does not enable a second authorization mode.
For example, a request may contain a valid project token and a lead_id in the context. This means that the call uses project credentials in that lead's conversation. The lead_id itself does not prove that the person signed in to an external account. For personal data, use lead authorization, verify the external identity confirmation, and check permissions for the specific action.
Context does not contain external_id, user_hash, the identity_verified flag, names, email addresses, or account secrets. The JWT is signed but not encrypted: the recipient can read its contents.
Header format
X-Senler-MCP-Context: eyJ...eyJ...signature
The value is a compact JWT without the Bearer prefix. The header name is reserved: it cannot be used in custom authorization headers or as the lead JWT header.
The JWT header contains alg: "ES256", typ: "senler-mcp-context+jwt", and kid, the public key identifier. It uses an EC P-256 key. The payload contains these fields:
| Field | Value |
|---|---|
v | Format version: 1 |
iss | The public Senler API origin; https://api.senler.io for the main environment |
aud | The public target MCP address: origin and path without query parameters |
iat, exp | Issued-at and expiration times in Unix seconds; exp - iat = 300 |
jti | Unique identifier of the issued JWT |
mcp_server_id | The MCP connection identifier in Senler |
auth_mode | project or lead, the selected authorization mode |
project_id | Project identifier, when available |
lead_id | Lead identifier, when the call has lead context |
agent_id, dialog_id, request_id | Agent, conversation, and run identifiers, when known |
Optional fields without values are omitted. For example, validating project credentials from settings may run without lead_id or dialog_id. The server must not infer the lead from a previous request.
The aud for https://crm.example.com/mcp?region=eu is https://crm.example.com/mcp. Senler preserves the public connection address here when routing internally. If one URL serves several projects or customers, also check the expected project_id and mcp_server_id.
Verifying the signature on an MCP server
Public keys are published as JWKS at the Senler context keys endpoint. Other environments use the same path on their public API. The private key is never sent to the MCP server.
- Configure the trusted
iss, JWKS URL, and expected MCP address on your server. Do not select a key download address from unverified incoming JWT fields. - Use
kidto select a key from the trusted JWKS and verify the signature, allowing onlyES256and typesenler-mcp-context+jwt. - Check
iss,aud,iat,exp, versionv = 1, required fields, and the allowedauth_mode. Keep clock tolerance small. - Match the connection and project against your allowed values. Then separately check account authorization and permissions for the requested action.
Example for Node.js using jose:
import { createRemoteJWKSet, jwtVerify } from 'jose';
const issuer = 'https://api.senler.io';
const audience = 'https://crm.example.com/mcp';
const jwks = createRemoteJWKSet(
new URL('/.well-known/senler-mcp-jwks.json', issuer),
);
export async function verifySenlerContext(token, expectedProjectId, expectedServerId) {
if (!token || !expectedProjectId || !expectedServerId) {
throw new Error('MCP context and expected connection are required');
}
const { payload } = await jwtVerify(token, jwks, {
issuer,
audience,
algorithms: ['ES256'],
typ: 'senler-mcp-context+jwt',
requiredClaims: ['v', 'iat', 'exp', 'jti', 'mcp_server_id', 'auth_mode'],
maxTokenAge: '5m',
clockTolerance: 30,
});
if (
payload.v !== 1 ||
!['project', 'lead'].includes(payload.auth_mode) ||
typeof payload.jti !== 'string' || !payload.jti ||
typeof payload.iat !== 'number' || typeof payload.exp !== 'number' ||
payload.exp <= payload.iat || payload.exp - payload.iat > 300 ||
payload.project_id !== expectedProjectId ||
payload.mcp_server_id !== expectedServerId
) {
throw new Error('Unexpected MCP context');
}
return payload;
}
Pass the HTTP header value to the function. Read the expected project and connection from your server configuration. Decoding the JWT without jwtVerify is insufficient.
Create the JWKS client once per process: the library caches keys and limits repeated downloads. It can refresh JWKS for an unknown kid. If the key is missing or signature verification fails, do not fall back to trusting unsigned data.
The shared JWKS applies only to senler-mcp-context+jwt. A senler-lead+jwt uses the separate key for that connection from its settings.
Lifetime and compatibility
The JWT lasts five minutes. This is the validity period of the presented context, not the lifetime of the conversation or user login. Senler signs data automatically; the lead does not enter a token or sign in again every five minutes.
- Through Senler or MCP Router: a new context is created for each outbound HTTP request to the MCP, including tool listing, tool calls, and transport service requests. Account authorization uses its own method.
- Direct AI-provider connection to MCP: Senler supplies the JWT in the run headers. The provider can reuse it for several requests, and Senler cannot update that header within the run. A server verifying context must reject requests after
exp. For long runs requiring verification, choose execution through Senler after checking tool compatibility with that mode.
For example, context issued at 12:00 lasts until 12:05. Through Senler, a call at 12:06 receives new context valid until 12:11. In a direct provider run, the old context is already invalid at 12:06. Custom servers using lead JWT authorization are an exception: Senler always executes their calls itself, even in the agent’s provider-direct mode, and supplies fresh authorization and request-context JWTs. Other connections keep their selected mode.
Check expiration when accepting the request. JWT expiration during an accepted operation does not itself require cancelling it. The signature does not include an HTTP request body hash and does not guarantee that an action runs only once. jti identifies a JWT, not a business operation; in direct mode, one JWT can accompany several calls. Use a separate operation idempotency key to protect against duplicate writes.
Servers that ignore the additional header continue using their selected authorization. If a proxy in front of MCP has an allowed header list, add X-Senler-MCP-Context. Do not require verification until you have configured trusted JWKS, expected values, and a suitable call mode.
Configuring keys in your own Senler environment
This section is for Senler environment administrators. Developers of receiving MCP servers only need the public JWKS.
API and MCP Router use one dedicated MCP_CONTEXT_SIGNING_PRIVATE_KEY_BASE64 key: an EC P-256 private key in PKCS#8 PEM format, encoded as base64. Set the same API_PUBLIC_URL in both services: an HTTPS origin without a path. HTTP on loopback is allowed for local development. Missing or invalid keys prevent the service from starting with unsigned requests.
The key is created once when preparing the environment and stored as a secret. It does not need to be created for every request or normal restart. Its public part is automatically included in the API's JWKS. The remote-dev environment generator preserves the existing key and supplies it to Router.
For planned rotation, MCP_CONTEXT_VERIFICATION_JWKS can hold a JSON JWKS document with up to four additional public keys. First publish the new public key alongside the active one and allow caches to refresh. Then replace the signing key in every API and Router instance, retaining the old public key in JWKS. Remove the old key after old JWTs are no longer issued and have expired, accounting for caching and clock tolerance. The API serves JWKS with Cache-Control: public, max-age=60; also account for the verifier library's cache.