@mcpfn/testing 0.0.1
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 +105 -0
- package/dist/auth.cjs +685 -0
- package/dist/auth.cjs.map +1 -0
- package/dist/auth.d.cts +199 -0
- package/dist/auth.d.ts +199 -0
- package/dist/auth.js +644 -0
- package/dist/auth.js.map +1 -0
- package/dist/index.cjs +1228 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +152 -0
- package/dist/index.d.ts +152 -0
- package/dist/index.js +1171 -0
- package/dist/index.js.map +1 -0
- package/dist/playwright.cjs +574 -0
- package/dist/playwright.cjs.map +1 -0
- package/dist/playwright.d.cts +75 -0
- package/dist/playwright.d.ts +75 -0
- package/dist/playwright.js +545 -0
- package/dist/playwright.js.map +1 -0
- package/package.json +64 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1228 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
MCPFN_HOST_PROFILES: () => MCPFN_HOST_PROFILES,
|
|
34
|
+
MCPFN_OAUTH_EXTENSION_GRANTS: () => MCPFN_OAUTH_EXTENSION_GRANTS,
|
|
35
|
+
McpFnAssertionError: () => McpFnAssertionError,
|
|
36
|
+
McpFnAuthAssertionError: () => McpFnAuthAssertionError,
|
|
37
|
+
McpFnMockOAuthAuthorizationServer: () => McpFnMockOAuthAuthorizationServer,
|
|
38
|
+
McpFnTestClient: () => McpFnTestClient,
|
|
39
|
+
OFFICIAL_CONFORMANCE_VERSION: () => OFFICIAL_CONFORMANCE_VERSION,
|
|
40
|
+
apiKeyCredential: () => apiKeyCredential,
|
|
41
|
+
assertAuthRegressionSuite: () => assertAuthRegressionSuite,
|
|
42
|
+
assertAuthorizationCodeClientMetadata: () => assertAuthorizationCodeClientMetadata,
|
|
43
|
+
assertBearerResourceChallenge: () => assertBearerResourceChallenge,
|
|
44
|
+
assertManifestContract: () => assertManifestContract,
|
|
45
|
+
assertStructuredTextParity: () => assertStructuredTextParity,
|
|
46
|
+
bearerCredential: () => bearerCredential,
|
|
47
|
+
buildOfficialConformanceArgs: () => buildOfficialConformanceArgs,
|
|
48
|
+
checkHostCompatibility: () => checkHostCompatibility,
|
|
49
|
+
createFetchAuthTarget: () => createFetchAuthTarget,
|
|
50
|
+
createOAuthClientMetadataFixture: () => createOAuthClientMetadataFixture,
|
|
51
|
+
createOAuthClientMetadataVariants: () => createOAuthClientMetadataVariants,
|
|
52
|
+
createPkcePair: () => createPkcePair,
|
|
53
|
+
parseBearerChallenge: () => parseBearerChallenge,
|
|
54
|
+
runAuthRegressionSuite: () => runAuthRegressionSuite,
|
|
55
|
+
runOfficialConformance: () => runOfficialConformance,
|
|
56
|
+
runScenarios: () => runScenarios,
|
|
57
|
+
stableJson: () => stableJson,
|
|
58
|
+
startMockOAuthAuthorizationServer: () => startMockOAuthAuthorizationServer
|
|
59
|
+
});
|
|
60
|
+
module.exports = __toCommonJS(index_exports);
|
|
61
|
+
|
|
62
|
+
// src/assertions.ts
|
|
63
|
+
var import_uriTemplate = require("@modelcontextprotocol/sdk/shared/uriTemplate.js");
|
|
64
|
+
function stableJson(value) {
|
|
65
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
66
|
+
if (value && typeof value === "object") {
|
|
67
|
+
return `{${Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`).join(",")}}`;
|
|
68
|
+
}
|
|
69
|
+
return JSON.stringify(value);
|
|
70
|
+
}
|
|
71
|
+
var McpFnAssertionError = class extends Error {
|
|
72
|
+
constructor(message) {
|
|
73
|
+
super(message);
|
|
74
|
+
this.name = "McpFnAssertionError";
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
async function assertManifestContract(client, manifest, options = {}) {
|
|
78
|
+
const actual = await client.listTools();
|
|
79
|
+
const actualByName = new Map(actual.map((tool) => [tool.name, tool]));
|
|
80
|
+
const manifestByName = new Map(manifest.tools.map((tool) => [tool.name, tool]));
|
|
81
|
+
const expectedNames = [...options.expectedToolNames ?? manifest.tools.map((tool) => tool.name)].sort();
|
|
82
|
+
const unknownExpectedNames = expectedNames.filter((name) => !manifestByName.has(name));
|
|
83
|
+
if (unknownExpectedNames.length) {
|
|
84
|
+
throw new McpFnAssertionError(
|
|
85
|
+
`Expected visible tools are absent from the manifest: ${stableJson(unknownExpectedNames)}`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const actualNames = actual.map((tool) => tool.name).sort();
|
|
89
|
+
if (stableJson(expectedNames) !== stableJson(actualNames)) {
|
|
90
|
+
throw new McpFnAssertionError(
|
|
91
|
+
`Tool inventory mismatch
|
|
92
|
+
expected: ${stableJson(expectedNames)}
|
|
93
|
+
actual: ${stableJson(actualNames)}`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
for (const name of expectedNames) {
|
|
97
|
+
const expected = manifestByName.get(name);
|
|
98
|
+
const tool = actualByName.get(expected.name);
|
|
99
|
+
const comparableExpected = {
|
|
100
|
+
name: expected.name,
|
|
101
|
+
description: expected.description,
|
|
102
|
+
inputSchema: expected.inputSchema,
|
|
103
|
+
outputSchema: expected.outputSchema,
|
|
104
|
+
annotations: expected.annotations,
|
|
105
|
+
execution: expected.execution,
|
|
106
|
+
icons: expected.icons,
|
|
107
|
+
title: expected.title,
|
|
108
|
+
_meta: expected.metadata
|
|
109
|
+
};
|
|
110
|
+
const comparableActual = {
|
|
111
|
+
name: tool.name,
|
|
112
|
+
description: tool.description,
|
|
113
|
+
inputSchema: tool.inputSchema,
|
|
114
|
+
outputSchema: tool.outputSchema,
|
|
115
|
+
annotations: tool.annotations,
|
|
116
|
+
execution: tool.execution,
|
|
117
|
+
icons: tool.icons,
|
|
118
|
+
title: tool.title,
|
|
119
|
+
_meta: tool._meta
|
|
120
|
+
};
|
|
121
|
+
if (stableJson(comparableExpected) !== stableJson(comparableActual)) {
|
|
122
|
+
throw new McpFnAssertionError(
|
|
123
|
+
`Contract mismatch for ${expected.name}
|
|
124
|
+
expected: ${stableJson(comparableExpected)}
|
|
125
|
+
actual: ${stableJson(comparableActual)}`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const actualCapabilities = client.client.getServerCapabilities() ?? {};
|
|
130
|
+
if (stableJson(manifest.capabilities ?? {}) !== stableJson(actualCapabilities)) {
|
|
131
|
+
throw new McpFnAssertionError(
|
|
132
|
+
`Capability mismatch
|
|
133
|
+
expected: ${stableJson(manifest.capabilities ?? {})}
|
|
134
|
+
actual: ${stableJson(actualCapabilities)}`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const actualResources = actualCapabilities.resources ? await client.listResources() : [];
|
|
138
|
+
const comparableResources = actualResources.map(({ _meta, ...resource }) => ({
|
|
139
|
+
...resource,
|
|
140
|
+
metadata: _meta
|
|
141
|
+
}));
|
|
142
|
+
const staticResourceUris = new Set(
|
|
143
|
+
(manifest.resources ?? []).map((resource) => resource.uri)
|
|
144
|
+
);
|
|
145
|
+
const resourceTemplates = (manifest.resourceTemplates ?? []).map(
|
|
146
|
+
(template) => new import_uriTemplate.UriTemplate(template.uriTemplate)
|
|
147
|
+
);
|
|
148
|
+
const comparableStaticResources = comparableResources.filter(
|
|
149
|
+
(resource) => staticResourceUris.has(resource.uri) || !resourceTemplates.some((template) => template.match(resource.uri))
|
|
150
|
+
);
|
|
151
|
+
const expectedResources = (manifest.resources ?? []).map(
|
|
152
|
+
({ subscribable: _subscribable, ...resource }) => resource
|
|
153
|
+
);
|
|
154
|
+
if (stableJson(expectedResources) !== stableJson(comparableStaticResources)) {
|
|
155
|
+
throw new McpFnAssertionError(
|
|
156
|
+
`Resource inventory mismatch
|
|
157
|
+
expected: ${stableJson(expectedResources)}
|
|
158
|
+
actual: ${stableJson(comparableStaticResources)}`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
const actualTemplates = actualCapabilities.resources ? await client.listResourceTemplates() : [];
|
|
162
|
+
const comparableTemplates = actualTemplates.map(({ _meta, ...template }) => ({
|
|
163
|
+
...template,
|
|
164
|
+
metadata: _meta
|
|
165
|
+
}));
|
|
166
|
+
const expectedTemplates = (manifest.resourceTemplates ?? []).map(
|
|
167
|
+
({ subscribable: _subscribable, ...template }) => template
|
|
168
|
+
);
|
|
169
|
+
if (stableJson(expectedTemplates) !== stableJson(comparableTemplates)) {
|
|
170
|
+
throw new McpFnAssertionError(
|
|
171
|
+
`Resource template inventory mismatch
|
|
172
|
+
expected: ${stableJson(expectedTemplates)}
|
|
173
|
+
actual: ${stableJson(comparableTemplates)}`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
for (const resource of comparableResources) {
|
|
177
|
+
const expected = (manifest.resources ?? []).find(
|
|
178
|
+
(candidate) => candidate.uri === resource.uri
|
|
179
|
+
) ?? (manifest.resourceTemplates ?? []).find(
|
|
180
|
+
(candidate) => new import_uriTemplate.UriTemplate(candidate.uriTemplate).match(resource.uri)
|
|
181
|
+
);
|
|
182
|
+
const subscribable = expected?.subscribable === true;
|
|
183
|
+
if (subscribable) {
|
|
184
|
+
try {
|
|
185
|
+
await client.subscribeResource(resource.uri);
|
|
186
|
+
await client.unsubscribeResource(resource.uri);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
throw new McpFnAssertionError(
|
|
189
|
+
`Resource subscription mismatch for ${resource.uri}: expected subscribe and unsubscribe to succeed (${error instanceof Error ? error.message : String(error)})`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
let accepted = false;
|
|
195
|
+
try {
|
|
196
|
+
await client.subscribeResource(resource.uri);
|
|
197
|
+
accepted = true;
|
|
198
|
+
await client.unsubscribeResource(resource.uri).catch(() => void 0);
|
|
199
|
+
} catch {
|
|
200
|
+
}
|
|
201
|
+
if (accepted) {
|
|
202
|
+
throw new McpFnAssertionError(
|
|
203
|
+
`Resource subscription mismatch for ${resource.uri}: expected subscribe to be rejected`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const actualPrompts = actualCapabilities.prompts ? await client.listPrompts() : [];
|
|
208
|
+
const comparablePrompts = actualPrompts.map(({ _meta, ...prompt }) => ({
|
|
209
|
+
...prompt,
|
|
210
|
+
metadata: _meta
|
|
211
|
+
}));
|
|
212
|
+
const expectedPrompts = (manifest.prompts ?? []).map(({ argumentsSchema: _schema, ...prompt }) => prompt);
|
|
213
|
+
if (stableJson(expectedPrompts) !== stableJson(comparablePrompts)) {
|
|
214
|
+
throw new McpFnAssertionError(
|
|
215
|
+
`Prompt inventory mismatch
|
|
216
|
+
expected: ${stableJson(expectedPrompts)}
|
|
217
|
+
actual: ${stableJson(comparablePrompts)}`
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
return actual;
|
|
221
|
+
}
|
|
222
|
+
function assertStructuredTextParity(result) {
|
|
223
|
+
if (!result.structuredContent) {
|
|
224
|
+
throw new McpFnAssertionError("Tool result has no structuredContent");
|
|
225
|
+
}
|
|
226
|
+
const text = (result.content ?? []).find(
|
|
227
|
+
(entry) => !!entry && typeof entry === "object" && entry.type === "text" && typeof entry.text === "string"
|
|
228
|
+
)?.text;
|
|
229
|
+
if (!text) {
|
|
230
|
+
throw new McpFnAssertionError("Tool result has no text content");
|
|
231
|
+
}
|
|
232
|
+
let parsed;
|
|
233
|
+
try {
|
|
234
|
+
parsed = JSON.parse(text);
|
|
235
|
+
} catch {
|
|
236
|
+
throw new McpFnAssertionError("Tool text content is not JSON");
|
|
237
|
+
}
|
|
238
|
+
if (stableJson(parsed) !== stableJson(result.structuredContent)) {
|
|
239
|
+
throw new McpFnAssertionError(
|
|
240
|
+
"Tool text content and structuredContent are not equivalent"
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/auth-core.ts
|
|
246
|
+
var McpFnAuthAssertionError = class extends Error {
|
|
247
|
+
constructor(message) {
|
|
248
|
+
super(message);
|
|
249
|
+
this.name = "McpFnAuthAssertionError";
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
function expectedStatus(response, expected, name) {
|
|
253
|
+
const statuses = Array.isArray(expected) ? expected : [expected];
|
|
254
|
+
if (!statuses.includes(response.status)) {
|
|
255
|
+
throw new McpFnAuthAssertionError(
|
|
256
|
+
`${name} expected HTTP ${statuses.join(" or ")}, received ${response.status}`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function parseBearerChallenge(header) {
|
|
261
|
+
if (!header) return void 0;
|
|
262
|
+
const matches = [...header.matchAll(/(?:^|,)\s*([A-Za-z][A-Za-z0-9_-]*)\s+/g)];
|
|
263
|
+
const selectedIndex = matches.findIndex((entry) => entry[1].toLowerCase() === "bearer");
|
|
264
|
+
if (selectedIndex < 0) return void 0;
|
|
265
|
+
const match = matches[selectedIndex];
|
|
266
|
+
const start = (match.index ?? 0) + match[0].length;
|
|
267
|
+
const end = matches[selectedIndex + 1]?.index ?? header.length;
|
|
268
|
+
const parameters = {};
|
|
269
|
+
const source = header.slice(start, end);
|
|
270
|
+
const pattern = /([A-Za-z][A-Za-z0-9_-]*)\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^,\s]+))/g;
|
|
271
|
+
for (const entry of source.matchAll(pattern)) {
|
|
272
|
+
const quoted = entry[2]?.replace(/\\(["\\])/g, "$1");
|
|
273
|
+
parameters[entry[1]] = quoted ?? entry[3] ?? "";
|
|
274
|
+
}
|
|
275
|
+
return { scheme: match[1], parameters };
|
|
276
|
+
}
|
|
277
|
+
function assertBearerResourceChallenge(response, expectedMetadataUrl) {
|
|
278
|
+
const challenge = parseBearerChallenge(response.headers.get("www-authenticate"));
|
|
279
|
+
if (!challenge || challenge.scheme.toLowerCase() !== "bearer") {
|
|
280
|
+
throw new McpFnAuthAssertionError("Expected a Bearer WWW-Authenticate challenge");
|
|
281
|
+
}
|
|
282
|
+
const metadataUrl = challenge.parameters.resource_metadata;
|
|
283
|
+
if (!metadataUrl) {
|
|
284
|
+
throw new McpFnAuthAssertionError(
|
|
285
|
+
"Bearer challenge is missing the resource_metadata parameter"
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
const parsed = new URL(metadataUrl);
|
|
290
|
+
if (!parsed.protocol || !parsed.host) throw new Error("not absolute");
|
|
291
|
+
} catch {
|
|
292
|
+
throw new McpFnAuthAssertionError(
|
|
293
|
+
`Bearer challenge has an invalid resource_metadata URL: ${metadataUrl}`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
if (expectedMetadataUrl && metadataUrl !== expectedMetadataUrl) {
|
|
297
|
+
throw new McpFnAuthAssertionError(
|
|
298
|
+
`Expected resource_metadata=${expectedMetadataUrl}, received ${metadataUrl}`
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
return challenge;
|
|
302
|
+
}
|
|
303
|
+
function bearerCredential(token, kind = "oauth") {
|
|
304
|
+
return {
|
|
305
|
+
kind,
|
|
306
|
+
headers: { authorization: `Bearer ${token}` }
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function apiKeyCredential(apiKey, options = {}) {
|
|
310
|
+
const headerName = options.headerName ?? "authorization";
|
|
311
|
+
const value = options.scheme === "" ? apiKey : `${options.scheme ?? "Bearer"} ${apiKey}`;
|
|
312
|
+
return { kind: "api-key", headers: { [headerName]: value } };
|
|
313
|
+
}
|
|
314
|
+
function invalidHeaders(options) {
|
|
315
|
+
return new Headers(options.invalidCredentialHeaders ?? {
|
|
316
|
+
authorization: `Bearer mcpfn-invalid-${options.kind}`
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
async function runOne(options, scenario, prepare, expected) {
|
|
320
|
+
let credential;
|
|
321
|
+
try {
|
|
322
|
+
credential = await prepare();
|
|
323
|
+
const headers = credential ? new Headers(credential.headers) : scenario.name === "invalid-credential" ? invalidHeaders(options) : new Headers();
|
|
324
|
+
const response = await options.target.request(headers, scenario);
|
|
325
|
+
expectedStatus(response, expected, scenario.name);
|
|
326
|
+
if (options.kind === "oauth" && scenario.name !== "valid-credential") {
|
|
327
|
+
assertBearerResourceChallenge(
|
|
328
|
+
response,
|
|
329
|
+
options.protectedResourceMetadataUrl
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
if (scenario.name === "valid-credential" && credential) {
|
|
333
|
+
await options.verifyAuthenticatedResponse?.(response.clone(), credential);
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
name: scenario.name,
|
|
337
|
+
status: "passed",
|
|
338
|
+
responseStatus: response.status
|
|
339
|
+
};
|
|
340
|
+
} catch (error) {
|
|
341
|
+
return {
|
|
342
|
+
name: scenario.name,
|
|
343
|
+
status: "failed",
|
|
344
|
+
error: error instanceof Error ? error.message : String(error)
|
|
345
|
+
};
|
|
346
|
+
} finally {
|
|
347
|
+
await credential?.dispose?.();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
async function runAuthRegressionSuite(options) {
|
|
351
|
+
const issue = (overrides = {}) => options.provider.issue({
|
|
352
|
+
kind: options.kind,
|
|
353
|
+
scopes: options.requiredScopes,
|
|
354
|
+
resource: options.resource,
|
|
355
|
+
...overrides
|
|
356
|
+
});
|
|
357
|
+
const results = [];
|
|
358
|
+
results.push(await runOne(
|
|
359
|
+
options,
|
|
360
|
+
{ name: "missing-credential", kind: options.kind },
|
|
361
|
+
async () => void 0,
|
|
362
|
+
401
|
|
363
|
+
));
|
|
364
|
+
results.push(await runOne(
|
|
365
|
+
options,
|
|
366
|
+
{ name: "invalid-credential", kind: options.kind },
|
|
367
|
+
async () => void 0,
|
|
368
|
+
401
|
|
369
|
+
));
|
|
370
|
+
results.push(await runOne(
|
|
371
|
+
options,
|
|
372
|
+
{ name: "valid-credential", kind: options.kind },
|
|
373
|
+
() => issue(),
|
|
374
|
+
options.expectedAuthenticatedStatuses ?? [200, 201, 202, 204]
|
|
375
|
+
));
|
|
376
|
+
const capabilities = options.provider.capabilities ?? {};
|
|
377
|
+
if (capabilities.scopes && options.requiredScopes?.length) {
|
|
378
|
+
results.push(await runOne(
|
|
379
|
+
options,
|
|
380
|
+
{ name: "insufficient-scope", kind: options.kind },
|
|
381
|
+
() => issue({ scopes: [] }),
|
|
382
|
+
403
|
|
383
|
+
));
|
|
384
|
+
}
|
|
385
|
+
if (capabilities.expiration) {
|
|
386
|
+
results.push(await runOne(
|
|
387
|
+
options,
|
|
388
|
+
{ name: "expired-credential", kind: options.kind },
|
|
389
|
+
() => issue({ expiresInSeconds: -60 }),
|
|
390
|
+
401
|
|
391
|
+
));
|
|
392
|
+
}
|
|
393
|
+
if (capabilities.resourceBinding && options.resource) {
|
|
394
|
+
results.push(await runOne(
|
|
395
|
+
options,
|
|
396
|
+
{ name: "missing-resource", kind: options.kind },
|
|
397
|
+
() => issue({ resource: void 0 }),
|
|
398
|
+
401
|
|
399
|
+
));
|
|
400
|
+
}
|
|
401
|
+
if (capabilities.resourceBinding && options.wrongResource) {
|
|
402
|
+
results.push(await runOne(
|
|
403
|
+
options,
|
|
404
|
+
{ name: "wrong-resource", kind: options.kind },
|
|
405
|
+
() => issue({ resource: options.wrongResource }),
|
|
406
|
+
401
|
|
407
|
+
));
|
|
408
|
+
}
|
|
409
|
+
if (capabilities.revocation && options.provider.revoke) {
|
|
410
|
+
results.push(await runOne(
|
|
411
|
+
options,
|
|
412
|
+
{ name: "revoked-credential", kind: options.kind },
|
|
413
|
+
async () => {
|
|
414
|
+
const credential = await issue();
|
|
415
|
+
await options.provider.revoke?.(credential);
|
|
416
|
+
return credential;
|
|
417
|
+
},
|
|
418
|
+
401
|
|
419
|
+
));
|
|
420
|
+
}
|
|
421
|
+
return results;
|
|
422
|
+
}
|
|
423
|
+
async function assertAuthRegressionSuite(options) {
|
|
424
|
+
const results = await runAuthRegressionSuite(options);
|
|
425
|
+
const failures = results.filter((result) => result.status === "failed");
|
|
426
|
+
if (failures.length) {
|
|
427
|
+
throw new McpFnAuthAssertionError(
|
|
428
|
+
failures.map((failure) => `${failure.name}: ${failure.error}`).join("\n")
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
return results;
|
|
432
|
+
}
|
|
433
|
+
function createFetchAuthTarget(options) {
|
|
434
|
+
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
435
|
+
if (!fetchImplementation) {
|
|
436
|
+
throw new Error("A fetch implementation is required");
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
async request(authHeaders, scenario) {
|
|
440
|
+
const configured = typeof options.requestInit === "function" ? options.requestInit(scenario) : options.requestInit ?? {};
|
|
441
|
+
const headers = new Headers(configured.headers);
|
|
442
|
+
headers.set("accept", "application/json, text/event-stream");
|
|
443
|
+
headers.set("content-type", "application/json");
|
|
444
|
+
authHeaders.forEach((value, name) => headers.set(name, value));
|
|
445
|
+
return fetchImplementation(options.url, {
|
|
446
|
+
method: "POST",
|
|
447
|
+
body: JSON.stringify({
|
|
448
|
+
jsonrpc: "2.0",
|
|
449
|
+
id: `mcpfn-auth-${scenario.name}`,
|
|
450
|
+
method: "initialize",
|
|
451
|
+
params: {
|
|
452
|
+
protocolVersion: "2025-11-25",
|
|
453
|
+
capabilities: {},
|
|
454
|
+
clientInfo: { name: "mcpfn-auth-test", version: "1.0.0" }
|
|
455
|
+
}
|
|
456
|
+
}),
|
|
457
|
+
...configured,
|
|
458
|
+
headers
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
var MCPFN_OAUTH_EXTENSION_GRANTS = {
|
|
464
|
+
jwtBearer: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
465
|
+
deviceCode: "urn:ietf:params:oauth:grant-type:device_code",
|
|
466
|
+
generic: "urn:example:params:oauth:grant-type:extension"
|
|
467
|
+
};
|
|
468
|
+
function createOAuthClientMetadataFixture(options) {
|
|
469
|
+
return {
|
|
470
|
+
...options.extraMetadata,
|
|
471
|
+
client_id: new URL(options.clientId).toString(),
|
|
472
|
+
client_name: options.clientName ?? "McpFn OAuth test client",
|
|
473
|
+
redirect_uris: [...new Set(options.redirectUris.map((uri) => new URL(uri).toString()))],
|
|
474
|
+
response_types: ["code"],
|
|
475
|
+
grant_types: [
|
|
476
|
+
"authorization_code",
|
|
477
|
+
"refresh_token",
|
|
478
|
+
...new Set(options.extraGrantTypes ?? [])
|
|
479
|
+
],
|
|
480
|
+
token_endpoint_auth_method: "none"
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function createOAuthClientMetadataVariants(baseUrl, redirectUri = new URL("/callback", baseUrl).toString()) {
|
|
484
|
+
const base = new URL(baseUrl);
|
|
485
|
+
const fixture = (name, extraGrantTypes = []) => createOAuthClientMetadataFixture({
|
|
486
|
+
clientId: new URL(`/client-metadata/${name}`, base).toString(),
|
|
487
|
+
redirectUris: [redirectUri],
|
|
488
|
+
clientName: `McpFn ${name} client`,
|
|
489
|
+
extraGrantTypes
|
|
490
|
+
});
|
|
491
|
+
return {
|
|
492
|
+
basic: fixture("basic"),
|
|
493
|
+
jwtBearerExtension: fixture("jwt-bearer-extension", [MCPFN_OAUTH_EXTENSION_GRANTS.jwtBearer]),
|
|
494
|
+
deviceCodeExtension: fixture("device-code-extension", [MCPFN_OAUTH_EXTENSION_GRANTS.deviceCode]),
|
|
495
|
+
genericExtension: fixture("generic-extension", [MCPFN_OAUTH_EXTENSION_GRANTS.generic])
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
function assertAuthorizationCodeClientMetadata(metadata, expectedClientId) {
|
|
499
|
+
if (expectedClientId && metadata.client_id !== new URL(expectedClientId).toString()) {
|
|
500
|
+
throw new McpFnAuthAssertionError("OAuth client metadata client_id does not match its document URL");
|
|
501
|
+
}
|
|
502
|
+
if (!metadata.response_types.includes("code")) {
|
|
503
|
+
throw new McpFnAuthAssertionError("OAuth client metadata does not support response_type=code");
|
|
504
|
+
}
|
|
505
|
+
if (!metadata.grant_types.includes("authorization_code")) {
|
|
506
|
+
throw new McpFnAuthAssertionError("OAuth client metadata does not support authorization_code");
|
|
507
|
+
}
|
|
508
|
+
if (!metadata.redirect_uris.length) {
|
|
509
|
+
throw new McpFnAuthAssertionError("OAuth client metadata must declare at least one redirect URI");
|
|
510
|
+
}
|
|
511
|
+
return metadata;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/mock-oauth-server.ts
|
|
515
|
+
var import_node_crypto = require("crypto");
|
|
516
|
+
var import_node_http = require("http");
|
|
517
|
+
function base64Url(value) {
|
|
518
|
+
return value.toString("base64url");
|
|
519
|
+
}
|
|
520
|
+
function createPkcePair() {
|
|
521
|
+
const verifier = base64Url((0, import_node_crypto.randomBytes)(48));
|
|
522
|
+
return {
|
|
523
|
+
verifier,
|
|
524
|
+
challenge: (0, import_node_crypto.createHash)("sha256").update(verifier).digest("base64url"),
|
|
525
|
+
method: "S256"
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
function uniqueScopes(value) {
|
|
529
|
+
return [...new Set((value ?? "").split(/\s+/).filter(Boolean))].sort();
|
|
530
|
+
}
|
|
531
|
+
function json(status, body) {
|
|
532
|
+
return Response.json(body, {
|
|
533
|
+
status,
|
|
534
|
+
headers: { "cache-control": "no-store" }
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
function oauthError(status, error, description) {
|
|
538
|
+
return json(status, { error, error_description: description });
|
|
539
|
+
}
|
|
540
|
+
function escapeHtml(value) {
|
|
541
|
+
return value.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<").replaceAll(">", ">");
|
|
542
|
+
}
|
|
543
|
+
function formValue(form, name) {
|
|
544
|
+
const value = form.get(name);
|
|
545
|
+
return value === null || value === "" ? void 0 : value;
|
|
546
|
+
}
|
|
547
|
+
var McpFnMockOAuthAuthorizationServer = class {
|
|
548
|
+
issuer;
|
|
549
|
+
clientId;
|
|
550
|
+
redirectUris;
|
|
551
|
+
scopesSupported;
|
|
552
|
+
clientMetadata;
|
|
553
|
+
codes = /* @__PURE__ */ new Map();
|
|
554
|
+
accessTokens = /* @__PURE__ */ new Map();
|
|
555
|
+
refreshTokens = /* @__PURE__ */ new Map();
|
|
556
|
+
callbacks = [];
|
|
557
|
+
clock;
|
|
558
|
+
accessTokenLifetimeSeconds;
|
|
559
|
+
authorizationCodeLifetimeSeconds;
|
|
560
|
+
refreshTokenLifetimeSeconds;
|
|
561
|
+
constructor(options) {
|
|
562
|
+
this.issuer = new URL(options.issuer);
|
|
563
|
+
this.issuer.pathname = this.issuer.pathname.replace(/\/?$/, "/");
|
|
564
|
+
this.clientId = options.clientId ?? "mcpfn-playwright";
|
|
565
|
+
this.redirectUris = options.redirectUris?.map((uri) => new URL(uri).toString()) ?? [
|
|
566
|
+
new URL("callback", this.issuer).toString()
|
|
567
|
+
];
|
|
568
|
+
this.scopesSupported = [...new Set(options.scopesSupported ?? ["mcp:read", "mcp:write"])].sort();
|
|
569
|
+
this.clock = options.clock ?? Date.now;
|
|
570
|
+
this.accessTokenLifetimeSeconds = options.accessTokenLifetimeSeconds ?? 300;
|
|
571
|
+
this.authorizationCodeLifetimeSeconds = options.authorizationCodeLifetimeSeconds ?? 120;
|
|
572
|
+
this.refreshTokenLifetimeSeconds = options.refreshTokenLifetimeSeconds ?? 3600;
|
|
573
|
+
this.clientMetadata = createOAuthClientMetadataVariants(
|
|
574
|
+
this.issuer,
|
|
575
|
+
this.redirectUris[0]
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
get metadataUrl() {
|
|
579
|
+
return new URL(".well-known/oauth-authorization-server", this.issuer).toString();
|
|
580
|
+
}
|
|
581
|
+
get authorizationEndpoint() {
|
|
582
|
+
return new URL("authorize", this.issuer).toString();
|
|
583
|
+
}
|
|
584
|
+
get tokenEndpoint() {
|
|
585
|
+
return new URL("token", this.issuer).toString();
|
|
586
|
+
}
|
|
587
|
+
get revocationEndpoint() {
|
|
588
|
+
return new URL("revoke", this.issuer).toString();
|
|
589
|
+
}
|
|
590
|
+
get callbackUrl() {
|
|
591
|
+
return this.redirectUris[0];
|
|
592
|
+
}
|
|
593
|
+
clientMetadataUrl(variant) {
|
|
594
|
+
return this.clientMetadata[variant].client_id;
|
|
595
|
+
}
|
|
596
|
+
authorizationUrl(input) {
|
|
597
|
+
const url = new URL(this.authorizationEndpoint);
|
|
598
|
+
url.searchParams.set("response_type", "code");
|
|
599
|
+
url.searchParams.set("client_id", input.clientId ?? this.clientId);
|
|
600
|
+
url.searchParams.set("redirect_uri", input.redirectUri ?? this.callbackUrl);
|
|
601
|
+
url.searchParams.set("code_challenge", input.codeChallenge);
|
|
602
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
603
|
+
url.searchParams.set("scope", (input.scopes ?? ["mcp:read"]).join(" "));
|
|
604
|
+
if (input.state) url.searchParams.set("state", input.state);
|
|
605
|
+
if (input.resource) url.searchParams.set("resource", input.resource);
|
|
606
|
+
for (const [name, value] of Object.entries(input.extraParameters ?? {})) {
|
|
607
|
+
url.searchParams.set(name, value);
|
|
608
|
+
}
|
|
609
|
+
return url.toString();
|
|
610
|
+
}
|
|
611
|
+
latestCallback() {
|
|
612
|
+
return this.callbacks.at(-1);
|
|
613
|
+
}
|
|
614
|
+
expireAccessToken(token) {
|
|
615
|
+
const record = this.accessTokens.get(token);
|
|
616
|
+
if (record) record.expiresAt = this.nowSeconds() - 1;
|
|
617
|
+
}
|
|
618
|
+
revokeToken(token) {
|
|
619
|
+
const access = this.accessTokens.get(token);
|
|
620
|
+
if (access) access.revoked = true;
|
|
621
|
+
const refresh = this.refreshTokens.get(token);
|
|
622
|
+
if (refresh) refresh.revoked = true;
|
|
623
|
+
}
|
|
624
|
+
async verifyAccessToken(token) {
|
|
625
|
+
const record = this.accessTokens.get(token);
|
|
626
|
+
if (!record || record.revoked || record.expiresAt <= this.nowSeconds()) {
|
|
627
|
+
throw new Error("Invalid access token");
|
|
628
|
+
}
|
|
629
|
+
return {
|
|
630
|
+
token,
|
|
631
|
+
clientId: record.clientId,
|
|
632
|
+
scopes: record.scopes,
|
|
633
|
+
expiresAt: record.expiresAt,
|
|
634
|
+
...record.resource ? { resource: new URL(record.resource) } : {}
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
async handle(request) {
|
|
638
|
+
const url = new URL(request.url);
|
|
639
|
+
const path2 = url.pathname.replace(this.issuer.pathname.replace(/\/$/, ""), "") || "/";
|
|
640
|
+
if (request.method === "GET" && path2 === "/.well-known/oauth-authorization-server") {
|
|
641
|
+
return json(200, {
|
|
642
|
+
issuer: this.issuer.toString(),
|
|
643
|
+
authorization_endpoint: this.authorizationEndpoint,
|
|
644
|
+
token_endpoint: this.tokenEndpoint,
|
|
645
|
+
revocation_endpoint: this.revocationEndpoint,
|
|
646
|
+
response_types_supported: ["code"],
|
|
647
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
648
|
+
code_challenge_methods_supported: ["S256"],
|
|
649
|
+
token_endpoint_auth_methods_supported: ["none"],
|
|
650
|
+
scopes_supported: this.scopesSupported
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
if (request.method === "GET" && path2.startsWith("/client-metadata/")) {
|
|
654
|
+
const metadata = Object.values(this.clientMetadata).find((candidate) => new URL(candidate.client_id).pathname === url.pathname);
|
|
655
|
+
return metadata ? json(200, metadata) : json(404, { error: "not_found" });
|
|
656
|
+
}
|
|
657
|
+
if (request.method === "GET" && path2 === "/authorize") {
|
|
658
|
+
return this.authorize(url);
|
|
659
|
+
}
|
|
660
|
+
if (request.method === "POST" && path2 === "/token") {
|
|
661
|
+
return this.token(await request.text());
|
|
662
|
+
}
|
|
663
|
+
if (request.method === "POST" && path2 === "/revoke") {
|
|
664
|
+
return this.revoke(await request.text());
|
|
665
|
+
}
|
|
666
|
+
if (request.method === "GET" && path2 === "/callback") {
|
|
667
|
+
const parameters = Object.fromEntries(url.searchParams);
|
|
668
|
+
this.callbacks.push({ url: url.toString(), parameters });
|
|
669
|
+
return new Response(
|
|
670
|
+
`<!doctype html><html><body><main><h1>OAuth callback received</h1><pre>${escapeHtml(JSON.stringify(parameters, null, 2))}</pre></main></body></html>`,
|
|
671
|
+
{ headers: { "content-type": "text/html; charset=utf-8" } }
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
return json(404, { error: "not_found" });
|
|
675
|
+
}
|
|
676
|
+
nowSeconds() {
|
|
677
|
+
return Math.floor(this.clock() / 1e3);
|
|
678
|
+
}
|
|
679
|
+
resolveClient(clientId) {
|
|
680
|
+
if (clientId === this.clientId) {
|
|
681
|
+
return { clientId, redirectUris: this.redirectUris };
|
|
682
|
+
}
|
|
683
|
+
const metadata = Object.values(this.clientMetadata).find((candidate) => candidate.client_id === clientId);
|
|
684
|
+
if (!metadata) return void 0;
|
|
685
|
+
assertAuthorizationCodeClientMetadata(metadata, clientId);
|
|
686
|
+
return { clientId, redirectUris: metadata.redirect_uris };
|
|
687
|
+
}
|
|
688
|
+
authorize(url) {
|
|
689
|
+
const clientId = url.searchParams.get("client_id") ?? "";
|
|
690
|
+
const redirectUri = url.searchParams.get("redirect_uri") ?? "";
|
|
691
|
+
const client = this.resolveClient(clientId);
|
|
692
|
+
if (!client) return oauthError(400, "invalid_client", "Unknown OAuth client");
|
|
693
|
+
if (!client.redirectUris.includes(redirectUri)) {
|
|
694
|
+
return oauthError(400, "invalid_request", "redirect_uri is not registered");
|
|
695
|
+
}
|
|
696
|
+
if (url.searchParams.get("response_type") !== "code") {
|
|
697
|
+
return this.authorizationRedirect(url, redirectUri, "unsupported_response_type");
|
|
698
|
+
}
|
|
699
|
+
const codeChallenge = url.searchParams.get("code_challenge");
|
|
700
|
+
if (!codeChallenge || url.searchParams.get("code_challenge_method") !== "S256") {
|
|
701
|
+
return this.authorizationRedirect(url, redirectUri, "invalid_request", "PKCE S256 is required");
|
|
702
|
+
}
|
|
703
|
+
const requestedScopes = uniqueScopes(url.searchParams.get("scope"));
|
|
704
|
+
if (requestedScopes.some((scope) => !this.scopesSupported.includes(scope))) {
|
|
705
|
+
return this.authorizationRedirect(url, redirectUri, "invalid_scope");
|
|
706
|
+
}
|
|
707
|
+
const decision = url.searchParams.get("decision");
|
|
708
|
+
if (!decision) {
|
|
709
|
+
const hidden = [...url.searchParams].filter(([name]) => name !== "decision").map(([name, value]) => `<input type="hidden" name="${escapeHtml(name)}" value="${escapeHtml(value)}">`).join("");
|
|
710
|
+
return new Response(
|
|
711
|
+
`<!doctype html><html><body><main><h1>Authorize MCP client</h1><p>${escapeHtml(clientId)}</p><form method="get" action="${escapeHtml(this.authorizationEndpoint)}">${hidden}<button name="decision" value="approve">Approve</button><button name="decision" value="deny">Deny</button></form></main></body></html>`,
|
|
712
|
+
{ headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" } }
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
if (decision === "deny") {
|
|
716
|
+
return this.authorizationRedirect(url, redirectUri, "access_denied");
|
|
717
|
+
}
|
|
718
|
+
if (decision !== "approve") {
|
|
719
|
+
return oauthError(400, "invalid_request", "Unknown authorization decision");
|
|
720
|
+
}
|
|
721
|
+
const code = `mcpfn_code_${(0, import_node_crypto.randomUUID)()}`;
|
|
722
|
+
this.codes.set(code, {
|
|
723
|
+
clientId,
|
|
724
|
+
redirectUri,
|
|
725
|
+
codeChallenge,
|
|
726
|
+
scopes: requestedScopes,
|
|
727
|
+
resource: url.searchParams.get("resource") ?? void 0,
|
|
728
|
+
expiresAt: this.nowSeconds() + this.authorizationCodeLifetimeSeconds,
|
|
729
|
+
used: false
|
|
730
|
+
});
|
|
731
|
+
const callback = new URL(redirectUri);
|
|
732
|
+
callback.searchParams.set("code", code);
|
|
733
|
+
const state = url.searchParams.get("state");
|
|
734
|
+
if (state) callback.searchParams.set("state", state);
|
|
735
|
+
return Response.redirect(callback, 302);
|
|
736
|
+
}
|
|
737
|
+
authorizationRedirect(requestUrl, redirectUri, error, description) {
|
|
738
|
+
const callback = new URL(redirectUri);
|
|
739
|
+
callback.searchParams.set("error", error);
|
|
740
|
+
if (description) callback.searchParams.set("error_description", description);
|
|
741
|
+
const state = requestUrl.searchParams.get("state");
|
|
742
|
+
if (state) callback.searchParams.set("state", state);
|
|
743
|
+
return Response.redirect(callback, 302);
|
|
744
|
+
}
|
|
745
|
+
token(source) {
|
|
746
|
+
const form = new URLSearchParams(source);
|
|
747
|
+
const grantType = formValue(form, "grant_type");
|
|
748
|
+
if (grantType === "authorization_code") return this.exchangeAuthorizationCode(form);
|
|
749
|
+
if (grantType === "refresh_token") return this.exchangeRefreshToken(form);
|
|
750
|
+
return oauthError(400, "unsupported_grant_type", "The grant type is not supported");
|
|
751
|
+
}
|
|
752
|
+
exchangeAuthorizationCode(form) {
|
|
753
|
+
const code = formValue(form, "code");
|
|
754
|
+
const record = code ? this.codes.get(code) : void 0;
|
|
755
|
+
if (!record || record.used || record.expiresAt <= this.nowSeconds()) {
|
|
756
|
+
return oauthError(400, "invalid_grant", "Authorization code is invalid or expired");
|
|
757
|
+
}
|
|
758
|
+
if (formValue(form, "client_id") !== record.clientId || formValue(form, "redirect_uri") !== record.redirectUri) {
|
|
759
|
+
return oauthError(400, "invalid_grant", "Authorization code binding does not match");
|
|
760
|
+
}
|
|
761
|
+
const verifier = formValue(form, "code_verifier");
|
|
762
|
+
const challenge = verifier ? (0, import_node_crypto.createHash)("sha256").update(verifier).digest("base64url") : void 0;
|
|
763
|
+
if (challenge !== record.codeChallenge) {
|
|
764
|
+
return oauthError(400, "invalid_grant", "PKCE verification failed");
|
|
765
|
+
}
|
|
766
|
+
record.used = true;
|
|
767
|
+
return json(200, this.issueTokenSet(record));
|
|
768
|
+
}
|
|
769
|
+
exchangeRefreshToken(form) {
|
|
770
|
+
const refreshToken = formValue(form, "refresh_token");
|
|
771
|
+
const record = refreshToken ? this.refreshTokens.get(refreshToken) : void 0;
|
|
772
|
+
if (!record || record.revoked || record.expiresAt <= this.nowSeconds()) {
|
|
773
|
+
return oauthError(400, "invalid_grant", "Refresh token is invalid or expired");
|
|
774
|
+
}
|
|
775
|
+
if (formValue(form, "client_id") !== record.clientId) {
|
|
776
|
+
return oauthError(400, "invalid_grant", "Refresh token client binding does not match");
|
|
777
|
+
}
|
|
778
|
+
const requestedScopes = form.has("scope") ? uniqueScopes(form.get("scope")) : record.scopes;
|
|
779
|
+
if (requestedScopes.some((scope) => !record.scopes.includes(scope))) {
|
|
780
|
+
return oauthError(400, "invalid_scope", "Refresh cannot increase granted scopes");
|
|
781
|
+
}
|
|
782
|
+
record.revoked = true;
|
|
783
|
+
return json(200, this.issueTokenSet({ ...record, scopes: requestedScopes }));
|
|
784
|
+
}
|
|
785
|
+
issueTokenSet(input) {
|
|
786
|
+
const accessToken = `mcpfn_access_${(0, import_node_crypto.randomUUID)()}`;
|
|
787
|
+
const refreshToken = `mcpfn_refresh_${(0, import_node_crypto.randomUUID)()}`;
|
|
788
|
+
this.accessTokens.set(accessToken, {
|
|
789
|
+
...input,
|
|
790
|
+
expiresAt: this.nowSeconds() + this.accessTokenLifetimeSeconds,
|
|
791
|
+
revoked: false
|
|
792
|
+
});
|
|
793
|
+
this.refreshTokens.set(refreshToken, {
|
|
794
|
+
...input,
|
|
795
|
+
expiresAt: this.nowSeconds() + this.refreshTokenLifetimeSeconds,
|
|
796
|
+
revoked: false
|
|
797
|
+
});
|
|
798
|
+
return {
|
|
799
|
+
access_token: accessToken,
|
|
800
|
+
token_type: "Bearer",
|
|
801
|
+
expires_in: this.accessTokenLifetimeSeconds,
|
|
802
|
+
refresh_token: refreshToken,
|
|
803
|
+
scope: input.scopes.join(" "),
|
|
804
|
+
...input.resource ? { resource: input.resource } : {}
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
revoke(source) {
|
|
808
|
+
const token = new URLSearchParams(source).get("token");
|
|
809
|
+
if (token) this.revokeToken(token);
|
|
810
|
+
return new Response(null, { status: 200 });
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
async function toRequest(req, origin) {
|
|
814
|
+
const chunks = [];
|
|
815
|
+
for await (const chunk of req) chunks.push(Buffer.from(chunk));
|
|
816
|
+
const headers = new Headers();
|
|
817
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
818
|
+
if (Array.isArray(value)) {
|
|
819
|
+
for (const entry of value) headers.append(name, entry);
|
|
820
|
+
} else if (value !== void 0) {
|
|
821
|
+
headers.set(name, value);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
const body = chunks.length ? Buffer.concat(chunks) : void 0;
|
|
825
|
+
const init = {
|
|
826
|
+
method: req.method,
|
|
827
|
+
headers,
|
|
828
|
+
...body ? { body, duplex: "half" } : {}
|
|
829
|
+
};
|
|
830
|
+
return new Request(new URL(req.url ?? "/", origin), init);
|
|
831
|
+
}
|
|
832
|
+
async function writeResponse(res, response) {
|
|
833
|
+
res.statusCode = response.status;
|
|
834
|
+
response.headers.forEach((value, name) => res.setHeader(name, value));
|
|
835
|
+
res.end(Buffer.from(await response.arrayBuffer()));
|
|
836
|
+
}
|
|
837
|
+
async function startMockOAuthAuthorizationServer(options = {}) {
|
|
838
|
+
let oauth;
|
|
839
|
+
let origin = "";
|
|
840
|
+
const server = (0, import_node_http.createServer)(async (req, res) => {
|
|
841
|
+
try {
|
|
842
|
+
if (!oauth) throw new Error("Mock OAuth server is not initialized");
|
|
843
|
+
await writeResponse(res, await oauth.handle(await toRequest(req, origin)));
|
|
844
|
+
} catch {
|
|
845
|
+
res.statusCode = 500;
|
|
846
|
+
res.setHeader("content-type", "application/json");
|
|
847
|
+
res.end(JSON.stringify({
|
|
848
|
+
error: "server_error",
|
|
849
|
+
error_description: "Internal Server Error"
|
|
850
|
+
}));
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
await new Promise((resolve, reject) => {
|
|
854
|
+
server.once("error", reject);
|
|
855
|
+
server.listen(options.port ?? 0, options.hostname ?? "127.0.0.1", () => {
|
|
856
|
+
server.off("error", reject);
|
|
857
|
+
resolve();
|
|
858
|
+
});
|
|
859
|
+
});
|
|
860
|
+
const address = server.address();
|
|
861
|
+
origin = `http://${address.address.includes(":") ? `[${address.address}]` : address.address}:${address.port}`;
|
|
862
|
+
oauth = new McpFnMockOAuthAuthorizationServer({ ...options, issuer: origin });
|
|
863
|
+
return {
|
|
864
|
+
origin,
|
|
865
|
+
oauth,
|
|
866
|
+
close: () => new Promise((resolve, reject) => {
|
|
867
|
+
server.close((error) => error ? reject(error) : resolve());
|
|
868
|
+
})
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// src/client.ts
|
|
873
|
+
var import_client = require("@modelcontextprotocol/sdk/client/index.js");
|
|
874
|
+
var import_inMemory = require("@modelcontextprotocol/sdk/inMemory.js");
|
|
875
|
+
var import_types = require("@modelcontextprotocol/sdk/types.js");
|
|
876
|
+
var McpFnTestClient = class _McpFnTestClient {
|
|
877
|
+
client;
|
|
878
|
+
server;
|
|
879
|
+
closed = false;
|
|
880
|
+
constructor(client, server) {
|
|
881
|
+
this.client = client;
|
|
882
|
+
this.server = server;
|
|
883
|
+
}
|
|
884
|
+
static async connect(server, info = { name: "mcpfn-test-client", version: "1.0.0" }, options = {}) {
|
|
885
|
+
const [clientTransport, serverTransport] = import_inMemory.InMemoryTransport.createLinkedPair();
|
|
886
|
+
const client = new import_client.Client(info, { capabilities: options.capabilities ?? {} });
|
|
887
|
+
await options.configure?.(client);
|
|
888
|
+
await server.connect(serverTransport);
|
|
889
|
+
await client.connect(clientTransport);
|
|
890
|
+
return new _McpFnTestClient(client, server);
|
|
891
|
+
}
|
|
892
|
+
async listTools() {
|
|
893
|
+
const values = [];
|
|
894
|
+
let cursor;
|
|
895
|
+
do {
|
|
896
|
+
const page = await this.client.listTools(cursor ? { cursor } : void 0);
|
|
897
|
+
values.push(...page.tools);
|
|
898
|
+
cursor = page.nextCursor;
|
|
899
|
+
} while (cursor);
|
|
900
|
+
return values;
|
|
901
|
+
}
|
|
902
|
+
async callTool(name, args = {}) {
|
|
903
|
+
return await this.client.callTool({ name, arguments: args });
|
|
904
|
+
}
|
|
905
|
+
async createToolTask(name, args = {}, options = {}) {
|
|
906
|
+
return this.client.request(
|
|
907
|
+
{
|
|
908
|
+
method: "tools/call",
|
|
909
|
+
params: { name, arguments: args, task: options }
|
|
910
|
+
},
|
|
911
|
+
import_types.CreateTaskResultSchema
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
getTask(taskId) {
|
|
915
|
+
return this.client.experimental.tasks.getTask(taskId);
|
|
916
|
+
}
|
|
917
|
+
getTaskResult(taskId) {
|
|
918
|
+
return this.client.experimental.tasks.getTaskResult(
|
|
919
|
+
taskId,
|
|
920
|
+
import_types.CallToolResultSchema
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
listTasks(cursor) {
|
|
924
|
+
return this.client.experimental.tasks.listTasks(cursor);
|
|
925
|
+
}
|
|
926
|
+
cancelTask(taskId) {
|
|
927
|
+
return this.client.experimental.tasks.cancelTask(taskId);
|
|
928
|
+
}
|
|
929
|
+
async listResources() {
|
|
930
|
+
const values = [];
|
|
931
|
+
let cursor;
|
|
932
|
+
do {
|
|
933
|
+
const page = await this.client.listResources(cursor ? { cursor } : void 0);
|
|
934
|
+
values.push(...page.resources);
|
|
935
|
+
cursor = page.nextCursor;
|
|
936
|
+
} while (cursor);
|
|
937
|
+
return values;
|
|
938
|
+
}
|
|
939
|
+
async listResourceTemplates() {
|
|
940
|
+
const values = [];
|
|
941
|
+
let cursor;
|
|
942
|
+
do {
|
|
943
|
+
const page = await this.client.listResourceTemplates(cursor ? { cursor } : void 0);
|
|
944
|
+
values.push(...page.resourceTemplates);
|
|
945
|
+
cursor = page.nextCursor;
|
|
946
|
+
} while (cursor);
|
|
947
|
+
return values;
|
|
948
|
+
}
|
|
949
|
+
readResource(uri) {
|
|
950
|
+
return this.client.readResource({ uri });
|
|
951
|
+
}
|
|
952
|
+
async subscribeResource(uri) {
|
|
953
|
+
await this.client.subscribeResource({ uri });
|
|
954
|
+
}
|
|
955
|
+
async unsubscribeResource(uri) {
|
|
956
|
+
await this.client.unsubscribeResource({ uri });
|
|
957
|
+
}
|
|
958
|
+
async listPrompts() {
|
|
959
|
+
const values = [];
|
|
960
|
+
let cursor;
|
|
961
|
+
do {
|
|
962
|
+
const page = await this.client.listPrompts(cursor ? { cursor } : void 0);
|
|
963
|
+
values.push(...page.prompts);
|
|
964
|
+
cursor = page.nextCursor;
|
|
965
|
+
} while (cursor);
|
|
966
|
+
return values;
|
|
967
|
+
}
|
|
968
|
+
getPrompt(name, args) {
|
|
969
|
+
return this.client.getPrompt({ name, ...args ? { arguments: args } : {} });
|
|
970
|
+
}
|
|
971
|
+
complete(params) {
|
|
972
|
+
return this.client.complete(params);
|
|
973
|
+
}
|
|
974
|
+
async close() {
|
|
975
|
+
if (this.closed) return;
|
|
976
|
+
this.closed = true;
|
|
977
|
+
await this.client.close().catch(() => void 0);
|
|
978
|
+
await this.server.close().catch(() => void 0);
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
|
|
982
|
+
// src/conformance.ts
|
|
983
|
+
var import_node_child_process = require("child_process");
|
|
984
|
+
var import_node_fs = require("fs");
|
|
985
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
986
|
+
var OFFICIAL_CONFORMANCE_VERSION = "0.1.16";
|
|
987
|
+
function npxInvocation(args) {
|
|
988
|
+
if (process.platform !== "win32") return { command: "npx", args };
|
|
989
|
+
const npmExecPath = process.env.npm_execpath;
|
|
990
|
+
const candidates = [
|
|
991
|
+
npmExecPath ? import_node_path.default.join(import_node_path.default.dirname(npmExecPath), "npx-cli.js") : void 0,
|
|
992
|
+
import_node_path.default.resolve(import_node_path.default.dirname(process.execPath), "node_modules/npm/bin/npx-cli.js"),
|
|
993
|
+
import_node_path.default.resolve(import_node_path.default.dirname(process.execPath), "../lib/node_modules/npm/bin/npx-cli.js")
|
|
994
|
+
].filter((candidate) => Boolean(candidate));
|
|
995
|
+
const npxCli = candidates.find((candidate) => (0, import_node_fs.existsSync)(candidate));
|
|
996
|
+
if (!npxCli) {
|
|
997
|
+
throw new Error("Unable to locate npm's npx-cli.js for the official MCP conformance runner");
|
|
998
|
+
}
|
|
999
|
+
return { command: process.execPath, args: [npxCli, ...args] };
|
|
1000
|
+
}
|
|
1001
|
+
function buildOfficialConformanceArgs(options) {
|
|
1002
|
+
const args = [
|
|
1003
|
+
"--yes",
|
|
1004
|
+
`@modelcontextprotocol/conformance@${OFFICIAL_CONFORMANCE_VERSION}`,
|
|
1005
|
+
"server",
|
|
1006
|
+
"--url",
|
|
1007
|
+
options.url
|
|
1008
|
+
];
|
|
1009
|
+
if (options.suite) args.push("--suite", options.suite);
|
|
1010
|
+
if (options.scenario) args.push("--scenario", options.scenario);
|
|
1011
|
+
if (options.expectedFailures) {
|
|
1012
|
+
args.push("--expected-failures", options.expectedFailures);
|
|
1013
|
+
}
|
|
1014
|
+
if (options.outputDir) args.push("--output-dir", options.outputDir);
|
|
1015
|
+
if (options.specVersion) args.push("--spec-version", options.specVersion);
|
|
1016
|
+
if (options.verbose) args.push("--verbose");
|
|
1017
|
+
return args;
|
|
1018
|
+
}
|
|
1019
|
+
async function runOfficialConformance(options) {
|
|
1020
|
+
const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
|
|
1021
|
+
if (nodeMajor < 22) {
|
|
1022
|
+
throw new Error(
|
|
1023
|
+
`Official MCP conformance ${OFFICIAL_CONFORMANCE_VERSION} requires Node.js 22 or newer; current runtime is ${process.versions.node}`
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
const args = buildOfficialConformanceArgs(options);
|
|
1027
|
+
const invocation = npxInvocation(args);
|
|
1028
|
+
return await new Promise((resolve, reject) => {
|
|
1029
|
+
const child = (0, import_node_child_process.spawn)(invocation.command, invocation.args, {
|
|
1030
|
+
cwd: options.cwd,
|
|
1031
|
+
env: {
|
|
1032
|
+
...process.env,
|
|
1033
|
+
PATH: [import_node_path.default.dirname(process.execPath), process.env.PATH].filter(Boolean).join(import_node_path.default.delimiter)
|
|
1034
|
+
},
|
|
1035
|
+
stdio: options.stdio === "inherit" ? "inherit" : "pipe"
|
|
1036
|
+
});
|
|
1037
|
+
let stdout = "";
|
|
1038
|
+
let stderr = "";
|
|
1039
|
+
child.stdout?.on("data", (chunk) => {
|
|
1040
|
+
stdout += chunk.toString();
|
|
1041
|
+
});
|
|
1042
|
+
child.stderr?.on("data", (chunk) => {
|
|
1043
|
+
stderr += chunk.toString();
|
|
1044
|
+
});
|
|
1045
|
+
child.once("error", reject);
|
|
1046
|
+
child.once("close", (code) => {
|
|
1047
|
+
resolve({ exitCode: code ?? 1, stdout, stderr });
|
|
1048
|
+
});
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// src/host-profiles.ts
|
|
1053
|
+
var import_core = require("@mcpfn/core");
|
|
1054
|
+
var MCPFN_HOST_PROFILES = {
|
|
1055
|
+
toolsOnly: {
|
|
1056
|
+
id: "mcpfn/tools-only",
|
|
1057
|
+
name: "Tools-only MCP host",
|
|
1058
|
+
protocolVersions: ["2025-03-26", "2025-06-18", "2025-11-25"],
|
|
1059
|
+
serverFeatures: ["tools"]
|
|
1060
|
+
},
|
|
1061
|
+
fullProtocol: {
|
|
1062
|
+
id: "mcpfn/full-protocol",
|
|
1063
|
+
name: "Full protocol MCP host",
|
|
1064
|
+
protocolVersions: ["2025-03-26", "2025-06-18", "2025-11-25"],
|
|
1065
|
+
serverFeatures: ["tools", "resources", "prompts", "completions", "tasks", "logging"],
|
|
1066
|
+
clientFeatures: { sampling: true, elicitation: ["form", "url"], roots: true }
|
|
1067
|
+
},
|
|
1068
|
+
mcpApps: {
|
|
1069
|
+
id: "mcpfn/mcp-apps",
|
|
1070
|
+
name: "Full protocol host with MCP Apps",
|
|
1071
|
+
protocolVersions: ["2025-11-25"],
|
|
1072
|
+
serverFeatures: ["tools", "resources", "prompts", "completions", "tasks", "logging"],
|
|
1073
|
+
clientFeatures: { sampling: true, elicitation: ["form", "url"], roots: true },
|
|
1074
|
+
extensions: [import_core.MCP_APP_EXTENSION_ID]
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
function checkHostCompatibility(manifest, profile) {
|
|
1078
|
+
const issues = [];
|
|
1079
|
+
if (manifest.protocolVersions?.length && !manifest.protocolVersions.some((version) => profile.protocolVersions.includes(version))) {
|
|
1080
|
+
issues.push({
|
|
1081
|
+
severity: "incompatible",
|
|
1082
|
+
code: "protocol-version-mismatch",
|
|
1083
|
+
path: "protocolVersions",
|
|
1084
|
+
message: `${profile.name} and ${manifest.server.name} have no protocol version in common`
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
const supportedServerFeatures = new Set(profile.serverFeatures);
|
|
1088
|
+
for (const feature of Object.keys(manifest.capabilities ?? {})) {
|
|
1089
|
+
if (feature === "experimental" || supportedServerFeatures.has(feature)) continue;
|
|
1090
|
+
issues.push({
|
|
1091
|
+
severity: "degraded",
|
|
1092
|
+
code: "server-feature-unavailable",
|
|
1093
|
+
path: `capabilities.${feature}`,
|
|
1094
|
+
message: `${profile.name} will not expose the server's ${feature} feature`
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
const required = manifest.clientRequirements;
|
|
1098
|
+
if (required?.sampling && !profile.clientFeatures?.sampling) {
|
|
1099
|
+
issues.push({
|
|
1100
|
+
severity: "incompatible",
|
|
1101
|
+
code: "sampling-required",
|
|
1102
|
+
path: "clientRequirements.sampling",
|
|
1103
|
+
message: `${manifest.server.name} requires client sampling`
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
if (required?.roots && !profile.clientFeatures?.roots) {
|
|
1107
|
+
issues.push({
|
|
1108
|
+
severity: "incompatible",
|
|
1109
|
+
code: "roots-required",
|
|
1110
|
+
path: "clientRequirements.roots",
|
|
1111
|
+
message: `${manifest.server.name} requires client roots`
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
for (const mode of required?.elicitation ?? []) {
|
|
1115
|
+
if (!profile.clientFeatures?.elicitation?.includes(mode)) {
|
|
1116
|
+
issues.push({
|
|
1117
|
+
severity: "incompatible",
|
|
1118
|
+
code: "elicitation-required",
|
|
1119
|
+
path: `clientRequirements.elicitation.${mode}`,
|
|
1120
|
+
message: `${manifest.server.name} requires ${mode} elicitation`
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
const extensions = new Set(profile.extensions ?? []);
|
|
1125
|
+
for (const [extension, configuration] of Object.entries(manifest.extensions ?? {})) {
|
|
1126
|
+
if (extensions.has(extension)) continue;
|
|
1127
|
+
const requiredExtension = configuration && typeof configuration === "object" && configuration.required === true;
|
|
1128
|
+
issues.push({
|
|
1129
|
+
severity: requiredExtension ? "incompatible" : "degraded",
|
|
1130
|
+
code: "extension-unavailable",
|
|
1131
|
+
path: `extensions.${extension}`,
|
|
1132
|
+
message: `${profile.name} does not support ${extension}`
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
if (!extensions.has(import_core.MCP_APP_EXTENSION_ID)) {
|
|
1136
|
+
for (const tool of manifest.tools) {
|
|
1137
|
+
const ui = tool.metadata?.ui;
|
|
1138
|
+
if (ui?.visibility?.length && !ui.visibility.includes("model")) {
|
|
1139
|
+
issues.push({
|
|
1140
|
+
severity: "incompatible",
|
|
1141
|
+
code: "app-only-tool-unavailable",
|
|
1142
|
+
path: `tools.${tool.name}.metadata.ui.visibility`,
|
|
1143
|
+
message: `${tool.name} is app-only but ${profile.name} does not support MCP Apps`
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
issues.sort((left, right) => left.path.localeCompare(right.path) || left.code.localeCompare(right.code));
|
|
1149
|
+
const status = issues.some((issue) => issue.severity === "incompatible") ? "incompatible" : issues.length ? "degraded" : "compatible";
|
|
1150
|
+
return { status, compatible: status !== "incompatible", issues };
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// src/scenarios.ts
|
|
1154
|
+
async function runScenarios(client, scenarios) {
|
|
1155
|
+
const results = [];
|
|
1156
|
+
for (const scenario of scenarios) {
|
|
1157
|
+
const startedAt = performance.now();
|
|
1158
|
+
try {
|
|
1159
|
+
const result = await client.callTool(scenario.tool, scenario.arguments);
|
|
1160
|
+
if (result.isError && scenario.expect?.isError === void 0) {
|
|
1161
|
+
throw new McpFnAssertionError(
|
|
1162
|
+
"Tool returned isError=true without an explicit expect.isError=true assertion"
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
if (scenario.expect?.isError !== void 0 && Boolean(result.isError) !== scenario.expect.isError) {
|
|
1166
|
+
throw new McpFnAssertionError(
|
|
1167
|
+
`Expected isError=${scenario.expect.isError}, received ${Boolean(result.isError)}`
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
if (scenario.expect && Object.prototype.hasOwnProperty.call(scenario.expect, "structuredContent") && stableJson(result.structuredContent) !== stableJson(scenario.expect.structuredContent)) {
|
|
1171
|
+
throw new McpFnAssertionError(
|
|
1172
|
+
`Structured result mismatch
|
|
1173
|
+
expected: ${stableJson(scenario.expect.structuredContent)}
|
|
1174
|
+
actual: ${stableJson(result.structuredContent)}`
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
if (scenario.expect?.structuredTextParity) {
|
|
1178
|
+
assertStructuredTextParity(result);
|
|
1179
|
+
}
|
|
1180
|
+
await scenario.verify?.(result);
|
|
1181
|
+
results.push({
|
|
1182
|
+
name: scenario.name,
|
|
1183
|
+
tool: scenario.tool,
|
|
1184
|
+
status: "passed",
|
|
1185
|
+
durationMs: performance.now() - startedAt
|
|
1186
|
+
});
|
|
1187
|
+
} catch (error) {
|
|
1188
|
+
results.push({
|
|
1189
|
+
name: scenario.name,
|
|
1190
|
+
tool: scenario.tool,
|
|
1191
|
+
status: "failed",
|
|
1192
|
+
durationMs: performance.now() - startedAt,
|
|
1193
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
return results;
|
|
1198
|
+
}
|
|
1199
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1200
|
+
0 && (module.exports = {
|
|
1201
|
+
MCPFN_HOST_PROFILES,
|
|
1202
|
+
MCPFN_OAUTH_EXTENSION_GRANTS,
|
|
1203
|
+
McpFnAssertionError,
|
|
1204
|
+
McpFnAuthAssertionError,
|
|
1205
|
+
McpFnMockOAuthAuthorizationServer,
|
|
1206
|
+
McpFnTestClient,
|
|
1207
|
+
OFFICIAL_CONFORMANCE_VERSION,
|
|
1208
|
+
apiKeyCredential,
|
|
1209
|
+
assertAuthRegressionSuite,
|
|
1210
|
+
assertAuthorizationCodeClientMetadata,
|
|
1211
|
+
assertBearerResourceChallenge,
|
|
1212
|
+
assertManifestContract,
|
|
1213
|
+
assertStructuredTextParity,
|
|
1214
|
+
bearerCredential,
|
|
1215
|
+
buildOfficialConformanceArgs,
|
|
1216
|
+
checkHostCompatibility,
|
|
1217
|
+
createFetchAuthTarget,
|
|
1218
|
+
createOAuthClientMetadataFixture,
|
|
1219
|
+
createOAuthClientMetadataVariants,
|
|
1220
|
+
createPkcePair,
|
|
1221
|
+
parseBearerChallenge,
|
|
1222
|
+
runAuthRegressionSuite,
|
|
1223
|
+
runOfficialConformance,
|
|
1224
|
+
runScenarios,
|
|
1225
|
+
stableJson,
|
|
1226
|
+
startMockOAuthAuthorizationServer
|
|
1227
|
+
});
|
|
1228
|
+
//# sourceMappingURL=index.cjs.map
|