mcp-proxy 5.9.0 → 5.10.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.10.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.10.0",
4
4
  "main": "dist/index.js",
5
5
  "scripts": {
6
6
  "build": "tsdown",
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";
@@ -1675,3 +1675,251 @@ it("succeeds when authenticate returns { authenticated: true } in stateless mode
1675
1675
  await httpServer.close();
1676
1676
  await stdioClient.close();
1677
1677
  });
1678
+
1679
+ // CORS Configuration Tests
1680
+
1681
+ it("supports wildcard CORS headers", async () => {
1682
+ const port = await getRandomPort();
1683
+
1684
+ const httpServer = await startHTTPServer({
1685
+ cors: {
1686
+ allowedHeaders: "*",
1687
+ },
1688
+ createServer: async () => {
1689
+ const mcpServer = new Server(
1690
+ { name: "test", version: "1.0.0" },
1691
+ { capabilities: {} },
1692
+ );
1693
+ return mcpServer;
1694
+ },
1695
+ port,
1696
+ });
1697
+
1698
+ // Test OPTIONS request to verify CORS headers
1699
+ const response = await fetch(`http://localhost:${port}/mcp`, {
1700
+ headers: {
1701
+ Origin: "https://example.com",
1702
+ },
1703
+ method: "OPTIONS",
1704
+ });
1705
+
1706
+ expect(response.status).toBe(204);
1707
+
1708
+ // Verify wildcard is used for allowed headers
1709
+ const allowedHeaders = response.headers.get("Access-Control-Allow-Headers");
1710
+ expect(allowedHeaders).toBe("*");
1711
+
1712
+ await httpServer.close();
1713
+ });
1714
+
1715
+ it("supports custom CORS headers array", async () => {
1716
+ const port = await getRandomPort();
1717
+
1718
+ const httpServer = await startHTTPServer({
1719
+ cors: {
1720
+ allowedHeaders: ["Content-Type", "X-Custom-Header", "X-API-Key"],
1721
+ },
1722
+ createServer: async () => {
1723
+ const mcpServer = new Server(
1724
+ { name: "test", version: "1.0.0" },
1725
+ { capabilities: {} },
1726
+ );
1727
+ return mcpServer;
1728
+ },
1729
+ port,
1730
+ });
1731
+
1732
+ // Test OPTIONS request to verify CORS headers
1733
+ const response = await fetch(`http://localhost:${port}/mcp`, {
1734
+ headers: {
1735
+ Origin: "https://example.com",
1736
+ },
1737
+ method: "OPTIONS",
1738
+ });
1739
+
1740
+ expect(response.status).toBe(204);
1741
+
1742
+ // Verify custom headers are used
1743
+ const allowedHeaders = response.headers.get("Access-Control-Allow-Headers");
1744
+ expect(allowedHeaders).toBe("Content-Type, X-Custom-Header, X-API-Key");
1745
+
1746
+ await httpServer.close();
1747
+ });
1748
+
1749
+ it("supports origin validation with array", async () => {
1750
+ const port = await getRandomPort();
1751
+
1752
+ const httpServer = await startHTTPServer({
1753
+ cors: {
1754
+ origin: ["https://app.example.com", "https://admin.example.com"],
1755
+ },
1756
+ createServer: async () => {
1757
+ const mcpServer = new Server(
1758
+ { name: "test", version: "1.0.0" },
1759
+ { capabilities: {} },
1760
+ );
1761
+ return mcpServer;
1762
+ },
1763
+ port,
1764
+ });
1765
+
1766
+ // Test with allowed origin
1767
+ const response1 = await fetch(`http://localhost:${port}/mcp`, {
1768
+ headers: {
1769
+ Origin: "https://app.example.com",
1770
+ },
1771
+ method: "OPTIONS",
1772
+ });
1773
+
1774
+ expect(response1.status).toBe(204);
1775
+ expect(response1.headers.get("Access-Control-Allow-Origin")).toBe("https://app.example.com");
1776
+
1777
+ // Test with disallowed origin
1778
+ const response2 = await fetch(`http://localhost:${port}/mcp`, {
1779
+ headers: {
1780
+ Origin: "https://malicious.com",
1781
+ },
1782
+ method: "OPTIONS",
1783
+ });
1784
+
1785
+ expect(response2.status).toBe(204);
1786
+ expect(response2.headers.get("Access-Control-Allow-Origin")).toBeNull();
1787
+
1788
+ await httpServer.close();
1789
+ });
1790
+
1791
+ it("supports origin validation with function", async () => {
1792
+ const port = await getRandomPort();
1793
+
1794
+ const httpServer = await startHTTPServer({
1795
+ cors: {
1796
+ origin: (origin: string) => origin.endsWith(".example.com"),
1797
+ },
1798
+ createServer: async () => {
1799
+ const mcpServer = new Server(
1800
+ { name: "test", version: "1.0.0" },
1801
+ { capabilities: {} },
1802
+ );
1803
+ return mcpServer;
1804
+ },
1805
+ port,
1806
+ });
1807
+
1808
+ // Test with allowed origin
1809
+ const response1 = await fetch(`http://localhost:${port}/mcp`, {
1810
+ headers: {
1811
+ Origin: "https://subdomain.example.com",
1812
+ },
1813
+ method: "OPTIONS",
1814
+ });
1815
+
1816
+ expect(response1.status).toBe(204);
1817
+ expect(response1.headers.get("Access-Control-Allow-Origin")).toBe("https://subdomain.example.com");
1818
+
1819
+ // Test with disallowed origin
1820
+ const response2 = await fetch(`http://localhost:${port}/mcp`, {
1821
+ headers: {
1822
+ Origin: "https://malicious.com",
1823
+ },
1824
+ method: "OPTIONS",
1825
+ });
1826
+
1827
+ expect(response2.status).toBe(204);
1828
+ expect(response2.headers.get("Access-Control-Allow-Origin")).toBeNull();
1829
+
1830
+ await httpServer.close();
1831
+ });
1832
+
1833
+ it("disables CORS when cors: false", async () => {
1834
+ const port = await getRandomPort();
1835
+
1836
+ const httpServer = await startHTTPServer({
1837
+ cors: false,
1838
+ createServer: async () => {
1839
+ const mcpServer = new Server(
1840
+ { name: "test", version: "1.0.0" },
1841
+ { capabilities: {} },
1842
+ );
1843
+ return mcpServer;
1844
+ },
1845
+ port,
1846
+ });
1847
+
1848
+ // Test OPTIONS request - should not have CORS headers
1849
+ const response = await fetch(`http://localhost:${port}/mcp`, {
1850
+ headers: {
1851
+ Origin: "https://example.com",
1852
+ },
1853
+ method: "OPTIONS",
1854
+ });
1855
+
1856
+ expect(response.status).toBe(204);
1857
+ expect(response.headers.get("Access-Control-Allow-Origin")).toBeNull();
1858
+ expect(response.headers.get("Access-Control-Allow-Headers")).toBeNull();
1859
+
1860
+ await httpServer.close();
1861
+ });
1862
+
1863
+ it("uses default CORS settings when cors: true", async () => {
1864
+ const port = await getRandomPort();
1865
+
1866
+ const httpServer = await startHTTPServer({
1867
+ cors: true,
1868
+ createServer: async () => {
1869
+ const mcpServer = new Server(
1870
+ { name: "test", version: "1.0.0" },
1871
+ { capabilities: {} },
1872
+ );
1873
+ return mcpServer;
1874
+ },
1875
+ port,
1876
+ });
1877
+
1878
+ // Test OPTIONS request to verify default CORS headers
1879
+ const response = await fetch(`http://localhost:${port}/mcp`, {
1880
+ headers: {
1881
+ Origin: "https://example.com",
1882
+ },
1883
+ method: "OPTIONS",
1884
+ });
1885
+
1886
+ expect(response.status).toBe(204);
1887
+ expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*");
1888
+ expect(response.headers.get("Access-Control-Allow-Headers")).toBe("Content-Type, Authorization, Accept, Mcp-Session-Id, Last-Event-Id");
1889
+ expect(response.headers.get("Access-Control-Allow-Credentials")).toBe("true");
1890
+
1891
+ await httpServer.close();
1892
+ });
1893
+
1894
+ it("supports custom methods and maxAge", async () => {
1895
+ const port = await getRandomPort();
1896
+
1897
+ const httpServer = await startHTTPServer({
1898
+ cors: {
1899
+ maxAge: 86400,
1900
+ methods: ["GET", "POST", "PUT", "DELETE"],
1901
+ },
1902
+ createServer: async () => {
1903
+ const mcpServer = new Server(
1904
+ { name: "test", version: "1.0.0" },
1905
+ { capabilities: {} },
1906
+ );
1907
+ return mcpServer;
1908
+ },
1909
+ port,
1910
+ });
1911
+
1912
+ // Test OPTIONS request to verify custom settings
1913
+ const response = await fetch(`http://localhost:${port}/mcp`, {
1914
+ headers: {
1915
+ Origin: "https://example.com",
1916
+ },
1917
+ method: "OPTIONS",
1918
+ });
1919
+
1920
+ expect(response.status).toBe(204);
1921
+ expect(response.headers.get("Access-Control-Allow-Methods")).toBe("GET, POST, PUT, DELETE");
1922
+ expect(response.headers.get("Access-Control-Max-Age")).toBe("86400");
1923
+
1924
+ await httpServer.close();
1925
+ });
@@ -11,6 +11,15 @@ import { randomUUID } from "node:crypto";
11
11
  import { AuthConfig, AuthenticationMiddleware } from "./authentication.js";
12
12
  import { InMemoryEventStore } from "./InMemoryEventStore.js";
13
13
 
14
+ export interface CorsOptions {
15
+ allowedHeaders?: string | string[]; // Allow string[] or '*' for wildcard
16
+ credentials?: boolean;
17
+ exposedHeaders?: string[];
18
+ maxAge?: number;
19
+ methods?: string[];
20
+ origin?: ((origin: string) => boolean) | string | string[];
21
+ }
22
+
14
23
  export type SSEServer = {
15
24
  close: () => Promise<void>;
16
25
  };
@@ -102,6 +111,94 @@ const cleanupServer = async <T extends ServerLike>(
102
111
  }
103
112
  };
104
113
 
114
+ // Helper function to apply CORS headers
115
+ const applyCorsHeaders = (
116
+ req: http.IncomingMessage,
117
+ res: http.ServerResponse,
118
+ corsOptions?: boolean | CorsOptions,
119
+ ) => {
120
+ if (!req.headers.origin) {
121
+ return;
122
+ }
123
+
124
+ // Default CORS configuration for backward compatibility
125
+ const defaultCorsOptions: CorsOptions = {
126
+ allowedHeaders: "Content-Type, Authorization, Accept, Mcp-Session-Id, Last-Event-Id",
127
+ credentials: true,
128
+ exposedHeaders: ["Mcp-Session-Id"],
129
+ methods: ["GET", "POST", "OPTIONS"],
130
+ origin: "*",
131
+ };
132
+
133
+ let finalCorsOptions: CorsOptions;
134
+
135
+ if (corsOptions === false) {
136
+ // CORS disabled
137
+ return;
138
+ } else if (corsOptions === true || corsOptions === undefined) {
139
+ // Use default CORS settings
140
+ finalCorsOptions = defaultCorsOptions;
141
+ } else {
142
+ // Merge user options with defaults
143
+ finalCorsOptions = {
144
+ ...defaultCorsOptions,
145
+ ...corsOptions,
146
+ };
147
+ }
148
+
149
+ try {
150
+ const origin = new URL(req.headers.origin);
151
+
152
+ // Handle origin
153
+ let allowedOrigin = "*";
154
+ if (finalCorsOptions.origin) {
155
+ if (typeof finalCorsOptions.origin === "string") {
156
+ allowedOrigin = finalCorsOptions.origin;
157
+ } else if (Array.isArray(finalCorsOptions.origin)) {
158
+ allowedOrigin = finalCorsOptions.origin.includes(origin.origin)
159
+ ? origin.origin
160
+ : "false";
161
+ } else if (typeof finalCorsOptions.origin === "function") {
162
+ allowedOrigin = finalCorsOptions.origin(origin.origin) ? origin.origin : "false";
163
+ }
164
+ }
165
+
166
+ if (allowedOrigin !== "false") {
167
+ res.setHeader("Access-Control-Allow-Origin", allowedOrigin);
168
+ }
169
+
170
+ // Handle credentials
171
+ if (finalCorsOptions.credentials !== undefined) {
172
+ res.setHeader("Access-Control-Allow-Credentials", finalCorsOptions.credentials.toString());
173
+ }
174
+
175
+ // Handle methods
176
+ if (finalCorsOptions.methods) {
177
+ res.setHeader("Access-Control-Allow-Methods", finalCorsOptions.methods.join(", "));
178
+ }
179
+
180
+ // Handle allowed headers
181
+ if (finalCorsOptions.allowedHeaders) {
182
+ const allowedHeaders = typeof finalCorsOptions.allowedHeaders === "string"
183
+ ? finalCorsOptions.allowedHeaders
184
+ : finalCorsOptions.allowedHeaders.join(", ");
185
+ res.setHeader("Access-Control-Allow-Headers", allowedHeaders);
186
+ }
187
+
188
+ // Handle exposed headers
189
+ if (finalCorsOptions.exposedHeaders) {
190
+ res.setHeader("Access-Control-Expose-Headers", finalCorsOptions.exposedHeaders.join(", "));
191
+ }
192
+
193
+ // Handle max age
194
+ if (finalCorsOptions.maxAge !== undefined) {
195
+ res.setHeader("Access-Control-Max-Age", finalCorsOptions.maxAge.toString());
196
+ }
197
+ } catch (error) {
198
+ console.error("[mcp-proxy] error parsing origin", error);
199
+ }
200
+ };
201
+
105
202
  const handleStreamRequest = async <T extends ServerLike>({
106
203
  activeTransports,
107
204
  authenticate,
@@ -586,6 +683,7 @@ const handleSSERequest = async <T extends ServerLike>({
586
683
  export const startHTTPServer = async <T extends ServerLike>({
587
684
  apiKey,
588
685
  authenticate,
686
+ cors,
589
687
  createServer,
590
688
  enableJsonResponse,
591
689
  eventStore,
@@ -601,6 +699,7 @@ export const startHTTPServer = async <T extends ServerLike>({
601
699
  }: {
602
700
  apiKey?: string;
603
701
  authenticate?: (request: http.IncomingMessage) => Promise<unknown>;
702
+ cors?: boolean | CorsOptions;
604
703
  createServer: (request: http.IncomingMessage) => Promise<T>;
605
704
  enableJsonResponse?: boolean;
606
705
  eventStore?: EventStore;
@@ -633,19 +732,8 @@ export const startHTTPServer = async <T extends ServerLike>({
633
732
  * @author https://dev.classmethod.jp/articles/mcp-sse/
634
733
  */
635
734
  const httpServer = http.createServer(async (req, res) => {
636
- if (req.headers.origin) {
637
- try {
638
- const origin = new URL(req.headers.origin);
639
-
640
- res.setHeader("Access-Control-Allow-Origin", origin.origin);
641
- res.setHeader("Access-Control-Allow-Credentials", "true");
642
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
643
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Accept, Mcp-Session-Id, Last-Event-Id");
644
- res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
645
- } catch (error) {
646
- console.error("[mcp-proxy] error parsing origin", error);
647
- }
648
- }
735
+ // Apply CORS headers
736
+ applyCorsHeaders(req, res, cors);
649
737
 
650
738
  if (req.method === "OPTIONS") {
651
739
  res.writeHead(204);