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/README.md +148 -1
- package/dist/bin/mcp-proxy.js +1 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +1 -1
- package/dist/{stdio-CsjPjeWC.js → stdio-DF5lH8jj.js} +44 -12
- package/dist/stdio-DF5lH8jj.js.map +1 -0
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/startHTTPServer.test.ts +248 -0
- package/src/startHTTPServer.ts +101 -13
- package/dist/stdio-CsjPjeWC.js.map +0 -1
package/jsr.json
CHANGED
package/package.json
CHANGED
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
|
+
});
|
package/src/startHTTPServer.ts
CHANGED
|
@@ -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
|
-
|
|
637
|
-
|
|
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);
|