mcp-proxy 5.8.0 → 5.9.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/dist/bin/mcp-proxy.js +1 -1
- package/dist/index.d.ts +23 -3
- package/dist/index.js +2 -2
- package/dist/{stdio-so1-I7Pn.js → stdio-CsjPjeWC.js} +82 -35
- package/dist/stdio-CsjPjeWC.js.map +1 -0
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/authentication.test.ts +77 -2
- package/src/authentication.ts +16 -4
- package/src/index.ts +2 -0
- package/src/startHTTPServer.test.ts +494 -0
- package/src/startHTTPServer.ts +99 -5
- package/dist/stdio-so1-I7Pn.js.map +0 -1
package/jsr.json
CHANGED
package/package.json
CHANGED
|
@@ -103,15 +103,90 @@ describe("AuthenticationMiddleware", () => {
|
|
|
103
103
|
expect(body.id).toBe(null);
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
-
it("should have consistent format regardless of configuration", () => {
|
|
106
|
+
it("should have consistent body format regardless of configuration", () => {
|
|
107
107
|
const middleware1 = new AuthenticationMiddleware({});
|
|
108
108
|
const middleware2 = new AuthenticationMiddleware({ apiKey: "test" });
|
|
109
109
|
|
|
110
110
|
const response1 = middleware1.getUnauthorizedResponse();
|
|
111
111
|
const response2 = middleware2.getUnauthorizedResponse();
|
|
112
112
|
|
|
113
|
-
expect(response1.headers).toEqual(response2.headers);
|
|
114
113
|
expect(response1.body).toEqual(response2.body);
|
|
115
114
|
});
|
|
115
|
+
|
|
116
|
+
it("should not include WWW-Authenticate header without OAuth config", () => {
|
|
117
|
+
const middleware = new AuthenticationMiddleware({ apiKey: "test" });
|
|
118
|
+
const response = middleware.getUnauthorizedResponse();
|
|
119
|
+
|
|
120
|
+
expect(response.headers["WWW-Authenticate"]).toBeUndefined();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should include WWW-Authenticate header with OAuth config", () => {
|
|
124
|
+
const middleware = new AuthenticationMiddleware({
|
|
125
|
+
apiKey: "test",
|
|
126
|
+
oauth: {
|
|
127
|
+
protectedResource: {
|
|
128
|
+
resource: "https://example.com",
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
const response = middleware.getUnauthorizedResponse();
|
|
133
|
+
|
|
134
|
+
expect(response.headers["WWW-Authenticate"]).toBe(
|
|
135
|
+
'Bearer resource_metadata="https://example.com/.well-known/oauth-protected-resource"',
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("should handle OAuth config with trailing slash in resource URL", () => {
|
|
140
|
+
const middleware = new AuthenticationMiddleware({
|
|
141
|
+
apiKey: "test",
|
|
142
|
+
oauth: {
|
|
143
|
+
protectedResource: {
|
|
144
|
+
resource: "https://example.com/",
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
const response = middleware.getUnauthorizedResponse();
|
|
149
|
+
|
|
150
|
+
expect(response.headers["WWW-Authenticate"]).toBe(
|
|
151
|
+
'Bearer resource_metadata="https://example.com//.well-known/oauth-protected-resource"',
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("should not include WWW-Authenticate header when OAuth config is empty", () => {
|
|
156
|
+
const middleware = new AuthenticationMiddleware({
|
|
157
|
+
apiKey: "test",
|
|
158
|
+
oauth: {},
|
|
159
|
+
});
|
|
160
|
+
const response = middleware.getUnauthorizedResponse();
|
|
161
|
+
|
|
162
|
+
expect(response.headers["WWW-Authenticate"]).toBeUndefined();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("should not include WWW-Authenticate header when protectedResource is empty", () => {
|
|
166
|
+
const middleware = new AuthenticationMiddleware({
|
|
167
|
+
apiKey: "test",
|
|
168
|
+
oauth: {
|
|
169
|
+
protectedResource: {},
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
const response = middleware.getUnauthorizedResponse();
|
|
173
|
+
|
|
174
|
+
expect(response.headers["WWW-Authenticate"]).toBeUndefined();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("should include WWW-Authenticate header with OAuth config but no apiKey", () => {
|
|
178
|
+
const middleware = new AuthenticationMiddleware({
|
|
179
|
+
oauth: {
|
|
180
|
+
protectedResource: {
|
|
181
|
+
resource: "https://example.com",
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
const response = middleware.getUnauthorizedResponse();
|
|
186
|
+
|
|
187
|
+
expect(response.headers["WWW-Authenticate"]).toBe(
|
|
188
|
+
'Bearer resource_metadata="https://example.com/.well-known/oauth-protected-resource"',
|
|
189
|
+
);
|
|
190
|
+
});
|
|
116
191
|
});
|
|
117
192
|
});
|
package/src/authentication.ts
CHANGED
|
@@ -2,12 +2,26 @@ import type { IncomingMessage } from "http";
|
|
|
2
2
|
|
|
3
3
|
export interface AuthConfig {
|
|
4
4
|
apiKey?: string;
|
|
5
|
+
oauth?: {
|
|
6
|
+
protectedResource?: {
|
|
7
|
+
resource?: string;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
5
10
|
}
|
|
6
11
|
|
|
7
12
|
export class AuthenticationMiddleware {
|
|
8
13
|
constructor(private config: AuthConfig = {}) {}
|
|
9
14
|
|
|
10
|
-
getUnauthorizedResponse() {
|
|
15
|
+
getUnauthorizedResponse(): { body: string; headers: Record<string, string> } {
|
|
16
|
+
const headers: Record<string, string> = {
|
|
17
|
+
"Content-Type": "application/json",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Add WWW-Authenticate header if OAuth config is available
|
|
21
|
+
if (this.config.oauth?.protectedResource?.resource) {
|
|
22
|
+
headers["WWW-Authenticate"] = `Bearer resource_metadata="${this.config.oauth.protectedResource.resource}/.well-known/oauth-protected-resource"`;
|
|
23
|
+
}
|
|
24
|
+
|
|
11
25
|
return {
|
|
12
26
|
body: JSON.stringify({
|
|
13
27
|
error: {
|
|
@@ -17,9 +31,7 @@ export class AuthenticationMiddleware {
|
|
|
17
31
|
id: null,
|
|
18
32
|
jsonrpc: "2.0",
|
|
19
33
|
}),
|
|
20
|
-
headers
|
|
21
|
-
"Content-Type": "application/json",
|
|
22
|
-
},
|
|
34
|
+
headers,
|
|
23
35
|
};
|
|
24
36
|
}
|
|
25
37
|
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export type { AuthConfig } from "./authentication.js";
|
|
2
|
+
export { AuthenticationMiddleware } from "./authentication.js";
|
|
1
3
|
export { InMemoryEventStore } from "./InMemoryEventStore.js";
|
|
2
4
|
export { proxyServer } from "./proxyServer.js";
|
|
3
5
|
export { startHTTPServer } from "./startHTTPServer.js";
|
|
@@ -1181,3 +1181,497 @@ it("includes Authorization in CORS allowed headers", async () => {
|
|
|
1181
1181
|
|
|
1182
1182
|
await httpServer.close();
|
|
1183
1183
|
});
|
|
1184
|
+
|
|
1185
|
+
// Tests for FastMCP-style authentication with { authenticated: false } pattern
|
|
1186
|
+
|
|
1187
|
+
it("returns 401 when authenticate callback returns { authenticated: false } in stateless mode", async () => {
|
|
1188
|
+
const stdioTransport = new StdioClientTransport({
|
|
1189
|
+
args: ["src/fixtures/simple-stdio-server.ts"],
|
|
1190
|
+
command: "tsx",
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
const stdioClient = new Client(
|
|
1194
|
+
{
|
|
1195
|
+
name: "mcp-proxy",
|
|
1196
|
+
version: "1.0.0",
|
|
1197
|
+
},
|
|
1198
|
+
{
|
|
1199
|
+
capabilities: {},
|
|
1200
|
+
},
|
|
1201
|
+
);
|
|
1202
|
+
|
|
1203
|
+
await stdioClient.connect(stdioTransport);
|
|
1204
|
+
|
|
1205
|
+
const serverVersion = stdioClient.getServerVersion() as {
|
|
1206
|
+
name: string;
|
|
1207
|
+
version: string;
|
|
1208
|
+
};
|
|
1209
|
+
|
|
1210
|
+
const serverCapabilities = stdioClient.getServerCapabilities() as {
|
|
1211
|
+
capabilities: Record<string, unknown>;
|
|
1212
|
+
};
|
|
1213
|
+
|
|
1214
|
+
const port = await getRandomPort();
|
|
1215
|
+
|
|
1216
|
+
// Mock authenticate callback that returns { authenticated: false }
|
|
1217
|
+
const authenticate = vi.fn().mockResolvedValue({
|
|
1218
|
+
authenticated: false,
|
|
1219
|
+
error: "Invalid JWT token",
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
const httpServer = await startHTTPServer({
|
|
1223
|
+
authenticate,
|
|
1224
|
+
createServer: async () => {
|
|
1225
|
+
const mcpServer = new Server(serverVersion, {
|
|
1226
|
+
capabilities: serverCapabilities,
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
await proxyServer({
|
|
1230
|
+
client: stdioClient,
|
|
1231
|
+
server: mcpServer,
|
|
1232
|
+
serverCapabilities,
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
return mcpServer;
|
|
1236
|
+
},
|
|
1237
|
+
port,
|
|
1238
|
+
stateless: true,
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
// Create client with invalid Bearer token
|
|
1242
|
+
const streamTransport = new StreamableHTTPClientTransport(
|
|
1243
|
+
new URL(`http://localhost:${port}/mcp`),
|
|
1244
|
+
{
|
|
1245
|
+
requestInit: {
|
|
1246
|
+
headers: {
|
|
1247
|
+
Authorization: "Bearer invalid-jwt-token",
|
|
1248
|
+
},
|
|
1249
|
+
},
|
|
1250
|
+
},
|
|
1251
|
+
);
|
|
1252
|
+
|
|
1253
|
+
const streamClient = new Client(
|
|
1254
|
+
{
|
|
1255
|
+
name: "stream-client-auth-false",
|
|
1256
|
+
version: "1.0.0",
|
|
1257
|
+
},
|
|
1258
|
+
{
|
|
1259
|
+
capabilities: {},
|
|
1260
|
+
},
|
|
1261
|
+
);
|
|
1262
|
+
|
|
1263
|
+
// Connection should fail due to authentication returning false
|
|
1264
|
+
await expect(streamClient.connect(streamTransport)).rejects.toThrow();
|
|
1265
|
+
|
|
1266
|
+
// Verify authenticate callback was called
|
|
1267
|
+
expect(authenticate).toHaveBeenCalled();
|
|
1268
|
+
|
|
1269
|
+
await httpServer.close();
|
|
1270
|
+
await stdioClient.close();
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
it("returns 401 with custom error message when { authenticated: false, error: '...' }", async () => {
|
|
1274
|
+
const stdioTransport = new StdioClientTransport({
|
|
1275
|
+
args: ["src/fixtures/simple-stdio-server.ts"],
|
|
1276
|
+
command: "tsx",
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
const stdioClient = new Client(
|
|
1280
|
+
{
|
|
1281
|
+
name: "mcp-proxy",
|
|
1282
|
+
version: "1.0.0",
|
|
1283
|
+
},
|
|
1284
|
+
{
|
|
1285
|
+
capabilities: {},
|
|
1286
|
+
},
|
|
1287
|
+
);
|
|
1288
|
+
|
|
1289
|
+
await stdioClient.connect(stdioTransport);
|
|
1290
|
+
|
|
1291
|
+
const serverVersion = stdioClient.getServerVersion() as {
|
|
1292
|
+
name: string;
|
|
1293
|
+
version: string;
|
|
1294
|
+
};
|
|
1295
|
+
|
|
1296
|
+
const serverCapabilities = stdioClient.getServerCapabilities() as {
|
|
1297
|
+
capabilities: Record<string, unknown>;
|
|
1298
|
+
};
|
|
1299
|
+
|
|
1300
|
+
const port = await getRandomPort();
|
|
1301
|
+
|
|
1302
|
+
const customErrorMessage = "Token expired at 2025-10-06T12:00:00Z";
|
|
1303
|
+
|
|
1304
|
+
// Mock authenticate callback with custom error message
|
|
1305
|
+
const authenticate = vi.fn().mockResolvedValue({
|
|
1306
|
+
authenticated: false,
|
|
1307
|
+
error: customErrorMessage,
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
const httpServer = await startHTTPServer({
|
|
1311
|
+
authenticate,
|
|
1312
|
+
createServer: async () => {
|
|
1313
|
+
const mcpServer = new Server(serverVersion, {
|
|
1314
|
+
capabilities: serverCapabilities,
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
await proxyServer({
|
|
1318
|
+
client: stdioClient,
|
|
1319
|
+
server: mcpServer,
|
|
1320
|
+
serverCapabilities,
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
return mcpServer;
|
|
1324
|
+
},
|
|
1325
|
+
port,
|
|
1326
|
+
stateless: true,
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
// Make request directly with fetch to check error message
|
|
1330
|
+
const response = await fetch(`http://localhost:${port}/mcp`, {
|
|
1331
|
+
body: JSON.stringify({
|
|
1332
|
+
id: 1,
|
|
1333
|
+
jsonrpc: "2.0",
|
|
1334
|
+
method: "initialize",
|
|
1335
|
+
params: {
|
|
1336
|
+
capabilities: {},
|
|
1337
|
+
clientInfo: { name: "test", version: "1.0.0" },
|
|
1338
|
+
protocolVersion: "2024-11-05",
|
|
1339
|
+
},
|
|
1340
|
+
}),
|
|
1341
|
+
headers: {
|
|
1342
|
+
"Accept": "application/json, text/event-stream",
|
|
1343
|
+
"Authorization": "Bearer expired-token",
|
|
1344
|
+
"Content-Type": "application/json",
|
|
1345
|
+
},
|
|
1346
|
+
method: "POST",
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
expect(response.status).toBe(401);
|
|
1350
|
+
|
|
1351
|
+
const errorResponse = (await response.json()) as {
|
|
1352
|
+
error: { code: number; message: string };
|
|
1353
|
+
id: null | number;
|
|
1354
|
+
jsonrpc: string;
|
|
1355
|
+
};
|
|
1356
|
+
expect(errorResponse.error.message).toBe(customErrorMessage);
|
|
1357
|
+
|
|
1358
|
+
await httpServer.close();
|
|
1359
|
+
await stdioClient.close();
|
|
1360
|
+
});
|
|
1361
|
+
|
|
1362
|
+
it("returns 401 when createServer throws authentication error", async () => {
|
|
1363
|
+
const stdioTransport = new StdioClientTransport({
|
|
1364
|
+
args: ["src/fixtures/simple-stdio-server.ts"],
|
|
1365
|
+
command: "tsx",
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
const stdioClient = new Client(
|
|
1369
|
+
{
|
|
1370
|
+
name: "mcp-proxy",
|
|
1371
|
+
version: "1.0.0",
|
|
1372
|
+
},
|
|
1373
|
+
{
|
|
1374
|
+
capabilities: {},
|
|
1375
|
+
},
|
|
1376
|
+
);
|
|
1377
|
+
|
|
1378
|
+
await stdioClient.connect(stdioTransport);
|
|
1379
|
+
|
|
1380
|
+
const port = await getRandomPort();
|
|
1381
|
+
|
|
1382
|
+
// Mock authenticate that passes, but createServer throws auth error
|
|
1383
|
+
const authenticate = vi.fn().mockResolvedValue({
|
|
1384
|
+
authenticated: true,
|
|
1385
|
+
session: { userId: "test" },
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
const httpServer = await startHTTPServer({
|
|
1389
|
+
authenticate,
|
|
1390
|
+
createServer: async () => {
|
|
1391
|
+
// Simulate FastMCP throwing error for authenticated: false
|
|
1392
|
+
throw new Error("Authentication failed: Invalid JWT payload");
|
|
1393
|
+
},
|
|
1394
|
+
port,
|
|
1395
|
+
stateless: true,
|
|
1396
|
+
});
|
|
1397
|
+
|
|
1398
|
+
// Make request
|
|
1399
|
+
const response = await fetch(`http://localhost:${port}/mcp`, {
|
|
1400
|
+
body: JSON.stringify({
|
|
1401
|
+
id: 1,
|
|
1402
|
+
jsonrpc: "2.0",
|
|
1403
|
+
method: "initialize",
|
|
1404
|
+
params: {
|
|
1405
|
+
capabilities: {},
|
|
1406
|
+
clientInfo: { name: "test", version: "1.0.0" },
|
|
1407
|
+
protocolVersion: "2024-11-05",
|
|
1408
|
+
},
|
|
1409
|
+
}),
|
|
1410
|
+
headers: {
|
|
1411
|
+
"Accept": "application/json, text/event-stream",
|
|
1412
|
+
"Authorization": "Bearer test-token",
|
|
1413
|
+
"Content-Type": "application/json",
|
|
1414
|
+
},
|
|
1415
|
+
method: "POST",
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
expect(response.status).toBe(401);
|
|
1419
|
+
|
|
1420
|
+
const errorResponse = (await response.json()) as {
|
|
1421
|
+
error: { code: number; message: string };
|
|
1422
|
+
id: null | number;
|
|
1423
|
+
jsonrpc: string;
|
|
1424
|
+
};
|
|
1425
|
+
expect(errorResponse.error.message).toContain("Authentication failed");
|
|
1426
|
+
|
|
1427
|
+
await httpServer.close();
|
|
1428
|
+
await stdioClient.close();
|
|
1429
|
+
});
|
|
1430
|
+
|
|
1431
|
+
it("returns 401 when createServer throws JWT-related error", async () => {
|
|
1432
|
+
const port = await getRandomPort();
|
|
1433
|
+
|
|
1434
|
+
const httpServer = await startHTTPServer({
|
|
1435
|
+
createServer: async () => {
|
|
1436
|
+
throw new Error("Invalid JWT signature");
|
|
1437
|
+
},
|
|
1438
|
+
port,
|
|
1439
|
+
stateless: true,
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
const response = await fetch(`http://localhost:${port}/mcp`, {
|
|
1443
|
+
body: JSON.stringify({
|
|
1444
|
+
id: 1,
|
|
1445
|
+
jsonrpc: "2.0",
|
|
1446
|
+
method: "initialize",
|
|
1447
|
+
params: {
|
|
1448
|
+
capabilities: {},
|
|
1449
|
+
clientInfo: { name: "test", version: "1.0.0" },
|
|
1450
|
+
protocolVersion: "2024-11-05",
|
|
1451
|
+
},
|
|
1452
|
+
}),
|
|
1453
|
+
headers: {
|
|
1454
|
+
"Accept": "application/json, text/event-stream",
|
|
1455
|
+
"Content-Type": "application/json",
|
|
1456
|
+
},
|
|
1457
|
+
method: "POST",
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1460
|
+
expect(response.status).toBe(401);
|
|
1461
|
+
|
|
1462
|
+
const errorResponse = (await response.json()) as {
|
|
1463
|
+
error: { code: number; message: string };
|
|
1464
|
+
id: null | number;
|
|
1465
|
+
jsonrpc: string;
|
|
1466
|
+
};
|
|
1467
|
+
expect(errorResponse.error.message).toContain("Invalid JWT");
|
|
1468
|
+
|
|
1469
|
+
await httpServer.close();
|
|
1470
|
+
});
|
|
1471
|
+
|
|
1472
|
+
it("returns 401 when createServer throws Token-related error", async () => {
|
|
1473
|
+
const port = await getRandomPort();
|
|
1474
|
+
|
|
1475
|
+
const httpServer = await startHTTPServer({
|
|
1476
|
+
createServer: async () => {
|
|
1477
|
+
throw new Error("Token has been revoked");
|
|
1478
|
+
},
|
|
1479
|
+
port,
|
|
1480
|
+
stateless: true,
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
const response = await fetch(`http://localhost:${port}/mcp`, {
|
|
1484
|
+
body: JSON.stringify({
|
|
1485
|
+
id: 1,
|
|
1486
|
+
jsonrpc: "2.0",
|
|
1487
|
+
method: "initialize",
|
|
1488
|
+
params: {
|
|
1489
|
+
capabilities: {},
|
|
1490
|
+
clientInfo: { name: "test", version: "1.0.0" },
|
|
1491
|
+
protocolVersion: "2024-11-05",
|
|
1492
|
+
},
|
|
1493
|
+
}),
|
|
1494
|
+
headers: {
|
|
1495
|
+
"Accept": "application/json, text/event-stream",
|
|
1496
|
+
"Content-Type": "application/json",
|
|
1497
|
+
},
|
|
1498
|
+
method: "POST",
|
|
1499
|
+
});
|
|
1500
|
+
|
|
1501
|
+
expect(response.status).toBe(401);
|
|
1502
|
+
|
|
1503
|
+
const errorResponse = (await response.json()) as {
|
|
1504
|
+
error: { code: number; message: string };
|
|
1505
|
+
id: null | number;
|
|
1506
|
+
jsonrpc: string;
|
|
1507
|
+
};
|
|
1508
|
+
expect(errorResponse.error.message).toContain("Token");
|
|
1509
|
+
|
|
1510
|
+
await httpServer.close();
|
|
1511
|
+
});
|
|
1512
|
+
|
|
1513
|
+
it("returns 401 when createServer throws Unauthorized error", async () => {
|
|
1514
|
+
const port = await getRandomPort();
|
|
1515
|
+
|
|
1516
|
+
const httpServer = await startHTTPServer({
|
|
1517
|
+
createServer: async () => {
|
|
1518
|
+
throw new Error("Unauthorized access");
|
|
1519
|
+
},
|
|
1520
|
+
port,
|
|
1521
|
+
stateless: true,
|
|
1522
|
+
});
|
|
1523
|
+
|
|
1524
|
+
const response = await fetch(`http://localhost:${port}/mcp`, {
|
|
1525
|
+
body: JSON.stringify({
|
|
1526
|
+
id: 1,
|
|
1527
|
+
jsonrpc: "2.0",
|
|
1528
|
+
method: "initialize",
|
|
1529
|
+
params: {
|
|
1530
|
+
capabilities: {},
|
|
1531
|
+
clientInfo: { name: "test", version: "1.0.0" },
|
|
1532
|
+
protocolVersion: "2024-11-05",
|
|
1533
|
+
},
|
|
1534
|
+
}),
|
|
1535
|
+
headers: {
|
|
1536
|
+
"Accept": "application/json, text/event-stream",
|
|
1537
|
+
"Content-Type": "application/json",
|
|
1538
|
+
},
|
|
1539
|
+
method: "POST",
|
|
1540
|
+
});
|
|
1541
|
+
|
|
1542
|
+
expect(response.status).toBe(401);
|
|
1543
|
+
|
|
1544
|
+
const errorResponse = (await response.json()) as {
|
|
1545
|
+
error: { code: number; message: string };
|
|
1546
|
+
id: null | number;
|
|
1547
|
+
jsonrpc: string;
|
|
1548
|
+
};
|
|
1549
|
+
expect(errorResponse.error.message).toContain("Unauthorized");
|
|
1550
|
+
|
|
1551
|
+
await httpServer.close();
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1554
|
+
it("returns 500 when createServer throws non-auth error", async () => {
|
|
1555
|
+
const port = await getRandomPort();
|
|
1556
|
+
|
|
1557
|
+
const httpServer = await startHTTPServer({
|
|
1558
|
+
createServer: async () => {
|
|
1559
|
+
throw new Error("Database connection failed");
|
|
1560
|
+
},
|
|
1561
|
+
port,
|
|
1562
|
+
stateless: true,
|
|
1563
|
+
});
|
|
1564
|
+
|
|
1565
|
+
const response = await fetch(`http://localhost:${port}/mcp`, {
|
|
1566
|
+
body: JSON.stringify({
|
|
1567
|
+
id: 1,
|
|
1568
|
+
jsonrpc: "2.0",
|
|
1569
|
+
method: "initialize",
|
|
1570
|
+
params: {
|
|
1571
|
+
capabilities: {},
|
|
1572
|
+
clientInfo: { name: "test", version: "1.0.0" },
|
|
1573
|
+
protocolVersion: "2024-11-05",
|
|
1574
|
+
},
|
|
1575
|
+
}),
|
|
1576
|
+
headers: {
|
|
1577
|
+
"Accept": "application/json, text/event-stream",
|
|
1578
|
+
"Content-Type": "application/json",
|
|
1579
|
+
},
|
|
1580
|
+
method: "POST",
|
|
1581
|
+
});
|
|
1582
|
+
|
|
1583
|
+
expect(response.status).toBe(500);
|
|
1584
|
+
|
|
1585
|
+
await httpServer.close();
|
|
1586
|
+
});
|
|
1587
|
+
|
|
1588
|
+
it("succeeds when authenticate returns { authenticated: true } in stateless mode", async () => {
|
|
1589
|
+
const stdioTransport = new StdioClientTransport({
|
|
1590
|
+
args: ["src/fixtures/simple-stdio-server.ts"],
|
|
1591
|
+
command: "tsx",
|
|
1592
|
+
});
|
|
1593
|
+
|
|
1594
|
+
const stdioClient = new Client(
|
|
1595
|
+
{
|
|
1596
|
+
name: "mcp-proxy",
|
|
1597
|
+
version: "1.0.0",
|
|
1598
|
+
},
|
|
1599
|
+
{
|
|
1600
|
+
capabilities: {},
|
|
1601
|
+
},
|
|
1602
|
+
);
|
|
1603
|
+
|
|
1604
|
+
await stdioClient.connect(stdioTransport);
|
|
1605
|
+
|
|
1606
|
+
const serverVersion = stdioClient.getServerVersion() as {
|
|
1607
|
+
name: string;
|
|
1608
|
+
version: string;
|
|
1609
|
+
};
|
|
1610
|
+
|
|
1611
|
+
const serverCapabilities = stdioClient.getServerCapabilities() as {
|
|
1612
|
+
capabilities: Record<string, unknown>;
|
|
1613
|
+
};
|
|
1614
|
+
|
|
1615
|
+
const port = await getRandomPort();
|
|
1616
|
+
|
|
1617
|
+
// Mock authenticate callback that returns { authenticated: true }
|
|
1618
|
+
const authenticate = vi.fn().mockResolvedValue({
|
|
1619
|
+
authenticated: true,
|
|
1620
|
+
session: { email: "test@example.com", userId: "user123" },
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
const httpServer = await startHTTPServer({
|
|
1624
|
+
authenticate,
|
|
1625
|
+
createServer: async () => {
|
|
1626
|
+
const mcpServer = new Server(serverVersion, {
|
|
1627
|
+
capabilities: serverCapabilities,
|
|
1628
|
+
});
|
|
1629
|
+
|
|
1630
|
+
await proxyServer({
|
|
1631
|
+
client: stdioClient,
|
|
1632
|
+
server: mcpServer,
|
|
1633
|
+
serverCapabilities,
|
|
1634
|
+
});
|
|
1635
|
+
|
|
1636
|
+
return mcpServer;
|
|
1637
|
+
},
|
|
1638
|
+
port,
|
|
1639
|
+
stateless: true,
|
|
1640
|
+
});
|
|
1641
|
+
|
|
1642
|
+
// Create client with valid Bearer token
|
|
1643
|
+
const streamTransport = new StreamableHTTPClientTransport(
|
|
1644
|
+
new URL(`http://localhost:${port}/mcp`),
|
|
1645
|
+
{
|
|
1646
|
+
requestInit: {
|
|
1647
|
+
headers: {
|
|
1648
|
+
Authorization: "Bearer valid-jwt-token",
|
|
1649
|
+
},
|
|
1650
|
+
},
|
|
1651
|
+
},
|
|
1652
|
+
);
|
|
1653
|
+
|
|
1654
|
+
const streamClient = new Client(
|
|
1655
|
+
{
|
|
1656
|
+
name: "stream-client-auth-true",
|
|
1657
|
+
version: "1.0.0",
|
|
1658
|
+
},
|
|
1659
|
+
{
|
|
1660
|
+
capabilities: {},
|
|
1661
|
+
},
|
|
1662
|
+
);
|
|
1663
|
+
|
|
1664
|
+
// Should connect successfully
|
|
1665
|
+
await streamClient.connect(streamTransport);
|
|
1666
|
+
|
|
1667
|
+
// Should be able to make requests
|
|
1668
|
+
const result = await streamClient.listResources();
|
|
1669
|
+
expect(result.resources).toBeDefined();
|
|
1670
|
+
|
|
1671
|
+
// Verify authenticate callback was called
|
|
1672
|
+
expect(authenticate).toHaveBeenCalled();
|
|
1673
|
+
|
|
1674
|
+
await streamClient.close();
|
|
1675
|
+
await httpServer.close();
|
|
1676
|
+
await stdioClient.close();
|
|
1677
|
+
});
|