mcp-proxy 5.11.2 → 5.12.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.
@@ -17,6 +17,51 @@ export interface AuthConfig {
17
17
  export class AuthenticationMiddleware {
18
18
  constructor(private config: AuthConfig = {}) {}
19
19
 
20
+ getScopeChallengeResponse(
21
+ requiredScopes: string[],
22
+ errorDescription?: string,
23
+ requestId?: unknown,
24
+ ): { body: string; headers: Record<string, string>; statusCode: number } {
25
+ const headers: Record<string, string> = {
26
+ "Content-Type": "application/json",
27
+ };
28
+
29
+ // Build WWW-Authenticate header with all required parameters
30
+ if (this.config.oauth?.protectedResource?.resource) {
31
+ const parts = [
32
+ "Bearer",
33
+ 'error="insufficient_scope"',
34
+ `scope="${requiredScopes.join(" ")}"`,
35
+ `resource_metadata="${this.config.oauth.protectedResource.resource}/.well-known/oauth-protected-resource"`,
36
+ ];
37
+
38
+ if (errorDescription) {
39
+ // Escape quotes in description
40
+ const escaped = errorDescription.replace(/"/g, '\\"');
41
+ parts.push(`error_description="${escaped}"`);
42
+ }
43
+
44
+ headers["WWW-Authenticate"] = parts.join(", ");
45
+ }
46
+
47
+ return {
48
+ body: JSON.stringify({
49
+ error: {
50
+ code: -32001, // Custom error code for insufficient scope
51
+ data: {
52
+ error: "insufficient_scope",
53
+ required_scopes: requiredScopes,
54
+ },
55
+ message: errorDescription || "Insufficient scope",
56
+ },
57
+ id: requestId ?? null,
58
+ jsonrpc: "2.0",
59
+ }),
60
+ headers,
61
+ statusCode: 403,
62
+ };
63
+ }
64
+
20
65
  getUnauthorizedResponse(options?: {
21
66
  error?: string;
22
67
  error_description?: string;
@@ -38,15 +83,21 @@ export class AuthenticationMiddleware {
38
83
 
39
84
  // Add resource_metadata if configured
40
85
  if (this.config.oauth.protectedResource?.resource) {
41
- params.push(`resource_metadata="${this.config.oauth.protectedResource.resource}/.well-known/oauth-protected-resource"`);
86
+ params.push(
87
+ `resource_metadata="${this.config.oauth.protectedResource.resource}/.well-known/oauth-protected-resource"`,
88
+ );
42
89
  }
43
90
 
44
91
  // Add error from options or config (options takes precedence)
45
- const error = options?.error || this.config.oauth.error || "invalid_token";
92
+ const error =
93
+ options?.error || this.config.oauth.error || "invalid_token";
46
94
  params.push(`error="${error}"`);
47
95
 
48
96
  // 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";
97
+ const error_description =
98
+ options?.error_description ||
99
+ this.config.oauth.error_description ||
100
+ "Unauthorized: Invalid or missing API key";
50
101
  // Escape quotes in error description
51
102
  const escaped = error_description.replace(/"/g, '\\"');
52
103
  params.push(`error_description="${escaped}"`);
@@ -72,7 +123,9 @@ export class AuthenticationMiddleware {
72
123
  body: JSON.stringify({
73
124
  error: {
74
125
  code: 401,
75
- message: options?.error_description || "Unauthorized: Invalid or missing API key",
126
+ message:
127
+ options?.error_description ||
128
+ "Unauthorized: Invalid or missing API key",
76
129
  },
77
130
  id: null,
78
131
  jsonrpc: "2.0",
@@ -98,4 +151,3 @@ export class AuthenticationMiddleware {
98
151
  return apiKey === this.config.apiKey;
99
152
  }
100
153
  }
101
-
@@ -68,7 +68,8 @@ const argv = await yargs(hideBin(process.argv))
68
68
  },
69
69
  requestTimeout: {
70
70
  default: 300000,
71
- describe: "The timeout (in milliseconds) for requests to the MCP server (default: 5 minutes)",
71
+ describe:
72
+ "The timeout (in milliseconds) for requests to the MCP server (default: 5 minutes)",
72
73
  type: "number",
73
74
  },
74
75
  server: {
@@ -28,33 +28,47 @@ interface TestEnvironment {
28
28
  streamClient: Client;
29
29
  }
30
30
 
31
- async function createTestEnvironment(config: TestConfig = {}): Promise<TestEnvironment> {
32
- const {
33
- requestTimeout,
34
- serverDelay,
35
- serverFixture = "simple-stdio-server.ts"
31
+ async function createTestEnvironment(
32
+ config: TestConfig = {},
33
+ ): Promise<TestEnvironment> {
34
+ const {
35
+ requestTimeout,
36
+ serverDelay,
37
+ serverFixture = "simple-stdio-server.ts",
36
38
  } = config;
37
-
39
+
38
40
  const stdioTransport = new StdioClientTransport({
39
41
  args: [`src/fixtures/${serverFixture}`],
40
42
  command: "tsx",
41
- env: serverDelay ? { ...process.env, RESPONSE_DELAY: serverDelay } as Record<string, string> : process.env as Record<string, string>,
43
+ env: serverDelay
44
+ ? ({ ...process.env, RESPONSE_DELAY: serverDelay } as Record<
45
+ string,
46
+ string
47
+ >)
48
+ : (process.env as Record<string, string>),
42
49
  });
43
50
 
44
51
  const stdioClient = new Client(
45
52
  { name: "mcp-proxy-test", version: "1.0.0" },
46
- { capabilities: {} }
53
+ { capabilities: {} },
47
54
  );
48
55
 
49
56
  await stdioClient.connect(stdioTransport);
50
57
 
51
- const serverVersion = stdioClient.getServerVersion() as { name: string; version: string };
52
- const serverCapabilities = stdioClient.getServerCapabilities() as { capabilities: Record<string, unknown> };
58
+ const serverVersion = stdioClient.getServerVersion() as {
59
+ name: string;
60
+ version: string;
61
+ };
62
+ const serverCapabilities = stdioClient.getServerCapabilities() as {
63
+ capabilities: Record<string, unknown>;
64
+ };
53
65
  const port = await getRandomPort();
54
66
 
55
67
  const httpServer = await startHTTPServer({
56
68
  createServer: async () => {
57
- const mcpServer = new Server(serverVersion, { capabilities: serverCapabilities });
69
+ const mcpServer = new Server(serverVersion, {
70
+ capabilities: serverCapabilities,
71
+ });
58
72
  await proxyServer({
59
73
  client: stdioClient,
60
74
  requestTimeout,
@@ -68,20 +82,22 @@ async function createTestEnvironment(config: TestConfig = {}): Promise<TestEnvir
68
82
 
69
83
  const streamClient = new Client(
70
84
  { name: "stream-client", version: "1.0.0" },
71
- { capabilities: {} }
85
+ { capabilities: {} },
72
86
  );
73
87
 
74
- const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`));
88
+ const transport = new StreamableHTTPClientTransport(
89
+ new URL(`http://localhost:${port}/mcp`),
90
+ );
75
91
  await streamClient.connect(transport);
76
92
 
77
- return {
93
+ return {
78
94
  cleanup: async () => {
79
95
  await streamClient.close();
80
96
  await stdioClient.close();
81
- },
82
- httpServer,
83
- stdioClient,
84
- streamClient
97
+ },
98
+ httpServer,
99
+ stdioClient,
100
+ streamClient,
85
101
  };
86
102
  }
87
103
 
@@ -90,7 +106,7 @@ describe("proxyServer timeout functionality", () => {
90
106
  const { cleanup, streamClient } = await createTestEnvironment({
91
107
  requestTimeout: 1000,
92
108
  serverDelay: "500",
93
- serverFixture: "slow-stdio-server.ts"
109
+ serverFixture: "slow-stdio-server.ts",
94
110
  });
95
111
 
96
112
  // This should succeed as timeout (1s) > delay (500ms)
@@ -105,7 +121,7 @@ describe("proxyServer timeout functionality", () => {
105
121
  const { cleanup, streamClient } = await createTestEnvironment({
106
122
  requestTimeout: 500,
107
123
  serverDelay: "1000",
108
- serverFixture: "slow-stdio-server.ts"
124
+ serverFixture: "slow-stdio-server.ts",
109
125
  });
110
126
 
111
127
  // This should throw a timeout error as delay (1s) > timeout (500ms)
@@ -128,7 +144,7 @@ describe("proxyServer timeout functionality", () => {
128
144
  const { cleanup, streamClient } = await createTestEnvironment({
129
145
  requestTimeout: 600,
130
146
  serverDelay: "300",
131
- serverFixture: "slow-stdio-server.ts"
147
+ serverFixture: "slow-stdio-server.ts",
132
148
  });
133
149
 
134
150
  // First get the resources
@@ -141,7 +157,9 @@ describe("proxyServer timeout functionality", () => {
141
157
  });
142
158
 
143
159
  expect(resourceContent.contents).toBeDefined();
144
- expect(resourceContent.contents[0].text).toContain("300ms delay");
160
+ expect((resourceContent.contents[0] as { text: string }).text).toContain(
161
+ "300ms delay",
162
+ );
145
163
 
146
164
  await cleanup();
147
165
  }, 10000);
@@ -1339,8 +1339,8 @@ it("returns 401 with custom error message when { authenticated: false, error: '.
1339
1339
  },
1340
1340
  }),
1341
1341
  headers: {
1342
- "Accept": "application/json, text/event-stream",
1343
- "Authorization": "Bearer expired-token",
1342
+ Accept: "application/json, text/event-stream",
1343
+ Authorization: "Bearer expired-token",
1344
1344
  "Content-Type": "application/json",
1345
1345
  },
1346
1346
  method: "POST",
@@ -1408,8 +1408,8 @@ it("returns 401 when createServer throws authentication error", async () => {
1408
1408
  },
1409
1409
  }),
1410
1410
  headers: {
1411
- "Accept": "application/json, text/event-stream",
1412
- "Authorization": "Bearer test-token",
1411
+ Accept: "application/json, text/event-stream",
1412
+ Authorization: "Bearer test-token",
1413
1413
  "Content-Type": "application/json",
1414
1414
  },
1415
1415
  method: "POST",
@@ -1451,7 +1451,7 @@ it("returns 401 when createServer throws JWT-related error", async () => {
1451
1451
  },
1452
1452
  }),
1453
1453
  headers: {
1454
- "Accept": "application/json, text/event-stream",
1454
+ Accept: "application/json, text/event-stream",
1455
1455
  "Content-Type": "application/json",
1456
1456
  },
1457
1457
  method: "POST",
@@ -1492,7 +1492,7 @@ it("returns 401 when createServer throws Token-related error", async () => {
1492
1492
  },
1493
1493
  }),
1494
1494
  headers: {
1495
- "Accept": "application/json, text/event-stream",
1495
+ Accept: "application/json, text/event-stream",
1496
1496
  "Content-Type": "application/json",
1497
1497
  },
1498
1498
  method: "POST",
@@ -1533,7 +1533,7 @@ it("returns 401 when createServer throws Unauthorized error", async () => {
1533
1533
  },
1534
1534
  }),
1535
1535
  headers: {
1536
- "Accept": "application/json, text/event-stream",
1536
+ Accept: "application/json, text/event-stream",
1537
1537
  "Content-Type": "application/json",
1538
1538
  },
1539
1539
  method: "POST",
@@ -1574,7 +1574,7 @@ it("returns 500 when createServer throws non-auth error", async () => {
1574
1574
  },
1575
1575
  }),
1576
1576
  headers: {
1577
- "Accept": "application/json, text/event-stream",
1577
+ Accept: "application/json, text/event-stream",
1578
1578
  "Content-Type": "application/json",
1579
1579
  },
1580
1580
  method: "POST",
@@ -1614,7 +1614,7 @@ it("includes WWW-Authenticate header in 401 response with OAuth config", async (
1614
1614
  },
1615
1615
  }),
1616
1616
  headers: {
1617
- "Accept": "application/json, text/event-stream",
1617
+ Accept: "application/json, text/event-stream",
1618
1618
  "Content-Type": "application/json",
1619
1619
  },
1620
1620
  method: "POST",
@@ -1624,9 +1624,11 @@ it("includes WWW-Authenticate header in 401 response with OAuth config", async (
1624
1624
 
1625
1625
  const wwwAuthHeader = response.headers.get("WWW-Authenticate");
1626
1626
  expect(wwwAuthHeader).toBeTruthy();
1627
- expect(wwwAuthHeader).toContain('Bearer');
1627
+ expect(wwwAuthHeader).toContain("Bearer");
1628
1628
  expect(wwwAuthHeader).toContain('realm="mcp-server"');
1629
- expect(wwwAuthHeader).toContain('resource_metadata="https://example.com/.well-known/oauth-protected-resource"');
1629
+ expect(wwwAuthHeader).toContain(
1630
+ 'resource_metadata="https://example.com/.well-known/oauth-protected-resource"',
1631
+ );
1630
1632
  expect(wwwAuthHeader).toContain('error="invalid_token"');
1631
1633
  expect(wwwAuthHeader).toContain('error_description="Invalid JWT token"');
1632
1634
 
@@ -1636,7 +1638,9 @@ it("includes WWW-Authenticate header in 401 response with OAuth config", async (
1636
1638
  it("includes WWW-Authenticate header when authenticate callback fails with OAuth", async () => {
1637
1639
  const port = await getRandomPort();
1638
1640
 
1639
- const authenticate = vi.fn().mockRejectedValue(new Error("Token signature verification failed"));
1641
+ const authenticate = vi
1642
+ .fn()
1643
+ .mockRejectedValue(new Error("Token signature verification failed"));
1640
1644
 
1641
1645
  const httpServer = await startHTTPServer({
1642
1646
  authenticate,
@@ -1670,8 +1674,8 @@ it("includes WWW-Authenticate header when authenticate callback fails with OAuth
1670
1674
  },
1671
1675
  }),
1672
1676
  headers: {
1673
- "Accept": "application/json, text/event-stream",
1674
- "Authorization": "Bearer expired-token",
1677
+ Accept: "application/json, text/event-stream",
1678
+ Authorization: "Bearer expired-token",
1675
1679
  "Content-Type": "application/json",
1676
1680
  },
1677
1681
  method: "POST",
@@ -1682,12 +1686,18 @@ it("includes WWW-Authenticate header when authenticate callback fails with OAuth
1682
1686
 
1683
1687
  const wwwAuthHeader = response.headers.get("WWW-Authenticate");
1684
1688
  expect(wwwAuthHeader).toBeTruthy();
1685
- expect(wwwAuthHeader).toContain('Bearer');
1689
+ expect(wwwAuthHeader).toContain("Bearer");
1686
1690
  expect(wwwAuthHeader).toContain('realm="example-api"');
1687
- expect(wwwAuthHeader).toContain('resource_metadata="https://api.example.com/.well-known/oauth-protected-resource"');
1691
+ expect(wwwAuthHeader).toContain(
1692
+ 'resource_metadata="https://api.example.com/.well-known/oauth-protected-resource"',
1693
+ );
1688
1694
  expect(wwwAuthHeader).toContain('error="invalid_token"');
1689
- expect(wwwAuthHeader).toContain('error_description="Token signature verification failed"');
1690
- expect(wwwAuthHeader).toContain('error_uri="https://example.com/docs/errors"');
1695
+ expect(wwwAuthHeader).toContain(
1696
+ 'error_description="Token signature verification failed"',
1697
+ );
1698
+ expect(wwwAuthHeader).toContain(
1699
+ 'error_uri="https://example.com/docs/errors"',
1700
+ );
1691
1701
 
1692
1702
  await httpServer.close();
1693
1703
  });
@@ -1715,7 +1725,7 @@ it("does not include WWW-Authenticate header in 401 response without OAuth confi
1715
1725
  },
1716
1726
  }),
1717
1727
  headers: {
1718
- "Accept": "application/json, text/event-stream",
1728
+ Accept: "application/json, text/event-stream",
1719
1729
  "Content-Type": "application/json",
1720
1730
  },
1721
1731
  method: "POST",
@@ -1916,7 +1926,9 @@ it("supports origin validation with array", async () => {
1916
1926
  });
1917
1927
 
1918
1928
  expect(response1.status).toBe(204);
1919
- expect(response1.headers.get("Access-Control-Allow-Origin")).toBe("https://app.example.com");
1929
+ expect(response1.headers.get("Access-Control-Allow-Origin")).toBe(
1930
+ "https://app.example.com",
1931
+ );
1920
1932
 
1921
1933
  // Test with disallowed origin
1922
1934
  const response2 = await fetch(`http://localhost:${port}/mcp`, {
@@ -1958,7 +1970,9 @@ it("supports origin validation with function", async () => {
1958
1970
  });
1959
1971
 
1960
1972
  expect(response1.status).toBe(204);
1961
- expect(response1.headers.get("Access-Control-Allow-Origin")).toBe("https://subdomain.example.com");
1973
+ expect(response1.headers.get("Access-Control-Allow-Origin")).toBe(
1974
+ "https://subdomain.example.com",
1975
+ );
1962
1976
 
1963
1977
  // Test with disallowed origin
1964
1978
  const response2 = await fetch(`http://localhost:${port}/mcp`, {
@@ -2029,7 +2043,9 @@ it("uses default CORS settings when cors: true", async () => {
2029
2043
 
2030
2044
  expect(response.status).toBe(204);
2031
2045
  expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*");
2032
- expect(response.headers.get("Access-Control-Allow-Headers")).toBe("Content-Type, Authorization, Accept, Mcp-Session-Id, Last-Event-Id");
2046
+ expect(response.headers.get("Access-Control-Allow-Headers")).toBe(
2047
+ "Content-Type, Authorization, Accept, Mcp-Session-Id, Last-Event-Id",
2048
+ );
2033
2049
  expect(response.headers.get("Access-Control-Allow-Credentials")).toBe("true");
2034
2050
 
2035
2051
  await httpServer.close();
@@ -2062,7 +2078,9 @@ it("supports custom methods and maxAge", async () => {
2062
2078
  });
2063
2079
 
2064
2080
  expect(response.status).toBe(204);
2065
- expect(response.headers.get("Access-Control-Allow-Methods")).toBe("GET, POST, PUT, DELETE");
2081
+ expect(response.headers.get("Access-Control-Allow-Methods")).toBe(
2082
+ "GET, POST, PUT, DELETE",
2083
+ );
2066
2084
  expect(response.headers.get("Access-Control-Max-Age")).toBe("86400");
2067
2085
 
2068
2086
  await httpServer.close();