mcp-proxy 6.5.3 → 6.5.4
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.mjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{stdio-DNR9B0BZ.mjs → stdio-BQunCFor.mjs} +72 -1
- package/dist/stdio-BQunCFor.mjs.map +1 -0
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/startHTTPServer.test.ts +130 -0
- package/src/startHTTPServer.ts +103 -6
- package/dist/stdio-DNR9B0BZ.mjs.map +0 -1
package/jsr.json
CHANGED
package/package.json
CHANGED
|
@@ -993,6 +993,136 @@ it("accepts requests with valid Bearer token in stateless mode", async () => {
|
|
|
993
993
|
await stdioClient.close();
|
|
994
994
|
});
|
|
995
995
|
|
|
996
|
+
it("returns 401 for authenticated stream requests without a session ID", async () => {
|
|
997
|
+
const port = await getRandomPort();
|
|
998
|
+
const authenticate = vi.fn().mockResolvedValue({ userId: "test-user" });
|
|
999
|
+
const createServer = vi.fn(async () => {
|
|
1000
|
+
return new Server({ name: "test", version: "1.0.0" }, { capabilities: {} });
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
const httpServer = await startHTTPServer({
|
|
1004
|
+
authenticate,
|
|
1005
|
+
createServer,
|
|
1006
|
+
port,
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
try {
|
|
1010
|
+
const response = await fetch(`http://localhost:${port}/mcp`, {
|
|
1011
|
+
body: JSON.stringify({
|
|
1012
|
+
id: 1,
|
|
1013
|
+
jsonrpc: "2.0",
|
|
1014
|
+
method: "tools/list",
|
|
1015
|
+
}),
|
|
1016
|
+
headers: {
|
|
1017
|
+
Accept: "application/json, text/event-stream",
|
|
1018
|
+
Authorization: "Bearer valid-token",
|
|
1019
|
+
"Content-Type": "application/json",
|
|
1020
|
+
},
|
|
1021
|
+
method: "POST",
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
expect(response.status).toBe(401);
|
|
1025
|
+
|
|
1026
|
+
const errorResponse = (await response.json()) as {
|
|
1027
|
+
error: { code: number; message: string };
|
|
1028
|
+
id: null | number;
|
|
1029
|
+
jsonrpc: string;
|
|
1030
|
+
};
|
|
1031
|
+
expect(errorResponse.error).toEqual({
|
|
1032
|
+
code: -32000,
|
|
1033
|
+
message: "Unauthorized: No valid session ID provided",
|
|
1034
|
+
});
|
|
1035
|
+
expect(errorResponse.id).toBe(1);
|
|
1036
|
+
expect(authenticate).toHaveBeenCalledTimes(1);
|
|
1037
|
+
expect(createServer).not.toHaveBeenCalled();
|
|
1038
|
+
} finally {
|
|
1039
|
+
await httpServer.close();
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
it("returns 401 for authenticated stream GET requests without a session ID", async () => {
|
|
1044
|
+
const port = await getRandomPort();
|
|
1045
|
+
const authenticate = vi.fn().mockResolvedValue({ userId: "test-user" });
|
|
1046
|
+
|
|
1047
|
+
const httpServer = await startHTTPServer({
|
|
1048
|
+
authenticate,
|
|
1049
|
+
createServer: async () => {
|
|
1050
|
+
return new Server(
|
|
1051
|
+
{ name: "test", version: "1.0.0" },
|
|
1052
|
+
{ capabilities: {} },
|
|
1053
|
+
);
|
|
1054
|
+
},
|
|
1055
|
+
port,
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
try {
|
|
1059
|
+
const response = await fetch(`http://localhost:${port}/mcp`, {
|
|
1060
|
+
headers: {
|
|
1061
|
+
Accept: "text/event-stream",
|
|
1062
|
+
Authorization: "Bearer valid-token",
|
|
1063
|
+
},
|
|
1064
|
+
method: "GET",
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
expect(response.status).toBe(401);
|
|
1068
|
+
|
|
1069
|
+
const errorResponse = (await response.json()) as {
|
|
1070
|
+
error: { code: number; message: string };
|
|
1071
|
+
id: null | number;
|
|
1072
|
+
jsonrpc: string;
|
|
1073
|
+
};
|
|
1074
|
+
expect(errorResponse.error.message).toBe(
|
|
1075
|
+
"Unauthorized: No valid session ID provided",
|
|
1076
|
+
);
|
|
1077
|
+
expect(errorResponse.id).toBeNull();
|
|
1078
|
+
expect(authenticate).not.toHaveBeenCalled();
|
|
1079
|
+
} finally {
|
|
1080
|
+
await httpServer.close();
|
|
1081
|
+
}
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
it("keeps malformed authenticated stream requests as 400", async () => {
|
|
1085
|
+
const port = await getRandomPort();
|
|
1086
|
+
const authenticate = vi.fn().mockResolvedValue({ userId: "test-user" });
|
|
1087
|
+
|
|
1088
|
+
const httpServer = await startHTTPServer({
|
|
1089
|
+
authenticate,
|
|
1090
|
+
createServer: async () => {
|
|
1091
|
+
return new Server(
|
|
1092
|
+
{ name: "test", version: "1.0.0" },
|
|
1093
|
+
{ capabilities: {} },
|
|
1094
|
+
);
|
|
1095
|
+
},
|
|
1096
|
+
port,
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
try {
|
|
1100
|
+
const response = await fetch(`http://localhost:${port}/mcp`, {
|
|
1101
|
+
body: JSON.stringify({ malformed: true }),
|
|
1102
|
+
headers: {
|
|
1103
|
+
Accept: "application/json, text/event-stream",
|
|
1104
|
+
Authorization: "Bearer valid-token",
|
|
1105
|
+
"Content-Type": "application/json",
|
|
1106
|
+
},
|
|
1107
|
+
method: "POST",
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
expect(response.status).toBe(400);
|
|
1111
|
+
|
|
1112
|
+
const errorResponse = (await response.json()) as {
|
|
1113
|
+
error: { code: number; message: string };
|
|
1114
|
+
id: null | number;
|
|
1115
|
+
jsonrpc: string;
|
|
1116
|
+
};
|
|
1117
|
+
expect(errorResponse.error.message).toBe(
|
|
1118
|
+
"Bad Request: No valid session ID provided",
|
|
1119
|
+
);
|
|
1120
|
+
expect(authenticate).toHaveBeenCalledTimes(1);
|
|
1121
|
+
} finally {
|
|
1122
|
+
await httpServer.close();
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
1125
|
+
|
|
996
1126
|
it("returns 401 when authenticate callback returns null in stateless mode", async () => {
|
|
997
1127
|
const stdioTransport = new StdioClientTransport({
|
|
998
1128
|
args: ["src/fixtures/simple-stdio-server.ts"],
|
package/src/startHTTPServer.ts
CHANGED
|
@@ -4,7 +4,10 @@ import {
|
|
|
4
4
|
EventStore,
|
|
5
5
|
StreamableHTTPServerTransport,
|
|
6
6
|
} from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
isInitializeRequest,
|
|
9
|
+
JSONRPCMessageSchema,
|
|
10
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
8
11
|
import fs from "fs";
|
|
9
12
|
import http from "http";
|
|
10
13
|
import https from "https";
|
|
@@ -62,6 +65,35 @@ const createJsonRpcErrorResponse = (code: number, message: string) => {
|
|
|
62
65
|
});
|
|
63
66
|
};
|
|
64
67
|
|
|
68
|
+
type SessionUnauthorizedResponseOptions = {
|
|
69
|
+
readonly body?: unknown;
|
|
70
|
+
readonly oauth?: AuthConfig["oauth"];
|
|
71
|
+
readonly res: http.ServerResponse;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const getRequestId = (body: unknown): unknown => {
|
|
75
|
+
if (
|
|
76
|
+
typeof body !== "object" ||
|
|
77
|
+
body === null ||
|
|
78
|
+
Array.isArray(body) ||
|
|
79
|
+
!("id" in body)
|
|
80
|
+
) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return body.id;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const isJsonRpcMessage = (message: unknown): boolean => {
|
|
88
|
+
return JSONRPCMessageSchema.safeParse(message).success;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const isJsonRpcBody = (body: unknown): boolean => {
|
|
92
|
+
return Array.isArray(body)
|
|
93
|
+
? body.every(isJsonRpcMessage)
|
|
94
|
+
: isJsonRpcMessage(body);
|
|
95
|
+
};
|
|
96
|
+
|
|
65
97
|
// Helper function to get WWW-Authenticate header value
|
|
66
98
|
const getWWWAuthenticateHeader = (
|
|
67
99
|
oauth?: AuthConfig["oauth"],
|
|
@@ -125,6 +157,35 @@ const getWWWAuthenticateHeader = (
|
|
|
125
157
|
return `Bearer ${params.join(", ")}`;
|
|
126
158
|
};
|
|
127
159
|
|
|
160
|
+
const sendSessionUnauthorizedResponse = ({
|
|
161
|
+
body,
|
|
162
|
+
oauth,
|
|
163
|
+
res,
|
|
164
|
+
}: SessionUnauthorizedResponseOptions): void => {
|
|
165
|
+
const message = "Unauthorized: No valid session ID provided";
|
|
166
|
+
|
|
167
|
+
res.setHeader("Content-Type", "application/json");
|
|
168
|
+
|
|
169
|
+
const wwwAuthHeader = getWWWAuthenticateHeader(oauth, {
|
|
170
|
+
error: "invalid_token",
|
|
171
|
+
error_description: message,
|
|
172
|
+
});
|
|
173
|
+
if (wwwAuthHeader) {
|
|
174
|
+
res.setHeader("WWW-Authenticate", wwwAuthHeader);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
res.writeHead(401).end(
|
|
178
|
+
JSON.stringify({
|
|
179
|
+
error: {
|
|
180
|
+
code: -32000,
|
|
181
|
+
message,
|
|
182
|
+
},
|
|
183
|
+
id: getRequestId(body),
|
|
184
|
+
jsonrpc: "2.0",
|
|
185
|
+
}),
|
|
186
|
+
);
|
|
187
|
+
};
|
|
188
|
+
|
|
128
189
|
// Helper function to detect scope challenge errors
|
|
129
190
|
const isScopeChallengeError = (
|
|
130
191
|
error: unknown,
|
|
@@ -353,9 +414,9 @@ const handleStreamRequest = async <T extends ServerLike>({
|
|
|
353
414
|
// In stateless mode, ignore session ID header entirely (like Python MCP SDK)
|
|
354
415
|
const sessionId = stateless
|
|
355
416
|
? undefined
|
|
356
|
-
:
|
|
357
|
-
|
|
358
|
-
|
|
417
|
+
: Array.isArray(req.headers["mcp-session-id"])
|
|
418
|
+
? req.headers["mcp-session-id"][0]
|
|
419
|
+
: req.headers["mcp-session-id"];
|
|
359
420
|
|
|
360
421
|
let transport: StreamableHTTPServerTransport;
|
|
361
422
|
|
|
@@ -449,6 +510,12 @@ const handleStreamRequest = async <T extends ServerLike>({
|
|
|
449
510
|
if (sessionId) {
|
|
450
511
|
const activeTransport = activeTransports[sessionId];
|
|
451
512
|
if (!activeTransport) {
|
|
513
|
+
if (authenticate && isJsonRpcBody(body)) {
|
|
514
|
+
sendSessionUnauthorizedResponse({ body, oauth, res });
|
|
515
|
+
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
|
|
452
519
|
res.setHeader("Content-Type", "application/json");
|
|
453
520
|
res
|
|
454
521
|
.writeHead(404)
|
|
@@ -634,6 +701,12 @@ const handleStreamRequest = async <T extends ServerLike>({
|
|
|
634
701
|
|
|
635
702
|
return true;
|
|
636
703
|
} else {
|
|
704
|
+
if (authenticate && isJsonRpcBody(body)) {
|
|
705
|
+
sendSessionUnauthorizedResponse({ body, oauth, res });
|
|
706
|
+
|
|
707
|
+
return true;
|
|
708
|
+
}
|
|
709
|
+
|
|
637
710
|
// Error if the server is not created but the request is not an initialize request
|
|
638
711
|
res.setHeader("Content-Type", "application/json");
|
|
639
712
|
|
|
@@ -690,7 +763,7 @@ const handleStreamRequest = async <T extends ServerLike>({
|
|
|
690
763
|
}
|
|
691
764
|
| undefined = sessionId ? activeTransports[sessionId] : undefined;
|
|
692
765
|
|
|
693
|
-
if (!sessionId) {
|
|
766
|
+
if (!sessionId) {
|
|
694
767
|
// Return METHOD_NOT_ALLOWED so stateless clients' transport stops reconnecting
|
|
695
768
|
if (stateless) {
|
|
696
769
|
res.writeHead(405, { Allow: "POST" }).end("Method Not Allowed");
|
|
@@ -698,12 +771,24 @@ const handleStreamRequest = async <T extends ServerLike>({
|
|
|
698
771
|
return true;
|
|
699
772
|
}
|
|
700
773
|
|
|
774
|
+
if (authenticate) {
|
|
775
|
+
sendSessionUnauthorizedResponse({ oauth, res });
|
|
776
|
+
|
|
777
|
+
return true;
|
|
778
|
+
}
|
|
779
|
+
|
|
701
780
|
res.writeHead(400).end("No sessionId");
|
|
702
781
|
|
|
703
782
|
return true;
|
|
704
783
|
}
|
|
705
784
|
|
|
706
785
|
if (!activeTransport) {
|
|
786
|
+
if (authenticate) {
|
|
787
|
+
sendSessionUnauthorizedResponse({ oauth, res });
|
|
788
|
+
|
|
789
|
+
return true;
|
|
790
|
+
}
|
|
791
|
+
|
|
707
792
|
res.writeHead(400).end("No active transport");
|
|
708
793
|
|
|
709
794
|
return true;
|
|
@@ -735,6 +820,12 @@ const handleStreamRequest = async <T extends ServerLike>({
|
|
|
735
820
|
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
|
736
821
|
|
|
737
822
|
if (!sessionId) {
|
|
823
|
+
if (authenticate) {
|
|
824
|
+
sendSessionUnauthorizedResponse({ oauth, res });
|
|
825
|
+
|
|
826
|
+
return true;
|
|
827
|
+
}
|
|
828
|
+
|
|
738
829
|
res.writeHead(400).end("Invalid or missing sessionId");
|
|
739
830
|
|
|
740
831
|
return true;
|
|
@@ -745,6 +836,12 @@ const handleStreamRequest = async <T extends ServerLike>({
|
|
|
745
836
|
const activeTransport = activeTransports[sessionId];
|
|
746
837
|
|
|
747
838
|
if (!activeTransport) {
|
|
839
|
+
if (authenticate) {
|
|
840
|
+
sendSessionUnauthorizedResponse({ oauth, res });
|
|
841
|
+
|
|
842
|
+
return true;
|
|
843
|
+
}
|
|
844
|
+
|
|
748
845
|
res.writeHead(400).end("No active transport");
|
|
749
846
|
return true;
|
|
750
847
|
}
|
|
@@ -1024,7 +1121,7 @@ export const startHTTPServer = async <T extends ServerLike>({
|
|
|
1024
1121
|
res.writeHead(404).end();
|
|
1025
1122
|
};
|
|
1026
1123
|
|
|
1027
|
-
let httpServer;
|
|
1124
|
+
let httpServer: http.Server | https.Server;
|
|
1028
1125
|
if (sslCa || sslCert || sslKey) {
|
|
1029
1126
|
const options: https.ServerOptions = {};
|
|
1030
1127
|
if (sslCa) {
|