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.
- package/dist/bin/mcp-proxy.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -1
- package/dist/{stdio-AohZZTMh.js → stdio-9KZaSDCW.js} +61 -5
- package/dist/{stdio-AohZZTMh.js.map → stdio-9KZaSDCW.js.map} +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/startHTTPServer.test.ts +974 -0
- package/src/startHTTPServer.ts +92 -2
|
@@ -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
|
+
});
|