Skip to content

Commit 439314e

Browse files
committed
feat(mcp): add JWT → request context builder
1 parent 400c570 commit 439314e

1 file changed

Lines changed: 44 additions & 0 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { mcpError } from "./errors"
2+
3+
export interface McpRequestContext {
4+
userId: string
5+
clientId: string
6+
scopes: string[]
7+
}
8+
9+
interface VerifiedJwt {
10+
sub?: unknown
11+
client_id?: unknown
12+
azp?: unknown
13+
scope?: unknown
14+
scopes?: unknown
15+
}
16+
17+
/**
18+
* Build the per-request context from a JWT that mcpHandler has already
19+
* cryptographically verified. We still validate the *shape* of the claims —
20+
* a malformed JWT should never occur (we sign these ourselves) but if it
21+
* does we want a clean INVALID_INPUT rather than an undefined-property
22+
* crash deep in a tool handler.
23+
*/
24+
export function buildContextFromJwt(payload: VerifiedJwt): McpRequestContext {
25+
if (typeof payload.sub !== "string" || payload.sub.length === 0) {
26+
throw mcpError("INVALID_INPUT", "JWT missing 'sub' claim")
27+
}
28+
const clientId =
29+
typeof payload.client_id === "string"
30+
? payload.client_id
31+
: typeof payload.azp === "string"
32+
? payload.azp
33+
: null
34+
if (!clientId) {
35+
throw mcpError("INVALID_INPUT", "JWT missing 'client_id' / 'azp' claim")
36+
}
37+
const scopes =
38+
typeof payload.scope === "string"
39+
? payload.scope.split(/\s+/).filter(Boolean)
40+
: Array.isArray(payload.scopes)
41+
? payload.scopes.filter((s): s is string => typeof s === "string")
42+
: []
43+
return { userId: payload.sub, clientId, scopes }
44+
}

0 commit comments

Comments
 (0)