@corbits/mcp 0.3.0 → 0.4.0
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/README.md +120 -26
- package/dist/auth/callback.d.ts +14 -0
- package/dist/auth/callback.js +79 -0
- package/dist/auth/challenge.d.ts +9 -0
- package/dist/auth/challenge.js +52 -0
- package/dist/auth/contracts.d.ts +18 -0
- package/dist/auth/contracts.js +19 -0
- package/dist/auth/http.d.ts +2 -0
- package/dist/auth/http.js +2 -0
- package/dist/auth/index.d.ts +6 -0
- package/dist/auth/index.js +6 -0
- package/dist/auth/login.d.ts +62 -0
- package/dist/auth/login.js +175 -0
- package/dist/auth/oauth-http.d.ts +6 -0
- package/dist/auth/oauth-http.js +32 -0
- package/dist/auth/resource-server.d.ts +25 -0
- package/dist/auth/resource-server.js +94 -0
- package/dist/auth/url-policy.d.ts +10 -0
- package/dist/auth/url-policy.js +35 -0
- package/dist/client/resource-budget.d.ts +1 -0
- package/dist/client/resource-budget.js +1 -0
- package/dist/client/transport.d.ts +10 -0
- package/dist/client/transport.js +1 -0
- package/dist/client.d.ts +208 -26
- package/dist/client.js +1128 -90
- package/dist/extensions/apps.d.ts +58 -0
- package/dist/extensions/apps.js +55 -0
- package/dist/extensions/elicitation.d.ts +18 -0
- package/dist/extensions/elicitation.js +55 -0
- package/dist/extensions/index.d.ts +5 -0
- package/dist/extensions/index.js +5 -0
- package/dist/extensions/maturity.d.ts +44 -0
- package/dist/extensions/maturity.js +24 -0
- package/dist/extensions/skills/contracts.d.ts +53 -0
- package/dist/extensions/skills/contracts.js +1 -0
- package/dist/extensions/skills/index.d.ts +52 -0
- package/dist/extensions/skills/index.js +402 -0
- package/dist/extensions/tasks/contracts.d.ts +39 -0
- package/dist/extensions/tasks/contracts.js +1 -0
- package/dist/extensions/tasks/file-store.d.ts +12 -0
- package/dist/extensions/tasks/file-store.js +152 -0
- package/dist/extensions/tasks/index.d.ts +82 -0
- package/dist/extensions/tasks/index.js +221 -0
- package/dist/extensions/tasks/record.d.ts +3 -0
- package/dist/extensions/tasks/record.js +37 -0
- package/dist/hub/discover.d.ts +5 -1
- package/dist/hub/discover.js +36 -13
- package/dist/index.d.ts +3 -2
- package/dist/index.js +4 -2
- package/dist/interchange/client/capabilities.d.ts +4 -0
- package/dist/interchange/client/capabilities.js +53 -0
- package/dist/interchange/client/catalog.d.ts +14 -0
- package/dist/interchange/client/catalog.js +52 -0
- package/dist/interchange/client/contracts.d.ts +47 -0
- package/dist/interchange/client/contracts.js +1 -0
- package/dist/interchange/client/projection.d.ts +6 -0
- package/dist/interchange/client/projection.js +92 -0
- package/dist/interchange/client/skills.d.ts +16 -0
- package/dist/interchange/client/skills.js +87 -0
- package/dist/interchange/client.d.ts +8 -0
- package/dist/interchange/client.js +6 -0
- package/dist/interchange/server/contracts.d.ts +65 -0
- package/dist/interchange/server/contracts.js +1 -0
- package/dist/interchange/server.d.ts +5 -0
- package/dist/interchange/server.js +172 -0
- package/dist/naming.d.ts +2 -0
- package/dist/naming.js +7 -0
- package/dist/oauth-discovery.d.ts +107 -1
- package/dist/oauth-discovery.js +292 -56
- package/dist/protocol/content.d.ts +35 -0
- package/dist/protocol/content.js +64 -0
- package/dist/protocol/envelopes.d.ts +14 -0
- package/dist/protocol/envelopes.js +22 -0
- package/dist/protocol/errors.d.ts +26 -0
- package/dist/protocol/errors.js +31 -0
- package/dist/protocol/extensions.d.ts +34 -0
- package/dist/protocol/extensions.js +86 -0
- package/dist/protocol/headers.d.ts +12 -0
- package/dist/protocol/headers.js +128 -0
- package/dist/protocol/index.d.ts +11 -0
- package/dist/protocol/index.js +11 -0
- package/dist/protocol/json-schema.d.ts +8 -0
- package/dist/protocol/json-schema.js +152 -0
- package/dist/protocol/legacy.d.ts +12 -0
- package/dist/protocol/legacy.js +18 -0
- package/dist/protocol/methods.d.ts +116 -0
- package/dist/protocol/methods.js +78 -0
- package/dist/protocol/resource-budget.d.ts +9 -0
- package/dist/protocol/resource-budget.js +206 -0
- package/dist/protocol/schemas.d.ts +115 -0
- package/dist/protocol/schemas.js +553 -0
- package/dist/protocol/types.d.ts +295 -0
- package/dist/protocol/types.js +3 -0
- package/dist/protocol/ui.d.ts +3 -0
- package/dist/protocol/ui.js +18 -0
- package/dist/server/contracts.d.ts +67 -0
- package/dist/server/contracts.js +1 -0
- package/dist/server/errors.d.ts +6 -0
- package/dist/server/errors.js +21 -0
- package/dist/server/extensions.d.ts +7 -0
- package/dist/server/extensions.js +61 -0
- package/dist/server/fixtures.d.ts +2 -0
- package/dist/server/fixtures.js +34 -0
- package/dist/server/legacy.d.ts +9 -0
- package/dist/server/legacy.js +435 -0
- package/dist/server/tools.d.ts +17 -0
- package/dist/server/tools.js +93 -0
- package/dist/server.d.ts +14 -0
- package/dist/server.js +406 -0
- package/dist/sidecar-bundle.d.ts +7 -7
- package/dist/sidecar-bundle.js +36 -29
- package/dist/test-server.js +12 -3
- package/dist/tool.d.ts +8 -0
- package/dist/tool.js +35 -19
- package/dist/url.js +8 -3
- package/docs/conformance.md +135 -0
- package/docs/integration-status.md +98 -0
- package/docs/legacy-server.md +58 -0
- package/docs/migration.md +143 -0
- package/docs/public-api.md +106 -0
- package/examples/README.md +28 -0
- package/examples/standalone-client.ts +14 -0
- package/examples/standalone-server.ts +37 -0
- package/package.json +50 -15
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { buildAuthorizeUrl, exchangeCode, generatePkce, generateState, refreshTokenRequest, OAuthDiscoveryError, } from "@corbits/oauth-core";
|
|
2
|
+
import { type } from "arktype";
|
|
3
|
+
import { assertAuthorizationCallbackIssuer, assertCrossOriginAuthorizationIssuer, mcpClientConfig, preferredMcpClientRegistration, readAuthorizationCallback, registerMcpClient, resolveStoredMcpClient, } from "../oauth-discovery.js";
|
|
4
|
+
import { secureOAuthFetch } from "./http.js";
|
|
5
|
+
const Token = type({
|
|
6
|
+
access_token: "string > 0",
|
|
7
|
+
token_type: "string",
|
|
8
|
+
"refresh_token?": "string > 0",
|
|
9
|
+
"expires_in?": "number >= 0",
|
|
10
|
+
"scope?": "string",
|
|
11
|
+
});
|
|
12
|
+
function tokensFromResponse(raw, now, previous) {
|
|
13
|
+
const response = Token(raw);
|
|
14
|
+
if (response instanceof type.errors ||
|
|
15
|
+
response.token_type.toLowerCase() !== "bearer")
|
|
16
|
+
throw new OAuthDiscoveryError("Invalid bearer token response");
|
|
17
|
+
const refresh = response.refresh_token ?? previous?.refresh;
|
|
18
|
+
return {
|
|
19
|
+
access: response.access_token,
|
|
20
|
+
...(refresh !== undefined ? { refresh } : {}),
|
|
21
|
+
...(response.expires_in !== undefined
|
|
22
|
+
? { expiresAt: now + response.expires_in * 1000 }
|
|
23
|
+
: {}),
|
|
24
|
+
...(response.scope !== undefined
|
|
25
|
+
? { scope: response.scope }
|
|
26
|
+
: previous?.scope !== undefined
|
|
27
|
+
? { scope: previous.scope }
|
|
28
|
+
: {}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Performs real DCR only when neither issuer-bound pre-registration nor CIMD applies. */
|
|
32
|
+
export async function prepareMcpClient(opts) {
|
|
33
|
+
const issuer = opts.entry.authorizationServer.issuer;
|
|
34
|
+
if (opts.storedClient !== undefined) {
|
|
35
|
+
resolveStoredMcpClient(issuer, opts.storedClient);
|
|
36
|
+
return { ...opts.storedClient };
|
|
37
|
+
}
|
|
38
|
+
const plan = preferredMcpClientRegistration({
|
|
39
|
+
authorizationServer: opts.entry.authorizationServer,
|
|
40
|
+
...(opts.clientMetadataUrl !== undefined
|
|
41
|
+
? { clientMetadataUrl: opts.clientMetadataUrl }
|
|
42
|
+
: {}),
|
|
43
|
+
});
|
|
44
|
+
if (plan.mechanism === "client-id-metadata")
|
|
45
|
+
return { kind: "cimd", clientId: plan.clientId };
|
|
46
|
+
if (plan.mechanism === "pre-registered")
|
|
47
|
+
return { kind: "pre-registered", clientId: plan.clientId, issuer };
|
|
48
|
+
const registered = await registerMcpClient({
|
|
49
|
+
registrationEndpoint: plan.registrationEndpoint,
|
|
50
|
+
issuer,
|
|
51
|
+
redirectUris: [opts.redirectUri],
|
|
52
|
+
clientName: opts.clientName,
|
|
53
|
+
...(opts.applicationType !== undefined
|
|
54
|
+
? { applicationType: opts.applicationType }
|
|
55
|
+
: {}),
|
|
56
|
+
...(opts.entry.authorizationServer.grantTypesSupported !== undefined
|
|
57
|
+
? {
|
|
58
|
+
grantTypesSupported: opts.entry.authorizationServer.grantTypesSupported,
|
|
59
|
+
}
|
|
60
|
+
: {}),
|
|
61
|
+
...(opts.fetchImpl !== undefined ? { fetchImpl: opts.fetchImpl } : {}),
|
|
62
|
+
});
|
|
63
|
+
return { kind: "dynamic", clientId: registered.clientId, issuer };
|
|
64
|
+
}
|
|
65
|
+
/** A one-use callback session. The host mounts its registered callback route,
|
|
66
|
+
* passes the full URLSearchParams here, then persists secrets server-side.
|
|
67
|
+
* Unlike oauth-core BaseTokens this contract permits access-token-only grants.
|
|
68
|
+
* No automatic tool replay, persistence, browser opening or grant widening. */
|
|
69
|
+
export function createMcpAuthorizationSession(opts) {
|
|
70
|
+
if (!opts.tenantId || !opts.principalId)
|
|
71
|
+
throw new OAuthDiscoveryError("Login requires host-bound tenant and principal");
|
|
72
|
+
const client = resolveStoredMcpClient(opts.entry.authorizationServer.issuer, opts.client);
|
|
73
|
+
const config = mcpClientConfig(opts.entry, {
|
|
74
|
+
...opts.config,
|
|
75
|
+
clientId: client.clientId,
|
|
76
|
+
});
|
|
77
|
+
const pkce = generatePkce();
|
|
78
|
+
const state = generateState();
|
|
79
|
+
const now = opts.now ?? Date.now;
|
|
80
|
+
const expiresAt = now() + (opts.expiresInMs ?? 300_000);
|
|
81
|
+
const binding = Object.freeze({
|
|
82
|
+
tenantId: opts.tenantId,
|
|
83
|
+
principalId: opts.principalId,
|
|
84
|
+
issuer: opts.entry.authorizationServer.issuer,
|
|
85
|
+
resource: opts.entry.resourceUrl,
|
|
86
|
+
clientId: client.clientId,
|
|
87
|
+
});
|
|
88
|
+
let consumed = false;
|
|
89
|
+
let locked = false;
|
|
90
|
+
let exchanged;
|
|
91
|
+
const authorizeUrl = new URL(buildAuthorizeUrl(config, pkce, state));
|
|
92
|
+
if (config.scopes.length === 0)
|
|
93
|
+
authorizeUrl.searchParams.delete("scope");
|
|
94
|
+
return {
|
|
95
|
+
authorizeUrl: authorizeUrl.toString(),
|
|
96
|
+
binding,
|
|
97
|
+
cancel() {
|
|
98
|
+
consumed = true;
|
|
99
|
+
},
|
|
100
|
+
async complete(params) {
|
|
101
|
+
if (consumed || locked || now() >= expiresAt)
|
|
102
|
+
throw new OAuthDiscoveryError("Authorization session expired or already consumed");
|
|
103
|
+
if (params.getAll("state").length !== 1 || params.get("state") !== state)
|
|
104
|
+
throw new OAuthDiscoveryError("Authorization state mismatch");
|
|
105
|
+
const issuerOptions = {
|
|
106
|
+
recordedIssuer: binding.issuer,
|
|
107
|
+
...(opts.entry.authorizationServer
|
|
108
|
+
.authorizationResponseIssParameterSupported !== undefined
|
|
109
|
+
? {
|
|
110
|
+
authorizationResponseIssParameterSupported: opts.entry.authorizationServer
|
|
111
|
+
.authorizationResponseIssParameterSupported,
|
|
112
|
+
}
|
|
113
|
+
: {}),
|
|
114
|
+
};
|
|
115
|
+
assertAuthorizationCallbackIssuer(params, issuerOptions);
|
|
116
|
+
assertCrossOriginAuthorizationIssuer(params, opts.entry.authorizationServer);
|
|
117
|
+
// An authentic terminal denial consumes the one-use session. A code does
|
|
118
|
+
// not: cancellation during exchange must skip persistence, and a persist
|
|
119
|
+
// failure must leave the exchanged grant retryable.
|
|
120
|
+
locked = true;
|
|
121
|
+
try {
|
|
122
|
+
if (exchanged === undefined) {
|
|
123
|
+
let code;
|
|
124
|
+
try {
|
|
125
|
+
({ code } = readAuthorizationCallback(params, issuerOptions));
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
consumed = true;
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
let tokens;
|
|
132
|
+
try {
|
|
133
|
+
tokens = tokensFromResponse(await exchangeCode(config, code, pkce.verifier, secureOAuthFetch(opts.fetchImpl)), now());
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
throw new OAuthDiscoveryError("OAuth code exchange failed");
|
|
137
|
+
}
|
|
138
|
+
if (consumed || now() >= expiresAt)
|
|
139
|
+
throw new OAuthDiscoveryError("Authorization session expired or already consumed");
|
|
140
|
+
exchanged = tokens;
|
|
141
|
+
}
|
|
142
|
+
else if (consumed || now() >= expiresAt)
|
|
143
|
+
throw new OAuthDiscoveryError("Authorization session expired or already consumed");
|
|
144
|
+
await opts.persist(binding, exchanged);
|
|
145
|
+
consumed = true;
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
if (!consumed)
|
|
149
|
+
locked = false;
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/** Refresh under the same recorded issuer/resource/client binding. */
|
|
155
|
+
export async function refreshMcpOAuthTokens(opts) {
|
|
156
|
+
const { clientId } = resolveStoredMcpClient(opts.entry.authorizationServer.issuer, opts.client);
|
|
157
|
+
if (opts.binding.issuer !== opts.entry.authorizationServer.issuer ||
|
|
158
|
+
opts.binding.resource !== opts.entry.resourceUrl ||
|
|
159
|
+
opts.binding.clientId !== clientId)
|
|
160
|
+
throw new OAuthDiscoveryError("OAuth refresh binding mismatch");
|
|
161
|
+
if (opts.tokens.refresh === undefined)
|
|
162
|
+
throw new OAuthDiscoveryError("Access-token-only grant requires a new authorization");
|
|
163
|
+
try {
|
|
164
|
+
const response = await refreshTokenRequest({
|
|
165
|
+
clientId,
|
|
166
|
+
tokenUrl: opts.entry.authorizationServer.tokenEndpoint,
|
|
167
|
+
resource: opts.binding.resource,
|
|
168
|
+
tokenTimeoutMs: 30_000,
|
|
169
|
+
}, opts.tokens.refresh, secureOAuthFetch(opts.fetchImpl));
|
|
170
|
+
return tokensFromResponse(response, (opts.now ?? Date.now)(), opts.tokens);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
throw new OAuthDiscoveryError("OAuth refresh failed");
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { FetchLike } from "../protocol/types.js";
|
|
2
|
+
/** Compatibility facade retaining oauth-core error identity for OAuth callers. */
|
|
3
|
+
export declare function assertOAuthUrl(raw: string, issuer?: boolean): URL;
|
|
4
|
+
export declare function assertRedirectUri(raw: string): URL;
|
|
5
|
+
/** Use for every oauth-core token call: its default fetch follows redirects. */
|
|
6
|
+
export declare function secureOAuthFetch(inner?: FetchLike): typeof fetch;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { OAuthDiscoveryError } from "@corbits/oauth-core";
|
|
2
|
+
import { assertAuthServerUrl, assertAuthRedirectUri, McpAuthUrlError, } from "./url-policy.js";
|
|
3
|
+
function withOAuthError(validate) {
|
|
4
|
+
try {
|
|
5
|
+
return validate();
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
if (error instanceof McpAuthUrlError)
|
|
9
|
+
throw new OAuthDiscoveryError(error.detail);
|
|
10
|
+
throw error;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Compatibility facade retaining oauth-core error identity for OAuth callers. */
|
|
14
|
+
export function assertOAuthUrl(raw, issuer = false) {
|
|
15
|
+
return withOAuthError(() => assertAuthServerUrl(raw, issuer));
|
|
16
|
+
}
|
|
17
|
+
export function assertRedirectUri(raw) {
|
|
18
|
+
return withOAuthError(() => assertAuthRedirectUri(raw));
|
|
19
|
+
}
|
|
20
|
+
/** Use for every oauth-core token call: its default fetch follows redirects. */
|
|
21
|
+
export function secureOAuthFetch(inner = globalThis.fetch) {
|
|
22
|
+
const wrapped = async (input, init) => {
|
|
23
|
+
assertOAuthUrl(input instanceof Request ? input.url : String(input));
|
|
24
|
+
const response = await inner(input, { ...init, redirect: "error" });
|
|
25
|
+
if ((response.status >= 300 && response.status < 400) ||
|
|
26
|
+
response.redirected) {
|
|
27
|
+
throw new OAuthDiscoveryError("OAuth endpoint redirect refused");
|
|
28
|
+
}
|
|
29
|
+
return response;
|
|
30
|
+
};
|
|
31
|
+
return Object.assign(wrapped, { preconnect: globalThis.fetch.preconnect });
|
|
32
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type McpAuthOperation, type McpAuthPolicy, type McpIdentity } from "./contracts.js";
|
|
2
|
+
export type McpResourceServerOptions = {
|
|
3
|
+
resource: string;
|
|
4
|
+
resourceMetadataUrl: string;
|
|
5
|
+
authorizationServers: readonly string[];
|
|
6
|
+
scopesSupported?: readonly string[];
|
|
7
|
+
/** Must verify signature/algorithm/key trust or introspection, revocation and
|
|
8
|
+
* token type. Decoding a JWT is NOT verification. Returns trusted claims,
|
|
9
|
+
* expiresAt/notBefore in epoch milliseconds, and a host-assigned identity.
|
|
10
|
+
* Never map identity from unverified request params. */
|
|
11
|
+
verifyAccessToken(token: string, context: {
|
|
12
|
+
resource: string;
|
|
13
|
+
issuers: readonly string[];
|
|
14
|
+
signal: AbortSignal;
|
|
15
|
+
}): Promise<unknown>;
|
|
16
|
+
authorize(identity: McpIdentity, operation: McpAuthOperation): boolean | Promise<boolean>;
|
|
17
|
+
requiredScopes?(operation: McpAuthOperation): readonly string[];
|
|
18
|
+
now?: () => number;
|
|
19
|
+
};
|
|
20
|
+
/** No token implementation or public-mode bypass is implied by this seam. */
|
|
21
|
+
export declare function createMcpResourceServerAuth(opts: McpResourceServerOptions): {
|
|
22
|
+
policy: McpAuthPolicy;
|
|
23
|
+
metadataResponse(): Response;
|
|
24
|
+
challenge(status: 401 | 403, scopes?: readonly string[]): Response;
|
|
25
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
import { assertAuthServerUrl } from "./url-policy.js";
|
|
3
|
+
import { validateMcpIdentity, } from "./contracts.js";
|
|
4
|
+
const VerifiedToken = type({
|
|
5
|
+
issuer: "string > 0",
|
|
6
|
+
audience: "string[]",
|
|
7
|
+
expiresAt: "number",
|
|
8
|
+
"notBefore?": "number",
|
|
9
|
+
identity: "unknown",
|
|
10
|
+
});
|
|
11
|
+
function quote(value) {
|
|
12
|
+
if (/[^\x20-\x7e]/.test(value))
|
|
13
|
+
throw new Error("Invalid authentication challenge value");
|
|
14
|
+
return `"${value.replace(/["\\]/g, "\\$&")}"`;
|
|
15
|
+
}
|
|
16
|
+
/** No token implementation or public-mode bypass is implied by this seam. */
|
|
17
|
+
export function createMcpResourceServerAuth(opts) {
|
|
18
|
+
assertAuthServerUrl(opts.resource);
|
|
19
|
+
assertAuthServerUrl(opts.resourceMetadataUrl);
|
|
20
|
+
if (opts.authorizationServers.length === 0)
|
|
21
|
+
throw new Error("At least one authorization server is required");
|
|
22
|
+
const issuers = Object.freeze([...opts.authorizationServers]);
|
|
23
|
+
for (const issuer of issuers)
|
|
24
|
+
assertAuthServerUrl(issuer, true);
|
|
25
|
+
const policy = {
|
|
26
|
+
async authenticate(request) {
|
|
27
|
+
// RFC 6750 bearer use is header-only: query tokens never authenticate.
|
|
28
|
+
if (new URL(request.url).searchParams.has("access_token"))
|
|
29
|
+
return null;
|
|
30
|
+
const match = /^Bearer +([A-Za-z0-9\-._~+/]+=*)$/i.exec(request.headers.get("authorization") ?? "");
|
|
31
|
+
if (!match?.[1])
|
|
32
|
+
return null;
|
|
33
|
+
try {
|
|
34
|
+
const claims = VerifiedToken(await opts.verifyAccessToken(match[1], {
|
|
35
|
+
resource: opts.resource,
|
|
36
|
+
issuers,
|
|
37
|
+
signal: request.signal,
|
|
38
|
+
}));
|
|
39
|
+
if (claims instanceof type.errors)
|
|
40
|
+
return null;
|
|
41
|
+
const now = (opts.now ?? Date.now)();
|
|
42
|
+
if (!issuers.includes(claims.issuer) ||
|
|
43
|
+
!claims.audience.includes(opts.resource) ||
|
|
44
|
+
!Number.isFinite(claims.expiresAt) ||
|
|
45
|
+
claims.expiresAt <= now ||
|
|
46
|
+
(claims.notBefore !== undefined &&
|
|
47
|
+
(!Number.isFinite(claims.notBefore) || claims.notBefore > now)))
|
|
48
|
+
return null;
|
|
49
|
+
return validateMcpIdentity(claims.identity);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
async authorize(identity, operation) {
|
|
56
|
+
try {
|
|
57
|
+
const validated = validateMcpIdentity(identity);
|
|
58
|
+
if (!operation.resource || !operation.action)
|
|
59
|
+
return false;
|
|
60
|
+
const scopes = opts.requiredScopes?.(operation) ?? [];
|
|
61
|
+
if (!scopes.every((scope) => validated.scopes.includes(scope)))
|
|
62
|
+
return false;
|
|
63
|
+
return (await opts.authorize(validated, operation)) === true;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
return {
|
|
71
|
+
policy,
|
|
72
|
+
metadataResponse: () => Response.json({
|
|
73
|
+
resource: opts.resource,
|
|
74
|
+
authorization_servers: issuers,
|
|
75
|
+
...(opts.scopesSupported !== undefined
|
|
76
|
+
? { scopes_supported: [...opts.scopesSupported] }
|
|
77
|
+
: {}),
|
|
78
|
+
}),
|
|
79
|
+
challenge(status, scopes) {
|
|
80
|
+
const fields = [`resource_metadata=${quote(opts.resourceMetadataUrl)}`];
|
|
81
|
+
if (status === 403)
|
|
82
|
+
fields.push('error="insufficient_scope"');
|
|
83
|
+
if (scopes !== undefined)
|
|
84
|
+
fields.push(`scope=${quote(scopes.join(" "))}`);
|
|
85
|
+
return new Response(null, {
|
|
86
|
+
status,
|
|
87
|
+
headers: {
|
|
88
|
+
"www-authenticate": `Bearer ${fields.join(", ")}`,
|
|
89
|
+
"cache-control": "no-store",
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Neutral URL-policy error. Keep the existing diagnostic name/message, without
|
|
2
|
+
* loading the optional OAuth implementation solely to construct an exception.
|
|
3
|
+
* OAuth-facing facades translate this to the actual oauth-core error class. */
|
|
4
|
+
export declare class McpAuthUrlError extends Error {
|
|
5
|
+
readonly detail: string;
|
|
6
|
+
constructor(detail: string);
|
|
7
|
+
}
|
|
8
|
+
/** OAuth endpoints have no cleartext exception, including internal hosts. */
|
|
9
|
+
export declare function assertAuthServerUrl(raw: string, issuer?: boolean): URL;
|
|
10
|
+
export declare function assertAuthRedirectUri(raw: string): URL;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { parseMcpEndpoint } from "../url.js";
|
|
2
|
+
/** Neutral URL-policy error. Keep the existing diagnostic name/message, without
|
|
3
|
+
* loading the optional OAuth implementation solely to construct an exception.
|
|
4
|
+
* OAuth-facing facades translate this to the actual oauth-core error class. */
|
|
5
|
+
export class McpAuthUrlError extends Error {
|
|
6
|
+
detail;
|
|
7
|
+
constructor(detail) {
|
|
8
|
+
super(`OAuth discovery failed: ${detail}`);
|
|
9
|
+
this.name = "OAuthDiscoveryError";
|
|
10
|
+
this.detail = detail;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** OAuth endpoints have no cleartext exception, including internal hosts. */
|
|
14
|
+
export function assertAuthServerUrl(raw, issuer = false) {
|
|
15
|
+
let url;
|
|
16
|
+
try {
|
|
17
|
+
url = parseMcpEndpoint(raw);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
throw new McpAuthUrlError("Invalid OAuth URL");
|
|
21
|
+
}
|
|
22
|
+
if (url.protocol !== "https:" ||
|
|
23
|
+
(issuer && (url.search !== "" || raw.includes("?")))) {
|
|
24
|
+
throw new McpAuthUrlError("OAuth URL must use HTTPS; issuer must not contain a query");
|
|
25
|
+
}
|
|
26
|
+
return url;
|
|
27
|
+
}
|
|
28
|
+
export function assertAuthRedirectUri(raw) {
|
|
29
|
+
try {
|
|
30
|
+
return parseMcpEndpoint(raw);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw new McpAuthUrlError("Invalid OAuth redirect URI: HTTPS or loopback HTTP required");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createResourceBudget, type ResourceBudget, } from "../protocol/resource-budget.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createResourceBudget, } from "../protocol/resource-budget.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { McpClientOptions, McpNotification } from "../client.js";
|
|
2
|
+
import type { McpSubscriptionFilter, McpValidationContext } from "../protocol/types.js";
|
|
3
|
+
export interface McpRequestOptions extends McpClientOptions {
|
|
4
|
+
validation?: McpValidationContext;
|
|
5
|
+
}
|
|
6
|
+
/** Extensions receive this interface rather than importing the client facade. */
|
|
7
|
+
export interface McpRequestTransport {
|
|
8
|
+
request(method: string, params?: Record<string, unknown>, options?: McpRequestOptions): Promise<Record<string, unknown>>;
|
|
9
|
+
listen(filter: McpSubscriptionFilter, observer: (notification: McpNotification) => void, options?: McpRequestOptions): Promise<void>;
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/client.d.ts
CHANGED
|
@@ -1,37 +1,116 @@
|
|
|
1
|
-
import type
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
import { MCP_PROTOCOL_VERSION, type FetchLike, type McpValidationContext, type TaskStatus, type McpSubscriptionFilter } from "./protocol/types.js";
|
|
2
|
+
import { McpError, McpErrorCode } from "./protocol/errors.js";
|
|
3
|
+
import { MCP_EXT_TASKS, MCP_EXT_UI, MCP_EXT_SKILLS } from "./protocol/extensions.js";
|
|
4
|
+
import { McpToolSchema, type McpTool, PromptSchema, ResourceSchema, TemplateSchema, CompletionResult, SkillSchema } from "./protocol/schemas.js";
|
|
5
|
+
import type { McpRequestTransport } from "./client/transport.js";
|
|
6
|
+
export { MCP_PROTOCOL_VERSION, McpError, McpErrorCode, MCP_EXT_TASKS, MCP_EXT_UI, MCP_EXT_SKILLS, McpToolSchema, };
|
|
7
|
+
export type { McpTool, McpSubscriptionFilter };
|
|
8
|
+
export type { McpRequestTransport, McpRequestOptions, } from "./client/transport.js";
|
|
9
|
+
export type McpCacheScope = "public" | "private";
|
|
10
|
+
export type McpToolList = McpTool[] & {
|
|
11
|
+
ttlMs?: number;
|
|
12
|
+
cacheScope?: McpCacheScope;
|
|
13
|
+
};
|
|
14
14
|
export interface McpToolResult {
|
|
15
|
+
/** `"complete"` on a resolved result; absent while the call awaits input
|
|
16
|
+
* or runs a task. Optional so resolved results keep the previous contract. */
|
|
17
|
+
resultType?: "complete";
|
|
15
18
|
content: unknown;
|
|
16
19
|
isError?: boolean;
|
|
20
|
+
structuredContent?: unknown;
|
|
21
|
+
/** Tool-result metadata, including MCP Apps `_meta.ui` when the server sent it. */
|
|
22
|
+
_meta?: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
export interface McpInputRequired {
|
|
25
|
+
resultType: "input_required";
|
|
26
|
+
/** The method to retry. */
|
|
27
|
+
method: string;
|
|
28
|
+
/** Original params, without `_meta`, to send again with `inputResponses`. */
|
|
29
|
+
params: Record<string, unknown>;
|
|
30
|
+
inputRequests?: Record<string, unknown>;
|
|
31
|
+
/** Opaque server state. Echoed verbatim on retry; never inspected. */
|
|
32
|
+
requestState?: string;
|
|
33
|
+
_meta?: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
export type McpTaskStatus = TaskStatus;
|
|
36
|
+
export interface McpTaskHandle {
|
|
37
|
+
resultType: "task";
|
|
38
|
+
taskId: string;
|
|
39
|
+
status: McpTaskStatus;
|
|
40
|
+
createdAt: string;
|
|
41
|
+
lastUpdatedAt: string;
|
|
42
|
+
ttlMs: number | null;
|
|
43
|
+
pollIntervalMs?: number;
|
|
44
|
+
statusMessage?: string;
|
|
45
|
+
_meta?: Record<string, unknown>;
|
|
46
|
+
}
|
|
47
|
+
export interface McpTask {
|
|
48
|
+
resultType: "complete";
|
|
49
|
+
taskId: string;
|
|
50
|
+
status: McpTaskStatus;
|
|
51
|
+
createdAt: string;
|
|
52
|
+
lastUpdatedAt: string;
|
|
53
|
+
ttlMs: number | null;
|
|
54
|
+
pollIntervalMs?: number;
|
|
55
|
+
statusMessage?: string;
|
|
56
|
+
inputRequests?: Record<string, unknown>;
|
|
57
|
+
result?: Record<string, unknown>;
|
|
58
|
+
error?: {
|
|
59
|
+
code: number;
|
|
60
|
+
message: string;
|
|
61
|
+
data?: unknown;
|
|
62
|
+
};
|
|
63
|
+
_meta?: Record<string, unknown>;
|
|
17
64
|
}
|
|
65
|
+
export type McpToolCallResult = McpToolResult | McpInputRequired | McpTaskHandle;
|
|
66
|
+
export interface McpProgress {
|
|
67
|
+
progressToken: string | number;
|
|
68
|
+
progress: number;
|
|
69
|
+
total?: number;
|
|
70
|
+
message?: string;
|
|
71
|
+
}
|
|
72
|
+
export type McpClientCapabilities = Record<string, unknown>;
|
|
18
73
|
export interface McpClientOptions {
|
|
19
74
|
/** Injectable `fetch` for tests and mediated-credential handles. */
|
|
20
75
|
fetch?: FetchLike;
|
|
21
|
-
/** Abort the request
|
|
76
|
+
/** Abort the request and body after this long (default 60s).
|
|
77
|
+
* subscriptions/listen has no default deadline; set this to bound its lifetime. */
|
|
22
78
|
timeoutMs?: number;
|
|
23
79
|
/** Abort the request when this fires, e.g. a cancelled tool call. */
|
|
24
80
|
signal?: AbortSignal;
|
|
25
|
-
/** The session `mcpInitialize` opened
|
|
81
|
+
/** The session `mcpConnect` or `mcpInitialize` opened. */
|
|
26
82
|
session?: McpSession;
|
|
83
|
+
/**
|
|
84
|
+
* Extension ids to advertise in `clientCapabilities.extensions`.
|
|
85
|
+
* Unlisted extensions are not advertised and their methods are not sent.
|
|
86
|
+
*/
|
|
87
|
+
extensions?: readonly string[];
|
|
88
|
+
/** MIME types advertised for `io.modelcontextprotocol/ui`. */
|
|
89
|
+
uiMimeTypes?: readonly string[];
|
|
90
|
+
/** Receives `notifications/progress` from this call's response stream. */
|
|
91
|
+
onProgress?: (progress: McpProgress) => void;
|
|
92
|
+
/** Progress token. Defaults to the request id when `onProgress` is set. */
|
|
93
|
+
progressToken?: string | number;
|
|
94
|
+
/** Tool `inputSchema`, so `x-mcp-header` parameters are mirrored into headers. */
|
|
95
|
+
inputSchema?: Record<string, unknown>;
|
|
96
|
+
/** Validate successful structured tool output when its catalog schema is known. */
|
|
97
|
+
outputSchema?: Record<string, unknown>;
|
|
98
|
+
/** resources/read only: bound aggregate decoded text/blob bytes while streaming.
|
|
99
|
+
* Capped at 16 MiB decoded, with bounded JSON/base64 encoding overhead and
|
|
100
|
+
* a separate 64 KiB envelope cap. Unbounded reads retain the 4 MiB default. */
|
|
101
|
+
maxDecodedBytes?: number;
|
|
102
|
+
/** Optional host capabilities; never inferred from installed code. */
|
|
103
|
+
clientCapabilities?: McpClientCapabilities;
|
|
104
|
+
/** Advertise only modes for which the caller has configured user-facing host services. */
|
|
105
|
+
elicitation?: {
|
|
106
|
+
form?: boolean;
|
|
107
|
+
url?: boolean;
|
|
108
|
+
};
|
|
109
|
+
validation?: McpValidationContext;
|
|
27
110
|
}
|
|
28
111
|
/** The run-time `tools/call` and `initialize` bound when a caller sets none. */
|
|
29
112
|
export declare const DEFAULT_TIMEOUT_MS = 60000;
|
|
30
|
-
export declare
|
|
31
|
-
/** The HTTP status, when the server answered with a non-2xx one. */
|
|
32
|
-
readonly status: number | undefined;
|
|
33
|
-
constructor(message: string, status?: number);
|
|
34
|
-
}
|
|
113
|
+
export declare function createMcpRequestTransport(url: string, defaults?: McpClientOptions): McpRequestTransport;
|
|
35
114
|
declare const InitializeResult: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
36
115
|
protocolVersion?: string;
|
|
37
116
|
serverInfo?: {
|
|
@@ -43,15 +122,118 @@ export type McpServerInfo = typeof InitializeResult.infer;
|
|
|
43
122
|
export type McpSession = {
|
|
44
123
|
/** The negotiated version, sent as `MCP-Protocol-Version`. */
|
|
45
124
|
readonly protocolVersion: string;
|
|
46
|
-
/** The `Mcp-Session-Id` a stateful server assigned, if any. */
|
|
125
|
+
/** The `Mcp-Session-Id` a stateful legacy server assigned, if any. */
|
|
47
126
|
readonly sessionId?: string;
|
|
48
127
|
/** The server's own identification, for a caller that shows it. */
|
|
49
128
|
readonly serverInfo?: NonNullable<McpServerInfo["serverInfo"]>;
|
|
129
|
+
/** Server capabilities from `server/discover`. Absent on the legacy path. */
|
|
130
|
+
readonly capabilities?: Record<string, unknown>;
|
|
131
|
+
/** Capabilities this client advertised for the modern session. */
|
|
132
|
+
readonly clientCapabilities?: McpClientCapabilities;
|
|
133
|
+
/** Extension ids opted in when the session was opened. */
|
|
134
|
+
readonly extensions?: readonly string[];
|
|
135
|
+
readonly uiMimeTypes?: readonly string[];
|
|
136
|
+
readonly instructions?: string;
|
|
50
137
|
};
|
|
51
138
|
/** `initialize` handshake followed by `notifications/initialized`. Pass the
|
|
52
|
-
* returned session to every later call on
|
|
139
|
+
* returned session to every later call on a legacy server. */
|
|
53
140
|
export declare function mcpInitialize(url: string, opts?: McpClientOptions): Promise<McpSession>;
|
|
54
|
-
/**
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
141
|
+
/**
|
|
142
|
+
* Probe `server/discover`. A 2026-07-28 server returns a stateless session:
|
|
143
|
+
* later calls send no `initialize`, no `notifications/initialized`, and no
|
|
144
|
+
* `Mcp-Session-Id`. A method-not-found, a timeout, or a server that only
|
|
145
|
+
* speaks a 2025 (or earlier) version falls back to `initialize`. HTTP 400
|
|
146
|
+
* also falls back unless its body contains a recognized modern error. Authz
|
|
147
|
+
* failures, rate limits, server errors, and malformed responses are real
|
|
148
|
+
* failures and surface to the caller instead of triggering the legacy
|
|
149
|
+
* handshake.
|
|
150
|
+
*/
|
|
151
|
+
export declare function mcpConnect(url: string, opts?: McpClientOptions): Promise<McpSession>;
|
|
152
|
+
type PageCache = {
|
|
153
|
+
ttlMs?: number;
|
|
154
|
+
cacheScope?: McpCacheScope;
|
|
155
|
+
};
|
|
156
|
+
/** `tools/list`: the remote server's tool catalog, following cursors. */
|
|
157
|
+
export declare function mcpListTools(url: string, opts?: McpClientOptions): Promise<McpToolList>;
|
|
158
|
+
/** `tools/call`: invoke a remote tool by name with JSON arguments. */
|
|
159
|
+
export declare function mcpCallTool(url: string, name: string, toolArguments: Record<string, unknown>, opts?: McpClientOptions): Promise<McpToolCallResult>;
|
|
160
|
+
/** Retry a call that came back `input_required`, echoing `requestState`. */
|
|
161
|
+
export declare function mcpContinue(url: string, pending: McpInputRequired, inputResponses: Record<string, unknown>, opts?: McpClientOptions): Promise<McpToolCallResult | McpPromptResult | McpResourceReadResult>;
|
|
162
|
+
export type McpPrompt = typeof PromptSchema.infer;
|
|
163
|
+
export type McpPromptList = McpPrompt[] & PageCache;
|
|
164
|
+
export type McpPromptResult = {
|
|
165
|
+
resultType: "complete";
|
|
166
|
+
description?: string;
|
|
167
|
+
messages: unknown[];
|
|
168
|
+
_meta?: Record<string, unknown>;
|
|
169
|
+
} | McpInputRequired | McpTaskHandle;
|
|
170
|
+
/** `prompts/list`, following cursors. */
|
|
171
|
+
export declare function mcpListPrompts(url: string, opts?: McpClientOptions): Promise<McpPromptList>;
|
|
172
|
+
/** `prompts/get`. */
|
|
173
|
+
export declare function mcpGetPrompt(url: string, name: string, args: Record<string, string> | undefined, opts?: McpClientOptions): Promise<McpPromptResult>;
|
|
174
|
+
export type McpResource = typeof ResourceSchema.infer;
|
|
175
|
+
export type McpResourceList = McpResource[] & PageCache;
|
|
176
|
+
export type McpResourceTemplate = typeof TemplateSchema.infer;
|
|
177
|
+
export type McpResourceTemplateList = McpResourceTemplate[] & PageCache;
|
|
178
|
+
export type McpResourceReadResult = {
|
|
179
|
+
resultType: "complete";
|
|
180
|
+
contents: unknown[];
|
|
181
|
+
_meta?: Record<string, unknown>;
|
|
182
|
+
ttlMs?: number;
|
|
183
|
+
cacheScope?: McpCacheScope;
|
|
184
|
+
} | McpInputRequired | McpTaskHandle;
|
|
185
|
+
/** `resources/list`, following cursors. */
|
|
186
|
+
export declare function mcpListResources(url: string, opts?: McpClientOptions): Promise<McpResourceList>;
|
|
187
|
+
/** `resources/read`. */
|
|
188
|
+
export declare function mcpReadResource(url: string, uri: string, opts?: McpClientOptions): Promise<McpResourceReadResult>;
|
|
189
|
+
/** `resources/templates/list`, following cursors. */
|
|
190
|
+
export declare function mcpListResourceTemplates(url: string, opts?: McpClientOptions): Promise<McpResourceTemplateList>;
|
|
191
|
+
export type McpCompletion = typeof CompletionResult.infer;
|
|
192
|
+
export type McpCompletionRef = {
|
|
193
|
+
readonly type: "ref/prompt";
|
|
194
|
+
readonly name: string;
|
|
195
|
+
} | {
|
|
196
|
+
readonly type: "ref/resource";
|
|
197
|
+
readonly uri: string;
|
|
198
|
+
};
|
|
199
|
+
/** `completion/complete`. */
|
|
200
|
+
export declare function mcpComplete(url: string, ref: McpCompletionRef, argument: {
|
|
201
|
+
name: string;
|
|
202
|
+
value: string;
|
|
203
|
+
}, opts?: McpClientOptions & {
|
|
204
|
+
context?: Record<string, string>;
|
|
205
|
+
}): Promise<McpCompletion>;
|
|
206
|
+
export type McpNotification = {
|
|
207
|
+
method: string;
|
|
208
|
+
params?: Record<string, unknown>;
|
|
209
|
+
subscriptionId?: string | number;
|
|
210
|
+
};
|
|
211
|
+
/**
|
|
212
|
+
* `subscriptions/listen`. Resolves when the server closes the stream with a
|
|
213
|
+
* result after acknowledgement, or rejects when `signal` aborts and closes
|
|
214
|
+
* the stream. Long-lived by default; timeoutMs explicitly bounds its lifetime.
|
|
215
|
+
*/
|
|
216
|
+
export declare function mcpListen(url: string, filter: McpSubscriptionFilter, onNotification: (notification: McpNotification) => void, opts?: McpClientOptions): Promise<void>;
|
|
217
|
+
/** `tasks/get`. Requires the tasks extension. */
|
|
218
|
+
export declare function mcpGetTask(url: string, taskId: string, opts?: McpClientOptions): Promise<McpTask>;
|
|
219
|
+
/** `tasks/update`. Requires the tasks extension. */
|
|
220
|
+
export declare function mcpUpdateTask(url: string, taskId: string, inputResponses: Record<string, unknown>, opts?: McpClientOptions): Promise<void>;
|
|
221
|
+
/** `tasks/cancel`. Requires the tasks extension. Cooperative: the server only acknowledges. */
|
|
222
|
+
export declare function mcpCancelTask(url: string, taskId: string, opts?: McpClientOptions): Promise<void>;
|
|
223
|
+
export type McpSkill = typeof SkillSchema.infer;
|
|
224
|
+
export type McpSkillList = McpSkill[] & PageCache;
|
|
225
|
+
/** `skills/list`, following cursors. Requires the skills extension. */
|
|
226
|
+
export declare function mcpListSkills(url: string, opts?: McpClientOptions): Promise<McpSkillList>;
|
|
227
|
+
export type McpSkillResult = {
|
|
228
|
+
resultType: "complete";
|
|
229
|
+
skill: McpSkill;
|
|
230
|
+
ttlMs?: number;
|
|
231
|
+
cacheScope?: McpCacheScope;
|
|
232
|
+
};
|
|
233
|
+
/** `skills/get`. Requires the skills extension. */
|
|
234
|
+
export declare function mcpGetSkill(url: string, uri: string, opts?: McpClientOptions): Promise<McpSkillResult>;
|
|
235
|
+
/**
|
|
236
|
+
* `resources/directory/read`. Sent only when the skills extension is enabled
|
|
237
|
+
* and the server declared `directoryRead`.
|
|
238
|
+
*/
|
|
239
|
+
export declare function mcpReadResourceDirectory(url: string, uri: string, opts?: McpClientOptions): Promise<McpResourceList>;
|