toolcraft 0.0.112 → 0.0.113
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/composition.json +2 -2
- package/dist/composition.json +2 -2
- package/node_modules/tiny-mcp-client/src/http-oauth.integration.test.ts +181 -193
- package/node_modules/tiny-mcp-client/src/http-oauth.test.ts +98 -98
- package/node_modules/toolcraft-schema/package.json +1 -1
- package/package.json +2 -2
|
@@ -1,21 +1,15 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
2
|
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
type OAuthSessionStore,
|
|
6
|
-
type StoredOAuthSession,
|
|
7
|
-
} from "mcp-oauth";
|
|
8
|
-
import { nodeFetch } from "tiny-http-mcp-server/testing";
|
|
3
|
+
import { OAuthError, type OAuthSessionStore, type StoredOAuthSession } from "mcp-oauth";
|
|
4
|
+
import { installInMemoryHttp, nodeFetch } from "tiny-http-mcp-server/test-support";
|
|
9
5
|
import {
|
|
10
6
|
createMcpOAuthTestServer,
|
|
11
7
|
type McpOAuthTestServerHandle,
|
|
12
|
-
type McpOAuthTestServerOptions
|
|
8
|
+
type McpOAuthTestServerOptions
|
|
13
9
|
} from "tiny-http-mcp-oauth-test-server";
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
resolveAuthorizationServerMetadataUrl,
|
|
18
|
-
} from "./internal.js";
|
|
10
|
+
import { HttpTransport, McpClient, resolveAuthorizationServerMetadataUrl } from "./internal.js";
|
|
11
|
+
|
|
12
|
+
installInMemoryHttp();
|
|
19
13
|
|
|
20
14
|
interface RequestRecord {
|
|
21
15
|
url: string;
|
|
@@ -72,8 +66,8 @@ function createSessionStore(): SessionStoreWithMap {
|
|
|
72
66
|
},
|
|
73
67
|
async clear(resource: string): Promise<void> {
|
|
74
68
|
sessions.delete(resource);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
77
71
|
};
|
|
78
72
|
}
|
|
79
73
|
|
|
@@ -82,7 +76,7 @@ function cloneResponse(response: Response, body: string): Response {
|
|
|
82
76
|
return new Response(body, {
|
|
83
77
|
status: response.status,
|
|
84
78
|
statusText: response.statusText,
|
|
85
|
-
headers
|
|
79
|
+
headers
|
|
86
80
|
});
|
|
87
81
|
}
|
|
88
82
|
|
|
@@ -91,7 +85,9 @@ interface BoundLoopbackServerFactory {
|
|
|
91
85
|
port: number;
|
|
92
86
|
}
|
|
93
87
|
|
|
94
|
-
async function createBoundLoopbackServerFactory(
|
|
88
|
+
async function createBoundLoopbackServerFactory(
|
|
89
|
+
hostname: string
|
|
90
|
+
): Promise<BoundLoopbackServerFactory> {
|
|
95
91
|
const server = http.createServer();
|
|
96
92
|
|
|
97
93
|
await new Promise<void>((resolve, reject) => {
|
|
@@ -127,7 +123,7 @@ async function createBoundLoopbackServerFactory(hostname: string): Promise<Bound
|
|
|
127
123
|
createServer(): http.Server {
|
|
128
124
|
return server;
|
|
129
125
|
},
|
|
130
|
-
port: address.port
|
|
126
|
+
port: address.port
|
|
131
127
|
};
|
|
132
128
|
}
|
|
133
129
|
|
|
@@ -143,10 +139,7 @@ function parseFormBody(record: RequestRecord): URLSearchParams {
|
|
|
143
139
|
return new URLSearchParams(record.body ?? "");
|
|
144
140
|
}
|
|
145
141
|
|
|
146
|
-
function requireRequest(
|
|
147
|
-
request: RequestRecord | undefined,
|
|
148
|
-
description: string
|
|
149
|
-
): RequestRecord {
|
|
142
|
+
function requireRequest(request: RequestRecord | undefined, description: string): RequestRecord {
|
|
150
143
|
if (request === undefined) {
|
|
151
144
|
throw new Error(`Expected ${description}`);
|
|
152
145
|
}
|
|
@@ -179,17 +172,14 @@ function summarizeTraffic(
|
|
|
179
172
|
const tokenUrl = `${handle.oauth.issuer}/token`;
|
|
180
173
|
|
|
181
174
|
return {
|
|
182
|
-
prm: requests.filter(
|
|
183
|
-
|
|
184
|
-
).length,
|
|
175
|
+
prm: requests.filter((request) => request.method === "GET" && request.url === handle.prmUrl)
|
|
176
|
+
.length,
|
|
185
177
|
asMetadata: requests.filter(
|
|
186
178
|
(request) =>
|
|
187
|
-
request.method === "GET"
|
|
188
|
-
&& request.url === authorizationServerMetadataUrl.toString()
|
|
189
|
-
).length,
|
|
190
|
-
register: requests.filter(
|
|
191
|
-
(request) => request.method === "POST" && request.url === registerUrl
|
|
179
|
+
request.method === "GET" && request.url === authorizationServerMetadataUrl.toString()
|
|
192
180
|
).length,
|
|
181
|
+
register: requests.filter((request) => request.method === "POST" && request.url === registerUrl)
|
|
182
|
+
.length,
|
|
193
183
|
authorize: requests.filter(
|
|
194
184
|
(request) => request.method === "GET" && request.url.startsWith(authorizeUrl)
|
|
195
185
|
).length,
|
|
@@ -209,7 +199,7 @@ function summarizeTraffic(
|
|
|
209
199
|
}).length,
|
|
210
200
|
mcpPost: requests.filter(
|
|
211
201
|
(request) => request.method === "POST" && request.url === handle.mcpUrl
|
|
212
|
-
).length
|
|
202
|
+
).length
|
|
213
203
|
};
|
|
214
204
|
}
|
|
215
205
|
|
|
@@ -225,8 +215,7 @@ function summarizeAuthorizationServerTraffic(
|
|
|
225
215
|
return {
|
|
226
216
|
metadata: requestLog.filter(
|
|
227
217
|
(request) =>
|
|
228
|
-
request.method === "GET"
|
|
229
|
-
&& request.url === authorizationServerMetadataUrl.toString()
|
|
218
|
+
request.method === "GET" && request.url === authorizationServerMetadataUrl.toString()
|
|
230
219
|
).length,
|
|
231
220
|
register: requestLog.filter(
|
|
232
221
|
(request) => request.method === "POST" && request.url === registerUrl
|
|
@@ -247,7 +236,7 @@ function summarizeAuthorizationServerTraffic(
|
|
|
247
236
|
}
|
|
248
237
|
|
|
249
238
|
return new URLSearchParams(request.body ?? "").get("grant_type") === "refresh_token";
|
|
250
|
-
}).length
|
|
239
|
+
}).length
|
|
251
240
|
};
|
|
252
241
|
}
|
|
253
242
|
|
|
@@ -266,20 +255,20 @@ function getJsonRpcMethod(record: RequestRecord): string | undefined {
|
|
|
266
255
|
|
|
267
256
|
function getTextContent(result: unknown): string | undefined {
|
|
268
257
|
if (
|
|
269
|
-
typeof result !== "object"
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
258
|
+
typeof result !== "object" ||
|
|
259
|
+
result === null ||
|
|
260
|
+
!("content" in result) ||
|
|
261
|
+
!Array.isArray(result.content)
|
|
273
262
|
) {
|
|
274
263
|
return undefined;
|
|
275
264
|
}
|
|
276
265
|
|
|
277
266
|
const [firstItem] = result.content;
|
|
278
267
|
if (
|
|
279
|
-
typeof firstItem !== "object"
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
268
|
+
typeof firstItem !== "object" ||
|
|
269
|
+
firstItem === null ||
|
|
270
|
+
!("text" in firstItem) ||
|
|
271
|
+
typeof firstItem.text !== "string"
|
|
283
272
|
) {
|
|
284
273
|
return undefined;
|
|
285
274
|
}
|
|
@@ -296,41 +285,43 @@ function getStoredSession(harness: OAuthClientHarness): StoredOAuthSession {
|
|
|
296
285
|
return session;
|
|
297
286
|
}
|
|
298
287
|
|
|
299
|
-
async function createHarness(
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
288
|
+
async function createHarness(
|
|
289
|
+
options: {
|
|
290
|
+
now?: () => number;
|
|
291
|
+
oauthClient?:
|
|
292
|
+
| {
|
|
293
|
+
mode: "dynamic";
|
|
294
|
+
metadata?: {
|
|
295
|
+
clientName?: string;
|
|
296
|
+
scope?: string;
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
| {
|
|
300
|
+
mode: "static";
|
|
301
|
+
clientId: string;
|
|
302
|
+
clientSecret?: string;
|
|
303
|
+
metadata?: {
|
|
304
|
+
clientName?: string;
|
|
305
|
+
scope?: string;
|
|
306
|
+
};
|
|
316
307
|
};
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
308
|
+
responseTransform?: (input: {
|
|
309
|
+
handle: McpOAuthTestServerHandle;
|
|
310
|
+
record: RequestRecord;
|
|
311
|
+
response: Response;
|
|
312
|
+
}) => Promise<Response | undefined>;
|
|
313
|
+
serverOptions?: McpOAuthTestServerOptions;
|
|
314
|
+
createServer?: () => http.Server;
|
|
315
|
+
} = {}
|
|
316
|
+
): Promise<OAuthClientHarness> {
|
|
326
317
|
const server = createMcpOAuthTestServer({
|
|
327
318
|
autoApprove: true,
|
|
328
319
|
scopes: ["mcp.read"],
|
|
329
|
-
...(options.serverOptions ?? {})
|
|
320
|
+
...(options.serverOptions ?? {})
|
|
330
321
|
});
|
|
331
322
|
const handle = await server.listen({
|
|
332
323
|
port: 0,
|
|
333
|
-
hostname: "127.0.0.1"
|
|
324
|
+
hostname: "127.0.0.1"
|
|
334
325
|
});
|
|
335
326
|
const requests: RequestRecord[] = [];
|
|
336
327
|
const sessionStore = createSessionStore();
|
|
@@ -341,16 +332,17 @@ async function createHarness(options: {
|
|
|
341
332
|
method: init.method ?? "GET",
|
|
342
333
|
authorization: new Headers(init.headers).get("authorization"),
|
|
343
334
|
sessionId: new Headers(init.headers).get("mcp-session-id"),
|
|
344
|
-
body: typeof init.body === "string" ? init.body : undefined
|
|
335
|
+
body: typeof init.body === "string" ? init.body : undefined
|
|
345
336
|
};
|
|
346
337
|
const response = await nodeFetch(record.url, init);
|
|
347
|
-
const transformed =
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
338
|
+
const transformed =
|
|
339
|
+
options.responseTransform === undefined
|
|
340
|
+
? undefined
|
|
341
|
+
: await options.responseTransform({
|
|
342
|
+
handle,
|
|
343
|
+
record,
|
|
344
|
+
response
|
|
345
|
+
});
|
|
354
346
|
|
|
355
347
|
requests.push(record);
|
|
356
348
|
return transformed ?? response;
|
|
@@ -358,8 +350,8 @@ async function createHarness(options: {
|
|
|
358
350
|
const client = new McpClient({
|
|
359
351
|
clientInfo: {
|
|
360
352
|
name: "tiny-mcp-client-http-oauth-integration-test",
|
|
361
|
-
version: "1.0.0"
|
|
362
|
-
}
|
|
353
|
+
version: "1.0.0"
|
|
354
|
+
}
|
|
363
355
|
});
|
|
364
356
|
const transport = new HttpTransport({
|
|
365
357
|
url: handle.mcpUrl,
|
|
@@ -368,13 +360,13 @@ async function createHarness(options: {
|
|
|
368
360
|
client: options.oauthClient ?? {
|
|
369
361
|
mode: "dynamic",
|
|
370
362
|
metadata: {
|
|
371
|
-
clientName: "tiny-mcp-client integration test"
|
|
372
|
-
}
|
|
363
|
+
clientName: "tiny-mcp-client integration test"
|
|
364
|
+
}
|
|
373
365
|
},
|
|
374
366
|
browser: {
|
|
375
367
|
async openBrowser(authorizationUrl) {
|
|
376
368
|
const authorizationResponse = await fetchImpl(authorizationUrl, {
|
|
377
|
-
method: "GET"
|
|
369
|
+
method: "GET"
|
|
378
370
|
});
|
|
379
371
|
if (authorizationResponse.status !== 302) {
|
|
380
372
|
throw new Error(
|
|
@@ -386,7 +378,7 @@ async function createHarness(options: {
|
|
|
386
378
|
expect(callbackUrl).toBeTruthy();
|
|
387
379
|
|
|
388
380
|
const callbackResponse = await fetchImpl(callbackUrl ?? "", {
|
|
389
|
-
method: "GET"
|
|
381
|
+
method: "GET"
|
|
390
382
|
});
|
|
391
383
|
expect(callbackResponse.ok).toBe(true);
|
|
392
384
|
await callbackResponse.text();
|
|
@@ -394,12 +386,12 @@ async function createHarness(options: {
|
|
|
394
386
|
...(options.createServer === undefined
|
|
395
387
|
? {}
|
|
396
388
|
: {
|
|
397
|
-
createServer: options.createServer
|
|
398
|
-
})
|
|
389
|
+
createServer: options.createServer
|
|
390
|
+
})
|
|
399
391
|
},
|
|
400
392
|
now: () => currentNow,
|
|
401
|
-
sessionStore: sessionStore.store
|
|
402
|
-
}
|
|
393
|
+
sessionStore: sessionStore.store
|
|
394
|
+
}
|
|
403
395
|
});
|
|
404
396
|
|
|
405
397
|
return {
|
|
@@ -414,7 +406,7 @@ async function createHarness(options: {
|
|
|
414
406
|
setNow(value: number): void {
|
|
415
407
|
currentNow = value;
|
|
416
408
|
},
|
|
417
|
-
transport
|
|
409
|
+
transport
|
|
418
410
|
};
|
|
419
411
|
}
|
|
420
412
|
|
|
@@ -438,8 +430,8 @@ describe("HttpTransport OAuth integration", () => {
|
|
|
438
430
|
const firstResult = await harness.client.callTool({
|
|
439
431
|
name: "echo",
|
|
440
432
|
arguments: {
|
|
441
|
-
text: "first-call"
|
|
442
|
-
}
|
|
433
|
+
text: "first-call"
|
|
434
|
+
}
|
|
443
435
|
});
|
|
444
436
|
|
|
445
437
|
expect(getTextContent(firstResult)).toBe("first-call");
|
|
@@ -452,21 +444,21 @@ describe("HttpTransport OAuth integration", () => {
|
|
|
452
444
|
prm: 1,
|
|
453
445
|
register: 1,
|
|
454
446
|
tokenAuthorizationCode: 1,
|
|
455
|
-
tokenRefresh: 0
|
|
447
|
+
tokenRefresh: 0
|
|
456
448
|
});
|
|
457
449
|
expect(summarizeAuthorizationServerTraffic(harness.handle)).toEqual({
|
|
458
450
|
authorize: 1,
|
|
459
451
|
metadata: 1,
|
|
460
452
|
register: 1,
|
|
461
453
|
tokenAuthorizationCode: 1,
|
|
462
|
-
tokenRefresh: 0
|
|
454
|
+
tokenRefresh: 0
|
|
463
455
|
});
|
|
464
456
|
|
|
465
457
|
const initializePosts = harness.requests.filter(
|
|
466
458
|
(request) =>
|
|
467
|
-
request.method === "POST"
|
|
468
|
-
|
|
469
|
-
|
|
459
|
+
request.method === "POST" &&
|
|
460
|
+
request.url === harness.handle.mcpUrl &&
|
|
461
|
+
getJsonRpcMethod(request) === "initialize"
|
|
470
462
|
);
|
|
471
463
|
expect(initializePosts).toHaveLength(2);
|
|
472
464
|
expect(initializePosts[0]?.authorization).toBeNull();
|
|
@@ -474,52 +466,56 @@ describe("HttpTransport OAuth integration", () => {
|
|
|
474
466
|
|
|
475
467
|
const callToolPosts = harness.requests.filter(
|
|
476
468
|
(request) =>
|
|
477
|
-
request.method === "POST"
|
|
478
|
-
|
|
479
|
-
|
|
469
|
+
request.method === "POST" &&
|
|
470
|
+
request.url === harness.handle.mcpUrl &&
|
|
471
|
+
getJsonRpcMethod(request) === "tools/call"
|
|
480
472
|
);
|
|
481
473
|
expect(callToolPosts).toHaveLength(1);
|
|
482
474
|
expect(callToolPosts[0]?.authorization).toMatch(/^Bearer /);
|
|
483
475
|
|
|
484
476
|
const registrationRequest = harness.requests.find(
|
|
485
|
-
(request) =>
|
|
477
|
+
(request) =>
|
|
478
|
+
request.method === "POST" && request.url === `${harness.handle.oauth.issuer}/register`
|
|
486
479
|
);
|
|
487
|
-
expect(
|
|
480
|
+
expect(
|
|
481
|
+
parseJsonBody(requireRequest(registrationRequest, "registration request"))
|
|
482
|
+
).toMatchObject({
|
|
488
483
|
client_name: "tiny-mcp-client integration test",
|
|
489
484
|
grant_types: ["authorization_code", "refresh_token"],
|
|
490
485
|
response_types: ["code"],
|
|
491
|
-
token_endpoint_auth_method: "none"
|
|
486
|
+
token_endpoint_auth_method: "none"
|
|
492
487
|
});
|
|
493
488
|
|
|
494
489
|
const authorizationRequest = harness.requests.find(
|
|
495
490
|
(request) =>
|
|
496
|
-
request.method === "GET"
|
|
497
|
-
|
|
498
|
-
);
|
|
499
|
-
expect(new URL(requireRequest(authorizationRequest, "authorization request").url).searchParams.get("resource")).toBe(
|
|
500
|
-
harness.handle.mcpUrl
|
|
491
|
+
request.method === "GET" &&
|
|
492
|
+
request.url.startsWith(`${harness.handle.oauth.issuer}/authorize`)
|
|
501
493
|
);
|
|
494
|
+
expect(
|
|
495
|
+
new URL(requireRequest(authorizationRequest, "authorization request").url).searchParams.get(
|
|
496
|
+
"resource"
|
|
497
|
+
)
|
|
498
|
+
).toBe(harness.handle.mcpUrl);
|
|
502
499
|
|
|
503
500
|
const tokenRequest = harness.requests.find((request) => {
|
|
504
|
-
if (
|
|
505
|
-
request.method !== "POST"
|
|
506
|
-
|| request.url !== `${harness.handle.oauth.issuer}/token`
|
|
507
|
-
) {
|
|
501
|
+
if (request.method !== "POST" || request.url !== `${harness.handle.oauth.issuer}/token`) {
|
|
508
502
|
return false;
|
|
509
503
|
}
|
|
510
504
|
|
|
511
505
|
return parseFormBody(request).get("grant_type") === "authorization_code";
|
|
512
506
|
});
|
|
513
|
-
expect(
|
|
514
|
-
|
|
515
|
-
|
|
507
|
+
expect(
|
|
508
|
+
parseFormBody(requireRequest(tokenRequest, "authorization-code token request")).get(
|
|
509
|
+
"resource"
|
|
510
|
+
)
|
|
511
|
+
).toBe(harness.handle.mcpUrl);
|
|
516
512
|
|
|
517
513
|
const secondCallBaseline = summarizeTraffic(harness.requests, harness.handle);
|
|
518
514
|
const secondResult = await harness.client.callTool({
|
|
519
515
|
name: "echo",
|
|
520
516
|
arguments: {
|
|
521
|
-
text: "second-call"
|
|
522
|
-
}
|
|
517
|
+
text: "second-call"
|
|
518
|
+
}
|
|
523
519
|
});
|
|
524
520
|
|
|
525
521
|
expect(getTextContent(secondResult)).toBe("second-call");
|
|
@@ -529,7 +525,9 @@ describe("HttpTransport OAuth integration", () => {
|
|
|
529
525
|
harness.handle
|
|
530
526
|
);
|
|
531
527
|
expect(summaryAfterSecondCall.authorize - secondCallBaseline.authorize).toBe(0);
|
|
532
|
-
expect(
|
|
528
|
+
expect(
|
|
529
|
+
summaryAfterSecondCall.tokenAuthorizationCode - secondCallBaseline.tokenAuthorizationCode
|
|
530
|
+
).toBe(0);
|
|
533
531
|
expect(summaryAfterSecondCall.tokenRefresh - secondCallBaseline.tokenRefresh).toBe(0);
|
|
534
532
|
expect(summaryAfterSecondCall.mcpPost - secondCallBaseline.mcpPost).toBe(1);
|
|
535
533
|
expect(authorizationServerSummaryAfterSecondCall).toEqual({
|
|
@@ -537,7 +535,7 @@ describe("HttpTransport OAuth integration", () => {
|
|
|
537
535
|
metadata: 1,
|
|
538
536
|
register: 1,
|
|
539
537
|
tokenAuthorizationCode: 1,
|
|
540
|
-
tokenRefresh: 0
|
|
538
|
+
tokenRefresh: 0
|
|
541
539
|
});
|
|
542
540
|
});
|
|
543
541
|
|
|
@@ -548,17 +546,17 @@ describe("HttpTransport OAuth integration", () => {
|
|
|
548
546
|
createServer: loopbackServer.createServer,
|
|
549
547
|
oauthClient: {
|
|
550
548
|
mode: "static",
|
|
551
|
-
clientId: "static-client"
|
|
549
|
+
clientId: "static-client"
|
|
552
550
|
},
|
|
553
551
|
serverOptions: {
|
|
554
552
|
staticClients: [
|
|
555
553
|
{
|
|
556
554
|
clientId: "static-client",
|
|
557
555
|
redirectUris: [redirectUri],
|
|
558
|
-
scopes: ["mcp.read"]
|
|
559
|
-
}
|
|
560
|
-
]
|
|
561
|
-
}
|
|
556
|
+
scopes: ["mcp.read"]
|
|
557
|
+
}
|
|
558
|
+
]
|
|
559
|
+
}
|
|
562
560
|
});
|
|
563
561
|
cleanups.add(harness.close);
|
|
564
562
|
|
|
@@ -567,16 +565,14 @@ describe("HttpTransport OAuth integration", () => {
|
|
|
567
565
|
const result = await harness.client.callTool({
|
|
568
566
|
name: "echo",
|
|
569
567
|
arguments: {
|
|
570
|
-
text: "static-client"
|
|
571
|
-
}
|
|
568
|
+
text: "static-client"
|
|
569
|
+
}
|
|
572
570
|
});
|
|
573
571
|
|
|
574
572
|
expect(getTextContent(result)).toBe("static-client");
|
|
575
573
|
|
|
576
574
|
const summary = summarizeTraffic(harness.requests, harness.handle);
|
|
577
|
-
const authorizationServerSummary = summarizeAuthorizationServerTraffic(
|
|
578
|
-
harness.handle
|
|
579
|
-
);
|
|
575
|
+
const authorizationServerSummary = summarizeAuthorizationServerTraffic(harness.handle);
|
|
580
576
|
expect(summary.register).toBe(0);
|
|
581
577
|
expect(summary.authorize).toBe(1);
|
|
582
578
|
expect(summary.tokenAuthorizationCode).toBe(1);
|
|
@@ -585,31 +581,32 @@ describe("HttpTransport OAuth integration", () => {
|
|
|
585
581
|
metadata: 1,
|
|
586
582
|
register: 0,
|
|
587
583
|
tokenAuthorizationCode: 1,
|
|
588
|
-
tokenRefresh: 0
|
|
584
|
+
tokenRefresh: 0
|
|
589
585
|
});
|
|
590
586
|
|
|
591
587
|
const authorizationRequest = harness.requests.find(
|
|
592
588
|
(request) =>
|
|
593
|
-
request.method === "GET"
|
|
594
|
-
|
|
595
|
-
);
|
|
596
|
-
expect(new URL(requireRequest(authorizationRequest, "authorization request").url).searchParams.get("client_id")).toBe(
|
|
597
|
-
"static-client"
|
|
589
|
+
request.method === "GET" &&
|
|
590
|
+
request.url.startsWith(`${harness.handle.oauth.issuer}/authorize`)
|
|
598
591
|
);
|
|
592
|
+
expect(
|
|
593
|
+
new URL(requireRequest(authorizationRequest, "authorization request").url).searchParams.get(
|
|
594
|
+
"client_id"
|
|
595
|
+
)
|
|
596
|
+
).toBe("static-client");
|
|
599
597
|
|
|
600
598
|
const tokenRequest = harness.requests.find((request) => {
|
|
601
|
-
if (
|
|
602
|
-
request.method !== "POST"
|
|
603
|
-
|| request.url !== `${harness.handle.oauth.issuer}/token`
|
|
604
|
-
) {
|
|
599
|
+
if (request.method !== "POST" || request.url !== `${harness.handle.oauth.issuer}/token`) {
|
|
605
600
|
return false;
|
|
606
601
|
}
|
|
607
602
|
|
|
608
603
|
return parseFormBody(request).get("grant_type") === "authorization_code";
|
|
609
604
|
});
|
|
610
|
-
expect(
|
|
611
|
-
"
|
|
612
|
-
|
|
605
|
+
expect(
|
|
606
|
+
parseFormBody(requireRequest(tokenRequest, "authorization-code token request")).get(
|
|
607
|
+
"client_id"
|
|
608
|
+
)
|
|
609
|
+
).toBe("static-client");
|
|
613
610
|
});
|
|
614
611
|
|
|
615
612
|
it("refreshes once after the current access token is revoked and the MCP server returns invalid_token", async () => {
|
|
@@ -620,8 +617,8 @@ describe("HttpTransport OAuth integration", () => {
|
|
|
620
617
|
await harness.client.callTool({
|
|
621
618
|
name: "echo",
|
|
622
619
|
arguments: {
|
|
623
|
-
text: "before-revoke"
|
|
624
|
-
}
|
|
620
|
+
text: "before-revoke"
|
|
621
|
+
}
|
|
625
622
|
});
|
|
626
623
|
|
|
627
624
|
const initialSession = getStoredSession(harness);
|
|
@@ -630,42 +627,37 @@ describe("HttpTransport OAuth integration", () => {
|
|
|
630
627
|
harness.handle.oauth.revoke(revokedAccessToken);
|
|
631
628
|
|
|
632
629
|
const baseline = summarizeTraffic(harness.requests, harness.handle);
|
|
633
|
-
const authorizationServerBaseline = summarizeAuthorizationServerTraffic(
|
|
634
|
-
harness.handle
|
|
635
|
-
);
|
|
630
|
+
const authorizationServerBaseline = summarizeAuthorizationServerTraffic(harness.handle);
|
|
636
631
|
const refreshedResult = await harness.client.callTool({
|
|
637
632
|
name: "echo",
|
|
638
633
|
arguments: {
|
|
639
|
-
text: "after-revoke"
|
|
640
|
-
}
|
|
634
|
+
text: "after-revoke"
|
|
635
|
+
}
|
|
641
636
|
});
|
|
642
637
|
|
|
643
638
|
expect(getTextContent(refreshedResult)).toBe("after-revoke");
|
|
644
639
|
|
|
645
640
|
const summary = summarizeTraffic(harness.requests, harness.handle);
|
|
646
|
-
const authorizationServerSummary = summarizeAuthorizationServerTraffic(
|
|
647
|
-
harness.handle
|
|
648
|
-
);
|
|
641
|
+
const authorizationServerSummary = summarizeAuthorizationServerTraffic(harness.handle);
|
|
649
642
|
expect(summary.authorize - baseline.authorize).toBe(0);
|
|
650
643
|
expect(summary.tokenRefresh - baseline.tokenRefresh).toBe(1);
|
|
651
644
|
expect(summary.mcpPost - baseline.mcpPost).toBe(2);
|
|
652
|
-
expect(
|
|
653
|
-
|
|
654
|
-
)
|
|
655
|
-
|
|
656
|
-
const refreshRequest = findLastRequest(harness.requests, (request) => {
|
|
657
|
-
if (
|
|
658
|
-
request.method !== "POST"
|
|
659
|
-
|| request.url !== `${harness.handle.oauth.issuer}/token`
|
|
660
|
-
) {
|
|
661
|
-
return false;
|
|
662
|
-
}
|
|
645
|
+
expect(authorizationServerSummary.tokenRefresh - authorizationServerBaseline.tokenRefresh).toBe(
|
|
646
|
+
1
|
|
647
|
+
);
|
|
663
648
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
649
|
+
const refreshRequest = findLastRequest(
|
|
650
|
+
harness.requests,
|
|
651
|
+
(request) => {
|
|
652
|
+
if (request.method !== "POST" || request.url !== `${harness.handle.oauth.issuer}/token`) {
|
|
653
|
+
return false;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return parseFormBody(request).get("grant_type") === "refresh_token";
|
|
657
|
+
},
|
|
658
|
+
"refresh token request"
|
|
668
659
|
);
|
|
660
|
+
expect(parseFormBody(refreshRequest).get("resource")).toBe(harness.handle.mcpUrl);
|
|
669
661
|
|
|
670
662
|
const updatedSession = getStoredSession(harness);
|
|
671
663
|
expect(updatedSession.tokens?.accessToken).toBeTruthy();
|
|
@@ -680,22 +672,20 @@ describe("HttpTransport OAuth integration", () => {
|
|
|
680
672
|
await harness.client.callTool({
|
|
681
673
|
name: "echo",
|
|
682
674
|
arguments: {
|
|
683
|
-
text: "seed-token"
|
|
684
|
-
}
|
|
675
|
+
text: "seed-token"
|
|
676
|
+
}
|
|
685
677
|
});
|
|
686
678
|
|
|
687
679
|
harness.setNow(80_000);
|
|
688
680
|
const baseline = summarizeTraffic(harness.requests, harness.handle);
|
|
689
|
-
const authorizationServerBaseline = summarizeAuthorizationServerTraffic(
|
|
690
|
-
harness.handle
|
|
691
|
-
);
|
|
681
|
+
const authorizationServerBaseline = summarizeAuthorizationServerTraffic(harness.handle);
|
|
692
682
|
const results = await Promise.all(
|
|
693
683
|
Array.from({ length: 5 }, (_, index) =>
|
|
694
684
|
harness.client.callTool({
|
|
695
685
|
name: "echo",
|
|
696
686
|
arguments: {
|
|
697
|
-
text: `parallel-${index}
|
|
698
|
-
}
|
|
687
|
+
text: `parallel-${index}`
|
|
688
|
+
}
|
|
699
689
|
})
|
|
700
690
|
)
|
|
701
691
|
);
|
|
@@ -705,44 +695,42 @@ describe("HttpTransport OAuth integration", () => {
|
|
|
705
695
|
"parallel-1",
|
|
706
696
|
"parallel-2",
|
|
707
697
|
"parallel-3",
|
|
708
|
-
"parallel-4"
|
|
698
|
+
"parallel-4"
|
|
709
699
|
]);
|
|
710
700
|
|
|
711
701
|
const summary = summarizeTraffic(harness.requests, harness.handle);
|
|
712
|
-
const authorizationServerSummary = summarizeAuthorizationServerTraffic(
|
|
713
|
-
harness.handle
|
|
714
|
-
);
|
|
702
|
+
const authorizationServerSummary = summarizeAuthorizationServerTraffic(harness.handle);
|
|
715
703
|
expect(summary.authorize - baseline.authorize).toBe(0);
|
|
716
704
|
expect(summary.tokenRefresh - baseline.tokenRefresh).toBe(1);
|
|
717
705
|
expect(summary.mcpPost - baseline.mcpPost).toBe(5);
|
|
718
|
-
expect(
|
|
719
|
-
|
|
720
|
-
)
|
|
706
|
+
expect(authorizationServerSummary.tokenRefresh - authorizationServerBaseline.tokenRefresh).toBe(
|
|
707
|
+
1
|
|
708
|
+
);
|
|
721
709
|
});
|
|
722
710
|
|
|
723
711
|
it("maps verifier audience mismatches to a typed OAuthError instead of a generic transport error", async () => {
|
|
724
712
|
const harness = await createHarness({
|
|
725
713
|
responseTransform: async ({ handle, record, response }) => {
|
|
726
714
|
if (
|
|
727
|
-
record.method !== "POST"
|
|
728
|
-
|
|
729
|
-
|
|
715
|
+
record.method !== "POST" ||
|
|
716
|
+
record.url !== `${handle.oauth.issuer}/token` ||
|
|
717
|
+
parseFormBody(record).get("grant_type") !== "authorization_code"
|
|
730
718
|
) {
|
|
731
719
|
return undefined;
|
|
732
720
|
}
|
|
733
721
|
|
|
734
|
-
const payload = await response.clone().json() as Record<string, unknown>;
|
|
722
|
+
const payload = (await response.clone().json()) as Record<string, unknown>;
|
|
735
723
|
const wrongAudienceToken = await handle.oauth.issueTokenFor({
|
|
736
724
|
clientId: parseFormBody(record).get("client_id") ?? "unknown-client",
|
|
737
725
|
resource: `${handle.mcpUrl}/wrong-audience`,
|
|
738
|
-
scopes: ["mcp.read"]
|
|
726
|
+
scopes: ["mcp.read"]
|
|
739
727
|
});
|
|
740
728
|
|
|
741
729
|
payload.access_token = wrongAudienceToken;
|
|
742
730
|
delete payload.refresh_token;
|
|
743
731
|
|
|
744
732
|
return cloneResponse(response, JSON.stringify(payload));
|
|
745
|
-
}
|
|
733
|
+
}
|
|
746
734
|
});
|
|
747
735
|
cleanups.add(harness.close);
|
|
748
736
|
|
|
@@ -758,7 +746,7 @@ describe("HttpTransport OAuth integration", () => {
|
|
|
758
746
|
expect(caughtError).toMatchObject({
|
|
759
747
|
error: "invalid_token",
|
|
760
748
|
errorDescription: "audience mismatch",
|
|
761
|
-
status: 401
|
|
749
|
+
status: 401
|
|
762
750
|
});
|
|
763
751
|
expect((caughtError as Error).message).toBe("audience mismatch");
|
|
764
752
|
});
|
|
@@ -767,25 +755,25 @@ describe("HttpTransport OAuth integration", () => {
|
|
|
767
755
|
const harness = await createHarness({
|
|
768
756
|
responseTransform: async ({ handle, record, response }) => {
|
|
769
757
|
if (
|
|
770
|
-
record.method !== "POST"
|
|
771
|
-
|
|
772
|
-
|
|
758
|
+
record.method !== "POST" ||
|
|
759
|
+
record.url !== `${handle.oauth.issuer}/token` ||
|
|
760
|
+
parseFormBody(record).get("grant_type") !== "authorization_code"
|
|
773
761
|
) {
|
|
774
762
|
return undefined;
|
|
775
763
|
}
|
|
776
764
|
|
|
777
|
-
const payload = await response.clone().json() as Record<string, unknown>;
|
|
765
|
+
const payload = (await response.clone().json()) as Record<string, unknown>;
|
|
778
766
|
const insufficientScopeToken = await handle.oauth.issueTokenFor({
|
|
779
767
|
clientId: parseFormBody(record).get("client_id") ?? "unknown-client",
|
|
780
768
|
resource: handle.mcpUrl,
|
|
781
|
-
scopes: ["mcp.write"]
|
|
769
|
+
scopes: ["mcp.write"]
|
|
782
770
|
});
|
|
783
771
|
|
|
784
772
|
payload.access_token = insufficientScopeToken;
|
|
785
773
|
delete payload.refresh_token;
|
|
786
774
|
|
|
787
775
|
return cloneResponse(response, JSON.stringify(payload));
|
|
788
|
-
}
|
|
776
|
+
}
|
|
789
777
|
});
|
|
790
778
|
cleanups.add(harness.close);
|
|
791
779
|
|
|
@@ -801,7 +789,7 @@ describe("HttpTransport OAuth integration", () => {
|
|
|
801
789
|
expect(caughtError).toMatchObject({
|
|
802
790
|
error: "insufficient_scope",
|
|
803
791
|
errorDescription: "insufficient scope",
|
|
804
|
-
status: 403
|
|
792
|
+
status: 403
|
|
805
793
|
});
|
|
806
794
|
expect((caughtError as Error).message).toBe("insufficient scope");
|
|
807
795
|
});
|