@frockbot/plugin-mcp 0.0.0 → 0.1.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/frockbot.json +183 -0
- package/package.json +41 -6
- package/src/agent.test.ts +409 -0
- package/src/agent.ts +516 -0
- package/src/backend.test.ts +333 -0
- package/src/backend.ts +490 -0
- package/src/connect-card.test.ts +226 -0
- package/src/index.ts +7 -0
- package/src/lifecycle-tools.test.ts +182 -0
- package/src/lifecycle-tools.ts +401 -0
- package/src/lifecycle.test.ts +504 -0
- package/src/manifest.ts +3 -0
- package/src/mcp-client.test.ts +389 -0
- package/src/mcp-client.ts +645 -0
- package/src/oauth-records.ts +330 -0
- package/src/oauth-user.test.ts +776 -0
- package/src/oauth.test.ts +433 -0
- package/src/oauth.ts +747 -0
- package/src/records.test.ts +331 -0
- package/src/records.ts +754 -0
- package/src/ssrf.test.ts +38 -0
- package/src/ssrf.ts +44 -0
- package/src/user.test.ts +390 -0
- package/src/user.ts +2068 -0
- package/tsconfig.json +15 -0
- package/README.md +0 -3
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
createPkcePairV1,
|
|
4
|
+
decodeMcpAuthorizationServerMetadataV1,
|
|
5
|
+
decodeMcpProtectedResourceMetadataV1,
|
|
6
|
+
McpAuthorizationError,
|
|
7
|
+
McpOAuthClient,
|
|
8
|
+
mcpAuthorizationServerMetadataUrlsV1,
|
|
9
|
+
mcpAuthorizeUrlV1,
|
|
10
|
+
mcpCanonicalResourceV1,
|
|
11
|
+
mcpCodeChallengeV1,
|
|
12
|
+
mcpResourceMetadataUrlsV1,
|
|
13
|
+
parseResourceMetadataChallengeV1,
|
|
14
|
+
} from "./oauth.js";
|
|
15
|
+
import type { McpFetch } from "./mcp-client.js";
|
|
16
|
+
|
|
17
|
+
const ISSUER = "https://auth.example.test";
|
|
18
|
+
|
|
19
|
+
function metadata(overrides: Record<string, unknown> = {}) {
|
|
20
|
+
return {
|
|
21
|
+
issuer: ISSUER,
|
|
22
|
+
authorization_endpoint: `${ISSUER}/authorize`,
|
|
23
|
+
token_endpoint: `${ISSUER}/token`,
|
|
24
|
+
registration_endpoint: `${ISSUER}/register`,
|
|
25
|
+
revocation_endpoint: `${ISSUER}/revoke`,
|
|
26
|
+
code_challenge_methods_supported: ["S256"],
|
|
27
|
+
...overrides,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("discovery URLs", () => {
|
|
32
|
+
test("insert the server's path after the well-known segment, then fall back", () => {
|
|
33
|
+
expect(
|
|
34
|
+
mcpResourceMetadataUrlsV1(new URL("https://mcp.example.test/tenant/mcp")),
|
|
35
|
+
).toEqual([
|
|
36
|
+
"https://mcp.example.test/.well-known/oauth-protected-resource/tenant/mcp",
|
|
37
|
+
"https://mcp.example.test/.well-known/oauth-protected-resource",
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("collapse to the two bare documents for a server at the origin", () => {
|
|
42
|
+
expect(
|
|
43
|
+
mcpResourceMetadataUrlsV1(new URL("https://mcp.example.test/")),
|
|
44
|
+
).toEqual([
|
|
45
|
+
"https://mcp.example.test/.well-known/oauth-protected-resource",
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("try RFC 8414 insertion, then both OpenID variants, for a path issuer", () => {
|
|
50
|
+
expect(
|
|
51
|
+
mcpAuthorizationServerMetadataUrlsV1(new URL(`${ISSUER}/tenant`)),
|
|
52
|
+
).toEqual([
|
|
53
|
+
`${ISSUER}/.well-known/oauth-authorization-server/tenant`,
|
|
54
|
+
`${ISSUER}/.well-known/openid-configuration/tenant`,
|
|
55
|
+
`${ISSUER}/tenant/.well-known/openid-configuration`,
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("protected resource metadata", () => {
|
|
61
|
+
test("is refused when it names no authorization server", () => {
|
|
62
|
+
expect(() =>
|
|
63
|
+
decodeMcpProtectedResourceMetadataV1({
|
|
64
|
+
resource: "https://mcp.example.test/mcp",
|
|
65
|
+
}),
|
|
66
|
+
).toThrow(/no authorization server/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("is refused when it names no resource", () => {
|
|
70
|
+
expect(() =>
|
|
71
|
+
decodeMcpProtectedResourceMetadataV1({
|
|
72
|
+
authorization_servers: [ISSUER],
|
|
73
|
+
}),
|
|
74
|
+
).toThrow(/no resource/);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("authorization server metadata", () => {
|
|
79
|
+
test("is refused when the issuer is not the one it was fetched for", () => {
|
|
80
|
+
expect(() =>
|
|
81
|
+
decodeMcpAuthorizationServerMetadataV1(
|
|
82
|
+
metadata({ issuer: "https://elsewhere.example.test" }),
|
|
83
|
+
ISSUER,
|
|
84
|
+
),
|
|
85
|
+
).toThrow(/not the issuer it was fetched for/);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("is refused when an endpoint sits on another origin", () => {
|
|
89
|
+
for (const key of [
|
|
90
|
+
"authorization_endpoint",
|
|
91
|
+
"token_endpoint",
|
|
92
|
+
"registration_endpoint",
|
|
93
|
+
"revocation_endpoint",
|
|
94
|
+
]) {
|
|
95
|
+
expect(() =>
|
|
96
|
+
decodeMcpAuthorizationServerMetadataV1(
|
|
97
|
+
metadata({ [key]: "https://elsewhere.example.test/x" }),
|
|
98
|
+
ISSUER,
|
|
99
|
+
),
|
|
100
|
+
).toThrow(/different origin from its issuer/);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("is refused when an endpoint is not https", () => {
|
|
105
|
+
expect(() =>
|
|
106
|
+
decodeMcpAuthorizationServerMetadataV1(
|
|
107
|
+
metadata({ token_endpoint: "http://auth.example.test/token" }),
|
|
108
|
+
ISSUER,
|
|
109
|
+
),
|
|
110
|
+
).toThrow(/token endpoint is invalid/);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("is refused when an endpoint names a private address", () => {
|
|
114
|
+
expect(() =>
|
|
115
|
+
decodeMcpAuthorizationServerMetadataV1(
|
|
116
|
+
{ ...metadata(), issuer: "https://127.0.0.1", token_endpoint: "x" },
|
|
117
|
+
"https://127.0.0.1",
|
|
118
|
+
),
|
|
119
|
+
).toThrow(/invalid/);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("is refused when the server does not advertise S256 PKCE", () => {
|
|
123
|
+
expect(() =>
|
|
124
|
+
decodeMcpAuthorizationServerMetadataV1(
|
|
125
|
+
metadata({ code_challenge_methods_supported: ["plain"] }),
|
|
126
|
+
ISSUER,
|
|
127
|
+
),
|
|
128
|
+
).toThrow(/S256/);
|
|
129
|
+
expect(() =>
|
|
130
|
+
decodeMcpAuthorizationServerMetadataV1(
|
|
131
|
+
metadata({ code_challenge_methods_supported: undefined }),
|
|
132
|
+
ISSUER,
|
|
133
|
+
),
|
|
134
|
+
).toThrow(/S256/);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("decodes a well-formed document", () => {
|
|
138
|
+
expect(
|
|
139
|
+
decodeMcpAuthorizationServerMetadataV1(metadata(), ISSUER),
|
|
140
|
+
).toMatchObject({
|
|
141
|
+
issuer: ISSUER,
|
|
142
|
+
tokenEndpoint: `${ISSUER}/token`,
|
|
143
|
+
revocationEndpoint: `${ISSUER}/revoke`,
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("PKCE", () => {
|
|
149
|
+
test("produces an S256 challenge that is the base64url SHA-256 of the verifier", async () => {
|
|
150
|
+
const pair = await createPkcePairV1();
|
|
151
|
+
expect(pair.codeChallengeMethod).toBe("S256");
|
|
152
|
+
expect(pair.codeVerifier).toMatch(/^[A-Za-z0-9_-]{43,128}$/);
|
|
153
|
+
expect(pair.codeChallenge).toMatch(/^[A-Za-z0-9_-]{43}$/);
|
|
154
|
+
expect(await mcpCodeChallengeV1(pair.codeVerifier)).toBe(
|
|
155
|
+
pair.codeChallenge,
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("matches the RFC 7636 appendix B vector", async () => {
|
|
160
|
+
expect(
|
|
161
|
+
await mcpCodeChallengeV1("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"),
|
|
162
|
+
).toBe("E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("gives a different verifier every time", async () => {
|
|
166
|
+
const first = await createPkcePairV1();
|
|
167
|
+
const second = await createPkcePairV1();
|
|
168
|
+
expect(first.codeVerifier).not.toBe(second.codeVerifier);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("the resource indicator", () => {
|
|
173
|
+
test("is the canonical server URI: lowercased, no query, no trailing slash", () => {
|
|
174
|
+
expect(
|
|
175
|
+
mcpCanonicalResourceV1(new URL("HTTPS://MCP.Example.Test/mcp/?x=1#y")),
|
|
176
|
+
).toBe("https://mcp.example.test/mcp");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("is on the authorization request, beside the PKCE challenge", () => {
|
|
180
|
+
const url = new URL(
|
|
181
|
+
mcpAuthorizeUrlV1({
|
|
182
|
+
authorizationEndpoint: `${ISSUER}/authorize`,
|
|
183
|
+
clientId: "client-1",
|
|
184
|
+
redirectUri: "https://bot.example.test/callback",
|
|
185
|
+
state: "signed-state",
|
|
186
|
+
codeChallenge: "challenge",
|
|
187
|
+
resource: "https://mcp.example.test/mcp",
|
|
188
|
+
scope: "mcp:tools",
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
expect(Object.fromEntries(url.searchParams)).toEqual({
|
|
192
|
+
response_type: "code",
|
|
193
|
+
client_id: "client-1",
|
|
194
|
+
redirect_uri: "https://bot.example.test/callback",
|
|
195
|
+
state: "signed-state",
|
|
196
|
+
code_challenge: "challenge",
|
|
197
|
+
code_challenge_method: "S256",
|
|
198
|
+
resource: "https://mcp.example.test/mcp",
|
|
199
|
+
scope: "mcp:tools",
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("the WWW-Authenticate challenge", () => {
|
|
205
|
+
test("yields the resource_metadata URL a 401 named", () => {
|
|
206
|
+
expect(
|
|
207
|
+
parseResourceMetadataChallengeV1(
|
|
208
|
+
'Bearer realm="mcp", resource_metadata="https://mcp.example.test/.well-known/oauth-protected-resource/mcp"',
|
|
209
|
+
),
|
|
210
|
+
).toBe("https://mcp.example.test/.well-known/oauth-protected-resource/mcp");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("is undefined when the header names none", () => {
|
|
214
|
+
expect(
|
|
215
|
+
parseResourceMetadataChallengeV1('Bearer realm="mcp"'),
|
|
216
|
+
).toBeUndefined();
|
|
217
|
+
expect(parseResourceMetadataChallengeV1(null)).toBeUndefined();
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
/** A fake authorization server that records every request it was sent. */
|
|
222
|
+
function authorizationServer(options: {
|
|
223
|
+
register?: Record<string, unknown>;
|
|
224
|
+
token?: Record<string, unknown>;
|
|
225
|
+
tokenStatus?: number;
|
|
226
|
+
revokeStatus?: number;
|
|
227
|
+
}) {
|
|
228
|
+
const seen: Array<{ url: string; body: string; method: string }> = [];
|
|
229
|
+
const fetchImpl: McpFetch = async (input, init) => {
|
|
230
|
+
const url = String(input);
|
|
231
|
+
const body =
|
|
232
|
+
typeof init?.body === "string" ? init.body : String(init?.body ?? "");
|
|
233
|
+
seen.push({ url, body, method: init?.method ?? "GET" });
|
|
234
|
+
if (url.endsWith("/.well-known/oauth-protected-resource/mcp")) {
|
|
235
|
+
return Response.json({
|
|
236
|
+
resource: "https://mcp.example.test/mcp",
|
|
237
|
+
authorization_servers: [ISSUER],
|
|
238
|
+
scopes_supported: ["mcp:tools"],
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
if (url.endsWith("/.well-known/oauth-authorization-server")) {
|
|
242
|
+
return Response.json(metadata());
|
|
243
|
+
}
|
|
244
|
+
if (url.endsWith("/register")) {
|
|
245
|
+
return Response.json(options.register ?? { client_id: "client-1" }, {
|
|
246
|
+
status: 201,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
if (url.endsWith("/token")) {
|
|
250
|
+
return Response.json(
|
|
251
|
+
options.token ?? {
|
|
252
|
+
access_token: "access-1",
|
|
253
|
+
token_type: "Bearer",
|
|
254
|
+
expires_in: 3_600,
|
|
255
|
+
refresh_token: "refresh-1",
|
|
256
|
+
},
|
|
257
|
+
{ status: options.tokenStatus ?? 200 },
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
if (url.endsWith("/revoke")) {
|
|
261
|
+
return new Response(null, { status: options.revokeStatus ?? 200 });
|
|
262
|
+
}
|
|
263
|
+
return new Response("not found", { status: 404 });
|
|
264
|
+
};
|
|
265
|
+
return { seen, client: new McpOAuthClient({ fetch: fetchImpl }) };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
describe("the OAuth client", () => {
|
|
269
|
+
test("discovers the resource and its authorization server", async () => {
|
|
270
|
+
const { client } = authorizationServer({});
|
|
271
|
+
const resource = await client.discoverProtectedResource({
|
|
272
|
+
serverUrl: new URL("https://mcp.example.test/mcp"),
|
|
273
|
+
});
|
|
274
|
+
expect(resource.authorizationServers).toEqual([ISSUER]);
|
|
275
|
+
const server = await client.discoverAuthorizationServer(ISSUER);
|
|
276
|
+
expect(server.tokenEndpoint).toBe(`${ISSUER}/token`);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("refuses metadata that describes a different resource", async () => {
|
|
280
|
+
const fetchImpl: McpFetch = async () =>
|
|
281
|
+
Response.json({
|
|
282
|
+
resource: "https://other.example.test/mcp",
|
|
283
|
+
authorization_servers: [ISSUER],
|
|
284
|
+
});
|
|
285
|
+
const client = new McpOAuthClient({ fetch: fetchImpl });
|
|
286
|
+
await expect(
|
|
287
|
+
client.discoverProtectedResource({
|
|
288
|
+
serverUrl: new URL("https://mcp.example.test/mcp"),
|
|
289
|
+
}),
|
|
290
|
+
).rejects.toThrow(/not the server it was fetched for/);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("registers as a public client", async () => {
|
|
294
|
+
const { client, seen } = authorizationServer({});
|
|
295
|
+
expect(
|
|
296
|
+
await client.register({
|
|
297
|
+
registrationEndpoint: `${ISSUER}/register`,
|
|
298
|
+
redirectUri: "https://bot.example.test/callback",
|
|
299
|
+
}),
|
|
300
|
+
).toEqual({ clientId: "client-1" });
|
|
301
|
+
const body = JSON.parse(seen.at(-1)!.body) as Record<string, unknown>;
|
|
302
|
+
expect(body).toMatchObject({
|
|
303
|
+
token_endpoint_auth_method: "none",
|
|
304
|
+
redirect_uris: ["https://bot.example.test/callback"],
|
|
305
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
306
|
+
});
|
|
307
|
+
expect(body).not.toHaveProperty("client_secret");
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("refuses a confidential client durably, with its own code", async () => {
|
|
311
|
+
const { client } = authorizationServer({
|
|
312
|
+
register: { client_id: "client-1", client_secret: "s3cret" },
|
|
313
|
+
});
|
|
314
|
+
const failure = await client
|
|
315
|
+
.register({
|
|
316
|
+
registrationEndpoint: `${ISSUER}/register`,
|
|
317
|
+
redirectUri: "https://bot.example.test/callback",
|
|
318
|
+
})
|
|
319
|
+
.catch((error: unknown) => error);
|
|
320
|
+
expect(failure).toBeInstanceOf(McpAuthorizationError);
|
|
321
|
+
expect((failure as McpAuthorizationError).code).toBe(
|
|
322
|
+
"unsupported-client-authentication",
|
|
323
|
+
);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("sends the verifier and the resource on the code exchange", async () => {
|
|
327
|
+
const { client, seen } = authorizationServer({});
|
|
328
|
+
const tokens = await client.exchangeCode({
|
|
329
|
+
tokenEndpoint: `${ISSUER}/token`,
|
|
330
|
+
clientId: "client-1",
|
|
331
|
+
code: "code-1",
|
|
332
|
+
codeVerifier: "verifier-1",
|
|
333
|
+
redirectUri: "https://bot.example.test/callback",
|
|
334
|
+
resource: "https://mcp.example.test/mcp",
|
|
335
|
+
});
|
|
336
|
+
expect(tokens.accessToken).toBe("access-1");
|
|
337
|
+
expect(tokens.refreshToken).toBe("refresh-1");
|
|
338
|
+
expect(tokens.expiresAt).toBeGreaterThan(Date.now());
|
|
339
|
+
const form = Object.fromEntries(new URLSearchParams(seen.at(-1)!.body));
|
|
340
|
+
expect(form).toEqual({
|
|
341
|
+
grant_type: "authorization_code",
|
|
342
|
+
code: "code-1",
|
|
343
|
+
redirect_uri: "https://bot.example.test/callback",
|
|
344
|
+
client_id: "client-1",
|
|
345
|
+
code_verifier: "verifier-1",
|
|
346
|
+
resource: "https://mcp.example.test/mcp",
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("sends the resource on the refresh too, so the new token is bound as well", async () => {
|
|
351
|
+
const { client, seen } = authorizationServer({});
|
|
352
|
+
await client.refresh({
|
|
353
|
+
tokenEndpoint: `${ISSUER}/token`,
|
|
354
|
+
clientId: "client-1",
|
|
355
|
+
refreshToken: "refresh-1",
|
|
356
|
+
resource: "https://mcp.example.test/mcp",
|
|
357
|
+
scope: "mcp:tools",
|
|
358
|
+
});
|
|
359
|
+
const form = Object.fromEntries(new URLSearchParams(seen.at(-1)!.body));
|
|
360
|
+
expect(form).toEqual({
|
|
361
|
+
grant_type: "refresh_token",
|
|
362
|
+
refresh_token: "refresh-1",
|
|
363
|
+
client_id: "client-1",
|
|
364
|
+
resource: "https://mcp.example.test/mcp",
|
|
365
|
+
scope: "mcp:tools",
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("reports a refused token request rather than inventing a token", async () => {
|
|
370
|
+
const { client } = authorizationServer({
|
|
371
|
+
tokenStatus: 400,
|
|
372
|
+
token: { error: "invalid_grant", error_description: "code is spent" },
|
|
373
|
+
});
|
|
374
|
+
await expect(
|
|
375
|
+
client.exchangeCode({
|
|
376
|
+
tokenEndpoint: `${ISSUER}/token`,
|
|
377
|
+
clientId: "client-1",
|
|
378
|
+
code: "code-1",
|
|
379
|
+
codeVerifier: "verifier-1",
|
|
380
|
+
redirectUri: "https://bot.example.test/callback",
|
|
381
|
+
resource: "https://mcp.example.test/mcp",
|
|
382
|
+
}),
|
|
383
|
+
).rejects.toThrow(/invalid_grant: code is spent/);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("counts an unknown token as revoked, and a server error as not", async () => {
|
|
387
|
+
const revoke = (status: number) =>
|
|
388
|
+
authorizationServer({ revokeStatus: status }).client.revoke({
|
|
389
|
+
revocationEndpoint: `${ISSUER}/revoke`,
|
|
390
|
+
token: "refresh-1",
|
|
391
|
+
tokenTypeHint: "refresh_token",
|
|
392
|
+
clientId: "client-1",
|
|
393
|
+
});
|
|
394
|
+
expect(await revoke(200)).toBe(true);
|
|
395
|
+
expect(await revoke(503)).toBe(false);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("sends the RFC 7009 form on revocation", async () => {
|
|
399
|
+
const { client, seen } = authorizationServer({});
|
|
400
|
+
await client.revoke({
|
|
401
|
+
revocationEndpoint: `${ISSUER}/revoke`,
|
|
402
|
+
token: "refresh-1",
|
|
403
|
+
tokenTypeHint: "refresh_token",
|
|
404
|
+
clientId: "client-1",
|
|
405
|
+
});
|
|
406
|
+
expect(Object.fromEntries(new URLSearchParams(seen.at(-1)!.body))).toEqual({
|
|
407
|
+
token: "refresh-1",
|
|
408
|
+
token_type_hint: "refresh_token",
|
|
409
|
+
client_id: "client-1",
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test("refuses a metadata document past the byte ceiling", async () => {
|
|
414
|
+
const oversized = JSON.stringify({
|
|
415
|
+
resource: "https://mcp.example.test/mcp",
|
|
416
|
+
authorization_servers: [ISSUER],
|
|
417
|
+
padding: "x".repeat(4_000),
|
|
418
|
+
});
|
|
419
|
+
const fetchImpl: McpFetch = async () =>
|
|
420
|
+
new Response(oversized, {
|
|
421
|
+
headers: { "content-type": "application/json" },
|
|
422
|
+
});
|
|
423
|
+
const client = new McpOAuthClient({
|
|
424
|
+
fetch: fetchImpl,
|
|
425
|
+
maxMetadataBytes: 512,
|
|
426
|
+
});
|
|
427
|
+
await expect(
|
|
428
|
+
client.discoverProtectedResource({
|
|
429
|
+
serverUrl: new URL("https://mcp.example.test/mcp"),
|
|
430
|
+
}),
|
|
431
|
+
).rejects.toThrow(/too large/);
|
|
432
|
+
});
|
|
433
|
+
});
|