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/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.9.0"
6
+ "version": "5.11.0"
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-proxy",
3
- "version": "5.9.0",
3
+ "version": "5.11.0",
4
4
  "main": "dist/index.js",
5
5
  "scripts": {
6
6
  "build": "tsdown",
@@ -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 not include WWW-Authenticate header when OAuth config is empty", () => {
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
- expect(response.headers["WWW-Authenticate"]).toBeUndefined();
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 not include WWW-Authenticate header when protectedResource is empty", () => {
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
- expect(response.headers["WWW-Authenticate"]).toBeUndefined();
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
  });
@@ -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(): { body: string; headers: Record<string, string> } {
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
- // Add WWW-Authenticate header if OAuth config is available
21
- if (this.config.oauth?.protectedResource?.resource) {
22
- headers["WWW-Authenticate"] = `Bearer resource_metadata="${this.config.oauth.protectedResource.resource}/.well-known/oauth-protected-resource"`;
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";