@code-yeongyu/senpi 2026.11.10-1 → 2026.11.10-2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/dist/bundle/chunks/{chunk-GPXZTSEX.js → chunk-2M54EAAY.js} +3 -3
- package/dist/bundle/chunks/{chunk-YPAQXRP3.js → chunk-47T6YMI2.js} +2 -2
- package/dist/bundle/chunks/{chunk-I5ASHSLZ.js → chunk-4JMGWVRH.js} +1 -1
- package/dist/bundle/chunks/{chunk-HI5JSLG5.js → chunk-74IB3JPP.js} +2 -2
- package/dist/bundle/chunks/{chunk-NH7SYIGN.js → chunk-DLPV4B3R.js} +1 -1
- package/dist/bundle/chunks/{chunk-Y7Z7XPUF.js → chunk-DZQA5LVN.js} +1 -1
- package/dist/bundle/chunks/{chunk-2CNSCMYC.js → chunk-E5O4SP5I.js} +1 -1
- package/dist/bundle/chunks/{chunk-3IYWH6RO.js → chunk-IKJCA5SH.js} +1 -1
- package/dist/bundle/chunks/{chunk-H33A33M4.js → chunk-M2VJVJMK.js} +1 -1
- package/dist/bundle/chunks/{chunk-L6NRDUWJ.js → chunk-OF3XVGG4.js} +1 -1
- package/dist/bundle/chunks/{chunk-THYEZBUG.js → chunk-Q4G5DECT.js} +2 -2
- package/dist/bundle/chunks/{chunk-ARUV3US4.js → chunk-QATU6BBC.js} +1 -1
- package/dist/bundle/chunks/{chunk-FAFIGBUD.js → chunk-TUEZNIBK.js} +1 -1
- package/dist/bundle/chunks/{chunk-BELNOPAI.js → chunk-UOZDDYBN.js} +2 -2
- package/dist/bundle/chunks/{chunk-DBQBJTMF.js → chunk-VUY4BZDI.js} +1 -1
- package/dist/bundle/chunks/{chunk-TGW4R7SI.js → chunk-XH24X5B2.js} +1 -1
- package/dist/bundle/chunks/chunk-XI6THO4F.js +2 -0
- package/dist/bundle/chunks/{chunk-X4XWAB3J.js → chunk-YCPURLPU.js} +38 -38
- package/dist/bundle/chunks/{chunk-LHEP3ACI.js → chunk-YNGEZBTP.js} +1 -1
- package/dist/bundle/chunks/{cli-main-VG3C2EXI.js → cli-main-Y6ALA5OI.js} +1 -1
- package/dist/bundle/chunks/{hand-entry-3LQKUTYS.js → hand-entry-IPDAIOGI.js} +1 -1
- package/dist/bundle/chunks/{host-command-CCASS7W4.js → host-command-2326GCUP.js} +1 -1
- package/dist/bundle/chunks/{host-lifecycle-WWC7HXQ6.js → host-lifecycle-TNHASSFJ.js} +1 -1
- package/dist/bundle/chunks/host-lifecycle.js +1 -1
- package/dist/bundle/chunks/{host-session-command-YJB2FLKU.js → host-session-command-BZYBDOIE.js} +1 -1
- package/dist/bundle/chunks/{host-socket-request-DMYXC56U.js → host-socket-request-QN4Q2P5E.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-CUN3WMUF.js → interactive-mode-LBQB7DDR.js} +1 -1
- package/dist/bundle/chunks/{main-P7XRC2OI.js → main-FFRFP34W.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-244X3P6L.js → multi-session-host-NDIHHJ2L.js} +11 -8
- package/dist/bundle/chunks/{package-manager-cli-UVWJOQA3.js → package-manager-cli-XCPAMOYI.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-G4I3HHPB.js → rpc-mode-TKNZM4PZ.js} +1 -1
- package/dist/bundle/chunks/{rpc-monitor-source-C6VQ3BC4.js → rpc-monitor-source-PGTDSGWP.js} +1 -1
- package/dist/bundle/chunks/{run-attached-tui-R3RB5P7G.js → run-attached-tui-JDUKXDI2.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-5FG2OKSR.js → schedule-command-HF2DHABB.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-XWCFLJYC.js → session-control-endpoint-A6R6W2JQ.js} +1 -1
- package/dist/bundle/chunks/{session-picker-2FZSNFS7.js → session-picker-GWA4S2PO.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +119 -116
- package/dist/bundle/cli.js +2 -2
- package/dist/bundle/host-supervisor.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/core/agent-session-runtime.d.ts +2 -0
- package/dist/core/agent-session-runtime.js +5 -0
- package/dist/core/agent-session.d.ts +1 -1
- package/dist/core/agent-session.js +30 -10
- package/dist/core/extensions/builtin/config-reload/change-groups.js +6 -2
- package/dist/core/extensions/builtin/mcp/auth/catalog-identity.js +32 -8
- package/dist/core/extensions/builtin/mcp/auth/oauth-grant.d.ts +43 -0
- package/dist/core/extensions/builtin/mcp/auth/oauth-grant.js +196 -0
- package/dist/core/extensions/builtin/mcp/auth/oauth-provider.d.ts +4 -17
- package/dist/core/extensions/builtin/mcp/auth/oauth-provider.js +64 -75
- package/dist/core/extensions/builtin/mcp/auth/oauth-refresh.d.ts +1 -1
- package/dist/core/extensions/builtin/mcp/auth/oauth-refresh.js +25 -4
- package/dist/core/extensions/builtin/mcp/auth/oauth.js +10 -7
- package/dist/core/extensions/builtin/mcp/auth/token-store.d.ts +18 -0
- package/dist/core/extensions/builtin/mcp/auth/token-store.js +3 -0
- package/dist/core/extensions/builtin/mcp/catalog-cache.js +7 -6
- package/dist/core/extensions/builtin/mcp/catalog.js +3 -2
- package/dist/core/extensions/builtin/mcp/invocation.js +6 -0
- package/dist/core/extensions/builtin/mcp/service-register.js +16 -2
- package/dist/core/extensions/builtin/mcp/service-tools-changed.js +61 -54
- package/dist/core/extensions/builtin/mcp/startup-race.d.ts +2 -0
- package/dist/core/extensions/builtin/mcp/startup-race.js +25 -7
- package/dist/core/extensions/builtin/prompt-preset/claude-fable-5-2.js +6 -6
- package/dist/core/open-timing.d.ts +2 -0
- package/dist/core/open-timing.js +7 -1
- package/dist/core/session-manager.d.ts +2 -0
- package/dist/core/session-manager.js +15 -1
- package/dist/modes/interactive/session-control-rpc-commands.d.ts +1 -1
- package/dist/modes/interactive/session-control-rpc-commands.js +5 -0
- package/dist/modes/rpc/compact-admission-validation.js +4 -1
- package/dist/modes/rpc/connection-handler.d.ts +2 -0
- package/dist/modes/rpc/connection-handler.js +10 -2
- package/dist/modes/rpc/custom-capability.d.ts +2 -0
- package/dist/modes/rpc/custom-capability.js +2 -0
- package/dist/modes/rpc/effect-commands.d.ts +21 -0
- package/dist/modes/rpc/host-capabilities.d.ts +1 -0
- package/dist/modes/rpc/host-capabilities.js +3 -1
- package/dist/modes/rpc/host-observe-request.d.ts +2 -0
- package/dist/modes/rpc/host-observe-request.js +6 -1
- package/dist/modes/rpc/host-park-ledger.d.ts +19 -0
- package/dist/modes/rpc/host-park-ledger.js +110 -0
- package/dist/modes/rpc/lifecycle-delivery.js +4 -1
- package/dist/modes/rpc/media-placeholders.js +1 -0
- package/dist/modes/rpc/method-classes-schema-host.d.ts +42 -1
- package/dist/modes/rpc/method-classes-schema-host.js +24 -21
- package/dist/modes/rpc/method-classes-schema.d.ts +42 -1
- package/dist/modes/rpc/method-classes-schema.js +5 -2
- package/dist/modes/rpc/method-classes.d.ts +2 -0
- package/dist/modes/rpc/method-classes.js +3 -0
- package/dist/modes/rpc/multi-session-host.js +1 -1
- package/dist/modes/rpc/rpc-host-command-types.d.ts +14 -1
- package/dist/modes/rpc/rpc-host-response-types.d.ts +21 -0
- package/dist/modes/rpc/session-attachment-gate.js +2 -0
- package/dist/modes/rpc/session-binding.d.ts +1 -0
- package/dist/modes/rpc/session-binding.js +6 -1
- package/dist/modes/rpc/session-connection-context.d.ts +2 -0
- package/dist/modes/rpc/session-connection-context.js +4 -0
- package/dist/modes/rpc/session-registry-attach.d.ts +2 -0
- package/dist/modes/rpc/session-registry-attach.js +15 -11
- package/dist/modes/rpc/session-registry-claims.js +2 -0
- package/dist/modes/rpc/session-registry-open.js +13 -7
- package/dist/modes/rpc/session-registry-opening.js +2 -1
- package/dist/modes/rpc/session-registry-types.d.ts +4 -0
- package/dist/modes/rpc/session-registry.d.ts +3 -0
- package/dist/modes/rpc/session-registry.js +27 -3
- package/dist/modes/rpc/session-router-close.js +1 -0
- package/dist/modes/rpc/session-router-context.d.ts +4 -2
- package/dist/modes/rpc/session-router-context.js +3 -0
- package/dist/modes/rpc/session-router-host-reads.js +1 -0
- package/dist/modes/rpc/session-router-occupancy.js +10 -1
- package/dist/modes/rpc/session-router-open-result.d.ts +12 -0
- package/dist/modes/rpc/session-router-open-result.js +63 -0
- package/dist/modes/rpc/session-router-open.d.ts +3 -0
- package/dist/modes/rpc/session-router-open.js +15 -63
- package/dist/modes/rpc/session-router-park.d.ts +15 -0
- package/dist/modes/rpc/session-router-park.js +176 -0
- package/dist/modes/rpc/session-router-routing.d.ts +1 -1
- package/dist/modes/rpc/session-router-routing.js +7 -0
- package/dist/modes/rpc/session-status-source.js +2 -2
- package/docs/engine-socket/fixtures/admission-routes/h-legacy.json +1 -1
- package/docs/engine-socket/fixtures/rpc-inbound-samples.json +22 -2
- package/docs/rpc.md +50 -0
- package/native/socket-secret/prebuilds/win32-arm64/senpi_socket_secret.win32-arm64.node +0 -0
- package/native/socket-secret/prebuilds/win32-x64/senpi_socket_secret.win32-x64.node +0 -0
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-436DNN4Z.js +0 -2
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
|
|
2
|
+
import type { McpAuthorizationProof, McpClientRegistration, McpStoredAuth } from "./token-store.ts";
|
|
3
|
+
export type McpGrantRecord = Pick<McpStoredAuth, "accessToken" | "refreshToken" | "issuer" | "resource" | "scope" | "refreshScope"> & {
|
|
4
|
+
readonly authorization?: unknown;
|
|
5
|
+
readonly clientInfo?: Pick<McpClientRegistration, "client_id" | "client_secret" | "token_endpoint_auth_method" | "issuer">;
|
|
6
|
+
readonly discoveryState?: {
|
|
7
|
+
readonly authorizationServerUrl?: string | URL;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export interface McpGrantContext {
|
|
11
|
+
readonly serverName: string;
|
|
12
|
+
readonly serverUrl: string;
|
|
13
|
+
readonly agentDir: string;
|
|
14
|
+
readonly clientId?: string;
|
|
15
|
+
readonly clientMetadataUrl?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function storedGrantIssuer(record: McpGrantRecord | undefined): string | undefined;
|
|
18
|
+
export declare function freezeGrantAttribution(record: McpStoredAuth | undefined): McpStoredAuth;
|
|
19
|
+
export declare function storedAuthToTokens(record: McpStoredAuth | undefined, now?: number): OAuthTokens | undefined;
|
|
20
|
+
/** Maps one response; the caller decides which preceding fields it is entitled to inherit. */
|
|
21
|
+
export declare function mergeTokensIntoStoredAuth(current: McpStoredAuth, tokens: OAuthTokens): McpStoredAuth;
|
|
22
|
+
export interface McpTokenResponse {
|
|
23
|
+
readonly tokens: OAuthTokens;
|
|
24
|
+
readonly resource: string;
|
|
25
|
+
readonly fresh: boolean;
|
|
26
|
+
readonly presented?: McpStoredAuth;
|
|
27
|
+
readonly clientInfo?: McpClientRegistration;
|
|
28
|
+
}
|
|
29
|
+
/** An SDK response has no locked predecessor; only explicit fresh acquisition can establish its initial proof. */
|
|
30
|
+
export declare function mergeOAuthTokenResponse(current: McpStoredAuth, context: McpGrantContext, response: McpTokenResponse): McpStoredAuth;
|
|
31
|
+
/** Active binding excludes pending registrations and display/redirect metadata. */
|
|
32
|
+
export declare function grantBinding(record: McpGrantRecord, context: McpGrantContext): {
|
|
33
|
+
issuer: string | undefined;
|
|
34
|
+
resource: string | undefined;
|
|
35
|
+
clientIdentity: string | undefined;
|
|
36
|
+
scope: string | undefined;
|
|
37
|
+
refreshScope: string | undefined;
|
|
38
|
+
};
|
|
39
|
+
export declare function validGrantProof(record: McpGrantRecord, context: McpGrantContext): McpAuthorizationProof | undefined;
|
|
40
|
+
/** Only the refresh transaction passes a predecessor, read while holding its existing token-store lock. */
|
|
41
|
+
export declare function withGrantAuthorization(record: McpStoredAuth, context: McpGrantContext, previous?: McpStoredAuth): McpStoredAuth;
|
|
42
|
+
export declare function oauthGrantIdentity(record: McpGrantRecord, context: McpGrantContext): string;
|
|
43
|
+
//# sourceMappingURL=oauth-grant.d.ts.map
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { Check } from "typebox/value";
|
|
4
|
+
import { isEphemeralIdentity, keyedIdentity } from "../config-identity-key.js";
|
|
5
|
+
import { canonicalAuthorizationServerIssuer } from "./oauth-issuer.js";
|
|
6
|
+
const authorizationSchema = Type.Object({
|
|
7
|
+
version: Type.Literal(1),
|
|
8
|
+
id: Type.String({ pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" }),
|
|
9
|
+
proof: Type.Optional(Type.Object({
|
|
10
|
+
credentialIdentity: Type.String({ pattern: "^[0-9a-f]{64}$" }),
|
|
11
|
+
previousCredentialIdentity: Type.Optional(Type.String({ pattern: "^[0-9a-f]{64}$" })),
|
|
12
|
+
issuer: Type.String({ minLength: 1 }),
|
|
13
|
+
resource: Type.String({ minLength: 1 }),
|
|
14
|
+
clientIdentity: Type.String({ pattern: "^[0-9a-f]{64}$" }),
|
|
15
|
+
scope: Type.String({ minLength: 1 }),
|
|
16
|
+
refreshScope: Type.Optional(Type.String({ minLength: 1 })),
|
|
17
|
+
})),
|
|
18
|
+
});
|
|
19
|
+
const epochSchema = Type.Object({
|
|
20
|
+
version: Type.Literal(1),
|
|
21
|
+
id: authorizationSchema.properties.id,
|
|
22
|
+
});
|
|
23
|
+
function authorizationEpoch(record) {
|
|
24
|
+
const authorization = record.authorization;
|
|
25
|
+
return Check(epochSchema, authorization) ? authorization.id : undefined;
|
|
26
|
+
}
|
|
27
|
+
function normalizeGrantedScope(scope) {
|
|
28
|
+
if (scope === undefined || !/^[\x21\x23-\x5b\x5d-\x7e]+(?: +[\x21\x23-\x5b\x5d-\x7e]+)*$/.test(scope))
|
|
29
|
+
return undefined;
|
|
30
|
+
return [...new Set(scope.split(" "))].filter(Boolean).sort().join(" ");
|
|
31
|
+
}
|
|
32
|
+
export function storedGrantIssuer(record) {
|
|
33
|
+
return record?.issuer ?? record?.discoveryState?.authorizationServerUrl?.toString();
|
|
34
|
+
}
|
|
35
|
+
export function freezeGrantAttribution(record) {
|
|
36
|
+
const next = { ...(record ?? {}) };
|
|
37
|
+
if (next.issuer !== undefined || (next.accessToken === undefined && next.refreshToken === undefined))
|
|
38
|
+
return next;
|
|
39
|
+
const issuer = storedGrantIssuer(next);
|
|
40
|
+
if (issuer !== undefined)
|
|
41
|
+
next.issuer = issuer;
|
|
42
|
+
else
|
|
43
|
+
delete next.refreshToken;
|
|
44
|
+
return next;
|
|
45
|
+
}
|
|
46
|
+
export function storedAuthToTokens(record, now = Date.now()) {
|
|
47
|
+
if (record?.accessToken === undefined || record.accessToken.length === 0)
|
|
48
|
+
return undefined;
|
|
49
|
+
const expiresIn = record.expiresAt === undefined ? undefined : Math.max(0, Math.ceil((record.expiresAt - now) / 1000));
|
|
50
|
+
const issuer = storedGrantIssuer(record);
|
|
51
|
+
return {
|
|
52
|
+
access_token: record.accessToken,
|
|
53
|
+
refresh_token: issuer === undefined ? undefined : record.refreshToken,
|
|
54
|
+
token_type: "Bearer",
|
|
55
|
+
scope: normalizeGrantedScope(record.scope),
|
|
56
|
+
...(expiresIn === undefined ? {} : { expires_in: expiresIn }),
|
|
57
|
+
...(issuer === undefined ? {} : { issuer }),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** Maps one response; the caller decides which preceding fields it is entitled to inherit. */
|
|
61
|
+
export function mergeTokensIntoStoredAuth(current, tokens) {
|
|
62
|
+
return {
|
|
63
|
+
...current,
|
|
64
|
+
accessToken: tokens.access_token,
|
|
65
|
+
refreshToken: tokens.refresh_token ?? current.refreshToken,
|
|
66
|
+
scope: normalizeGrantedScope(tokens.scope),
|
|
67
|
+
expiresAt: tokens.expires_in === undefined ? undefined : Date.now() + tokens.expires_in * 1000,
|
|
68
|
+
issuer: tokens.issuer ?? current.issuer,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** An SDK response has no locked predecessor; only explicit fresh acquisition can establish its initial proof. */
|
|
72
|
+
export function mergeOAuthTokenResponse(current, context, response) {
|
|
73
|
+
const { tokens, resource, fresh, presented, clientInfo } = response;
|
|
74
|
+
// Attribute a response to the client selected before the request, never to a later store writer.
|
|
75
|
+
const active = {
|
|
76
|
+
...current,
|
|
77
|
+
clientInfo,
|
|
78
|
+
pendingClientInfo: current.pendingClientInfo?.client_id === clientInfo?.client_id ? undefined : current.pendingClientInfo,
|
|
79
|
+
};
|
|
80
|
+
const binding = grantBinding({ ...active, issuer: tokens.issuer, resource }, context);
|
|
81
|
+
const canInherit = !fresh &&
|
|
82
|
+
presented !== undefined &&
|
|
83
|
+
JSON.stringify(grantBinding(presented, context)) === JSON.stringify(grantBinding(current, context)) &&
|
|
84
|
+
JSON.stringify(grantBinding(presented, context)) === JSON.stringify(binding) &&
|
|
85
|
+
presented.accessToken === current.accessToken &&
|
|
86
|
+
presented.refreshToken === current.refreshToken &&
|
|
87
|
+
presented.authorization?.id === current.authorization?.id;
|
|
88
|
+
const base = {
|
|
89
|
+
...active,
|
|
90
|
+
issuer: tokens.issuer,
|
|
91
|
+
resource,
|
|
92
|
+
refreshToken: canInherit ? presented.refreshToken : undefined,
|
|
93
|
+
scope: undefined,
|
|
94
|
+
refreshScope: undefined,
|
|
95
|
+
authorization: undefined,
|
|
96
|
+
};
|
|
97
|
+
const next = mergeTokensIntoStoredAuth(base, tokens);
|
|
98
|
+
return fresh
|
|
99
|
+
? withGrantAuthorization({ ...next, refreshScope: next.scope }, context)
|
|
100
|
+
: { ...next, authorization: { version: 1, id: randomUUID() } };
|
|
101
|
+
}
|
|
102
|
+
/** Active binding excludes pending registrations and display/redirect metadata. */
|
|
103
|
+
export function grantBinding(record, context) {
|
|
104
|
+
const issuer = storedGrantIssuer(record);
|
|
105
|
+
const registration = record.clientInfo;
|
|
106
|
+
const clientId = context.clientId ?? context.clientMetadataUrl ?? registration?.client_id;
|
|
107
|
+
const configured = context.clientId !== undefined || context.clientMetadataUrl !== undefined;
|
|
108
|
+
const clientIssuer = configured ? issuer : (registration?.issuer ?? issuer);
|
|
109
|
+
const clientIdentity = clientId === undefined
|
|
110
|
+
? undefined
|
|
111
|
+
: keyedIdentity(context.agentDir, JSON.stringify([
|
|
112
|
+
"mcp-oauth-client-v1",
|
|
113
|
+
clientId,
|
|
114
|
+
configured ? null : (registration?.client_secret ?? null),
|
|
115
|
+
configured ? null : (registration?.token_endpoint_auth_method ?? null),
|
|
116
|
+
clientIssuer === undefined ? null : canonicalAuthorizationServerIssuer(clientIssuer),
|
|
117
|
+
]));
|
|
118
|
+
return {
|
|
119
|
+
issuer: issuer === undefined ? undefined : canonicalAuthorizationServerIssuer(issuer),
|
|
120
|
+
resource: record.resource,
|
|
121
|
+
clientIdentity,
|
|
122
|
+
scope: normalizeGrantedScope(record.scope),
|
|
123
|
+
refreshScope: normalizeGrantedScope(record.refreshScope),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function credentialCommitment(record, context, id) {
|
|
127
|
+
const binding = grantBinding(record, context);
|
|
128
|
+
return keyedIdentity(context.agentDir, JSON.stringify([
|
|
129
|
+
"mcp-oauth-credential-v1",
|
|
130
|
+
context.serverName,
|
|
131
|
+
context.serverUrl,
|
|
132
|
+
id ?? null,
|
|
133
|
+
record.accessToken ?? null,
|
|
134
|
+
record.refreshToken ?? null,
|
|
135
|
+
binding.issuer ?? null,
|
|
136
|
+
binding.resource ?? null,
|
|
137
|
+
binding.clientIdentity ?? null,
|
|
138
|
+
binding.scope ?? null,
|
|
139
|
+
binding.refreshScope ?? null,
|
|
140
|
+
]));
|
|
141
|
+
}
|
|
142
|
+
export function validGrantProof(record, context) {
|
|
143
|
+
const authorization = record.authorization;
|
|
144
|
+
if (!Check(authorizationSchema, authorization) || authorization.proof === undefined)
|
|
145
|
+
return undefined;
|
|
146
|
+
const proof = authorization.proof;
|
|
147
|
+
const binding = grantBinding(record, context);
|
|
148
|
+
if (Object.entries(binding).some(([key, value]) => Reflect.get(proof, key) !== value))
|
|
149
|
+
return undefined;
|
|
150
|
+
return proof.credentialIdentity === credentialCommitment(record, context, authorization.id) ? proof : undefined;
|
|
151
|
+
}
|
|
152
|
+
/** Only the refresh transaction passes a predecessor, read while holding its existing token-store lock. */
|
|
153
|
+
export function withGrantAuthorization(record, context, previous) {
|
|
154
|
+
const predecessor = previous === undefined ? undefined : validGrantProof(previous, context);
|
|
155
|
+
const binding = grantBinding(record, context);
|
|
156
|
+
const sameBinding = predecessor !== undefined &&
|
|
157
|
+
Object.entries(binding).every(([key, value]) => Reflect.get(predecessor, key) === value);
|
|
158
|
+
const id = sameBinding && previous?.authorization !== undefined ? previous.authorization.id : randomUUID();
|
|
159
|
+
const credentialIdentity = credentialCommitment(record, context, id);
|
|
160
|
+
const proof = binding.issuer !== undefined &&
|
|
161
|
+
binding.resource !== undefined &&
|
|
162
|
+
binding.clientIdentity !== undefined &&
|
|
163
|
+
binding.scope !== undefined &&
|
|
164
|
+
!isEphemeralIdentity(credentialIdentity)
|
|
165
|
+
? {
|
|
166
|
+
...binding,
|
|
167
|
+
issuer: binding.issuer,
|
|
168
|
+
resource: binding.resource,
|
|
169
|
+
clientIdentity: binding.clientIdentity,
|
|
170
|
+
scope: binding.scope,
|
|
171
|
+
credentialIdentity,
|
|
172
|
+
previousCredentialIdentity: previous === undefined
|
|
173
|
+
? undefined
|
|
174
|
+
: credentialCommitment(previous, context, authorizationEpoch(previous)),
|
|
175
|
+
}
|
|
176
|
+
: undefined;
|
|
177
|
+
return { ...record, authorization: { version: 1, id, proof } };
|
|
178
|
+
}
|
|
179
|
+
export function oauthGrantIdentity(record, context) {
|
|
180
|
+
const epoch = authorizationEpoch(record);
|
|
181
|
+
const proof = validGrantProof(record, context);
|
|
182
|
+
return proof === undefined
|
|
183
|
+
? keyedIdentity(context.agentDir, JSON.stringify(["mcp-oauth-opaque-v1", credentialCommitment(record, context, epoch)]))
|
|
184
|
+
: keyedIdentity(context.agentDir, JSON.stringify([
|
|
185
|
+
"mcp-oauth-authorization-v1",
|
|
186
|
+
context.serverName,
|
|
187
|
+
context.serverUrl,
|
|
188
|
+
epoch,
|
|
189
|
+
proof.issuer,
|
|
190
|
+
proof.resource,
|
|
191
|
+
proof.clientIdentity,
|
|
192
|
+
proof.scope,
|
|
193
|
+
proof.refreshScope ?? null,
|
|
194
|
+
]));
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=oauth-grant.js.map
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { OAuthClientProvider, OAuthDiscoveryState } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
2
2
|
import type { OAuthClientInformationFull, OAuthClientInformationMixed, OAuthClientMetadata, OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
|
|
3
3
|
import type { McpLogger } from "../log.ts";
|
|
4
|
-
import
|
|
5
|
-
|
|
4
|
+
import { type McpGrantContext } from "./oauth-grant.ts";
|
|
5
|
+
import type { McpTokenStore } from "./token-store.ts";
|
|
6
6
|
export interface McpOAuthProviderOptions {
|
|
7
7
|
serverName: string;
|
|
8
8
|
serverUrl: string;
|
|
@@ -16,21 +16,6 @@ export interface McpOAuthProviderOptions {
|
|
|
16
16
|
logger?: McpLogger;
|
|
17
17
|
onRedirect?: (url: URL) => void | Promise<void>;
|
|
18
18
|
}
|
|
19
|
-
export declare function tokenExpiresAt(tokens: OAuthTokens, now?: number): number | undefined;
|
|
20
|
-
/**
|
|
21
|
-
* The authorization server a stored grant belongs to (senpi#2940, GHSA-6qxp-vccf-f47h): its `issuer` stamp, or
|
|
22
|
-
* for a record saved before the stamp existed, the authorization server its sign-in discovered. A record with
|
|
23
|
-
* neither cannot be attributed, so its refresh token is never presented anywhere.
|
|
24
|
-
*/
|
|
25
|
-
export declare function storedGrantIssuer(record: McpStoredAuth | undefined): string | undefined;
|
|
26
|
-
/**
|
|
27
|
-
* Fixes who an unstamped grant belongs to before the discovery record it falls back to is replaced (senpi#2940).
|
|
28
|
-
* A later discovery reflects what the MCP server advertises now, not where the grant was issued, so it must never
|
|
29
|
-
* attribute an existing grant: the grant keeps the server it was attributed to, or loses its refresh token.
|
|
30
|
-
*/
|
|
31
|
-
export declare function freezeGrantAttribution(record: McpStoredAuth | undefined): McpStoredAuth;
|
|
32
|
-
export declare function storedAuthToTokens(record: McpStoredAuth | undefined, now?: number): OAuthTokens | undefined;
|
|
33
|
-
export declare function mergeTokensIntoStoredAuth(current: McpStoredAuth | undefined, tokens: OAuthTokens, serverUrl: string, issuer?: string): McpStoredAuth;
|
|
34
19
|
export declare class McpOAuthProvider implements OAuthClientProvider {
|
|
35
20
|
#private;
|
|
36
21
|
constructor(options: McpOAuthProviderOptions);
|
|
@@ -47,6 +32,7 @@ export declare class McpOAuthProvider implements OAuthClientProvider {
|
|
|
47
32
|
saveDiscoveryState(state: OAuthDiscoveryState): Promise<void>;
|
|
48
33
|
discoveryState(): OAuthDiscoveryState | undefined;
|
|
49
34
|
tokens(): OAuthTokens | undefined;
|
|
35
|
+
withFreshAuthorization<T>(exchange: () => Promise<T>): Promise<T>;
|
|
50
36
|
saveTokens(tokens: OAuthTokens): Promise<void>;
|
|
51
37
|
saveCodeVerifier(codeVerifier: string): Promise<void>;
|
|
52
38
|
codeVerifier(): string;
|
|
@@ -57,5 +43,6 @@ export declare class McpOAuthProvider implements OAuthClientProvider {
|
|
|
57
43
|
get serverUrl(): string;
|
|
58
44
|
get store(): McpTokenStore;
|
|
59
45
|
get scopes(): string[] | undefined;
|
|
46
|
+
get grantContext(): McpGrantContext;
|
|
60
47
|
}
|
|
61
48
|
//# sourceMappingURL=oauth-provider.d.ts.map
|
|
@@ -2,72 +2,9 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import { oauthBrandName } from "@earendil-works/pi-ai/utils/oauth-page";
|
|
3
3
|
import { fingerprintSecret } from "../log.js";
|
|
4
4
|
import { OAuthFlowError } from "./oauth-errors.js";
|
|
5
|
+
import { freezeGrantAttribution, grantBinding, mergeOAuthTokenResponse, storedAuthToTokens, storedGrantIssuer, } from "./oauth-grant.js";
|
|
5
6
|
import { canonicalAuthorizationServerIssuer, credentialsMatchIssuer, discoveryNamesItsServer, validatedAuthorizationServerIssuer, } from "./oauth-issuer.js";
|
|
6
7
|
import { grantForFlow, registrationForFlow, withPromotedRegistration, withRegistration, } from "./registered-redirect.js";
|
|
7
|
-
// Buffer before absolute token expiry at which a refresh is considered due.
|
|
8
|
-
export const REFRESH_LEEWAY_MS = 5 * 60 * 1000;
|
|
9
|
-
export function tokenExpiresAt(tokens, now = Date.now()) {
|
|
10
|
-
if (tokens.expires_in === undefined)
|
|
11
|
-
return undefined;
|
|
12
|
-
return now + tokens.expires_in * 1000;
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* The authorization server a stored grant belongs to (senpi#2940, GHSA-6qxp-vccf-f47h): its `issuer` stamp, or
|
|
16
|
-
* for a record saved before the stamp existed, the authorization server its sign-in discovered. A record with
|
|
17
|
-
* neither cannot be attributed, so its refresh token is never presented anywhere.
|
|
18
|
-
*/
|
|
19
|
-
export function storedGrantIssuer(record) {
|
|
20
|
-
return record?.issuer ?? record?.discoveryState?.authorizationServerUrl?.toString();
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Fixes who an unstamped grant belongs to before the discovery record it falls back to is replaced (senpi#2940).
|
|
24
|
-
* A later discovery reflects what the MCP server advertises now, not where the grant was issued, so it must never
|
|
25
|
-
* attribute an existing grant: the grant keeps the server it was attributed to, or loses its refresh token.
|
|
26
|
-
*/
|
|
27
|
-
export function freezeGrantAttribution(record) {
|
|
28
|
-
const next = { ...(record ?? {}) };
|
|
29
|
-
if (next.issuer !== undefined || (next.accessToken === undefined && next.refreshToken === undefined))
|
|
30
|
-
return next;
|
|
31
|
-
const issuer = storedGrantIssuer(next);
|
|
32
|
-
if (issuer !== undefined)
|
|
33
|
-
next.issuer = issuer;
|
|
34
|
-
else
|
|
35
|
-
delete next.refreshToken;
|
|
36
|
-
return next;
|
|
37
|
-
}
|
|
38
|
-
export function storedAuthToTokens(record, now = Date.now()) {
|
|
39
|
-
if (record?.accessToken === undefined || record.accessToken.length === 0)
|
|
40
|
-
return undefined;
|
|
41
|
-
const expiresIn = record.expiresAt === undefined ? undefined : Math.max(0, Math.ceil((record.expiresAt - now) / 1000));
|
|
42
|
-
const issuer = storedGrantIssuer(record);
|
|
43
|
-
return {
|
|
44
|
-
access_token: record.accessToken,
|
|
45
|
-
// An unattributed refresh token is withheld: the SDK would post it to whichever server it discovers.
|
|
46
|
-
refresh_token: issuer === undefined ? undefined : record.refreshToken,
|
|
47
|
-
token_type: "Bearer",
|
|
48
|
-
...(expiresIn === undefined ? {} : { expires_in: expiresIn }),
|
|
49
|
-
...(issuer === undefined ? {} : { issuer }),
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
export function mergeTokensIntoStoredAuth(current, tokens, serverUrl, issuer) {
|
|
53
|
-
const next = {
|
|
54
|
-
...(current ?? {}),
|
|
55
|
-
accessToken: tokens.access_token,
|
|
56
|
-
resource: current?.resource ?? serverUrl,
|
|
57
|
-
};
|
|
58
|
-
const refreshToken = tokens.refresh_token ?? current?.refreshToken;
|
|
59
|
-
if (refreshToken !== undefined)
|
|
60
|
-
next.refreshToken = refreshToken;
|
|
61
|
-
const expiresAt = tokenExpiresAt(tokens);
|
|
62
|
-
if (expiresAt !== undefined)
|
|
63
|
-
next.expiresAt = expiresAt;
|
|
64
|
-
else
|
|
65
|
-
delete next.expiresAt;
|
|
66
|
-
const stamp = tokens.issuer ?? issuer;
|
|
67
|
-
if (stamp !== undefined)
|
|
68
|
-
next.issuer = stamp;
|
|
69
|
-
return next;
|
|
70
|
-
}
|
|
71
8
|
// Implements the SDK OAuthClientProvider backed by the URL-bound token store.
|
|
72
9
|
// All persistence goes through the store so tokens survive process restarts and
|
|
73
10
|
// stay lockable across processes.
|
|
@@ -75,6 +12,10 @@ export class McpOAuthProvider {
|
|
|
75
12
|
#options;
|
|
76
13
|
#expectedState;
|
|
77
14
|
#lastAuthorizationUrl;
|
|
15
|
+
#freshAuthorization = false;
|
|
16
|
+
#resource;
|
|
17
|
+
#presentedGrant;
|
|
18
|
+
#selectedClient;
|
|
78
19
|
constructor(options) {
|
|
79
20
|
this.#options = options;
|
|
80
21
|
}
|
|
@@ -121,14 +62,23 @@ export class McpOAuthProvider {
|
|
|
121
62
|
return { client_id: this.#options.clientId };
|
|
122
63
|
if (this.#options.clientMetadataUrl !== undefined)
|
|
123
64
|
return { client_id: this.#options.clientMetadataUrl };
|
|
124
|
-
|
|
65
|
+
this.#selectedClient = registrationForFlow(this.#store.read(), this.#options);
|
|
66
|
+
return this.#selectedClient;
|
|
125
67
|
}
|
|
126
68
|
async saveClientInformation(info) {
|
|
127
69
|
const discovery = this.discoveryState();
|
|
128
70
|
const issuer = discovery === undefined
|
|
129
71
|
? info.issuer && canonicalAuthorizationServerIssuer(info.issuer)
|
|
130
72
|
: validatedAuthorizationServerIssuer(discovery, this.serverName);
|
|
131
|
-
await this.#store.update((current) =>
|
|
73
|
+
const saved = await this.#store.update((current) => {
|
|
74
|
+
const next = withRegistration(current, { ...info, issuer }, this.#options);
|
|
75
|
+
return current !== undefined &&
|
|
76
|
+
JSON.stringify(grantBinding(current, this.grantContext)) !==
|
|
77
|
+
JSON.stringify(grantBinding(next, this.grantContext))
|
|
78
|
+
? { ...next, scope: undefined, refreshScope: undefined, authorization: undefined }
|
|
79
|
+
: next;
|
|
80
|
+
});
|
|
81
|
+
this.#selectedClient = registrationForFlow(saved, this.#options);
|
|
132
82
|
}
|
|
133
83
|
async saveDiscoveryState(state) {
|
|
134
84
|
const issuer = validatedAuthorizationServerIssuer(state, this.serverName);
|
|
@@ -171,16 +121,37 @@ export class McpOAuthProvider {
|
|
|
171
121
|
return state;
|
|
172
122
|
}
|
|
173
123
|
tokens() {
|
|
174
|
-
|
|
124
|
+
const grant = grantForFlow(this.#store.read(), this.#options);
|
|
125
|
+
this.#presentedGrant = withPromotedRegistration(grant, this.#options);
|
|
126
|
+
return storedAuthToTokens(grant);
|
|
127
|
+
}
|
|
128
|
+
async withFreshAuthorization(exchange) {
|
|
129
|
+
const previous = this.#freshAuthorization;
|
|
130
|
+
this.#freshAuthorization = true;
|
|
131
|
+
try {
|
|
132
|
+
return await exchange();
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
this.#freshAuthorization = previous;
|
|
136
|
+
}
|
|
175
137
|
}
|
|
176
138
|
async saveTokens(tokens) {
|
|
177
139
|
const discovery = this.discoveryState();
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
140
|
+
// SDK results carry the exchange's issuer. Never restamp a late response from a newer file snapshot.
|
|
141
|
+
const issuer = tokens.issuer !== undefined
|
|
142
|
+
? canonicalAuthorizationServerIssuer(tokens.issuer)
|
|
143
|
+
: this.#freshAuthorization && discovery !== undefined
|
|
144
|
+
? validatedAuthorizationServerIssuer(discovery, this.serverName)
|
|
145
|
+
: storedGrantIssuer(this.#presentedGrant);
|
|
181
146
|
// A flow's staged registration replaces the signed-in client in the same write as its tokens (senpi#6327).
|
|
182
|
-
const
|
|
183
|
-
|
|
147
|
+
const response = {
|
|
148
|
+
tokens: { ...tokens, issuer },
|
|
149
|
+
resource: this.#resource ?? new URL(this.serverUrl).href,
|
|
150
|
+
fresh: this.#freshAuthorization,
|
|
151
|
+
presented: this.#presentedGrant,
|
|
152
|
+
clientInfo: this.#selectedClient ?? this.#presentedGrant?.clientInfo,
|
|
153
|
+
};
|
|
154
|
+
this.#presentedGrant = await this.#store.update((current) => mergeOAuthTokenResponse(current ?? {}, this.grantContext, response));
|
|
184
155
|
this.#logFingerprint("saveTokens", tokens.access_token);
|
|
185
156
|
}
|
|
186
157
|
async saveCodeVerifier(codeVerifier) {
|
|
@@ -201,7 +172,9 @@ export class McpOAuthProvider {
|
|
|
201
172
|
}
|
|
202
173
|
// RFC 8707: bind the token to this MCP resource.
|
|
203
174
|
validateResourceURL(serverUrl, resource) {
|
|
204
|
-
|
|
175
|
+
const selected = new URL(resource ?? String(serverUrl));
|
|
176
|
+
this.#resource = selected.href;
|
|
177
|
+
return Promise.resolve(selected);
|
|
205
178
|
}
|
|
206
179
|
async invalidateCredentials(scope) {
|
|
207
180
|
if (scope === "all") {
|
|
@@ -217,10 +190,17 @@ export class McpOAuthProvider {
|
|
|
217
190
|
delete next.refreshToken;
|
|
218
191
|
delete next.expiresAt;
|
|
219
192
|
}
|
|
193
|
+
if (scope === "tokens" || scope === "client") {
|
|
194
|
+
return {
|
|
195
|
+
...next,
|
|
196
|
+
scope: undefined,
|
|
197
|
+
refreshScope: undefined,
|
|
198
|
+
authorization: undefined,
|
|
199
|
+
...(scope === "client" ? { clientInfo: undefined } : {}),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
220
202
|
if (scope === "verifier")
|
|
221
203
|
next.codeVerifier = undefined;
|
|
222
|
-
if (scope === "client")
|
|
223
|
-
next.clientInfo = undefined;
|
|
224
204
|
if (scope === "discovery")
|
|
225
205
|
next.discoveryState = undefined;
|
|
226
206
|
return next;
|
|
@@ -238,6 +218,15 @@ export class McpOAuthProvider {
|
|
|
238
218
|
get scopes() {
|
|
239
219
|
return this.#options.scopes;
|
|
240
220
|
}
|
|
221
|
+
get grantContext() {
|
|
222
|
+
return {
|
|
223
|
+
serverName: this.serverName,
|
|
224
|
+
serverUrl: this.serverUrl,
|
|
225
|
+
agentDir: this.store.agentDir,
|
|
226
|
+
clientId: this.#options.clientId,
|
|
227
|
+
clientMetadataUrl: this.#options.clientMetadataUrl,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
241
230
|
get #store() {
|
|
242
231
|
return this.#options.store;
|
|
243
232
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { OAuthServerInfo } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
2
2
|
import type { AuthorizationServerMetadata, OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
|
|
3
3
|
import type { FetchLike } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
4
|
-
import {
|
|
4
|
+
import type { McpOAuthProvider } from "./oauth-provider.ts";
|
|
5
5
|
import type { McpStoredAuth } from "./token-store.ts";
|
|
6
6
|
export interface RefreshManagerOptions {
|
|
7
7
|
fetchFn?: FetchLike;
|
|
@@ -2,8 +2,9 @@ import { loadMcpSdkAuth } from "../sdk.lazy.js";
|
|
|
2
2
|
import { safeDelay } from "../wrap.js";
|
|
3
3
|
import { isInvalidGrant, isTransientTokenError, OAuthFlowError } from "./oauth-errors.js";
|
|
4
4
|
import { oauthFetch } from "./oauth-fetch.js";
|
|
5
|
-
import { credentialsMatchIssuer, validatedAuthorizationServerIssuer } from "./oauth-issuer.js";
|
|
6
|
-
import { mergeTokensIntoStoredAuth,
|
|
5
|
+
import { canonicalAuthorizationServerIssuer, credentialsMatchIssuer, validatedAuthorizationServerIssuer, } from "./oauth-issuer.js";
|
|
6
|
+
import { mergeTokensIntoStoredAuth, storedAuthToTokens, storedGrantIssuer, validGrantProof, withGrantAuthorization, } from "./oauth-grant.js";
|
|
7
|
+
const REFRESH_LEEWAY_MS = 5 * 60 * 1000;
|
|
7
8
|
export function isTokenStale(record, now) {
|
|
8
9
|
const token = record?.accessToken;
|
|
9
10
|
if (token === undefined || token.length === 0)
|
|
@@ -82,6 +83,7 @@ export class McpRefreshManager {
|
|
|
82
83
|
});
|
|
83
84
|
}
|
|
84
85
|
const resource = new URL(current.resource ?? this.#provider.serverUrl);
|
|
86
|
+
const proof = validGrantProof(current, this.#provider.grantContext);
|
|
85
87
|
const { refreshAuthorization } = await loadMcpSdkAuth();
|
|
86
88
|
const maxRetries = this.#options.maxRetries ?? 2;
|
|
87
89
|
for (let attempt = 0;; attempt++) {
|
|
@@ -93,10 +95,29 @@ export class McpRefreshManager {
|
|
|
93
95
|
resource,
|
|
94
96
|
fetchFn: oauthFetch(this.#options.fetchFn),
|
|
95
97
|
});
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
if (tokens.issuer !== undefined &&
|
|
99
|
+
canonicalAuthorizationServerIssuer(tokens.issuer) !==
|
|
100
|
+
canonicalAuthorizationServerIssuer(authorizationServer)) {
|
|
101
|
+
this.#provider.store.writeUnlocked(undefined);
|
|
102
|
+
throw new OAuthFlowError("needs_auth", "MCP refresh response names a different issuer.", {
|
|
103
|
+
serverName: this.#provider.serverName,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const next = mergeTokensIntoStoredAuth({ ...current, issuer: authorizationServer, resource: resource.href, refreshScope: proof?.refreshScope }, {
|
|
107
|
+
...tokens,
|
|
108
|
+
issuer: authorizationServer,
|
|
109
|
+
scope: tokens.scope === undefined ? proof?.refreshScope : tokens.scope,
|
|
110
|
+
});
|
|
111
|
+
const committed = withGrantAuthorization(next, this.#provider.grantContext, current);
|
|
112
|
+
this.#provider.store.writeUnlocked(committed);
|
|
113
|
+
const result = storedAuthToTokens(committed);
|
|
114
|
+
if (result === undefined)
|
|
115
|
+
throw new OAuthFlowError("needs_auth", "MCP refresh returned no access token.");
|
|
116
|
+
return result;
|
|
98
117
|
}
|
|
99
118
|
catch (error) {
|
|
119
|
+
if (error instanceof OAuthFlowError)
|
|
120
|
+
throw error;
|
|
100
121
|
if (isInvalidGrant(error)) {
|
|
101
122
|
// Terminal: drop credentials so the next use forces a clean re-auth.
|
|
102
123
|
this.#provider.store.writeUnlocked(undefined);
|
|
@@ -34,11 +34,11 @@ export async function finishAuthorization(provider, code, options = {}, iss) {
|
|
|
34
34
|
let result;
|
|
35
35
|
const { auth } = await loadMcpSdkAuth();
|
|
36
36
|
try {
|
|
37
|
-
result = await auth(provider, {
|
|
37
|
+
result = await provider.withFreshAuthorization(() => auth(provider, {
|
|
38
38
|
serverUrl: provider.serverUrl,
|
|
39
39
|
authorizationCode: code,
|
|
40
40
|
fetchFn: oauthFetch(options.fetchFn),
|
|
41
|
-
});
|
|
41
|
+
}));
|
|
42
42
|
}
|
|
43
43
|
catch (error) {
|
|
44
44
|
if (isInvalidGrant(error) || isRejectedAuthorizationCode(error)) {
|
|
@@ -111,12 +111,15 @@ export async function clientCredentialsGrant(provider, options = {}) {
|
|
|
111
111
|
},
|
|
112
112
|
};
|
|
113
113
|
const { fetchToken } = await loadMcpSdkAuth();
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
114
|
+
await provider.validateResourceURL(provider.serverUrl, provider.serverUrl);
|
|
115
|
+
await provider.withFreshAuthorization(async () => {
|
|
116
|
+
const tokens = await fetchToken(credentialsProvider, info.authorizationServerUrl, {
|
|
117
|
+
metadata: info.authorizationServerMetadata,
|
|
118
|
+
resource: new URL(provider.serverUrl),
|
|
119
|
+
fetchFn: oauthFetch(options.fetchFn),
|
|
120
|
+
});
|
|
121
|
+
await provider.saveTokens({ ...tokens, issuer: validatedAuthorizationServerIssuer(info, provider.serverName) });
|
|
118
122
|
});
|
|
119
|
-
await provider.saveTokens({ ...tokens, issuer: validatedAuthorizationServerIssuer(info, provider.serverName) });
|
|
120
123
|
}
|
|
121
124
|
export async function logout(provider) {
|
|
122
125
|
await provider.store.clear();
|
|
@@ -3,7 +3,24 @@ import type { OAuthClientInformationFull } from "@modelcontextprotocol/sdk/share
|
|
|
3
3
|
export type McpClientRegistration = OAuthClientInformationFull & {
|
|
4
4
|
redirectUrisSent?: string[];
|
|
5
5
|
};
|
|
6
|
+
export interface McpAuthorizationProof {
|
|
7
|
+
readonly credentialIdentity: string;
|
|
8
|
+
readonly previousCredentialIdentity?: string;
|
|
9
|
+
readonly issuer: string;
|
|
10
|
+
readonly resource: string;
|
|
11
|
+
readonly clientIdentity: string;
|
|
12
|
+
readonly scope: string;
|
|
13
|
+
readonly refreshScope?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface McpAuthorization {
|
|
16
|
+
readonly version: 1;
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly proof?: McpAuthorizationProof;
|
|
19
|
+
}
|
|
6
20
|
export interface McpStoredAuth {
|
|
21
|
+
readonly scope?: string;
|
|
22
|
+
readonly refreshScope?: string;
|
|
23
|
+
readonly authorization?: McpAuthorization;
|
|
7
24
|
accessToken?: string;
|
|
8
25
|
refreshToken?: string;
|
|
9
26
|
clientInfo?: McpClientRegistration;
|
|
@@ -37,6 +54,7 @@ export declare class McpTokenStore<TRecord extends McpStoredAuth = McpStoredAuth
|
|
|
37
54
|
readonly serverUrl: string;
|
|
38
55
|
constructor(options: TokenStoreOptions);
|
|
39
56
|
get rootDir(): string;
|
|
57
|
+
get agentDir(): string;
|
|
40
58
|
get dir(): string;
|
|
41
59
|
get tokensPath(): string;
|
|
42
60
|
get lockPath(): string;
|
|
@@ -39,24 +39,25 @@ export function cachedCatalogNeedsRefresh(cached, now = Date.now()) {
|
|
|
39
39
|
return now - cached.fetchedAt > CACHE_TTL_MS;
|
|
40
40
|
}
|
|
41
41
|
export async function collectServerCatalogForCache(connection, config, configHash, credentialIdentity) {
|
|
42
|
-
const
|
|
42
|
+
const client = connection.client;
|
|
43
|
+
const tools = await collectAllPages((cursor) => listMcpTools(client, cursor === undefined ? {} : { cursor }, {
|
|
43
44
|
timeout: config.requestTimeoutMs,
|
|
44
45
|
}));
|
|
45
|
-
const resources = await collectOptionalPages((cursor) =>
|
|
46
|
+
const resources = await collectOptionalPages((cursor) => client.listResources(cursor === undefined ? {} : { cursor }, {
|
|
46
47
|
timeout: config.requestTimeoutMs,
|
|
47
48
|
}));
|
|
48
|
-
const prompts = await collectOptionalPages((cursor) =>
|
|
49
|
+
const prompts = await collectOptionalPages((cursor) => client.listPrompts(cursor === undefined ? {} : { cursor }, {
|
|
49
50
|
timeout: config.requestTimeoutMs,
|
|
50
51
|
}));
|
|
51
|
-
const resourceTemplates = await collectOptionalPages((cursor) =>
|
|
52
|
+
const resourceTemplates = await collectOptionalPages((cursor) => client.listResourceTemplates(cursor === undefined ? {} : { cursor }, {
|
|
52
53
|
timeout: config.requestTimeoutMs,
|
|
53
54
|
}));
|
|
54
55
|
return {
|
|
55
56
|
configHash,
|
|
56
|
-
...(isModernMcpClient(
|
|
57
|
+
...(isModernMcpClient(client) ? { modern: true } : {}),
|
|
57
58
|
credentialIdentity,
|
|
58
59
|
fetchedAt: Date.now(),
|
|
59
|
-
instructions:
|
|
60
|
+
instructions: client.getInstructions(),
|
|
60
61
|
prompts,
|
|
61
62
|
resources,
|
|
62
63
|
resourceTemplates,
|