mcp-proxy 5.7.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.
@@ -701,3 +701,977 @@ it("does not require auth for OPTIONS requests", async () => {
701
701
 
702
702
  await httpServer.close();
703
703
  });
704
+
705
+ // Stateless OAuth 2.0 JWT Bearer Token Authentication Tests (PR #37)
706
+
707
+ it("accepts requests with valid Bearer token in stateless mode", async () => {
708
+ const stdioTransport = new StdioClientTransport({
709
+ args: ["src/fixtures/simple-stdio-server.ts"],
710
+ command: "tsx",
711
+ });
712
+
713
+ const stdioClient = new Client(
714
+ {
715
+ name: "mcp-proxy",
716
+ version: "1.0.0",
717
+ },
718
+ {
719
+ capabilities: {},
720
+ },
721
+ );
722
+
723
+ await stdioClient.connect(stdioTransport);
724
+
725
+ const serverVersion = stdioClient.getServerVersion() as {
726
+ name: string;
727
+ version: string;
728
+ };
729
+
730
+ const serverCapabilities = stdioClient.getServerCapabilities() as {
731
+ capabilities: Record<string, unknown>;
732
+ };
733
+
734
+ const port = await getRandomPort();
735
+
736
+ // Mock authenticate callback that validates JWT Bearer token
737
+ const mockAuthResult = { email: "test@example.com", userId: "user123" };
738
+ const authenticate = vi.fn().mockResolvedValue(mockAuthResult);
739
+
740
+ const httpServer = await startHTTPServer({
741
+ authenticate,
742
+ createServer: async () => {
743
+ const mcpServer = new Server(serverVersion, {
744
+ capabilities: serverCapabilities,
745
+ });
746
+
747
+ await proxyServer({
748
+ client: stdioClient,
749
+ server: mcpServer,
750
+ serverCapabilities,
751
+ });
752
+
753
+ return mcpServer;
754
+ },
755
+ port,
756
+ stateless: true, // Enable stateless mode
757
+ });
758
+
759
+ // Create a stateless streamable HTTP client with Bearer token
760
+ const streamTransport = new StreamableHTTPClientTransport(
761
+ new URL(`http://localhost:${port}/mcp`),
762
+ {
763
+ requestInit: {
764
+ headers: {
765
+ Authorization: "Bearer valid-jwt-token",
766
+ },
767
+ },
768
+ },
769
+ );
770
+
771
+ const streamClient = new Client(
772
+ {
773
+ name: "stream-client-oauth",
774
+ version: "1.0.0",
775
+ },
776
+ {
777
+ capabilities: {},
778
+ },
779
+ );
780
+
781
+ await streamClient.connect(streamTransport);
782
+
783
+ // Test that we can make requests with valid authentication
784
+ const result = await streamClient.listResources();
785
+ expect(result).toEqual({
786
+ resources: [
787
+ {
788
+ name: "Example Resource",
789
+ uri: "file:///example.txt",
790
+ },
791
+ ],
792
+ });
793
+
794
+ // Verify authenticate callback was called
795
+ expect(authenticate).toHaveBeenCalled();
796
+
797
+ await streamClient.close();
798
+ await httpServer.close();
799
+ await stdioClient.close();
800
+ });
801
+
802
+ it("returns 401 when authenticate callback returns null in stateless mode", async () => {
803
+ const stdioTransport = new StdioClientTransport({
804
+ args: ["src/fixtures/simple-stdio-server.ts"],
805
+ command: "tsx",
806
+ });
807
+
808
+ const stdioClient = new Client(
809
+ {
810
+ name: "mcp-proxy",
811
+ version: "1.0.0",
812
+ },
813
+ {
814
+ capabilities: {},
815
+ },
816
+ );
817
+
818
+ await stdioClient.connect(stdioTransport);
819
+
820
+ const serverVersion = stdioClient.getServerVersion() as {
821
+ name: string;
822
+ version: string;
823
+ };
824
+
825
+ const serverCapabilities = stdioClient.getServerCapabilities() as {
826
+ capabilities: Record<string, unknown>;
827
+ };
828
+
829
+ const port = await getRandomPort();
830
+
831
+ // Mock authenticate callback that rejects invalid token
832
+ const authenticate = vi.fn().mockResolvedValue(null);
833
+
834
+ const httpServer = await startHTTPServer({
835
+ authenticate,
836
+ createServer: async () => {
837
+ const mcpServer = new Server(serverVersion, {
838
+ capabilities: serverCapabilities,
839
+ });
840
+
841
+ await proxyServer({
842
+ client: stdioClient,
843
+ server: mcpServer,
844
+ serverCapabilities,
845
+ });
846
+
847
+ return mcpServer;
848
+ },
849
+ port,
850
+ stateless: true,
851
+ });
852
+
853
+ // Create client with invalid Bearer token
854
+ const streamTransport = new StreamableHTTPClientTransport(
855
+ new URL(`http://localhost:${port}/mcp`),
856
+ {
857
+ requestInit: {
858
+ headers: {
859
+ Authorization: "Bearer invalid-jwt-token",
860
+ },
861
+ },
862
+ },
863
+ );
864
+
865
+ const streamClient = new Client(
866
+ {
867
+ name: "stream-client-invalid-token",
868
+ version: "1.0.0",
869
+ },
870
+ {
871
+ capabilities: {},
872
+ },
873
+ );
874
+
875
+ // Connection should fail due to invalid authentication
876
+ await expect(streamClient.connect(streamTransport)).rejects.toThrow();
877
+
878
+ // Verify authenticate callback was called
879
+ expect(authenticate).toHaveBeenCalled();
880
+
881
+ await httpServer.close();
882
+ await stdioClient.close();
883
+ });
884
+
885
+ it("returns 401 when authenticate callback throws error in stateless mode", async () => {
886
+ const stdioTransport = new StdioClientTransport({
887
+ args: ["src/fixtures/simple-stdio-server.ts"],
888
+ command: "tsx",
889
+ });
890
+
891
+ const stdioClient = new Client(
892
+ {
893
+ name: "mcp-proxy",
894
+ version: "1.0.0",
895
+ },
896
+ {
897
+ capabilities: {},
898
+ },
899
+ );
900
+
901
+ await stdioClient.connect(stdioTransport);
902
+
903
+ const serverVersion = stdioClient.getServerVersion() as {
904
+ name: string;
905
+ version: string;
906
+ };
907
+
908
+ const serverCapabilities = stdioClient.getServerCapabilities() as {
909
+ capabilities: Record<string, unknown>;
910
+ };
911
+
912
+ const port = await getRandomPort();
913
+
914
+ // Mock authenticate callback that throws (e.g., JWKS endpoint failure)
915
+ const authenticate = vi
916
+ .fn()
917
+ .mockRejectedValue(new Error("JWKS fetch failed"));
918
+
919
+ const httpServer = await startHTTPServer({
920
+ authenticate,
921
+ createServer: async () => {
922
+ const mcpServer = new Server(serverVersion, {
923
+ capabilities: serverCapabilities,
924
+ });
925
+
926
+ await proxyServer({
927
+ client: stdioClient,
928
+ server: mcpServer,
929
+ serverCapabilities,
930
+ });
931
+
932
+ return mcpServer;
933
+ },
934
+ port,
935
+ stateless: true,
936
+ });
937
+
938
+ // Create client with Bearer token
939
+ const streamTransport = new StreamableHTTPClientTransport(
940
+ new URL(`http://localhost:${port}/mcp`),
941
+ {
942
+ requestInit: {
943
+ headers: {
944
+ Authorization: "Bearer some-token",
945
+ },
946
+ },
947
+ },
948
+ );
949
+
950
+ const streamClient = new Client(
951
+ {
952
+ name: "stream-client-auth-error",
953
+ version: "1.0.0",
954
+ },
955
+ {
956
+ capabilities: {},
957
+ },
958
+ );
959
+
960
+ // Connection should fail due to authentication error
961
+ await expect(streamClient.connect(streamTransport)).rejects.toThrow();
962
+
963
+ // Verify authenticate callback was called
964
+ expect(authenticate).toHaveBeenCalled();
965
+
966
+ await httpServer.close();
967
+ await stdioClient.close();
968
+ });
969
+
970
+ it("does not call authenticate on subsequent requests in stateful mode", async () => {
971
+ const stdioTransport = new StdioClientTransport({
972
+ args: ["src/fixtures/simple-stdio-server.ts"],
973
+ command: "tsx",
974
+ });
975
+
976
+ const stdioClient = new Client(
977
+ {
978
+ name: "mcp-proxy",
979
+ version: "1.0.0",
980
+ },
981
+ {
982
+ capabilities: {},
983
+ },
984
+ );
985
+
986
+ await stdioClient.connect(stdioTransport);
987
+
988
+ const serverVersion = stdioClient.getServerVersion() as {
989
+ name: string;
990
+ version: string;
991
+ };
992
+
993
+ const serverCapabilities = stdioClient.getServerCapabilities() as {
994
+ capabilities: Record<string, unknown>;
995
+ };
996
+
997
+ const port = await getRandomPort();
998
+
999
+ // Mock authenticate callback
1000
+ const authenticate = vi.fn().mockResolvedValue({ userId: "user123" });
1001
+
1002
+ const onConnect = vi.fn().mockResolvedValue(undefined);
1003
+ const onClose = vi.fn().mockResolvedValue(undefined);
1004
+
1005
+ const httpServer = await startHTTPServer({
1006
+ authenticate,
1007
+ createServer: async () => {
1008
+ const mcpServer = new Server(serverVersion, {
1009
+ capabilities: serverCapabilities,
1010
+ });
1011
+
1012
+ await proxyServer({
1013
+ client: stdioClient,
1014
+ server: mcpServer,
1015
+ serverCapabilities,
1016
+ });
1017
+
1018
+ return mcpServer;
1019
+ },
1020
+ onClose,
1021
+ onConnect,
1022
+ port,
1023
+ stateless: false, // Explicitly use stateful mode
1024
+ });
1025
+
1026
+ // Create client
1027
+ const streamTransport = new StreamableHTTPClientTransport(
1028
+ new URL(`http://localhost:${port}/mcp`),
1029
+ );
1030
+
1031
+ const streamClient = new Client(
1032
+ {
1033
+ name: "stream-client-stateful",
1034
+ version: "1.0.0",
1035
+ },
1036
+ {
1037
+ capabilities: {},
1038
+ },
1039
+ );
1040
+
1041
+ await streamClient.connect(streamTransport);
1042
+
1043
+ // Make first request
1044
+ await streamClient.listResources();
1045
+
1046
+ // Make second request
1047
+ await streamClient.listResources();
1048
+
1049
+ // In stateful mode, authenticate should NOT be called per-request
1050
+ // It may be called during initialization, but not on every tool call
1051
+ // The key is that it's not called multiple times for each request
1052
+ expect(authenticate).not.toHaveBeenCalled();
1053
+
1054
+ await streamClient.close();
1055
+ await httpServer.close();
1056
+ await stdioClient.close();
1057
+ });
1058
+
1059
+ it("calls authenticate on every request in stateless mode", async () => {
1060
+ const stdioTransport = new StdioClientTransport({
1061
+ args: ["src/fixtures/simple-stdio-server.ts"],
1062
+ command: "tsx",
1063
+ });
1064
+
1065
+ const stdioClient = new Client(
1066
+ {
1067
+ name: "mcp-proxy",
1068
+ version: "1.0.0",
1069
+ },
1070
+ {
1071
+ capabilities: {},
1072
+ },
1073
+ );
1074
+
1075
+ await stdioClient.connect(stdioTransport);
1076
+
1077
+ const serverVersion = stdioClient.getServerVersion() as {
1078
+ name: string;
1079
+ version: string;
1080
+ };
1081
+
1082
+ const serverCapabilities = stdioClient.getServerCapabilities() as {
1083
+ capabilities: Record<string, unknown>;
1084
+ };
1085
+
1086
+ const port = await getRandomPort();
1087
+
1088
+ // Mock authenticate callback
1089
+ const authenticate = vi.fn().mockResolvedValue({ userId: "user123" });
1090
+
1091
+ const httpServer = await startHTTPServer({
1092
+ authenticate,
1093
+ createServer: async () => {
1094
+ const mcpServer = new Server(serverVersion, {
1095
+ capabilities: serverCapabilities,
1096
+ });
1097
+
1098
+ await proxyServer({
1099
+ client: stdioClient,
1100
+ server: mcpServer,
1101
+ serverCapabilities,
1102
+ });
1103
+
1104
+ return mcpServer;
1105
+ },
1106
+ port,
1107
+ stateless: true, // Enable stateless mode
1108
+ });
1109
+
1110
+ // Create client with Bearer token
1111
+ const streamTransport = new StreamableHTTPClientTransport(
1112
+ new URL(`http://localhost:${port}/mcp`),
1113
+ {
1114
+ requestInit: {
1115
+ headers: {
1116
+ Authorization: "Bearer test-token",
1117
+ },
1118
+ },
1119
+ },
1120
+ );
1121
+
1122
+ const streamClient = new Client(
1123
+ {
1124
+ name: "stream-client-per-request",
1125
+ version: "1.0.0",
1126
+ },
1127
+ {
1128
+ capabilities: {},
1129
+ },
1130
+ );
1131
+
1132
+ await streamClient.connect(streamTransport);
1133
+
1134
+ const initialCallCount = authenticate.mock.calls.length;
1135
+
1136
+ // Make first request
1137
+ await streamClient.listResources();
1138
+ const firstRequestCallCount = authenticate.mock.calls.length;
1139
+
1140
+ // Make second request
1141
+ await streamClient.listResources();
1142
+ const secondRequestCallCount = authenticate.mock.calls.length;
1143
+
1144
+ // In stateless mode, authenticate should be called on EVERY request
1145
+ expect(firstRequestCallCount).toBeGreaterThan(initialCallCount);
1146
+ expect(secondRequestCallCount).toBeGreaterThan(firstRequestCallCount);
1147
+
1148
+ await streamClient.close();
1149
+ await httpServer.close();
1150
+ await stdioClient.close();
1151
+ });
1152
+
1153
+ it("includes Authorization in CORS allowed headers", async () => {
1154
+ const port = await getRandomPort();
1155
+
1156
+ const httpServer = await startHTTPServer({
1157
+ createServer: async () => {
1158
+ const mcpServer = new Server(
1159
+ { name: "test", version: "1.0.0" },
1160
+ { capabilities: {} },
1161
+ );
1162
+ return mcpServer;
1163
+ },
1164
+ port,
1165
+ });
1166
+
1167
+ // Test OPTIONS request to verify CORS headers
1168
+ const response = await fetch(`http://localhost:${port}/mcp`, {
1169
+ headers: {
1170
+ Origin: "https://example.com",
1171
+ },
1172
+ method: "OPTIONS",
1173
+ });
1174
+
1175
+ expect(response.status).toBe(204);
1176
+
1177
+ // Verify Authorization is in the allowed headers
1178
+ const allowedHeaders = response.headers.get("Access-Control-Allow-Headers");
1179
+ expect(allowedHeaders).toBeTruthy();
1180
+ expect(allowedHeaders).toContain("Authorization");
1181
+
1182
+ await httpServer.close();
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
+ });