mcp-proxy 5.8.0 → 5.8.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/jsr.json CHANGED
@@ -3,5 +3,5 @@
3
3
  "include": ["src/index.ts", "src/bin/mcp-proxy.ts"],
4
4
  "license": "MIT",
5
5
  "name": "@punkpeye/mcp-proxy",
6
- "version": "5.8.0"
6
+ "version": "5.8.1"
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-proxy",
3
- "version": "5.8.0",
3
+ "version": "5.8.1",
4
4
  "main": "dist/index.js",
5
5
  "scripts": {
6
6
  "build": "tsdown",
@@ -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
+ });
@@ -138,13 +138,21 @@ const handleStreamRequest = async <T extends ServerLike>({
138
138
  if (stateless && authenticate) {
139
139
  try {
140
140
  const authResult = await authenticate(req);
141
- if (!authResult) {
141
+
142
+ // Check for both falsy AND { authenticated: false } pattern
143
+ if (!authResult || (typeof authResult === 'object' && 'authenticated' in authResult && !authResult.authenticated)) {
144
+ // Extract error message if available
145
+ const errorMessage =
146
+ authResult && typeof authResult === 'object' && 'error' in authResult && typeof authResult.error === 'string'
147
+ ? authResult.error
148
+ : "Unauthorized: Authentication failed";
149
+
142
150
  res.setHeader("Content-Type", "application/json");
143
151
  res.writeHead(401).end(
144
152
  JSON.stringify({
145
153
  error: {
146
154
  code: -32000,
147
- message: "Unauthorized: Authentication failed"
155
+ message: errorMessage
148
156
  },
149
157
  id: (body as { id?: unknown })?.id ?? null,
150
158
  jsonrpc: "2.0"
@@ -153,13 +161,15 @@ const handleStreamRequest = async <T extends ServerLike>({
153
161
  return true;
154
162
  }
155
163
  } catch (error) {
164
+ // Extract error details from thrown errors
165
+ const errorMessage = error instanceof Error ? error.message : "Unauthorized: Authentication error";
156
166
  console.error("Authentication error:", error);
157
167
  res.setHeader("Content-Type", "application/json");
158
168
  res.writeHead(401).end(
159
169
  JSON.stringify({
160
170
  error: {
161
171
  code: -32000,
162
- message: "Unauthorized: Authentication error"
172
+ message: errorMessage
163
173
  },
164
174
  id: (body as { id?: unknown })?.id ?? null,
165
175
  jsonrpc: "2.0"
@@ -223,6 +233,26 @@ const handleStreamRequest = async <T extends ServerLike>({
223
233
  try {
224
234
  server = await createServer(req);
225
235
  } catch (error) {
236
+ // Detect authentication errors and return HTTP 401
237
+ const errorMessage = error instanceof Error ? error.message : String(error);
238
+ const isAuthError = errorMessage.includes('Authentication') ||
239
+ errorMessage.includes('Invalid JWT') ||
240
+ errorMessage.includes('Token') ||
241
+ errorMessage.includes('Unauthorized');
242
+
243
+ if (isAuthError) {
244
+ res.setHeader("Content-Type", "application/json");
245
+ res.writeHead(401).end(JSON.stringify({
246
+ error: {
247
+ code: -32000,
248
+ message: errorMessage
249
+ },
250
+ id: (body as { id?: unknown })?.id ?? null,
251
+ jsonrpc: "2.0"
252
+ }));
253
+ return true;
254
+ }
255
+
226
256
  if (handleResponseError(error, res)) {
227
257
  return true;
228
258
  }
@@ -255,6 +285,26 @@ const handleStreamRequest = async <T extends ServerLike>({
255
285
  try {
256
286
  server = await createServer(req);
257
287
  } catch (error) {
288
+ // Detect authentication errors and return HTTP 401
289
+ const errorMessage = error instanceof Error ? error.message : String(error);
290
+ const isAuthError = errorMessage.includes('Authentication') ||
291
+ errorMessage.includes('Invalid JWT') ||
292
+ errorMessage.includes('Token') ||
293
+ errorMessage.includes('Unauthorized');
294
+
295
+ if (isAuthError) {
296
+ res.setHeader("Content-Type", "application/json");
297
+ res.writeHead(401).end(JSON.stringify({
298
+ error: {
299
+ code: -32000,
300
+ message: errorMessage
301
+ },
302
+ id: (body as { id?: unknown })?.id ?? null,
303
+ jsonrpc: "2.0"
304
+ }));
305
+ return true;
306
+ }
307
+
258
308
  if (handleResponseError(error, res)) {
259
309
  return true;
260
310
  }