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/README.md +148 -1
- package/dist/bin/mcp-proxy.js +1 -1
- package/dist/index.d.ts +22 -2
- package/dist/index.js +1 -1
- package/dist/{stdio-CsjPjeWC.js → stdio-BEX6di72.js} +105 -29
- package/dist/stdio-BEX6di72.js.map +1 -0
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/authentication.test.ts +145 -7
- package/src/authentication.ts +51 -5
- package/src/index.ts +1 -0
- package/src/startHTTPServer.test.ts +392 -0
- package/src/startHTTPServer.ts +207 -35
- package/dist/stdio-CsjPjeWC.js.map +0 -1
|
@@ -1585,6 +1585,150 @@ it("returns 500 when createServer throws non-auth error", async () => {
|
|
|
1585
1585
|
await httpServer.close();
|
|
1586
1586
|
});
|
|
1587
1587
|
|
|
1588
|
+
it("includes WWW-Authenticate header in 401 response with OAuth config", async () => {
|
|
1589
|
+
const port = await getRandomPort();
|
|
1590
|
+
|
|
1591
|
+
const httpServer = await startHTTPServer({
|
|
1592
|
+
createServer: async () => {
|
|
1593
|
+
throw new Error("Invalid JWT token");
|
|
1594
|
+
},
|
|
1595
|
+
oauth: {
|
|
1596
|
+
protectedResource: {
|
|
1597
|
+
resource: "https://example.com",
|
|
1598
|
+
},
|
|
1599
|
+
realm: "mcp-server",
|
|
1600
|
+
},
|
|
1601
|
+
port,
|
|
1602
|
+
stateless: true,
|
|
1603
|
+
});
|
|
1604
|
+
|
|
1605
|
+
const response = await fetch(`http://localhost:${port}/mcp`, {
|
|
1606
|
+
body: JSON.stringify({
|
|
1607
|
+
id: 1,
|
|
1608
|
+
jsonrpc: "2.0",
|
|
1609
|
+
method: "initialize",
|
|
1610
|
+
params: {
|
|
1611
|
+
capabilities: {},
|
|
1612
|
+
clientInfo: { name: "test", version: "1.0.0" },
|
|
1613
|
+
protocolVersion: "2024-11-05",
|
|
1614
|
+
},
|
|
1615
|
+
}),
|
|
1616
|
+
headers: {
|
|
1617
|
+
"Accept": "application/json, text/event-stream",
|
|
1618
|
+
"Content-Type": "application/json",
|
|
1619
|
+
},
|
|
1620
|
+
method: "POST",
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
expect(response.status).toBe(401);
|
|
1624
|
+
|
|
1625
|
+
const wwwAuthHeader = response.headers.get("WWW-Authenticate");
|
|
1626
|
+
expect(wwwAuthHeader).toBeTruthy();
|
|
1627
|
+
expect(wwwAuthHeader).toContain('Bearer');
|
|
1628
|
+
expect(wwwAuthHeader).toContain('realm="mcp-server"');
|
|
1629
|
+
expect(wwwAuthHeader).toContain('resource_metadata="https://example.com/.well-known/oauth-protected-resource"');
|
|
1630
|
+
expect(wwwAuthHeader).toContain('error="invalid_token"');
|
|
1631
|
+
expect(wwwAuthHeader).toContain('error_description="Invalid JWT token"');
|
|
1632
|
+
|
|
1633
|
+
await httpServer.close();
|
|
1634
|
+
});
|
|
1635
|
+
|
|
1636
|
+
it("includes WWW-Authenticate header when authenticate callback fails with OAuth", async () => {
|
|
1637
|
+
const port = await getRandomPort();
|
|
1638
|
+
|
|
1639
|
+
const authenticate = vi.fn().mockRejectedValue(new Error("Token signature verification failed"));
|
|
1640
|
+
|
|
1641
|
+
const httpServer = await startHTTPServer({
|
|
1642
|
+
authenticate,
|
|
1643
|
+
createServer: async () => {
|
|
1644
|
+
const mcpServer = new Server(
|
|
1645
|
+
{ name: "test", version: "1.0.0" },
|
|
1646
|
+
{ capabilities: {} },
|
|
1647
|
+
);
|
|
1648
|
+
return mcpServer;
|
|
1649
|
+
},
|
|
1650
|
+
oauth: {
|
|
1651
|
+
error_uri: "https://example.com/docs/errors",
|
|
1652
|
+
protectedResource: {
|
|
1653
|
+
resource: "https://api.example.com",
|
|
1654
|
+
},
|
|
1655
|
+
realm: "example-api",
|
|
1656
|
+
},
|
|
1657
|
+
port,
|
|
1658
|
+
stateless: true,
|
|
1659
|
+
});
|
|
1660
|
+
|
|
1661
|
+
const response = await fetch(`http://localhost:${port}/mcp`, {
|
|
1662
|
+
body: JSON.stringify({
|
|
1663
|
+
id: 1,
|
|
1664
|
+
jsonrpc: "2.0",
|
|
1665
|
+
method: "initialize",
|
|
1666
|
+
params: {
|
|
1667
|
+
capabilities: {},
|
|
1668
|
+
clientInfo: { name: "test", version: "1.0.0" },
|
|
1669
|
+
protocolVersion: "2024-11-05",
|
|
1670
|
+
},
|
|
1671
|
+
}),
|
|
1672
|
+
headers: {
|
|
1673
|
+
"Accept": "application/json, text/event-stream",
|
|
1674
|
+
"Authorization": "Bearer expired-token",
|
|
1675
|
+
"Content-Type": "application/json",
|
|
1676
|
+
},
|
|
1677
|
+
method: "POST",
|
|
1678
|
+
});
|
|
1679
|
+
|
|
1680
|
+
expect(response.status).toBe(401);
|
|
1681
|
+
expect(authenticate).toHaveBeenCalled();
|
|
1682
|
+
|
|
1683
|
+
const wwwAuthHeader = response.headers.get("WWW-Authenticate");
|
|
1684
|
+
expect(wwwAuthHeader).toBeTruthy();
|
|
1685
|
+
expect(wwwAuthHeader).toContain('Bearer');
|
|
1686
|
+
expect(wwwAuthHeader).toContain('realm="example-api"');
|
|
1687
|
+
expect(wwwAuthHeader).toContain('resource_metadata="https://api.example.com/.well-known/oauth-protected-resource"');
|
|
1688
|
+
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"');
|
|
1691
|
+
|
|
1692
|
+
await httpServer.close();
|
|
1693
|
+
});
|
|
1694
|
+
|
|
1695
|
+
it("does not include WWW-Authenticate header in 401 response without OAuth config", async () => {
|
|
1696
|
+
const port = await getRandomPort();
|
|
1697
|
+
|
|
1698
|
+
const httpServer = await startHTTPServer({
|
|
1699
|
+
createServer: async () => {
|
|
1700
|
+
throw new Error("Authentication required");
|
|
1701
|
+
},
|
|
1702
|
+
port,
|
|
1703
|
+
stateless: true,
|
|
1704
|
+
});
|
|
1705
|
+
|
|
1706
|
+
const response = await fetch(`http://localhost:${port}/mcp`, {
|
|
1707
|
+
body: JSON.stringify({
|
|
1708
|
+
id: 1,
|
|
1709
|
+
jsonrpc: "2.0",
|
|
1710
|
+
method: "initialize",
|
|
1711
|
+
params: {
|
|
1712
|
+
capabilities: {},
|
|
1713
|
+
clientInfo: { name: "test", version: "1.0.0" },
|
|
1714
|
+
protocolVersion: "2024-11-05",
|
|
1715
|
+
},
|
|
1716
|
+
}),
|
|
1717
|
+
headers: {
|
|
1718
|
+
"Accept": "application/json, text/event-stream",
|
|
1719
|
+
"Content-Type": "application/json",
|
|
1720
|
+
},
|
|
1721
|
+
method: "POST",
|
|
1722
|
+
});
|
|
1723
|
+
|
|
1724
|
+
expect(response.status).toBe(401);
|
|
1725
|
+
|
|
1726
|
+
const wwwAuthHeader = response.headers.get("WWW-Authenticate");
|
|
1727
|
+
expect(wwwAuthHeader).toBeNull();
|
|
1728
|
+
|
|
1729
|
+
await httpServer.close();
|
|
1730
|
+
});
|
|
1731
|
+
|
|
1588
1732
|
it("succeeds when authenticate returns { authenticated: true } in stateless mode", async () => {
|
|
1589
1733
|
const stdioTransport = new StdioClientTransport({
|
|
1590
1734
|
args: ["src/fixtures/simple-stdio-server.ts"],
|
|
@@ -1675,3 +1819,251 @@ it("succeeds when authenticate returns { authenticated: true } in stateless mode
|
|
|
1675
1819
|
await httpServer.close();
|
|
1676
1820
|
await stdioClient.close();
|
|
1677
1821
|
});
|
|
1822
|
+
|
|
1823
|
+
// CORS Configuration Tests
|
|
1824
|
+
|
|
1825
|
+
it("supports wildcard CORS headers", async () => {
|
|
1826
|
+
const port = await getRandomPort();
|
|
1827
|
+
|
|
1828
|
+
const httpServer = await startHTTPServer({
|
|
1829
|
+
cors: {
|
|
1830
|
+
allowedHeaders: "*",
|
|
1831
|
+
},
|
|
1832
|
+
createServer: async () => {
|
|
1833
|
+
const mcpServer = new Server(
|
|
1834
|
+
{ name: "test", version: "1.0.0" },
|
|
1835
|
+
{ capabilities: {} },
|
|
1836
|
+
);
|
|
1837
|
+
return mcpServer;
|
|
1838
|
+
},
|
|
1839
|
+
port,
|
|
1840
|
+
});
|
|
1841
|
+
|
|
1842
|
+
// Test OPTIONS request to verify CORS headers
|
|
1843
|
+
const response = await fetch(`http://localhost:${port}/mcp`, {
|
|
1844
|
+
headers: {
|
|
1845
|
+
Origin: "https://example.com",
|
|
1846
|
+
},
|
|
1847
|
+
method: "OPTIONS",
|
|
1848
|
+
});
|
|
1849
|
+
|
|
1850
|
+
expect(response.status).toBe(204);
|
|
1851
|
+
|
|
1852
|
+
// Verify wildcard is used for allowed headers
|
|
1853
|
+
const allowedHeaders = response.headers.get("Access-Control-Allow-Headers");
|
|
1854
|
+
expect(allowedHeaders).toBe("*");
|
|
1855
|
+
|
|
1856
|
+
await httpServer.close();
|
|
1857
|
+
});
|
|
1858
|
+
|
|
1859
|
+
it("supports custom CORS headers array", async () => {
|
|
1860
|
+
const port = await getRandomPort();
|
|
1861
|
+
|
|
1862
|
+
const httpServer = await startHTTPServer({
|
|
1863
|
+
cors: {
|
|
1864
|
+
allowedHeaders: ["Content-Type", "X-Custom-Header", "X-API-Key"],
|
|
1865
|
+
},
|
|
1866
|
+
createServer: async () => {
|
|
1867
|
+
const mcpServer = new Server(
|
|
1868
|
+
{ name: "test", version: "1.0.0" },
|
|
1869
|
+
{ capabilities: {} },
|
|
1870
|
+
);
|
|
1871
|
+
return mcpServer;
|
|
1872
|
+
},
|
|
1873
|
+
port,
|
|
1874
|
+
});
|
|
1875
|
+
|
|
1876
|
+
// Test OPTIONS request to verify CORS headers
|
|
1877
|
+
const response = await fetch(`http://localhost:${port}/mcp`, {
|
|
1878
|
+
headers: {
|
|
1879
|
+
Origin: "https://example.com",
|
|
1880
|
+
},
|
|
1881
|
+
method: "OPTIONS",
|
|
1882
|
+
});
|
|
1883
|
+
|
|
1884
|
+
expect(response.status).toBe(204);
|
|
1885
|
+
|
|
1886
|
+
// Verify custom headers are used
|
|
1887
|
+
const allowedHeaders = response.headers.get("Access-Control-Allow-Headers");
|
|
1888
|
+
expect(allowedHeaders).toBe("Content-Type, X-Custom-Header, X-API-Key");
|
|
1889
|
+
|
|
1890
|
+
await httpServer.close();
|
|
1891
|
+
});
|
|
1892
|
+
|
|
1893
|
+
it("supports origin validation with array", async () => {
|
|
1894
|
+
const port = await getRandomPort();
|
|
1895
|
+
|
|
1896
|
+
const httpServer = await startHTTPServer({
|
|
1897
|
+
cors: {
|
|
1898
|
+
origin: ["https://app.example.com", "https://admin.example.com"],
|
|
1899
|
+
},
|
|
1900
|
+
createServer: async () => {
|
|
1901
|
+
const mcpServer = new Server(
|
|
1902
|
+
{ name: "test", version: "1.0.0" },
|
|
1903
|
+
{ capabilities: {} },
|
|
1904
|
+
);
|
|
1905
|
+
return mcpServer;
|
|
1906
|
+
},
|
|
1907
|
+
port,
|
|
1908
|
+
});
|
|
1909
|
+
|
|
1910
|
+
// Test with allowed origin
|
|
1911
|
+
const response1 = await fetch(`http://localhost:${port}/mcp`, {
|
|
1912
|
+
headers: {
|
|
1913
|
+
Origin: "https://app.example.com",
|
|
1914
|
+
},
|
|
1915
|
+
method: "OPTIONS",
|
|
1916
|
+
});
|
|
1917
|
+
|
|
1918
|
+
expect(response1.status).toBe(204);
|
|
1919
|
+
expect(response1.headers.get("Access-Control-Allow-Origin")).toBe("https://app.example.com");
|
|
1920
|
+
|
|
1921
|
+
// Test with disallowed origin
|
|
1922
|
+
const response2 = await fetch(`http://localhost:${port}/mcp`, {
|
|
1923
|
+
headers: {
|
|
1924
|
+
Origin: "https://malicious.com",
|
|
1925
|
+
},
|
|
1926
|
+
method: "OPTIONS",
|
|
1927
|
+
});
|
|
1928
|
+
|
|
1929
|
+
expect(response2.status).toBe(204);
|
|
1930
|
+
expect(response2.headers.get("Access-Control-Allow-Origin")).toBeNull();
|
|
1931
|
+
|
|
1932
|
+
await httpServer.close();
|
|
1933
|
+
});
|
|
1934
|
+
|
|
1935
|
+
it("supports origin validation with function", async () => {
|
|
1936
|
+
const port = await getRandomPort();
|
|
1937
|
+
|
|
1938
|
+
const httpServer = await startHTTPServer({
|
|
1939
|
+
cors: {
|
|
1940
|
+
origin: (origin: string) => origin.endsWith(".example.com"),
|
|
1941
|
+
},
|
|
1942
|
+
createServer: async () => {
|
|
1943
|
+
const mcpServer = new Server(
|
|
1944
|
+
{ name: "test", version: "1.0.0" },
|
|
1945
|
+
{ capabilities: {} },
|
|
1946
|
+
);
|
|
1947
|
+
return mcpServer;
|
|
1948
|
+
},
|
|
1949
|
+
port,
|
|
1950
|
+
});
|
|
1951
|
+
|
|
1952
|
+
// Test with allowed origin
|
|
1953
|
+
const response1 = await fetch(`http://localhost:${port}/mcp`, {
|
|
1954
|
+
headers: {
|
|
1955
|
+
Origin: "https://subdomain.example.com",
|
|
1956
|
+
},
|
|
1957
|
+
method: "OPTIONS",
|
|
1958
|
+
});
|
|
1959
|
+
|
|
1960
|
+
expect(response1.status).toBe(204);
|
|
1961
|
+
expect(response1.headers.get("Access-Control-Allow-Origin")).toBe("https://subdomain.example.com");
|
|
1962
|
+
|
|
1963
|
+
// Test with disallowed origin
|
|
1964
|
+
const response2 = await fetch(`http://localhost:${port}/mcp`, {
|
|
1965
|
+
headers: {
|
|
1966
|
+
Origin: "https://malicious.com",
|
|
1967
|
+
},
|
|
1968
|
+
method: "OPTIONS",
|
|
1969
|
+
});
|
|
1970
|
+
|
|
1971
|
+
expect(response2.status).toBe(204);
|
|
1972
|
+
expect(response2.headers.get("Access-Control-Allow-Origin")).toBeNull();
|
|
1973
|
+
|
|
1974
|
+
await httpServer.close();
|
|
1975
|
+
});
|
|
1976
|
+
|
|
1977
|
+
it("disables CORS when cors: false", async () => {
|
|
1978
|
+
const port = await getRandomPort();
|
|
1979
|
+
|
|
1980
|
+
const httpServer = await startHTTPServer({
|
|
1981
|
+
cors: false,
|
|
1982
|
+
createServer: async () => {
|
|
1983
|
+
const mcpServer = new Server(
|
|
1984
|
+
{ name: "test", version: "1.0.0" },
|
|
1985
|
+
{ capabilities: {} },
|
|
1986
|
+
);
|
|
1987
|
+
return mcpServer;
|
|
1988
|
+
},
|
|
1989
|
+
port,
|
|
1990
|
+
});
|
|
1991
|
+
|
|
1992
|
+
// Test OPTIONS request - should not have CORS headers
|
|
1993
|
+
const response = await fetch(`http://localhost:${port}/mcp`, {
|
|
1994
|
+
headers: {
|
|
1995
|
+
Origin: "https://example.com",
|
|
1996
|
+
},
|
|
1997
|
+
method: "OPTIONS",
|
|
1998
|
+
});
|
|
1999
|
+
|
|
2000
|
+
expect(response.status).toBe(204);
|
|
2001
|
+
expect(response.headers.get("Access-Control-Allow-Origin")).toBeNull();
|
|
2002
|
+
expect(response.headers.get("Access-Control-Allow-Headers")).toBeNull();
|
|
2003
|
+
|
|
2004
|
+
await httpServer.close();
|
|
2005
|
+
});
|
|
2006
|
+
|
|
2007
|
+
it("uses default CORS settings when cors: true", async () => {
|
|
2008
|
+
const port = await getRandomPort();
|
|
2009
|
+
|
|
2010
|
+
const httpServer = await startHTTPServer({
|
|
2011
|
+
cors: true,
|
|
2012
|
+
createServer: async () => {
|
|
2013
|
+
const mcpServer = new Server(
|
|
2014
|
+
{ name: "test", version: "1.0.0" },
|
|
2015
|
+
{ capabilities: {} },
|
|
2016
|
+
);
|
|
2017
|
+
return mcpServer;
|
|
2018
|
+
},
|
|
2019
|
+
port,
|
|
2020
|
+
});
|
|
2021
|
+
|
|
2022
|
+
// Test OPTIONS request to verify default CORS headers
|
|
2023
|
+
const response = await fetch(`http://localhost:${port}/mcp`, {
|
|
2024
|
+
headers: {
|
|
2025
|
+
Origin: "https://example.com",
|
|
2026
|
+
},
|
|
2027
|
+
method: "OPTIONS",
|
|
2028
|
+
});
|
|
2029
|
+
|
|
2030
|
+
expect(response.status).toBe(204);
|
|
2031
|
+
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");
|
|
2033
|
+
expect(response.headers.get("Access-Control-Allow-Credentials")).toBe("true");
|
|
2034
|
+
|
|
2035
|
+
await httpServer.close();
|
|
2036
|
+
});
|
|
2037
|
+
|
|
2038
|
+
it("supports custom methods and maxAge", async () => {
|
|
2039
|
+
const port = await getRandomPort();
|
|
2040
|
+
|
|
2041
|
+
const httpServer = await startHTTPServer({
|
|
2042
|
+
cors: {
|
|
2043
|
+
maxAge: 86400,
|
|
2044
|
+
methods: ["GET", "POST", "PUT", "DELETE"],
|
|
2045
|
+
},
|
|
2046
|
+
createServer: async () => {
|
|
2047
|
+
const mcpServer = new Server(
|
|
2048
|
+
{ name: "test", version: "1.0.0" },
|
|
2049
|
+
{ capabilities: {} },
|
|
2050
|
+
);
|
|
2051
|
+
return mcpServer;
|
|
2052
|
+
},
|
|
2053
|
+
port,
|
|
2054
|
+
});
|
|
2055
|
+
|
|
2056
|
+
// Test OPTIONS request to verify custom settings
|
|
2057
|
+
const response = await fetch(`http://localhost:${port}/mcp`, {
|
|
2058
|
+
headers: {
|
|
2059
|
+
Origin: "https://example.com",
|
|
2060
|
+
},
|
|
2061
|
+
method: "OPTIONS",
|
|
2062
|
+
});
|
|
2063
|
+
|
|
2064
|
+
expect(response.status).toBe(204);
|
|
2065
|
+
expect(response.headers.get("Access-Control-Allow-Methods")).toBe("GET, POST, PUT, DELETE");
|
|
2066
|
+
expect(response.headers.get("Access-Control-Max-Age")).toBe("86400");
|
|
2067
|
+
|
|
2068
|
+
await httpServer.close();
|
|
2069
|
+
});
|