mcp-proxy 5.9.0 → 5.11.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/README.md +148 -1
- package/dist/bin/mcp-proxy.js +1 -1
- package/dist/index.d.ts +22 -2
- package/dist/index.js +1 -1
- package/dist/{stdio-CsjPjeWC.js → stdio-BEX6di72.js} +105 -29
- package/dist/stdio-BEX6di72.js.map +1 -0
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/authentication.test.ts +145 -7
- package/src/authentication.ts +51 -5
- package/src/index.ts +1 -0
- package/src/startHTTPServer.test.ts +392 -0
- package/src/startHTTPServer.ts +207 -35
- package/dist/stdio-CsjPjeWC.js.map +0 -1
package/jsr.json
CHANGED
package/package.json
CHANGED
|
@@ -132,7 +132,7 @@ describe("AuthenticationMiddleware", () => {
|
|
|
132
132
|
const response = middleware.getUnauthorizedResponse();
|
|
133
133
|
|
|
134
134
|
expect(response.headers["WWW-Authenticate"]).toBe(
|
|
135
|
-
'Bearer resource_metadata="https://example.com/.well-known/oauth-protected-resource"',
|
|
135
|
+
'Bearer resource_metadata="https://example.com/.well-known/oauth-protected-resource", error="invalid_token", error_description="Unauthorized: Invalid or missing API key"',
|
|
136
136
|
);
|
|
137
137
|
});
|
|
138
138
|
|
|
@@ -148,21 +148,24 @@ describe("AuthenticationMiddleware", () => {
|
|
|
148
148
|
const response = middleware.getUnauthorizedResponse();
|
|
149
149
|
|
|
150
150
|
expect(response.headers["WWW-Authenticate"]).toBe(
|
|
151
|
-
'Bearer resource_metadata="https://example.com//.well-known/oauth-protected-resource"',
|
|
151
|
+
'Bearer resource_metadata="https://example.com//.well-known/oauth-protected-resource", error="invalid_token", error_description="Unauthorized: Invalid or missing API key"',
|
|
152
152
|
);
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
-
it("should
|
|
155
|
+
it("should include minimal WWW-Authenticate header when OAuth config is empty object", () => {
|
|
156
156
|
const middleware = new AuthenticationMiddleware({
|
|
157
157
|
apiKey: "test",
|
|
158
158
|
oauth: {},
|
|
159
159
|
});
|
|
160
160
|
const response = middleware.getUnauthorizedResponse();
|
|
161
161
|
|
|
162
|
-
|
|
162
|
+
// Even with empty oauth object, default error and error_description are added
|
|
163
|
+
expect(response.headers["WWW-Authenticate"]).toBe(
|
|
164
|
+
'Bearer error="invalid_token", error_description="Unauthorized: Invalid or missing API key"',
|
|
165
|
+
);
|
|
163
166
|
});
|
|
164
167
|
|
|
165
|
-
it("should
|
|
168
|
+
it("should include minimal WWW-Authenticate header when protectedResource is empty", () => {
|
|
166
169
|
const middleware = new AuthenticationMiddleware({
|
|
167
170
|
apiKey: "test",
|
|
168
171
|
oauth: {
|
|
@@ -171,7 +174,10 @@ describe("AuthenticationMiddleware", () => {
|
|
|
171
174
|
});
|
|
172
175
|
const response = middleware.getUnauthorizedResponse();
|
|
173
176
|
|
|
174
|
-
|
|
177
|
+
// Even without resource_metadata, default error and error_description are added
|
|
178
|
+
expect(response.headers["WWW-Authenticate"]).toBe(
|
|
179
|
+
'Bearer error="invalid_token", error_description="Unauthorized: Invalid or missing API key"',
|
|
180
|
+
);
|
|
175
181
|
});
|
|
176
182
|
|
|
177
183
|
it("should include WWW-Authenticate header with OAuth config but no apiKey", () => {
|
|
@@ -185,8 +191,140 @@ describe("AuthenticationMiddleware", () => {
|
|
|
185
191
|
const response = middleware.getUnauthorizedResponse();
|
|
186
192
|
|
|
187
193
|
expect(response.headers["WWW-Authenticate"]).toBe(
|
|
188
|
-
'Bearer resource_metadata="https://example.com/.well-known/oauth-protected-resource"',
|
|
194
|
+
'Bearer resource_metadata="https://example.com/.well-known/oauth-protected-resource", error="invalid_token", error_description="Unauthorized: Invalid or missing API key"',
|
|
189
195
|
);
|
|
190
196
|
});
|
|
197
|
+
|
|
198
|
+
it("should include realm in WWW-Authenticate header when configured", () => {
|
|
199
|
+
const middleware = new AuthenticationMiddleware({
|
|
200
|
+
apiKey: "test",
|
|
201
|
+
oauth: {
|
|
202
|
+
protectedResource: {
|
|
203
|
+
resource: "https://example.com",
|
|
204
|
+
},
|
|
205
|
+
realm: "example-realm",
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
const response = middleware.getUnauthorizedResponse();
|
|
209
|
+
|
|
210
|
+
expect(response.headers["WWW-Authenticate"]).toContain('realm="example-realm"');
|
|
211
|
+
expect(response.headers["WWW-Authenticate"]).toContain('resource_metadata="https://example.com/.well-known/oauth-protected-resource"');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("should include custom error in WWW-Authenticate header via options", () => {
|
|
215
|
+
const middleware = new AuthenticationMiddleware({
|
|
216
|
+
apiKey: "test",
|
|
217
|
+
oauth: {
|
|
218
|
+
protectedResource: {
|
|
219
|
+
resource: "https://example.com",
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
const response = middleware.getUnauthorizedResponse({
|
|
224
|
+
error: "insufficient_scope",
|
|
225
|
+
error_description: "The request requires higher privileges",
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
expect(response.headers["WWW-Authenticate"]).toContain('error="insufficient_scope"');
|
|
229
|
+
expect(response.headers["WWW-Authenticate"]).toContain('error_description="The request requires higher privileges"');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("should include scope in WWW-Authenticate header", () => {
|
|
233
|
+
const middleware = new AuthenticationMiddleware({
|
|
234
|
+
apiKey: "test",
|
|
235
|
+
oauth: {
|
|
236
|
+
protectedResource: {
|
|
237
|
+
resource: "https://example.com",
|
|
238
|
+
},
|
|
239
|
+
scope: "read write",
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
const response = middleware.getUnauthorizedResponse();
|
|
243
|
+
|
|
244
|
+
expect(response.headers["WWW-Authenticate"]).toContain('scope="read write"');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("should override config error with options error", () => {
|
|
248
|
+
const middleware = new AuthenticationMiddleware({
|
|
249
|
+
apiKey: "test",
|
|
250
|
+
oauth: {
|
|
251
|
+
error: "invalid_request",
|
|
252
|
+
error_description: "Config error description",
|
|
253
|
+
protectedResource: {
|
|
254
|
+
resource: "https://example.com",
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
const response = middleware.getUnauthorizedResponse({
|
|
259
|
+
error: "invalid_token",
|
|
260
|
+
error_description: "Options error description",
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
expect(response.headers["WWW-Authenticate"]).toContain('error="invalid_token"');
|
|
264
|
+
expect(response.headers["WWW-Authenticate"]).toContain('error_description="Options error description"');
|
|
265
|
+
expect(response.headers["WWW-Authenticate"]).not.toContain("invalid_request");
|
|
266
|
+
expect(response.headers["WWW-Authenticate"]).not.toContain("Config error description");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("should include error_uri in WWW-Authenticate header", () => {
|
|
270
|
+
const middleware = new AuthenticationMiddleware({
|
|
271
|
+
apiKey: "test",
|
|
272
|
+
oauth: {
|
|
273
|
+
error_uri: "https://example.com/errors/auth",
|
|
274
|
+
protectedResource: {
|
|
275
|
+
resource: "https://example.com",
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
const response = middleware.getUnauthorizedResponse();
|
|
280
|
+
|
|
281
|
+
expect(response.headers["WWW-Authenticate"]).toContain('error_uri="https://example.com/errors/auth"');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("should properly escape quotes in error_description", () => {
|
|
285
|
+
const middleware = new AuthenticationMiddleware({
|
|
286
|
+
apiKey: "test",
|
|
287
|
+
oauth: {
|
|
288
|
+
protectedResource: {
|
|
289
|
+
resource: "https://example.com",
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
const response = middleware.getUnauthorizedResponse({
|
|
294
|
+
error_description: 'Token "abc123" is invalid',
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
expect(response.headers["WWW-Authenticate"]).toContain('error_description="Token \\"abc123\\" is invalid"');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("should include all parameters in correct order", () => {
|
|
301
|
+
const middleware = new AuthenticationMiddleware({
|
|
302
|
+
apiKey: "test",
|
|
303
|
+
oauth: {
|
|
304
|
+
error: "invalid_token",
|
|
305
|
+
error_description: "Token expired",
|
|
306
|
+
error_uri: "https://example.com/errors",
|
|
307
|
+
protectedResource: {
|
|
308
|
+
resource: "https://example.com",
|
|
309
|
+
},
|
|
310
|
+
realm: "my-realm",
|
|
311
|
+
scope: "read write",
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
const response = middleware.getUnauthorizedResponse();
|
|
315
|
+
|
|
316
|
+
const header = response.headers["WWW-Authenticate"];
|
|
317
|
+
expect(header).toContain('realm="my-realm"');
|
|
318
|
+
expect(header).toContain('resource_metadata="https://example.com/.well-known/oauth-protected-resource"');
|
|
319
|
+
expect(header).toContain('error="invalid_token"');
|
|
320
|
+
expect(header).toContain('error_description="Token expired"');
|
|
321
|
+
expect(header).toContain('error_uri="https://example.com/errors"');
|
|
322
|
+
expect(header).toContain('scope="read write"');
|
|
323
|
+
|
|
324
|
+
// Check order: realm, resource_metadata, error, error_description, error_uri, scope
|
|
325
|
+
expect(header.indexOf('realm=')).toBeLessThan(header.indexOf('resource_metadata='));
|
|
326
|
+
expect(header.indexOf('resource_metadata=')).toBeLessThan(header.indexOf('error='));
|
|
327
|
+
expect(header.indexOf('error=')).toBeLessThan(header.indexOf('error_description='));
|
|
328
|
+
});
|
|
191
329
|
});
|
|
192
330
|
});
|
package/src/authentication.ts
CHANGED
|
@@ -3,30 +3,76 @@ import type { IncomingMessage } from "http";
|
|
|
3
3
|
export interface AuthConfig {
|
|
4
4
|
apiKey?: string;
|
|
5
5
|
oauth?: {
|
|
6
|
+
error?: string;
|
|
7
|
+
error_description?: string;
|
|
8
|
+
error_uri?: string;
|
|
6
9
|
protectedResource?: {
|
|
7
10
|
resource?: string;
|
|
8
11
|
};
|
|
12
|
+
realm?: string;
|
|
13
|
+
scope?: string;
|
|
9
14
|
};
|
|
10
15
|
}
|
|
11
16
|
|
|
12
17
|
export class AuthenticationMiddleware {
|
|
13
18
|
constructor(private config: AuthConfig = {}) {}
|
|
14
19
|
|
|
15
|
-
getUnauthorizedResponse(
|
|
20
|
+
getUnauthorizedResponse(options?: {
|
|
21
|
+
error?: string;
|
|
22
|
+
error_description?: string;
|
|
23
|
+
error_uri?: string;
|
|
24
|
+
scope?: string;
|
|
25
|
+
}): { body: string; headers: Record<string, string> } {
|
|
16
26
|
const headers: Record<string, string> = {
|
|
17
27
|
"Content-Type": "application/json",
|
|
18
28
|
};
|
|
19
29
|
|
|
20
|
-
//
|
|
21
|
-
if (this.config.oauth
|
|
22
|
-
|
|
30
|
+
// Build WWW-Authenticate header if OAuth config is available
|
|
31
|
+
if (this.config.oauth) {
|
|
32
|
+
const params: string[] = [];
|
|
33
|
+
|
|
34
|
+
// Add realm if configured
|
|
35
|
+
if (this.config.oauth.realm) {
|
|
36
|
+
params.push(`realm="${this.config.oauth.realm}"`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Add resource_metadata if configured
|
|
40
|
+
if (this.config.oauth.protectedResource?.resource) {
|
|
41
|
+
params.push(`resource_metadata="${this.config.oauth.protectedResource.resource}/.well-known/oauth-protected-resource"`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Add error from options or config (options takes precedence)
|
|
45
|
+
const error = options?.error || this.config.oauth.error || "invalid_token";
|
|
46
|
+
params.push(`error="${error}"`);
|
|
47
|
+
|
|
48
|
+
// Add error_description from options or config (options takes precedence)
|
|
49
|
+
const error_description = options?.error_description || this.config.oauth.error_description || "Unauthorized: Invalid or missing API key";
|
|
50
|
+
// Escape quotes in error description
|
|
51
|
+
const escaped = error_description.replace(/"/g, '\\"');
|
|
52
|
+
params.push(`error_description="${escaped}"`);
|
|
53
|
+
|
|
54
|
+
// Add error_uri from options or config (options takes precedence)
|
|
55
|
+
const error_uri = options?.error_uri || this.config.oauth.error_uri;
|
|
56
|
+
if (error_uri) {
|
|
57
|
+
params.push(`error_uri="${error_uri}"`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Add scope from options or config (options takes precedence)
|
|
61
|
+
const scope = options?.scope || this.config.oauth.scope;
|
|
62
|
+
if (scope) {
|
|
63
|
+
params.push(`scope="${scope}"`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (params.length > 0) {
|
|
67
|
+
headers["WWW-Authenticate"] = `Bearer ${params.join(", ")}`;
|
|
68
|
+
}
|
|
23
69
|
}
|
|
24
70
|
|
|
25
71
|
return {
|
|
26
72
|
body: JSON.stringify({
|
|
27
73
|
error: {
|
|
28
74
|
code: 401,
|
|
29
|
-
message: "Unauthorized: Invalid or missing API key",
|
|
75
|
+
message: options?.error_description || "Unauthorized: Invalid or missing API key",
|
|
30
76
|
},
|
|
31
77
|
id: null,
|
|
32
78
|
jsonrpc: "2.0",
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ export type { AuthConfig } from "./authentication.js";
|
|
|
2
2
|
export { AuthenticationMiddleware } from "./authentication.js";
|
|
3
3
|
export { InMemoryEventStore } from "./InMemoryEventStore.js";
|
|
4
4
|
export { proxyServer } from "./proxyServer.js";
|
|
5
|
+
export type { CorsOptions } from "./startHTTPServer.js";
|
|
5
6
|
export { startHTTPServer } from "./startHTTPServer.js";
|
|
6
7
|
export { ServerType, startStdioServer } from "./startStdioServer.js";
|
|
7
8
|
export { tapTransport } from "./tapTransport.js";
|