mcp-proxy 5.8.1 → 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 +33 -3
- package/dist/index.js +2 -2
- package/dist/{stdio-9KZaSDCW.js → stdio-DF5lH8jj.js} +92 -42
- package/dist/stdio-DF5lH8jj.js.map +1 -0
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/authentication.test.ts +77 -2
- package/src/authentication.ts +16 -4
- package/src/index.ts +3 -0
- package/src/startHTTPServer.test.ts +248 -0
- package/src/startHTTPServer.ts +147 -15
- package/dist/stdio-9KZaSDCW.js.map +0 -1
package/jsr.json
CHANGED
package/package.json
CHANGED
|
@@ -103,15 +103,90 @@ describe("AuthenticationMiddleware", () => {
|
|
|
103
103
|
expect(body.id).toBe(null);
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
-
it("should have consistent format regardless of configuration", () => {
|
|
106
|
+
it("should have consistent body format regardless of configuration", () => {
|
|
107
107
|
const middleware1 = new AuthenticationMiddleware({});
|
|
108
108
|
const middleware2 = new AuthenticationMiddleware({ apiKey: "test" });
|
|
109
109
|
|
|
110
110
|
const response1 = middleware1.getUnauthorizedResponse();
|
|
111
111
|
const response2 = middleware2.getUnauthorizedResponse();
|
|
112
112
|
|
|
113
|
-
expect(response1.headers).toEqual(response2.headers);
|
|
114
113
|
expect(response1.body).toEqual(response2.body);
|
|
115
114
|
});
|
|
115
|
+
|
|
116
|
+
it("should not include WWW-Authenticate header without OAuth config", () => {
|
|
117
|
+
const middleware = new AuthenticationMiddleware({ apiKey: "test" });
|
|
118
|
+
const response = middleware.getUnauthorizedResponse();
|
|
119
|
+
|
|
120
|
+
expect(response.headers["WWW-Authenticate"]).toBeUndefined();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should include WWW-Authenticate header with OAuth config", () => {
|
|
124
|
+
const middleware = new AuthenticationMiddleware({
|
|
125
|
+
apiKey: "test",
|
|
126
|
+
oauth: {
|
|
127
|
+
protectedResource: {
|
|
128
|
+
resource: "https://example.com",
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
const response = middleware.getUnauthorizedResponse();
|
|
133
|
+
|
|
134
|
+
expect(response.headers["WWW-Authenticate"]).toBe(
|
|
135
|
+
'Bearer resource_metadata="https://example.com/.well-known/oauth-protected-resource"',
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("should handle OAuth config with trailing slash in resource URL", () => {
|
|
140
|
+
const middleware = new AuthenticationMiddleware({
|
|
141
|
+
apiKey: "test",
|
|
142
|
+
oauth: {
|
|
143
|
+
protectedResource: {
|
|
144
|
+
resource: "https://example.com/",
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
const response = middleware.getUnauthorizedResponse();
|
|
149
|
+
|
|
150
|
+
expect(response.headers["WWW-Authenticate"]).toBe(
|
|
151
|
+
'Bearer resource_metadata="https://example.com//.well-known/oauth-protected-resource"',
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("should not include WWW-Authenticate header when OAuth config is empty", () => {
|
|
156
|
+
const middleware = new AuthenticationMiddleware({
|
|
157
|
+
apiKey: "test",
|
|
158
|
+
oauth: {},
|
|
159
|
+
});
|
|
160
|
+
const response = middleware.getUnauthorizedResponse();
|
|
161
|
+
|
|
162
|
+
expect(response.headers["WWW-Authenticate"]).toBeUndefined();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("should not include WWW-Authenticate header when protectedResource is empty", () => {
|
|
166
|
+
const middleware = new AuthenticationMiddleware({
|
|
167
|
+
apiKey: "test",
|
|
168
|
+
oauth: {
|
|
169
|
+
protectedResource: {},
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
const response = middleware.getUnauthorizedResponse();
|
|
173
|
+
|
|
174
|
+
expect(response.headers["WWW-Authenticate"]).toBeUndefined();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("should include WWW-Authenticate header with OAuth config but no apiKey", () => {
|
|
178
|
+
const middleware = new AuthenticationMiddleware({
|
|
179
|
+
oauth: {
|
|
180
|
+
protectedResource: {
|
|
181
|
+
resource: "https://example.com",
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
const response = middleware.getUnauthorizedResponse();
|
|
186
|
+
|
|
187
|
+
expect(response.headers["WWW-Authenticate"]).toBe(
|
|
188
|
+
'Bearer resource_metadata="https://example.com/.well-known/oauth-protected-resource"',
|
|
189
|
+
);
|
|
190
|
+
});
|
|
116
191
|
});
|
|
117
192
|
});
|
package/src/authentication.ts
CHANGED
|
@@ -2,12 +2,26 @@ import type { IncomingMessage } from "http";
|
|
|
2
2
|
|
|
3
3
|
export interface AuthConfig {
|
|
4
4
|
apiKey?: string;
|
|
5
|
+
oauth?: {
|
|
6
|
+
protectedResource?: {
|
|
7
|
+
resource?: string;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
5
10
|
}
|
|
6
11
|
|
|
7
12
|
export class AuthenticationMiddleware {
|
|
8
13
|
constructor(private config: AuthConfig = {}) {}
|
|
9
14
|
|
|
10
|
-
getUnauthorizedResponse() {
|
|
15
|
+
getUnauthorizedResponse(): { body: string; headers: Record<string, string> } {
|
|
16
|
+
const headers: Record<string, string> = {
|
|
17
|
+
"Content-Type": "application/json",
|
|
18
|
+
};
|
|
19
|
+
|
|
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"`;
|
|
23
|
+
}
|
|
24
|
+
|
|
11
25
|
return {
|
|
12
26
|
body: JSON.stringify({
|
|
13
27
|
error: {
|
|
@@ -17,9 +31,7 @@ export class AuthenticationMiddleware {
|
|
|
17
31
|
id: null,
|
|
18
32
|
jsonrpc: "2.0",
|
|
19
33
|
}),
|
|
20
|
-
headers
|
|
21
|
-
"Content-Type": "application/json",
|
|
22
|
-
},
|
|
34
|
+
headers,
|
|
23
35
|
};
|
|
24
36
|
}
|
|
25
37
|
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
export type { AuthConfig } from "./authentication.js";
|
|
2
|
+
export { AuthenticationMiddleware } from "./authentication.js";
|
|
1
3
|
export { InMemoryEventStore } from "./InMemoryEventStore.js";
|
|
2
4
|
export { proxyServer } from "./proxyServer.js";
|
|
5
|
+
export type { CorsOptions } from "./startHTTPServer.js";
|
|
3
6
|
export { startHTTPServer } from "./startHTTPServer.js";
|
|
4
7
|
export { ServerType, startStdioServer } from "./startStdioServer.js";
|
|
5
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
|
@@ -8,9 +8,18 @@ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
|
8
8
|
import http from "http";
|
|
9
9
|
import { randomUUID } from "node:crypto";
|
|
10
10
|
|
|
11
|
-
import { AuthenticationMiddleware } from "./authentication.js";
|
|
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
|
};
|
|
@@ -49,6 +58,17 @@ const createJsonRpcErrorResponse = (code: number, message: string) => {
|
|
|
49
58
|
});
|
|
50
59
|
};
|
|
51
60
|
|
|
61
|
+
// Helper function to get WWW-Authenticate header value
|
|
62
|
+
const getWWWAuthenticateHeader = (
|
|
63
|
+
oauth?: AuthConfig["oauth"],
|
|
64
|
+
): string | undefined => {
|
|
65
|
+
if (!oauth?.protectedResource?.resource) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return `Bearer resource_metadata="${oauth.protectedResource.resource}/.well-known/oauth-protected-resource"`;
|
|
70
|
+
};
|
|
71
|
+
|
|
52
72
|
// Helper function to handle Response errors and send appropriate HTTP response
|
|
53
73
|
const handleResponseError = (
|
|
54
74
|
error: unknown,
|
|
@@ -91,6 +111,94 @@ const cleanupServer = async <T extends ServerLike>(
|
|
|
91
111
|
}
|
|
92
112
|
};
|
|
93
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
|
+
|
|
94
202
|
const handleStreamRequest = async <T extends ServerLike>({
|
|
95
203
|
activeTransports,
|
|
96
204
|
authenticate,
|
|
@@ -98,6 +206,7 @@ const handleStreamRequest = async <T extends ServerLike>({
|
|
|
98
206
|
enableJsonResponse,
|
|
99
207
|
endpoint,
|
|
100
208
|
eventStore,
|
|
209
|
+
oauth,
|
|
101
210
|
onClose,
|
|
102
211
|
onConnect,
|
|
103
212
|
req,
|
|
@@ -113,6 +222,7 @@ const handleStreamRequest = async <T extends ServerLike>({
|
|
|
113
222
|
enableJsonResponse?: boolean;
|
|
114
223
|
endpoint: string;
|
|
115
224
|
eventStore?: EventStore;
|
|
225
|
+
oauth?: AuthConfig["oauth"];
|
|
116
226
|
onClose?: (server: T) => Promise<void>;
|
|
117
227
|
onConnect?: (server: T) => Promise<void>;
|
|
118
228
|
req: http.IncomingMessage;
|
|
@@ -148,6 +258,13 @@ const handleStreamRequest = async <T extends ServerLike>({
|
|
|
148
258
|
: "Unauthorized: Authentication failed";
|
|
149
259
|
|
|
150
260
|
res.setHeader("Content-Type", "application/json");
|
|
261
|
+
|
|
262
|
+
// Add WWW-Authenticate header if OAuth config is available
|
|
263
|
+
const wwwAuthHeader = getWWWAuthenticateHeader(oauth);
|
|
264
|
+
if (wwwAuthHeader) {
|
|
265
|
+
res.setHeader("WWW-Authenticate", wwwAuthHeader);
|
|
266
|
+
}
|
|
267
|
+
|
|
151
268
|
res.writeHead(401).end(
|
|
152
269
|
JSON.stringify({
|
|
153
270
|
error: {
|
|
@@ -165,6 +282,13 @@ const handleStreamRequest = async <T extends ServerLike>({
|
|
|
165
282
|
const errorMessage = error instanceof Error ? error.message : "Unauthorized: Authentication error";
|
|
166
283
|
console.error("Authentication error:", error);
|
|
167
284
|
res.setHeader("Content-Type", "application/json");
|
|
285
|
+
|
|
286
|
+
// Add WWW-Authenticate header if OAuth config is available
|
|
287
|
+
const wwwAuthHeader = getWWWAuthenticateHeader(oauth);
|
|
288
|
+
if (wwwAuthHeader) {
|
|
289
|
+
res.setHeader("WWW-Authenticate", wwwAuthHeader);
|
|
290
|
+
}
|
|
291
|
+
|
|
168
292
|
res.writeHead(401).end(
|
|
169
293
|
JSON.stringify({
|
|
170
294
|
error: {
|
|
@@ -242,6 +366,13 @@ const handleStreamRequest = async <T extends ServerLike>({
|
|
|
242
366
|
|
|
243
367
|
if (isAuthError) {
|
|
244
368
|
res.setHeader("Content-Type", "application/json");
|
|
369
|
+
|
|
370
|
+
// Add WWW-Authenticate header if OAuth config is available
|
|
371
|
+
const wwwAuthHeader = getWWWAuthenticateHeader(oauth);
|
|
372
|
+
if (wwwAuthHeader) {
|
|
373
|
+
res.setHeader("WWW-Authenticate", wwwAuthHeader);
|
|
374
|
+
}
|
|
375
|
+
|
|
245
376
|
res.writeHead(401).end(JSON.stringify({
|
|
246
377
|
error: {
|
|
247
378
|
code: -32000,
|
|
@@ -294,6 +425,13 @@ const handleStreamRequest = async <T extends ServerLike>({
|
|
|
294
425
|
|
|
295
426
|
if (isAuthError) {
|
|
296
427
|
res.setHeader("Content-Type", "application/json");
|
|
428
|
+
|
|
429
|
+
// Add WWW-Authenticate header if OAuth config is available
|
|
430
|
+
const wwwAuthHeader = getWWWAuthenticateHeader(oauth);
|
|
431
|
+
if (wwwAuthHeader) {
|
|
432
|
+
res.setHeader("WWW-Authenticate", wwwAuthHeader);
|
|
433
|
+
}
|
|
434
|
+
|
|
297
435
|
res.writeHead(401).end(JSON.stringify({
|
|
298
436
|
error: {
|
|
299
437
|
code: -32000,
|
|
@@ -545,10 +683,12 @@ const handleSSERequest = async <T extends ServerLike>({
|
|
|
545
683
|
export const startHTTPServer = async <T extends ServerLike>({
|
|
546
684
|
apiKey,
|
|
547
685
|
authenticate,
|
|
686
|
+
cors,
|
|
548
687
|
createServer,
|
|
549
688
|
enableJsonResponse,
|
|
550
689
|
eventStore,
|
|
551
690
|
host = "::",
|
|
691
|
+
oauth,
|
|
552
692
|
onClose,
|
|
553
693
|
onConnect,
|
|
554
694
|
onUnhandledRequest,
|
|
@@ -559,10 +699,12 @@ export const startHTTPServer = async <T extends ServerLike>({
|
|
|
559
699
|
}: {
|
|
560
700
|
apiKey?: string;
|
|
561
701
|
authenticate?: (request: http.IncomingMessage) => Promise<unknown>;
|
|
702
|
+
cors?: boolean | CorsOptions;
|
|
562
703
|
createServer: (request: http.IncomingMessage) => Promise<T>;
|
|
563
704
|
enableJsonResponse?: boolean;
|
|
564
705
|
eventStore?: EventStore;
|
|
565
706
|
host?: string;
|
|
707
|
+
oauth?: AuthConfig["oauth"];
|
|
566
708
|
onClose?: (server: T) => Promise<void>;
|
|
567
709
|
onConnect?: (server: T) => Promise<void>;
|
|
568
710
|
onUnhandledRequest?: (
|
|
@@ -584,25 +726,14 @@ export const startHTTPServer = async <T extends ServerLike>({
|
|
|
584
726
|
}
|
|
585
727
|
> = {};
|
|
586
728
|
|
|
587
|
-
const authMiddleware = new AuthenticationMiddleware({ apiKey });
|
|
729
|
+
const authMiddleware = new AuthenticationMiddleware({ apiKey, oauth });
|
|
588
730
|
|
|
589
731
|
/**
|
|
590
732
|
* @author https://dev.classmethod.jp/articles/mcp-sse/
|
|
591
733
|
*/
|
|
592
734
|
const httpServer = http.createServer(async (req, res) => {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
const origin = new URL(req.headers.origin);
|
|
596
|
-
|
|
597
|
-
res.setHeader("Access-Control-Allow-Origin", origin.origin);
|
|
598
|
-
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
599
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
600
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Accept, Mcp-Session-Id, Last-Event-Id");
|
|
601
|
-
res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
|
|
602
|
-
} catch (error) {
|
|
603
|
-
console.error("[mcp-proxy] error parsing origin", error);
|
|
604
|
-
}
|
|
605
|
-
}
|
|
735
|
+
// Apply CORS headers
|
|
736
|
+
applyCorsHeaders(req, res, cors);
|
|
606
737
|
|
|
607
738
|
if (req.method === "OPTIONS") {
|
|
608
739
|
res.writeHead(204);
|
|
@@ -647,6 +778,7 @@ export const startHTTPServer = async <T extends ServerLike>({
|
|
|
647
778
|
enableJsonResponse,
|
|
648
779
|
endpoint: streamEndpoint,
|
|
649
780
|
eventStore,
|
|
781
|
+
oauth,
|
|
650
782
|
onClose,
|
|
651
783
|
onConnect,
|
|
652
784
|
req,
|