Merge branch 'main' into sebastian/patch-remove-addition-slash
This commit is contained in:
commit
5710318f45
20 changed files with 1100 additions and 291 deletions
0
.git-blame-ignore-revs
Normal file
0
.git-blame-ignore-revs
Normal file
47
README.md
47
README.md
|
|
@ -137,6 +137,53 @@ The inspector supports bearer token authentication for SSE connections. Enter yo
|
|||
|
||||
The MCP Inspector includes a proxy server that can run and communicate with local MCP processes. The proxy server should not be exposed to untrusted networks as it has permissions to spawn local processes and can connect to any specified MCP server.
|
||||
|
||||
#### Authentication
|
||||
|
||||
The MCP Inspector proxy server requires authentication by default. When starting the server, a random session token is generated and printed to the console:
|
||||
|
||||
```
|
||||
🔑 Session token: 3a1c267fad21f7150b7d624c160b7f09b0b8c4f623c7107bbf13378f051538d4
|
||||
|
||||
🔗 Open inspector with token pre-filled:
|
||||
http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=3a1c267fad21f7150b7d624c160b7f09b0b8c4f623c7107bbf13378f051538d4
|
||||
```
|
||||
|
||||
This token must be included as a Bearer token in the Authorization header for all requests to the server. When authentication is enabled, auto-open is disabled by default to ensure you use the secure URL.
|
||||
|
||||
**Recommended: Use the pre-filled URL** - Click or copy the link shown in the console to open the inspector with the token already configured.
|
||||
|
||||
**Alternative: Manual configuration** - If you already have the inspector open:
|
||||
|
||||
1. Click the "Configuration" button in the sidebar
|
||||
2. Find "Proxy Session Token" and enter the token displayed in the proxy console
|
||||
3. Click "Save" to apply the configuration
|
||||
|
||||
The token will be saved in your browser's local storage for future use.
|
||||
|
||||
If you need to disable authentication (NOT RECOMMENDED), you can set the `DANGEROUSLY_OMIT_AUTH` environment variable:
|
||||
|
||||
```bash
|
||||
DANGEROUSLY_OMIT_AUTH=true npm start
|
||||
```
|
||||
|
||||
#### Local-only Binding
|
||||
|
||||
By default, the MCP Inspector proxy server binds only to `127.0.0.1` (localhost) to prevent network access. This ensures the server is not accessible from other devices on the network. If you need to bind to all interfaces for development purposes, you can override this with the `HOST` environment variable:
|
||||
|
||||
```bash
|
||||
HOST=0.0.0.0 npm start
|
||||
```
|
||||
|
||||
**Warning:** Only bind to all interfaces in trusted network environments, as this exposes the proxy server's ability to execute local processes.
|
||||
|
||||
#### DNS Rebinding Protection
|
||||
|
||||
To prevent DNS rebinding attacks, the MCP Inspector validates the `Origin` header on incoming requests. By default, only requests from the client origin are allowed (respects `CLIENT_PORT` if set, defaulting to port 6274). You can configure additional allowed origins by setting the `ALLOWED_ORIGINS` environment variable (comma-separated list):
|
||||
|
||||
```bash
|
||||
ALLOWED_ORIGINS=http://localhost:6274,http://127.0.0.1:6274,http://localhost:8000 npm start
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
The MCP Inspector supports the following configuration settings. To change them, click on the `Configuration` button in the MCP Inspector UI:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@modelcontextprotocol/inspector-cli",
|
||||
"version": "0.14.0",
|
||||
"version": "0.14.2",
|
||||
"description": "CLI for the Model Context Protocol inspector",
|
||||
"license": "MIT",
|
||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||
|
|
|
|||
|
|
@ -100,7 +100,9 @@ async function main() {
|
|||
|
||||
if (serverOk) {
|
||||
try {
|
||||
if (process.env.MCP_AUTO_OPEN_ENABLED !== "false") {
|
||||
// Only auto-open when auth is disabled
|
||||
const authDisabled = !!process.env.DANGEROUSLY_OMIT_AUTH;
|
||||
if (process.env.MCP_AUTO_OPEN_ENABLED !== "false" && authDisabled) {
|
||||
open(`http://127.0.0.1:${CLIENT_PORT}`);
|
||||
}
|
||||
await spawnPromise("node", [inspectorClientPath], {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@modelcontextprotocol/inspector-client",
|
||||
"version": "0.14.0",
|
||||
"version": "0.14.2",
|
||||
"description": "Client-side application for the Model Context Protocol inspector",
|
||||
"license": "MIT",
|
||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||
|
|
|
|||
|
|
@ -63,11 +63,13 @@ import ToolsTab from "./components/ToolsTab";
|
|||
import { InspectorConfig } from "./lib/configurationTypes";
|
||||
import {
|
||||
getMCPProxyAddress,
|
||||
getMCPProxyAuthToken,
|
||||
getInitialSseUrl,
|
||||
getInitialTransportType,
|
||||
getInitialCommand,
|
||||
getInitialArgs,
|
||||
initializeInspectorConfig,
|
||||
saveInspectorConfig,
|
||||
} from "./utils/configUtils";
|
||||
|
||||
const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1";
|
||||
|
|
@ -226,7 +228,7 @@ const App = () => {
|
|||
}, [headerName]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config));
|
||||
saveInspectorConfig(CONFIG_LOCAL_STORAGE_KEY, config);
|
||||
}, [config]);
|
||||
|
||||
// Auto-connect to previously saved serverURL after OAuth callback
|
||||
|
|
@ -344,7 +346,14 @@ const App = () => {
|
|||
}, [sseUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${getMCPProxyAddress(config)}/config`)
|
||||
const headers: HeadersInit = {};
|
||||
const { token: proxyAuthToken, header: proxyAuthTokenHeader } =
|
||||
getMCPProxyAuthToken(config);
|
||||
if (proxyAuthToken) {
|
||||
headers[proxyAuthTokenHeader] = `Bearer ${proxyAuthToken}`;
|
||||
}
|
||||
|
||||
fetch(`${getMCPProxyAddress(config)}/config`, { headers })
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
setEnv(data.defaultEnvironment);
|
||||
|
|
@ -358,8 +367,7 @@ const App = () => {
|
|||
.catch((error) =>
|
||||
console.error("Error fetching default environment:", error),
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [config]);
|
||||
|
||||
useEffect(() => {
|
||||
rootsRef.current = roots;
|
||||
|
|
|
|||
240
client/src/__tests__/App.config.test.tsx
Normal file
240
client/src/__tests__/App.config.test.tsx
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
import { render, waitFor } from "@testing-library/react";
|
||||
import App from "../App";
|
||||
import { DEFAULT_INSPECTOR_CONFIG } from "../lib/constants";
|
||||
import { InspectorConfig } from "../lib/configurationTypes";
|
||||
import * as configUtils from "../utils/configUtils";
|
||||
|
||||
// Mock auth dependencies first
|
||||
jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
|
||||
auth: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../lib/oauth-state-machine", () => ({
|
||||
OAuthStateMachine: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../lib/auth", () => ({
|
||||
InspectorOAuthClientProvider: jest.fn().mockImplementation(() => ({
|
||||
tokens: jest.fn().mockResolvedValue(null),
|
||||
clear: jest.fn(),
|
||||
})),
|
||||
DebugInspectorOAuthClientProvider: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock the config utils
|
||||
jest.mock("../utils/configUtils", () => ({
|
||||
...jest.requireActual("../utils/configUtils"),
|
||||
getMCPProxyAddress: jest.fn(() => "http://localhost:6277"),
|
||||
getMCPProxyAuthToken: jest.fn((config: InspectorConfig) => ({
|
||||
token: config.MCP_PROXY_AUTH_TOKEN.value,
|
||||
header: "X-MCP-Proxy-Auth",
|
||||
})),
|
||||
getInitialTransportType: jest.fn(() => "stdio"),
|
||||
getInitialSseUrl: jest.fn(() => "http://localhost:3001/sse"),
|
||||
getInitialCommand: jest.fn(() => "mcp-server-everything"),
|
||||
getInitialArgs: jest.fn(() => ""),
|
||||
initializeInspectorConfig: jest.fn(() => DEFAULT_INSPECTOR_CONFIG),
|
||||
saveInspectorConfig: jest.fn(),
|
||||
}));
|
||||
|
||||
// Get references to the mocked functions
|
||||
const mockGetMCPProxyAuthToken = configUtils.getMCPProxyAuthToken as jest.Mock;
|
||||
const mockInitializeInspectorConfig =
|
||||
configUtils.initializeInspectorConfig as jest.Mock;
|
||||
|
||||
// Mock other dependencies
|
||||
jest.mock("../lib/hooks/useConnection", () => ({
|
||||
useConnection: () => ({
|
||||
connectionStatus: "disconnected",
|
||||
serverCapabilities: null,
|
||||
mcpClient: null,
|
||||
requestHistory: [],
|
||||
makeRequest: jest.fn(),
|
||||
sendNotification: jest.fn(),
|
||||
handleCompletion: jest.fn(),
|
||||
completionsSupported: false,
|
||||
connect: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock("../lib/hooks/useDraggablePane", () => ({
|
||||
useDraggablePane: () => ({
|
||||
height: 300,
|
||||
handleDragStart: jest.fn(),
|
||||
}),
|
||||
useDraggableSidebar: () => ({
|
||||
width: 320,
|
||||
isDragging: false,
|
||||
handleDragStart: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock("../components/Sidebar", () => ({
|
||||
__esModule: true,
|
||||
default: () => <div>Sidebar</div>,
|
||||
}));
|
||||
|
||||
// Mock fetch
|
||||
global.fetch = jest.fn();
|
||||
|
||||
describe("App - Config Endpoint", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(global.fetch as jest.Mock).mockResolvedValue({
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
defaultEnvironment: { TEST_ENV: "test" },
|
||||
defaultCommand: "test-command",
|
||||
defaultArgs: "test-args",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Reset getMCPProxyAuthToken to default behavior
|
||||
mockGetMCPProxyAuthToken.mockImplementation((config: InspectorConfig) => ({
|
||||
token: config.MCP_PROXY_AUTH_TOKEN.value,
|
||||
header: "X-MCP-Proxy-Auth",
|
||||
}));
|
||||
});
|
||||
|
||||
test("sends X-MCP-Proxy-Auth header when fetching config with proxy auth token", async () => {
|
||||
const mockConfig = {
|
||||
...DEFAULT_INSPECTOR_CONFIG,
|
||||
MCP_PROXY_AUTH_TOKEN: {
|
||||
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
|
||||
value: "test-proxy-token",
|
||||
},
|
||||
};
|
||||
|
||||
// Mock initializeInspectorConfig to return our test config
|
||||
mockInitializeInspectorConfig.mockReturnValue(mockConfig);
|
||||
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
"http://localhost:6277/config",
|
||||
{
|
||||
headers: {
|
||||
"X-MCP-Proxy-Auth": "Bearer test-proxy-token",
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("does not send auth header when proxy auth token is empty", async () => {
|
||||
const mockConfig = {
|
||||
...DEFAULT_INSPECTOR_CONFIG,
|
||||
MCP_PROXY_AUTH_TOKEN: {
|
||||
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
|
||||
value: "",
|
||||
},
|
||||
};
|
||||
|
||||
// Mock initializeInspectorConfig to return our test config
|
||||
mockInitializeInspectorConfig.mockReturnValue(mockConfig);
|
||||
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
"http://localhost:6277/config",
|
||||
{
|
||||
headers: {},
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("uses custom header name if getMCPProxyAuthToken returns different header", async () => {
|
||||
const mockConfig = {
|
||||
...DEFAULT_INSPECTOR_CONFIG,
|
||||
MCP_PROXY_AUTH_TOKEN: {
|
||||
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
|
||||
value: "test-proxy-token",
|
||||
},
|
||||
};
|
||||
|
||||
// Mock to return a custom header name
|
||||
mockGetMCPProxyAuthToken.mockReturnValue({
|
||||
token: "test-proxy-token",
|
||||
header: "X-Custom-Auth",
|
||||
});
|
||||
mockInitializeInspectorConfig.mockReturnValue(mockConfig);
|
||||
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
"http://localhost:6277/config",
|
||||
{
|
||||
headers: {
|
||||
"X-Custom-Auth": "Bearer test-proxy-token",
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("config endpoint response updates app state", async () => {
|
||||
const mockConfig = {
|
||||
...DEFAULT_INSPECTOR_CONFIG,
|
||||
MCP_PROXY_AUTH_TOKEN: {
|
||||
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
|
||||
value: "test-proxy-token",
|
||||
},
|
||||
};
|
||||
|
||||
mockInitializeInspectorConfig.mockReturnValue(mockConfig);
|
||||
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// Verify the fetch was called with correct parameters
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
"http://localhost:6277/config",
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
"X-MCP-Proxy-Auth": "Bearer test-proxy-token",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("handles config endpoint errors gracefully", async () => {
|
||||
const mockConfig = {
|
||||
...DEFAULT_INSPECTOR_CONFIG,
|
||||
MCP_PROXY_AUTH_TOKEN: {
|
||||
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
|
||||
value: "test-proxy-token",
|
||||
},
|
||||
};
|
||||
|
||||
mockInitializeInspectorConfig.mockReturnValue(mockConfig);
|
||||
|
||||
// Mock fetch to reject
|
||||
(global.fetch as jest.Mock).mockRejectedValue(new Error("Network error"));
|
||||
|
||||
// Spy on console.error
|
||||
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation();
|
||||
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
"Error fetching default environment:",
|
||||
expect.any(Error),
|
||||
);
|
||||
});
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
|
@ -666,8 +666,13 @@ const Sidebar = ({
|
|||
switch (connectionStatus) {
|
||||
case "connected":
|
||||
return "Connected";
|
||||
case "error":
|
||||
return "Connection Error, is your MCP server running?";
|
||||
case "error": {
|
||||
const hasProxyToken = config.MCP_PROXY_AUTH_TOKEN?.value;
|
||||
if (!hasProxyToken) {
|
||||
return "Connection Error - Did you add the proxy session token in Configuration?";
|
||||
}
|
||||
return "Connection Error - Check if your MCP server is running and proxy token is correct";
|
||||
}
|
||||
case "error-connecting-to-proxy":
|
||||
return "Error Connecting to MCP Inspector Proxy - Check Console logs";
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -84,12 +84,6 @@ Object.defineProperty(window, "sessionStorage", {
|
|||
value: sessionStorageMock,
|
||||
});
|
||||
|
||||
Object.defineProperty(window, "location", {
|
||||
value: {
|
||||
origin: "http://localhost:3000",
|
||||
},
|
||||
});
|
||||
|
||||
describe("AuthDebugger", () => {
|
||||
const defaultAuthState = EMPTY_DEBUGGER_STATE;
|
||||
|
||||
|
|
@ -104,7 +98,7 @@ describe("AuthDebugger", () => {
|
|||
jest.clearAllMocks();
|
||||
sessionStorageMock.getItem.mockReturnValue(null);
|
||||
|
||||
// Supress
|
||||
// Suppress console errors in tests to avoid JSDOM navigation noise
|
||||
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata);
|
||||
|
|
@ -447,18 +441,6 @@ describe("AuthDebugger", () => {
|
|||
it("should store auth state to sessionStorage before redirect in Quick OAuth Flow", async () => {
|
||||
const updateAuthState = jest.fn();
|
||||
|
||||
// Mock window.location.href setter
|
||||
const originalLocation = window.location;
|
||||
const locationMock = {
|
||||
...originalLocation,
|
||||
href: "",
|
||||
origin: "http://localhost:3000",
|
||||
};
|
||||
Object.defineProperty(window, "location", {
|
||||
writable: true,
|
||||
value: locationMock,
|
||||
});
|
||||
|
||||
// Setup mocks for OAuth flow
|
||||
mockStartAuthorization.mockResolvedValue({
|
||||
authorizationUrl: new URL(
|
||||
|
|
|
|||
|
|
@ -519,6 +519,7 @@ describe("Sidebar Environment Variables", () => {
|
|||
label: "Request Timeout",
|
||||
description: "Timeout for requests to the MCP server (ms)",
|
||||
value: 5000,
|
||||
is_session_item: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
@ -544,6 +545,7 @@ describe("Sidebar Environment Variables", () => {
|
|||
description:
|
||||
"Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577",
|
||||
value: "http://localhost:8080",
|
||||
is_session_item: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
@ -569,6 +571,7 @@ describe("Sidebar Environment Variables", () => {
|
|||
description:
|
||||
"Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications)",
|
||||
value: 10000,
|
||||
is_session_item: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
@ -591,6 +594,7 @@ describe("Sidebar Environment Variables", () => {
|
|||
label: "Request Timeout",
|
||||
description: "Timeout for requests to the MCP server (ms)",
|
||||
value: 0,
|
||||
is_session_item: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
@ -637,6 +641,7 @@ describe("Sidebar Environment Variables", () => {
|
|||
label: "Request Timeout",
|
||||
description: "Timeout for requests to the MCP server (ms)",
|
||||
value: 3000,
|
||||
is_session_item: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ export type ConfigItem = {
|
|||
label: string;
|
||||
description: string;
|
||||
value: string | number | boolean;
|
||||
is_session_item: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -33,4 +34,9 @@ export type InspectorConfig = {
|
|||
* The full address of the MCP Proxy Server, in case it is running on a non-default address. Example: http://10.1.1.22:5577
|
||||
*/
|
||||
MCP_PROXY_FULL_ADDRESS: ConfigItem;
|
||||
|
||||
/**
|
||||
* Session token for authenticating with the MCP Proxy Server. This token is displayed in the proxy server console on startup.
|
||||
*/
|
||||
MCP_PROXY_AUTH_TOKEN: ConfigItem;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -36,22 +36,33 @@ export const DEFAULT_INSPECTOR_CONFIG: InspectorConfig = {
|
|||
label: "Request Timeout",
|
||||
description: "Timeout for requests to the MCP server (ms)",
|
||||
value: 10000,
|
||||
is_session_item: false,
|
||||
},
|
||||
MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS: {
|
||||
label: "Reset Timeout on Progress",
|
||||
description: "Reset timeout on progress notifications",
|
||||
value: true,
|
||||
is_session_item: false,
|
||||
},
|
||||
MCP_REQUEST_MAX_TOTAL_TIMEOUT: {
|
||||
label: "Maximum Total Timeout",
|
||||
description:
|
||||
"Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications)",
|
||||
value: 60000,
|
||||
is_session_item: false,
|
||||
},
|
||||
MCP_PROXY_FULL_ADDRESS: {
|
||||
label: "Inspector Proxy Address",
|
||||
description:
|
||||
"Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577",
|
||||
value: "",
|
||||
is_session_item: false,
|
||||
},
|
||||
MCP_PROXY_AUTH_TOKEN: {
|
||||
label: "Proxy Session Token",
|
||||
description:
|
||||
"Session token for authenticating with the MCP Proxy Server (displayed in proxy console on startup)",
|
||||
value: "",
|
||||
is_session_item: true,
|
||||
},
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useConnection } from "../useConnection";
|
|||
import { z } from "zod";
|
||||
import { ClientRequest } from "@modelcontextprotocol/sdk/types.js";
|
||||
import { DEFAULT_INSPECTOR_CONFIG } from "../../constants";
|
||||
import { SSEClientTransportOptions } from "@modelcontextprotocol/sdk/client/sse.js";
|
||||
|
||||
// Mock fetch
|
||||
global.fetch = jest.fn().mockResolvedValue({
|
||||
|
|
@ -23,21 +24,46 @@ const mockClient = {
|
|||
setRequestHandler: jest.fn(),
|
||||
};
|
||||
|
||||
// Mock transport instances
|
||||
const mockSSETransport: {
|
||||
start: jest.Mock;
|
||||
url: URL | undefined;
|
||||
options: SSEClientTransportOptions | undefined;
|
||||
} = {
|
||||
start: jest.fn(),
|
||||
url: undefined,
|
||||
options: undefined,
|
||||
};
|
||||
|
||||
const mockStreamableHTTPTransport: {
|
||||
start: jest.Mock;
|
||||
url: URL | undefined;
|
||||
options: SSEClientTransportOptions | undefined;
|
||||
} = {
|
||||
start: jest.fn(),
|
||||
url: undefined,
|
||||
options: undefined,
|
||||
};
|
||||
|
||||
jest.mock("@modelcontextprotocol/sdk/client/index.js", () => ({
|
||||
Client: jest.fn().mockImplementation(() => mockClient),
|
||||
}));
|
||||
|
||||
jest.mock("@modelcontextprotocol/sdk/client/sse.js", () => ({
|
||||
SSEClientTransport: jest.fn((url) => ({
|
||||
toString: () => url,
|
||||
})),
|
||||
SSEClientTransport: jest.fn((url, options) => {
|
||||
mockSSETransport.url = url;
|
||||
mockSSETransport.options = options;
|
||||
return mockSSETransport;
|
||||
}),
|
||||
SseError: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
|
||||
StreamableHTTPClientTransport: jest.fn((url) => ({
|
||||
toString: () => url,
|
||||
})),
|
||||
StreamableHTTPClientTransport: jest.fn((url, options) => {
|
||||
mockStreamableHTTPTransport.url = url;
|
||||
mockStreamableHTTPTransport.options = options;
|
||||
return mockStreamableHTTPTransport;
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
|
||||
|
|
@ -259,4 +285,181 @@ describe("useConnection", () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Proxy Authentication Headers", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// Reset the mock transport objects
|
||||
mockSSETransport.url = undefined;
|
||||
mockSSETransport.options = undefined;
|
||||
mockStreamableHTTPTransport.url = undefined;
|
||||
mockStreamableHTTPTransport.options = undefined;
|
||||
});
|
||||
|
||||
test("sends X-MCP-Proxy-Auth header when proxy auth token is configured", async () => {
|
||||
const propsWithProxyAuth = {
|
||||
...defaultProps,
|
||||
config: {
|
||||
...DEFAULT_INSPECTOR_CONFIG,
|
||||
MCP_PROXY_AUTH_TOKEN: {
|
||||
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
|
||||
value: "test-proxy-token",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useConnection(propsWithProxyAuth));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.connect();
|
||||
});
|
||||
|
||||
// Check that the transport was created with the correct headers
|
||||
expect(mockSSETransport.options).toBeDefined();
|
||||
expect(mockSSETransport.options?.requestInit).toBeDefined();
|
||||
|
||||
expect(mockSSETransport.options?.requestInit?.headers).toHaveProperty(
|
||||
"X-MCP-Proxy-Auth",
|
||||
"Bearer test-proxy-token",
|
||||
);
|
||||
expect(mockSSETransport?.options?.eventSourceInit?.fetch).toBeDefined();
|
||||
|
||||
// Verify the fetch function includes the proxy auth header
|
||||
const mockFetch = mockSSETransport.options?.eventSourceInit?.fetch;
|
||||
const testUrl = "http://test.com";
|
||||
await mockFetch?.(testUrl, {
|
||||
headers: {
|
||||
Accept: "text/event-stream",
|
||||
},
|
||||
cache: "no-store",
|
||||
mode: "cors",
|
||||
signal: new AbortController().signal,
|
||||
redirect: "follow",
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledTimes(2);
|
||||
expect(
|
||||
(global.fetch as jest.Mock).mock.calls[0][1].headers,
|
||||
).toHaveProperty("X-MCP-Proxy-Auth", "Bearer test-proxy-token");
|
||||
expect((global.fetch as jest.Mock).mock.calls[1][0]).toBe(testUrl);
|
||||
expect(
|
||||
(global.fetch as jest.Mock).mock.calls[1][1].headers,
|
||||
).toHaveProperty("X-MCP-Proxy-Auth", "Bearer test-proxy-token");
|
||||
});
|
||||
|
||||
test("does NOT send Authorization header for proxy auth", async () => {
|
||||
const propsWithProxyAuth = {
|
||||
...defaultProps,
|
||||
config: {
|
||||
...DEFAULT_INSPECTOR_CONFIG,
|
||||
proxyAuthToken: "test-proxy-token",
|
||||
},
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useConnection(propsWithProxyAuth));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.connect();
|
||||
});
|
||||
|
||||
// Check that Authorization header is NOT used for proxy auth
|
||||
expect(mockSSETransport.options?.requestInit?.headers).not.toHaveProperty(
|
||||
"Authorization",
|
||||
"Bearer test-proxy-token",
|
||||
);
|
||||
});
|
||||
|
||||
test("preserves server Authorization header when proxy auth is configured", async () => {
|
||||
const propsWithBothAuth = {
|
||||
...defaultProps,
|
||||
bearerToken: "server-auth-token",
|
||||
config: {
|
||||
...DEFAULT_INSPECTOR_CONFIG,
|
||||
MCP_PROXY_AUTH_TOKEN: {
|
||||
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
|
||||
value: "test-proxy-token",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useConnection(propsWithBothAuth));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.connect();
|
||||
});
|
||||
|
||||
// Check that both headers are present and distinct
|
||||
const headers = mockSSETransport.options?.requestInit?.headers;
|
||||
expect(headers).toHaveProperty(
|
||||
"Authorization",
|
||||
"Bearer server-auth-token",
|
||||
);
|
||||
expect(headers).toHaveProperty(
|
||||
"X-MCP-Proxy-Auth",
|
||||
"Bearer test-proxy-token",
|
||||
);
|
||||
});
|
||||
|
||||
test("sends X-MCP-Proxy-Auth in health check requests", async () => {
|
||||
const fetchMock = global.fetch as jest.Mock;
|
||||
fetchMock.mockClear();
|
||||
|
||||
const propsWithProxyAuth = {
|
||||
...defaultProps,
|
||||
config: {
|
||||
...DEFAULT_INSPECTOR_CONFIG,
|
||||
MCP_PROXY_AUTH_TOKEN: {
|
||||
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
|
||||
value: "test-proxy-token",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useConnection(propsWithProxyAuth));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.connect();
|
||||
});
|
||||
|
||||
// Find the health check call
|
||||
const healthCheckCall = fetchMock.mock.calls.find(
|
||||
(call) => call[0].pathname === "/health",
|
||||
);
|
||||
|
||||
expect(healthCheckCall).toBeDefined();
|
||||
expect(healthCheckCall[1].headers).toHaveProperty(
|
||||
"X-MCP-Proxy-Auth",
|
||||
"Bearer test-proxy-token",
|
||||
);
|
||||
});
|
||||
|
||||
test("works correctly with streamable-http transport", async () => {
|
||||
const propsWithStreamableHttp = {
|
||||
...defaultProps,
|
||||
transportType: "streamable-http" as const,
|
||||
config: {
|
||||
...DEFAULT_INSPECTOR_CONFIG,
|
||||
MCP_PROXY_AUTH_TOKEN: {
|
||||
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
|
||||
value: "test-proxy-token",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useConnection(propsWithStreamableHttp),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.connect();
|
||||
});
|
||||
|
||||
// Check that the streamable HTTP transport was created with the correct headers
|
||||
expect(mockStreamableHTTPTransport.options).toBeDefined();
|
||||
expect(
|
||||
mockStreamableHTTPTransport.options?.requestInit?.headers,
|
||||
).toHaveProperty("X-MCP-Proxy-Auth", "Bearer test-proxy-token");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import {
|
|||
getMCPProxyAddress,
|
||||
getMCPServerRequestMaxTotalTimeout,
|
||||
resetRequestTimeoutOnProgress,
|
||||
getMCPProxyAuthToken,
|
||||
} from "@/utils/configUtils";
|
||||
import { getMCPServerRequestTimeout } from "@/utils/configUtils";
|
||||
import { InspectorConfig } from "../configurationTypes";
|
||||
|
|
@ -241,11 +242,14 @@ export function useConnection({
|
|||
|
||||
const checkProxyHealth = async () => {
|
||||
try {
|
||||
// Remove additional slashes
|
||||
const proxyBaseUrl = getMCPProxyAddress(config).replace(/\/+$/, "");
|
||||
const proxyHealthUrl = new URL(`${proxyBaseUrl}/health`);
|
||||
|
||||
const proxyHealthResponse = await fetch(proxyHealthUrl);
|
||||
const proxyHealthUrl = new URL(`${getMCPProxyAddress(config)}/health`);
|
||||
const { token: proxyAuthToken, header: proxyAuthTokenHeader } =
|
||||
getMCPProxyAuthToken(config);
|
||||
const headers: HeadersInit = {};
|
||||
if (proxyAuthToken) {
|
||||
headers[proxyAuthTokenHeader] = `Bearer ${proxyAuthToken}`;
|
||||
}
|
||||
const proxyHealthResponse = await fetch(proxyHealthUrl, { headers });
|
||||
const proxyHealth = await proxyHealthResponse.json();
|
||||
if (proxyHealth?.status !== "ok") {
|
||||
throw new Error("MCP Proxy Server is not healthy");
|
||||
|
|
@ -264,6 +268,13 @@ export function useConnection({
|
|||
);
|
||||
};
|
||||
|
||||
const isProxyAuthError = (error: unknown): boolean => {
|
||||
return (
|
||||
error instanceof Error &&
|
||||
error.message.includes("Authentication required. Use the session token")
|
||||
);
|
||||
};
|
||||
|
||||
const handleAuthError = async (error: unknown) => {
|
||||
if (is401Error(error)) {
|
||||
const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl);
|
||||
|
|
@ -321,6 +332,14 @@ export function useConnection({
|
|||
}
|
||||
}
|
||||
|
||||
// Add proxy authentication
|
||||
const { token: proxyAuthToken, header: proxyAuthTokenHeader } =
|
||||
getMCPProxyAuthToken(config);
|
||||
const proxyHeaders: HeadersInit = {};
|
||||
if (proxyAuthToken) {
|
||||
proxyHeaders[proxyAuthTokenHeader] = `Bearer ${proxyAuthToken}`;
|
||||
}
|
||||
|
||||
// Create appropriate transport
|
||||
let transportOptions:
|
||||
| StreamableHTTPClientTransportOptions
|
||||
|
|
@ -338,11 +357,15 @@ export function useConnection({
|
|||
eventSourceInit: {
|
||||
fetch: (
|
||||
url: string | URL | globalThis.Request,
|
||||
init: RequestInit | undefined,
|
||||
) => fetch(url, { ...init, headers }),
|
||||
init?: RequestInit,
|
||||
) =>
|
||||
fetch(url, {
|
||||
...init,
|
||||
headers: { ...headers, ...proxyHeaders },
|
||||
}),
|
||||
},
|
||||
requestInit: {
|
||||
headers,
|
||||
headers: { ...headers, ...proxyHeaders },
|
||||
},
|
||||
};
|
||||
break;
|
||||
|
|
@ -354,11 +377,15 @@ export function useConnection({
|
|||
eventSourceInit: {
|
||||
fetch: (
|
||||
url: string | URL | globalThis.Request,
|
||||
init: RequestInit | undefined,
|
||||
) => fetch(url, { ...init, headers }),
|
||||
init?: RequestInit,
|
||||
) =>
|
||||
fetch(url, {
|
||||
...init,
|
||||
headers: { ...headers, ...proxyHeaders },
|
||||
}),
|
||||
},
|
||||
requestInit: {
|
||||
headers,
|
||||
headers: { ...headers, ...proxyHeaders },
|
||||
},
|
||||
};
|
||||
break;
|
||||
|
|
@ -370,11 +397,15 @@ export function useConnection({
|
|||
eventSourceInit: {
|
||||
fetch: (
|
||||
url: string | URL | globalThis.Request,
|
||||
init: RequestInit | undefined,
|
||||
) => fetch(url, { ...init, headers }),
|
||||
init?: RequestInit,
|
||||
) =>
|
||||
fetch(url, {
|
||||
...init,
|
||||
headers: { ...headers, ...proxyHeaders },
|
||||
}),
|
||||
},
|
||||
requestInit: {
|
||||
headers,
|
||||
headers: { ...headers, ...proxyHeaders },
|
||||
},
|
||||
// TODO these should be configurable...
|
||||
reconnectionOptions: {
|
||||
|
|
@ -450,6 +481,18 @@ export function useConnection({
|
|||
error,
|
||||
);
|
||||
|
||||
// Check if it's a proxy auth error
|
||||
if (isProxyAuthError(error)) {
|
||||
toast({
|
||||
title: "Proxy Authentication Required",
|
||||
description:
|
||||
"Please enter the session token from the proxy server console in the Configuration settings.",
|
||||
variant: "destructive",
|
||||
});
|
||||
setConnectionStatus("error");
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldRetry = await handleAuthError(error);
|
||||
if (shouldRetry) {
|
||||
return connect(undefined, retryCount + 1);
|
||||
|
|
|
|||
72
client/src/utils/__tests__/configUtils.test.ts
Normal file
72
client/src/utils/__tests__/configUtils.test.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { getMCPProxyAuthToken } from "../configUtils";
|
||||
import { DEFAULT_INSPECTOR_CONFIG } from "../../lib/constants";
|
||||
import { InspectorConfig } from "../../lib/configurationTypes";
|
||||
|
||||
describe("configUtils", () => {
|
||||
describe("getMCPProxyAuthToken", () => {
|
||||
test("returns token and default header name", () => {
|
||||
const config: InspectorConfig = {
|
||||
...DEFAULT_INSPECTOR_CONFIG,
|
||||
MCP_PROXY_AUTH_TOKEN: {
|
||||
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
|
||||
value: "test-token-123",
|
||||
},
|
||||
};
|
||||
|
||||
const result = getMCPProxyAuthToken(config);
|
||||
|
||||
expect(result).toEqual({
|
||||
token: "test-token-123",
|
||||
header: "X-MCP-Proxy-Auth",
|
||||
});
|
||||
});
|
||||
|
||||
test("returns empty token when not configured", () => {
|
||||
const config: InspectorConfig = {
|
||||
...DEFAULT_INSPECTOR_CONFIG,
|
||||
MCP_PROXY_AUTH_TOKEN: {
|
||||
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
|
||||
value: "",
|
||||
},
|
||||
};
|
||||
|
||||
const result = getMCPProxyAuthToken(config);
|
||||
|
||||
expect(result).toEqual({
|
||||
token: "",
|
||||
header: "X-MCP-Proxy-Auth",
|
||||
});
|
||||
});
|
||||
|
||||
test("always returns X-MCP-Proxy-Auth as header name", () => {
|
||||
const config: InspectorConfig = {
|
||||
...DEFAULT_INSPECTOR_CONFIG,
|
||||
MCP_PROXY_AUTH_TOKEN: {
|
||||
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
|
||||
value: "any-token",
|
||||
},
|
||||
};
|
||||
|
||||
const result = getMCPProxyAuthToken(config);
|
||||
|
||||
expect(result.header).toBe("X-MCP-Proxy-Auth");
|
||||
});
|
||||
|
||||
test("handles null/undefined value gracefully", () => {
|
||||
const config: InspectorConfig = {
|
||||
...DEFAULT_INSPECTOR_CONFIG,
|
||||
MCP_PROXY_AUTH_TOKEN: {
|
||||
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
|
||||
value: null as unknown as string,
|
||||
},
|
||||
};
|
||||
|
||||
const result = getMCPProxyAuthToken(config);
|
||||
|
||||
expect(result).toEqual({
|
||||
token: null,
|
||||
header: "X-MCP-Proxy-Auth",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -29,6 +29,18 @@ export const getMCPServerRequestMaxTotalTimeout = (
|
|||
return config.MCP_REQUEST_MAX_TOTAL_TIMEOUT.value as number;
|
||||
};
|
||||
|
||||
export const getMCPProxyAuthToken = (
|
||||
config: InspectorConfig,
|
||||
): {
|
||||
token: string;
|
||||
header: string;
|
||||
} => {
|
||||
return {
|
||||
token: config.MCP_PROXY_AUTH_TOKEN.value as string,
|
||||
header: "X-MCP-Proxy-Auth",
|
||||
};
|
||||
};
|
||||
|
||||
const getSearchParam = (key: string): string | null => {
|
||||
try {
|
||||
const url = new URL(window.location.href);
|
||||
|
|
@ -101,27 +113,67 @@ export const getConfigOverridesFromQueryParams = (
|
|||
export const initializeInspectorConfig = (
|
||||
localStorageKey: string,
|
||||
): InspectorConfig => {
|
||||
const savedConfig = localStorage.getItem(localStorageKey);
|
||||
let baseConfig: InspectorConfig;
|
||||
if (savedConfig) {
|
||||
// merge default config with saved config
|
||||
const mergedConfig = {
|
||||
...DEFAULT_INSPECTOR_CONFIG,
|
||||
...JSON.parse(savedConfig),
|
||||
} as InspectorConfig;
|
||||
// Read persistent config from localStorage
|
||||
const savedPersistentConfig = localStorage.getItem(localStorageKey);
|
||||
// Read ephemeral config from sessionStorage
|
||||
const savedEphemeralConfig = sessionStorage.getItem(
|
||||
`${localStorageKey}_ephemeral`,
|
||||
);
|
||||
|
||||
// update description of keys to match the new description (in case of any updates to the default config description)
|
||||
for (const [key, value] of Object.entries(mergedConfig)) {
|
||||
mergedConfig[key as keyof InspectorConfig] = {
|
||||
...value,
|
||||
label: DEFAULT_INSPECTOR_CONFIG[key as keyof InspectorConfig].label,
|
||||
};
|
||||
}
|
||||
baseConfig = mergedConfig;
|
||||
} else {
|
||||
baseConfig = DEFAULT_INSPECTOR_CONFIG;
|
||||
// Start with default config
|
||||
let baseConfig = { ...DEFAULT_INSPECTOR_CONFIG };
|
||||
|
||||
// Apply saved persistent config
|
||||
if (savedPersistentConfig) {
|
||||
const parsedPersistentConfig = JSON.parse(savedPersistentConfig);
|
||||
baseConfig = { ...baseConfig, ...parsedPersistentConfig };
|
||||
}
|
||||
|
||||
// Apply saved ephemeral config
|
||||
if (savedEphemeralConfig) {
|
||||
const parsedEphemeralConfig = JSON.parse(savedEphemeralConfig);
|
||||
baseConfig = { ...baseConfig, ...parsedEphemeralConfig };
|
||||
}
|
||||
|
||||
// Ensure all config items have the latest labels/descriptions from defaults
|
||||
for (const [key, value] of Object.entries(baseConfig)) {
|
||||
baseConfig[key as keyof InspectorConfig] = {
|
||||
...value,
|
||||
label: DEFAULT_INSPECTOR_CONFIG[key as keyof InspectorConfig].label,
|
||||
description:
|
||||
DEFAULT_INSPECTOR_CONFIG[key as keyof InspectorConfig].description,
|
||||
is_session_item:
|
||||
DEFAULT_INSPECTOR_CONFIG[key as keyof InspectorConfig].is_session_item,
|
||||
};
|
||||
}
|
||||
|
||||
// Apply query param overrides
|
||||
const overrides = getConfigOverridesFromQueryParams(DEFAULT_INSPECTOR_CONFIG);
|
||||
return { ...baseConfig, ...overrides };
|
||||
};
|
||||
|
||||
export const saveInspectorConfig = (
|
||||
localStorageKey: string,
|
||||
config: InspectorConfig,
|
||||
): void => {
|
||||
const persistentConfig: Partial<InspectorConfig> = {};
|
||||
const ephemeralConfig: Partial<InspectorConfig> = {};
|
||||
|
||||
// Split config based on is_session_item flag
|
||||
for (const [key, value] of Object.entries(config)) {
|
||||
if (value.is_session_item) {
|
||||
ephemeralConfig[key as keyof InspectorConfig] = value;
|
||||
} else {
|
||||
persistentConfig[key as keyof InspectorConfig] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Save persistent config to localStorage
|
||||
localStorage.setItem(localStorageKey, JSON.stringify(persistentConfig));
|
||||
|
||||
// Save ephemeral config to sessionStorage
|
||||
sessionStorage.setItem(
|
||||
`${localStorageKey}_ephemeral`,
|
||||
JSON.stringify(ephemeralConfig),
|
||||
);
|
||||
};
|
||||
|
|
|
|||
16
package-lock.json
generated
16
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "@modelcontextprotocol/inspector",
|
||||
"version": "0.14.0",
|
||||
"version": "0.14.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@modelcontextprotocol/inspector",
|
||||
"version": "0.14.0",
|
||||
"version": "0.14.2",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"client",
|
||||
|
|
@ -14,9 +14,9 @@
|
|||
"cli"
|
||||
],
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/inspector-cli": "^0.14.0",
|
||||
"@modelcontextprotocol/inspector-client": "^0.14.0",
|
||||
"@modelcontextprotocol/inspector-server": "^0.14.0",
|
||||
"@modelcontextprotocol/inspector-cli": "^0.14.2",
|
||||
"@modelcontextprotocol/inspector-client": "^0.14.2",
|
||||
"@modelcontextprotocol/inspector-server": "^0.14.2",
|
||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||
"concurrently": "^9.0.1",
|
||||
"open": "^10.1.0",
|
||||
|
|
@ -40,7 +40,7 @@
|
|||
},
|
||||
"cli": {
|
||||
"name": "@modelcontextprotocol/inspector-cli",
|
||||
"version": "0.14.0",
|
||||
"version": "0.14.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||
|
|
@ -63,7 +63,7 @@
|
|||
},
|
||||
"client": {
|
||||
"name": "@modelcontextprotocol/inspector-client",
|
||||
"version": "0.14.0",
|
||||
"version": "0.14.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||
|
|
@ -10920,7 +10920,7 @@
|
|||
},
|
||||
"server": {
|
||||
"name": "@modelcontextprotocol/inspector-server",
|
||||
"version": "0.14.0",
|
||||
"version": "0.14.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||
|
|
|
|||
12
package.json
12
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@modelcontextprotocol/inspector",
|
||||
"version": "0.14.0",
|
||||
"version": "0.14.2",
|
||||
"description": "Model Context Protocol inspector",
|
||||
"license": "MIT",
|
||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||
|
|
@ -38,12 +38,14 @@
|
|||
"prettier-check": "prettier --check .",
|
||||
"lint": "prettier --check . && cd client && npm run lint",
|
||||
"prepare": "npm run build",
|
||||
"publish-all": "npm publish --workspaces --access public && npm publish --access public"
|
||||
"publish-all": "npm publish --workspaces --access public && npm publish --access public",
|
||||
"update-version": "node scripts/update-version.js",
|
||||
"check-version": "node scripts/check-version-consistency.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/inspector-cli": "^0.14.0",
|
||||
"@modelcontextprotocol/inspector-client": "^0.14.0",
|
||||
"@modelcontextprotocol/inspector-server": "^0.14.0",
|
||||
"@modelcontextprotocol/inspector-cli": "^0.14.2",
|
||||
"@modelcontextprotocol/inspector-client": "^0.14.2",
|
||||
"@modelcontextprotocol/inspector-server": "^0.14.2",
|
||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||
"concurrently": "^9.0.1",
|
||||
"open": "^10.1.0",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@modelcontextprotocol/inspector-server",
|
||||
"version": "0.14.0",
|
||||
"version": "0.14.2",
|
||||
"description": "Server-side application for the Model Context Protocol inspector",
|
||||
"license": "MIT",
|
||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|||
import express from "express";
|
||||
import { findActualExecutable } from "spawn-rx";
|
||||
import mcpProxy from "./mcpProxy.js";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { randomUUID, randomBytes, timingSafeEqual } from "node:crypto";
|
||||
|
||||
const SSE_HEADERS_PASSTHROUGH = ["authorization"];
|
||||
const STREAMABLE_HTTP_HEADERS_PASSTHROUGH = [
|
||||
|
|
@ -89,6 +89,87 @@ app.use((req, res, next) => {
|
|||
const webAppTransports: Map<string, Transport> = new Map<string, Transport>(); // Web app transports by web app sessionId
|
||||
const serverTransports: Map<string, Transport> = new Map<string, Transport>(); // Server Transports by web app sessionId
|
||||
|
||||
const sessionToken = randomBytes(32).toString("hex");
|
||||
const authDisabled = !!process.env.DANGEROUSLY_OMIT_AUTH;
|
||||
|
||||
// Origin validation middleware to prevent DNS rebinding attacks
|
||||
const originValidationMiddleware = (
|
||||
req: express.Request,
|
||||
res: express.Response,
|
||||
next: express.NextFunction,
|
||||
) => {
|
||||
const origin = req.headers.origin;
|
||||
|
||||
// Default origins based on CLIENT_PORT or use environment variable
|
||||
const clientPort = process.env.CLIENT_PORT || "6274";
|
||||
const defaultOrigins = [
|
||||
`http://localhost:${clientPort}`,
|
||||
`http://127.0.0.1:${clientPort}`,
|
||||
];
|
||||
const allowedOrigins =
|
||||
process.env.ALLOWED_ORIGINS?.split(",") || defaultOrigins;
|
||||
|
||||
if (origin && !allowedOrigins.includes(origin)) {
|
||||
console.error(`Invalid origin: ${origin}`);
|
||||
res.status(403).json({
|
||||
error: "Forbidden - invalid origin",
|
||||
message:
|
||||
"Request blocked to prevent DNS rebinding attacks. Configure allowed origins via environment variable.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
const authMiddleware = (
|
||||
req: express.Request,
|
||||
res: express.Response,
|
||||
next: express.NextFunction,
|
||||
) => {
|
||||
if (authDisabled) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const sendUnauthorized = () => {
|
||||
res.status(401).json({
|
||||
error: "Unauthorized",
|
||||
message:
|
||||
"Authentication required. Use the session token shown in the console when starting the server.",
|
||||
});
|
||||
};
|
||||
|
||||
const authHeader = req.headers["x-mcp-proxy-auth"];
|
||||
const authHeaderValue = Array.isArray(authHeader)
|
||||
? authHeader[0]
|
||||
: authHeader;
|
||||
|
||||
if (!authHeaderValue || !authHeaderValue.startsWith("Bearer ")) {
|
||||
sendUnauthorized();
|
||||
return;
|
||||
}
|
||||
|
||||
const providedToken = authHeaderValue.substring(7); // Remove 'Bearer ' prefix
|
||||
const expectedToken = sessionToken;
|
||||
|
||||
// Convert to buffers for timing-safe comparison
|
||||
const providedBuffer = Buffer.from(providedToken);
|
||||
const expectedBuffer = Buffer.from(expectedToken);
|
||||
|
||||
// Check length first to prevent timing attacks
|
||||
if (providedBuffer.length !== expectedBuffer.length) {
|
||||
sendUnauthorized();
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform timing-safe comparison
|
||||
if (!timingSafeEqual(providedBuffer, expectedBuffer)) {
|
||||
sendUnauthorized();
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
const createTransport = async (req: express.Request): Promise<Transport> => {
|
||||
const query = req.query;
|
||||
console.log("Query parameters:", JSON.stringify(query));
|
||||
|
|
@ -119,7 +200,9 @@ const createTransport = async (req: express.Request): Promise<Transport> => {
|
|||
|
||||
const headers = getHttpHeaders(req, transportType);
|
||||
|
||||
console.log(`SSE transport: url=${url}, headers=${Object.keys(headers)}`);
|
||||
console.log(
|
||||
`SSE transport: url=${url}, headers=${JSON.stringify(headers)}`,
|
||||
);
|
||||
|
||||
const transport = new SSEClientTransport(new URL(url), {
|
||||
eventSourceInit: {
|
||||
|
|
@ -150,38 +233,152 @@ const createTransport = async (req: express.Request): Promise<Transport> => {
|
|||
}
|
||||
};
|
||||
|
||||
app.get("/mcp", async (req, res) => {
|
||||
const sessionId = req.headers["mcp-session-id"] as string;
|
||||
console.log(`Received GET message for sessionId ${sessionId}`);
|
||||
try {
|
||||
const transport = webAppTransports.get(
|
||||
sessionId,
|
||||
) as StreamableHTTPServerTransport;
|
||||
if (!transport) {
|
||||
res.status(404).end("Session not found");
|
||||
return;
|
||||
} else {
|
||||
await transport.handleRequest(req, res);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in /mcp route:", error);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/mcp", async (req, res) => {
|
||||
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
||||
let serverTransport: Transport | undefined;
|
||||
if (!sessionId) {
|
||||
app.get(
|
||||
"/mcp",
|
||||
originValidationMiddleware,
|
||||
authMiddleware,
|
||||
async (req, res) => {
|
||||
const sessionId = req.headers["mcp-session-id"] as string;
|
||||
console.log(`Received GET message for sessionId ${sessionId}`);
|
||||
try {
|
||||
console.log("New StreamableHttp connection request");
|
||||
const transport = webAppTransports.get(
|
||||
sessionId,
|
||||
) as StreamableHTTPServerTransport;
|
||||
if (!transport) {
|
||||
res.status(404).end("Session not found");
|
||||
return;
|
||||
} else {
|
||||
await transport.handleRequest(req, res);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in /mcp route:", error);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
app.post(
|
||||
"/mcp",
|
||||
originValidationMiddleware,
|
||||
authMiddleware,
|
||||
async (req, res) => {
|
||||
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
||||
let serverTransport: Transport | undefined;
|
||||
if (!sessionId) {
|
||||
try {
|
||||
console.log("New StreamableHttp connection request");
|
||||
try {
|
||||
serverTransport = await createTransport(req);
|
||||
} catch (error) {
|
||||
if (error instanceof SseError && error.code === 401) {
|
||||
console.error(
|
||||
"Received 401 Unauthorized from MCP server:",
|
||||
error.message,
|
||||
);
|
||||
res.status(401).json(error);
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.log("Created StreamableHttp server transport");
|
||||
|
||||
const webAppTransport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: randomUUID,
|
||||
onsessioninitialized: (sessionId) => {
|
||||
webAppTransports.set(sessionId, webAppTransport);
|
||||
serverTransports.set(sessionId, serverTransport!);
|
||||
console.log("Client <-> Proxy sessionId: " + sessionId);
|
||||
},
|
||||
});
|
||||
console.log("Created StreamableHttp client transport");
|
||||
|
||||
await webAppTransport.start();
|
||||
|
||||
mcpProxy({
|
||||
transportToClient: webAppTransport,
|
||||
transportToServer: serverTransport,
|
||||
});
|
||||
|
||||
await (webAppTransport as StreamableHTTPServerTransport).handleRequest(
|
||||
req,
|
||||
res,
|
||||
req.body,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error in /mcp POST route:", error);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
} else {
|
||||
console.log(`Received POST message for sessionId ${sessionId}`);
|
||||
try {
|
||||
const transport = webAppTransports.get(
|
||||
sessionId,
|
||||
) as StreamableHTTPServerTransport;
|
||||
if (!transport) {
|
||||
res.status(404).end("Transport not found for sessionId " + sessionId);
|
||||
} else {
|
||||
await (transport as StreamableHTTPServerTransport).handleRequest(
|
||||
req,
|
||||
res,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in /mcp route:", error);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
app.delete(
|
||||
"/mcp",
|
||||
originValidationMiddleware,
|
||||
authMiddleware,
|
||||
async (req, res) => {
|
||||
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
||||
console.log(`Received DELETE message for sessionId ${sessionId}`);
|
||||
let serverTransport: Transport | undefined;
|
||||
if (sessionId) {
|
||||
try {
|
||||
serverTransport = serverTransports.get(
|
||||
sessionId,
|
||||
) as StreamableHTTPClientTransport;
|
||||
if (!serverTransport) {
|
||||
res.status(404).end("Transport not found for sessionId " + sessionId);
|
||||
} else {
|
||||
await (
|
||||
serverTransport as StreamableHTTPClientTransport
|
||||
).terminateSession();
|
||||
webAppTransports.delete(sessionId);
|
||||
serverTransports.delete(sessionId);
|
||||
console.log(`Transports removed for sessionId ${sessionId}`);
|
||||
}
|
||||
res.status(200).end();
|
||||
} catch (error) {
|
||||
console.error("Error in /mcp route:", error);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/stdio",
|
||||
originValidationMiddleware,
|
||||
authMiddleware,
|
||||
async (req, res) => {
|
||||
try {
|
||||
console.log("New STDIO connection request");
|
||||
let serverTransport: Transport | undefined;
|
||||
try {
|
||||
serverTransport = await createTransport(req);
|
||||
console.log("Created server transport");
|
||||
} catch (error) {
|
||||
if (error instanceof SseError && error.code === 401) {
|
||||
console.error(
|
||||
"Received 401 Unauthorized from MCP server:",
|
||||
error.message,
|
||||
"Received 401 Unauthorized from MCP server. Authentication failure.",
|
||||
);
|
||||
res.status(401).json(error);
|
||||
return;
|
||||
|
|
@ -190,211 +387,127 @@ app.post("/mcp", async (req, res) => {
|
|||
throw error;
|
||||
}
|
||||
|
||||
console.log("Created StreamableHttp server transport");
|
||||
|
||||
const webAppTransport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: randomUUID,
|
||||
onsessioninitialized: (sessionId) => {
|
||||
webAppTransports.set(sessionId, webAppTransport);
|
||||
serverTransports.set(sessionId, serverTransport!);
|
||||
console.log("Client <-> Proxy sessionId: " + sessionId);
|
||||
},
|
||||
});
|
||||
console.log("Created StreamableHttp client transport");
|
||||
|
||||
await webAppTransport.start();
|
||||
|
||||
mcpProxy({
|
||||
transportToClient: webAppTransport,
|
||||
transportToServer: serverTransport,
|
||||
});
|
||||
|
||||
await (webAppTransport as StreamableHTTPServerTransport).handleRequest(
|
||||
req,
|
||||
res,
|
||||
req.body,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error in /mcp POST route:", error);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
} else {
|
||||
console.log(`Received POST message for sessionId ${sessionId}`);
|
||||
try {
|
||||
const transport = webAppTransports.get(
|
||||
sessionId,
|
||||
) as StreamableHTTPServerTransport;
|
||||
if (!transport) {
|
||||
res.status(404).end("Transport not found for sessionId " + sessionId);
|
||||
} else {
|
||||
await (transport as StreamableHTTPServerTransport).handleRequest(
|
||||
req,
|
||||
res,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in /mcp route:", error);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.delete("/mcp", async (req, res) => {
|
||||
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
||||
console.log(`Received DELETE message for sessionId ${sessionId}`);
|
||||
let serverTransport: Transport | undefined;
|
||||
if (sessionId) {
|
||||
try {
|
||||
serverTransport = serverTransports.get(
|
||||
sessionId,
|
||||
) as StreamableHTTPClientTransport;
|
||||
if (!serverTransport) {
|
||||
res.status(404).end("Transport not found for sessionId " + sessionId);
|
||||
} else {
|
||||
await (
|
||||
serverTransport as StreamableHTTPClientTransport
|
||||
).terminateSession();
|
||||
webAppTransports.delete(sessionId);
|
||||
serverTransports.delete(sessionId);
|
||||
console.log(`Transports removed for sessionId ${sessionId}`);
|
||||
}
|
||||
res.status(200).end();
|
||||
} catch (error) {
|
||||
console.error("Error in /mcp route:", error);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/stdio", async (req, res) => {
|
||||
try {
|
||||
console.log("New STDIO connection request");
|
||||
let serverTransport: Transport | undefined;
|
||||
try {
|
||||
serverTransport = await createTransport(req);
|
||||
console.log("Created server transport");
|
||||
} catch (error) {
|
||||
if (error instanceof SseError && error.code === 401) {
|
||||
console.error(
|
||||
"Received 401 Unauthorized from MCP server. Authentication failure.",
|
||||
);
|
||||
res.status(401).json(error);
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
const webAppTransport = new SSEServerTransport("/message", res);
|
||||
console.log("Created client transport");
|
||||
|
||||
webAppTransports.set(webAppTransport.sessionId, webAppTransport);
|
||||
serverTransports.set(webAppTransport.sessionId, serverTransport);
|
||||
|
||||
await webAppTransport.start();
|
||||
|
||||
(serverTransport as StdioClientTransport).stderr!.on("data", (chunk) => {
|
||||
if (chunk.toString().includes("MODULE_NOT_FOUND")) {
|
||||
webAppTransport.send({
|
||||
jsonrpc: "2.0",
|
||||
method: "notifications/stderr",
|
||||
params: {
|
||||
content: "Command not found, transports removed",
|
||||
},
|
||||
});
|
||||
webAppTransport.close();
|
||||
serverTransport.close();
|
||||
webAppTransports.delete(webAppTransport.sessionId);
|
||||
serverTransports.delete(webAppTransport.sessionId);
|
||||
console.error("Command not found, transports removed");
|
||||
} else {
|
||||
webAppTransport.send({
|
||||
jsonrpc: "2.0",
|
||||
method: "notifications/stderr",
|
||||
params: {
|
||||
content: chunk.toString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
mcpProxy({
|
||||
transportToClient: webAppTransport,
|
||||
transportToServer: serverTransport,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error in /stdio route:", error);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/sse", async (req, res) => {
|
||||
try {
|
||||
console.log(
|
||||
"New SSE connection request. NOTE: The sse transport is deprecated and has been replaced by StreamableHttp",
|
||||
);
|
||||
let serverTransport: Transport | undefined;
|
||||
try {
|
||||
serverTransport = await createTransport(req);
|
||||
} catch (error) {
|
||||
if (error instanceof SseError && error.code === 401) {
|
||||
console.error(
|
||||
"Received 401 Unauthorized from MCP server. Authentication failure.",
|
||||
);
|
||||
res.status(401).json(error);
|
||||
return;
|
||||
} else if (error instanceof SseError && error.code === 404) {
|
||||
console.error(
|
||||
"Received 404 not found from MCP server. Does the MCP server support SSE?",
|
||||
);
|
||||
res.status(404).json(error);
|
||||
return;
|
||||
} else if (JSON.stringify(error).includes("ECONNREFUSED")) {
|
||||
console.error("Connection refused. Is the MCP server running?");
|
||||
res.status(500).json(error);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (serverTransport) {
|
||||
const webAppTransport = new SSEServerTransport("/message", res);
|
||||
webAppTransports.set(webAppTransport.sessionId, webAppTransport);
|
||||
console.log("Created client transport");
|
||||
serverTransports.set(webAppTransport.sessionId, serverTransport!);
|
||||
console.log("Created server transport");
|
||||
|
||||
webAppTransports.set(webAppTransport.sessionId, webAppTransport);
|
||||
serverTransports.set(webAppTransport.sessionId, serverTransport);
|
||||
|
||||
await webAppTransport.start();
|
||||
|
||||
(serverTransport as StdioClientTransport).stderr!.on("data", (chunk) => {
|
||||
if (chunk.toString().includes("MODULE_NOT_FOUND")) {
|
||||
webAppTransport.send({
|
||||
jsonrpc: "2.0",
|
||||
method: "notifications/stderr",
|
||||
params: {
|
||||
content: "Command not found, transports removed",
|
||||
},
|
||||
});
|
||||
webAppTransport.close();
|
||||
serverTransport.close();
|
||||
webAppTransports.delete(webAppTransport.sessionId);
|
||||
serverTransports.delete(webAppTransport.sessionId);
|
||||
console.error("Command not found, transports removed");
|
||||
} else {
|
||||
webAppTransport.send({
|
||||
jsonrpc: "2.0",
|
||||
method: "notifications/stderr",
|
||||
params: {
|
||||
content: chunk.toString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
mcpProxy({
|
||||
transportToClient: webAppTransport,
|
||||
transportToServer: serverTransport,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error in /stdio route:", error);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in /sse route:", error);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
app.post("/message", async (req, res) => {
|
||||
try {
|
||||
const sessionId = req.query.sessionId;
|
||||
console.log(`Received POST message for sessionId ${sessionId}`);
|
||||
app.get(
|
||||
"/sse",
|
||||
originValidationMiddleware,
|
||||
authMiddleware,
|
||||
async (req, res) => {
|
||||
try {
|
||||
console.log(
|
||||
"New SSE connection request. NOTE: The sse transport is deprecated and has been replaced by StreamableHttp",
|
||||
);
|
||||
let serverTransport: Transport | undefined;
|
||||
try {
|
||||
serverTransport = await createTransport(req);
|
||||
} catch (error) {
|
||||
if (error instanceof SseError && error.code === 401) {
|
||||
console.error(
|
||||
"Received 401 Unauthorized from MCP server. Authentication failure.",
|
||||
);
|
||||
res.status(401).json(error);
|
||||
return;
|
||||
} else if (error instanceof SseError && error.code === 404) {
|
||||
console.error(
|
||||
"Received 404 not found from MCP server. Does the MCP server support SSE?",
|
||||
);
|
||||
res.status(404).json(error);
|
||||
return;
|
||||
} else if (JSON.stringify(error).includes("ECONNREFUSED")) {
|
||||
console.error("Connection refused. Is the MCP server running?");
|
||||
res.status(500).json(error);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const transport = webAppTransports.get(
|
||||
sessionId as string,
|
||||
) as SSEServerTransport;
|
||||
if (!transport) {
|
||||
res.status(404).end("Session not found");
|
||||
return;
|
||||
if (serverTransport) {
|
||||
const webAppTransport = new SSEServerTransport("/message", res);
|
||||
webAppTransports.set(webAppTransport.sessionId, webAppTransport);
|
||||
console.log("Created client transport");
|
||||
serverTransports.set(webAppTransport.sessionId, serverTransport!);
|
||||
console.log("Created server transport");
|
||||
|
||||
await webAppTransport.start();
|
||||
|
||||
mcpProxy({
|
||||
transportToClient: webAppTransport,
|
||||
transportToServer: serverTransport,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in /sse route:", error);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
await transport.handlePostMessage(req, res);
|
||||
} catch (error) {
|
||||
console.error("Error in /message route:", error);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
app.post(
|
||||
"/message",
|
||||
originValidationMiddleware,
|
||||
authMiddleware,
|
||||
async (req, res) => {
|
||||
try {
|
||||
const sessionId = req.query.sessionId;
|
||||
console.log(`Received POST message for sessionId ${sessionId}`);
|
||||
|
||||
const transport = webAppTransports.get(
|
||||
sessionId as string,
|
||||
) as SSEServerTransport;
|
||||
if (!transport) {
|
||||
res.status(404).end("Session not found");
|
||||
return;
|
||||
}
|
||||
await transport.handlePostMessage(req, res);
|
||||
} catch (error) {
|
||||
console.error("Error in /message route:", error);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
app.get("/health", (req, res) => {
|
||||
res.json({
|
||||
|
|
@ -402,7 +515,7 @@ app.get("/health", (req, res) => {
|
|||
});
|
||||
});
|
||||
|
||||
app.get("/config", (req, res) => {
|
||||
app.get("/config", originValidationMiddleware, authMiddleware, (req, res) => {
|
||||
try {
|
||||
res.json({
|
||||
defaultEnvironment,
|
||||
|
|
@ -415,11 +528,29 @@ app.get("/config", (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 6277;
|
||||
const PORT = parseInt(process.env.PORT || "6277", 10);
|
||||
const HOST = process.env.HOST || "127.0.0.1";
|
||||
|
||||
const server = app.listen(PORT);
|
||||
const server = app.listen(PORT, HOST);
|
||||
server.on("listening", () => {
|
||||
console.log(`⚙️ Proxy server listening on port ${PORT}`);
|
||||
console.log(`⚙️ Proxy server listening on ${HOST}:${PORT}`);
|
||||
if (!authDisabled) {
|
||||
console.log(`🔑 Session token: ${sessionToken}`);
|
||||
console.log(
|
||||
`Use this token to authenticate requests or set DANGEROUSLY_OMIT_AUTH=true to disable auth`,
|
||||
);
|
||||
|
||||
// Display clickable URL with pre-filled token
|
||||
const clientPort = process.env.CLIENT_PORT || "6274";
|
||||
const clientUrl = `http://localhost:${clientPort}/?MCP_PROXY_AUTH_TOKEN=${sessionToken}`;
|
||||
console.log(
|
||||
`\n🔗 Open inspector with token pre-filled:\n ${clientUrl}\n (Auto-open is disabled when authentication is enabled)\n`,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`⚠️ WARNING: Authentication is disabled. This is not recommended.`,
|
||||
);
|
||||
}
|
||||
});
|
||||
server.on("error", (err) => {
|
||||
if (err.message.includes(`EADDRINUSE`)) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue