diff --git a/README.md b/README.md index 99356536..9d5017dc 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,16 @@ Note that the proxy is not a network proxy for intercepting traffic. Instead, it - Node.js: ^22.7.5 +### Quick Start (UI mode) + +To get up and running right away with the UI, just execute the following: + +```bash +npx @modelcontextprotocol/inspector +``` + +The server will start up and the UI will be accessible at `http://localhost:6274`. + ### From an MCP server repository To inspect an MCP server implementation, there's no need to clone this repo. Instead, use `npx`. For example, if your server is built at `build/index.js`: diff --git a/client/src/App.tsx b/client/src/App.tsx index cb411452..396f5051 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -611,7 +611,7 @@ const App = () => { className="w-full p-4" onValueChange={(value) => (window.location.hash = value)} > - + { !serverCapabilities?.tools ? ( <>
-

+

The connected server does not support any MCP capabilities

@@ -816,7 +816,7 @@ const App = () => { ) : (
-

+

Connect to an MCP server to start inspecting

@@ -841,7 +841,7 @@ const App = () => { }} >
diff --git a/client/src/components/History.tsx b/client/src/components/History.tsx index 0b05b55d..78394dea 100644 --- a/client/src/components/History.tsx +++ b/client/src/components/History.tsx @@ -29,7 +29,9 @@ const HistoryAndNotifications = ({

History

{requestHistory.length === 0 ? ( -

No history yet

+

+ No history yet +

) : (
    {requestHistory @@ -38,7 +40,7 @@ const HistoryAndNotifications = ({ .map((request, index) => (
  • Server Notifications

    {serverNotifications.length === 0 ? ( -

    No notifications yet

    +

    + No notifications yet +

    ) : (
      {serverNotifications @@ -102,7 +106,7 @@ const HistoryAndNotifications = ({ .map((notification, index) => (
    • ({ buttonText, isButtonDisabled, }: ListPaneProps) => ( -
      -
      +
      +

      {title}

      @@ -46,7 +46,7 @@ const ListPane = ({ {items.map((item, index) => (
      setSelectedItem(item)} > {renderItem(item)} diff --git a/client/src/components/PromptsTab.tsx b/client/src/components/PromptsTab.tsx index 5fdad823..66c15d8f 100644 --- a/client/src/components/PromptsTab.tsx +++ b/client/src/components/PromptsTab.tsx @@ -110,8 +110,8 @@ const PromptsTab = ({ isButtonDisabled={!nextCursor && prompts.length > 0} /> -
      -
      +
      +

      {selectedPrompt ? selectedPrompt.name : "Select a prompt"}

      @@ -126,7 +126,7 @@ const PromptsTab = ({ ) : selectedPrompt ? (
      {selectedPrompt.description && ( -

      +

      {selectedPrompt.description}

      )} diff --git a/client/src/components/ResourcesTab.tsx b/client/src/components/ResourcesTab.tsx index c0f8203f..35742799 100644 --- a/client/src/components/ResourcesTab.tsx +++ b/client/src/components/ResourcesTab.tsx @@ -173,8 +173,8 @@ const ResourcesTab = ({ isButtonDisabled={!nextTemplateCursor && resourceTemplates.length > 0} /> -
      -
      +
      +

      ) : selectedTemplate ? (
      -

      +

      {selectedTemplate.description}

      {selectedTemplate.uriTemplate diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index 516f48d5..eee8eeaa 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -215,7 +215,7 @@ const Sidebar = ({ return (
      -
      +

      MCP Inspector v{version} @@ -646,7 +646,7 @@ const Sidebar = ({ } })()}`} /> - + {(() => { switch (connectionStatus) { case "connected": diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 8c72bd2a..8a7f6578 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -77,8 +77,8 @@ const ToolsTab = ({ isButtonDisabled={!nextCursor && tools.length > 0} /> -
      -
      +
      +

      {selectedTool ? selectedTool.name : "Select a tool"}

      @@ -86,7 +86,7 @@ const ToolsTab = ({
      {selectedTool ? (
      -

      +

      {selectedTool.description}

      {Object.entries(selectedTool.inputSchema.properties ?? []).map( @@ -96,7 +96,7 @@ const ToolsTab = ({
      diff --git a/client/src/index.css b/client/src/index.css index 11c6f23d..858f9e7f 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -93,8 +93,8 @@ h1 { --accent-foreground: 210 40% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 210 40% 98%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; + --border: 217.2 24% 24%; + --input: 217.2 24% 24%; --ring: 212.7 26.8% 83.9%; --chart-1: 220 70% 50%; --chart-2: 160 60% 45%; diff --git a/client/src/lib/hooks/__tests__/useConnection.test.tsx b/client/src/lib/hooks/__tests__/useConnection.test.tsx index c6700dcf..6e5748d7 100644 --- a/client/src/lib/hooks/__tests__/useConnection.test.tsx +++ b/client/src/lib/hooks/__tests__/useConnection.test.tsx @@ -28,10 +28,18 @@ jest.mock("@modelcontextprotocol/sdk/client/index.js", () => ({ })); jest.mock("@modelcontextprotocol/sdk/client/sse.js", () => ({ - SSEClientTransport: jest.fn(), + SSEClientTransport: jest.fn((url) => ({ + toString: () => url, + })), SseError: jest.fn(), })); +jest.mock("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ + StreamableHTTPClientTransport: jest.fn((url) => ({ + toString: () => url, + })), +})); + jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({ auth: jest.fn().mockResolvedValue("AUTHORIZED"), })); @@ -163,4 +171,92 @@ describe("useConnection", () => { result.current.makeRequest(mockRequest, mockSchema), ).rejects.toThrow("MCP client not connected"); }); + + describe("URL Port Handling", () => { + const SSEClientTransport = jest.requireMock( + "@modelcontextprotocol/sdk/client/sse.js", + ).SSEClientTransport; + const StreamableHTTPClientTransport = jest.requireMock( + "@modelcontextprotocol/sdk/client/streamableHttp.js", + ).StreamableHTTPClientTransport; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test("preserves HTTPS port number when connecting", async () => { + const props = { + ...defaultProps, + sseUrl: "https://example.com:8443/api", + transportType: "sse" as const, + }; + + const { result } = renderHook(() => useConnection(props)); + + await act(async () => { + await result.current.connect(); + }); + + const call = SSEClientTransport.mock.calls[0][0]; + expect(call.toString()).toContain( + "url=https%3A%2F%2Fexample.com%3A8443%2Fapi", + ); + }); + + test("preserves HTTP port number when connecting", async () => { + const props = { + ...defaultProps, + sseUrl: "http://localhost:3000/api", + transportType: "sse" as const, + }; + + const { result } = renderHook(() => useConnection(props)); + + await act(async () => { + await result.current.connect(); + }); + + const call = SSEClientTransport.mock.calls[0][0]; + expect(call.toString()).toContain( + "url=http%3A%2F%2Flocalhost%3A3000%2Fapi", + ); + }); + + test("uses default port for HTTPS when not specified", async () => { + const props = { + ...defaultProps, + sseUrl: "https://example.com/api", + transportType: "sse" as const, + }; + + const { result } = renderHook(() => useConnection(props)); + + await act(async () => { + await result.current.connect(); + }); + + const call = SSEClientTransport.mock.calls[0][0]; + expect(call.toString()).toContain("url=https%3A%2F%2Fexample.com%2Fapi"); + expect(call.toString()).not.toContain("%3A443"); + }); + + test("preserves port number in streamable-http transport", async () => { + const props = { + ...defaultProps, + sseUrl: "https://example.com:8443/api", + transportType: "streamable-http" as const, + }; + + const { result } = renderHook(() => useConnection(props)); + + await act(async () => { + await result.current.connect(); + }); + + const call = StreamableHTTPClientTransport.mock.calls[0][0]; + expect(call.toString()).toContain( + "url=https%3A%2F%2Fexample.com%3A8443%2Fapi", + ); + }); + }); }); diff --git a/server/src/index.ts b/server/src/index.ts index c967b60c..73288f67 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -48,7 +48,8 @@ app.use((req, res, next) => { next(); }); -const webAppTransports: Map = new Map(); // Transports by sessionId +const webAppTransports: Map = new Map(); // Web app transports by web app sessionId +const serverTransports: Map = new Map(); // Server Transports by web app sessionId const createTransport = async (req: express.Request): Promise => { const query = req.query; @@ -137,8 +138,6 @@ const createTransport = async (req: express.Request): Promise => { } }; -let backingServerTransport: Transport | undefined; - app.get("/mcp", async (req, res) => { const sessionId = req.headers["mcp-session-id"] as string; console.log(`Received GET message for sessionId ${sessionId}`); @@ -161,12 +160,12 @@ app.get("/mcp", async (req, res) => { app.post("/mcp", async (req, res) => { const sessionId = req.headers["mcp-session-id"] as string | undefined; console.log(`Received POST message for sessionId ${sessionId}`); + let serverTransport: Transport | undefined; if (!sessionId) { try { console.log("New streamable-http connection"); try { - await backingServerTransport?.close(); - backingServerTransport = await createTransport(req); + serverTransport = await createTransport(req); } catch (error) { if (error instanceof SseError && error.code === 401) { console.error( @@ -180,12 +179,13 @@ app.post("/mcp", async (req, res) => { throw error; } - console.log("Connected MCP client to backing server transport"); + console.log("Connected MCP client to server transport"); const webAppTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: randomUUID, onsessioninitialized: (sessionId) => { webAppTransports.set(sessionId, webAppTransport); + serverTransports.set(sessionId, serverTransport!); console.log("Created streamable web app transport " + sessionId); }, }); @@ -194,7 +194,7 @@ app.post("/mcp", async (req, res) => { mcpProxy({ transportToClient: webAppTransport, - transportToServer: backingServerTransport, + transportToServer: serverTransport, }); await (webAppTransport as StreamableHTTPServerTransport).handleRequest( @@ -229,10 +229,9 @@ app.post("/mcp", async (req, res) => { app.get("/stdio", async (req, res) => { try { console.log("New connection"); - + let serverTransport: Transport | undefined; try { - await backingServerTransport?.close(); - backingServerTransport = await createTransport(req); + serverTransport = await createTransport(req); } catch (error) { if (error instanceof SseError && error.code === 401) { console.error( @@ -250,26 +249,24 @@ app.get("/stdio", async (req, res) => { const webAppTransport = new SSEServerTransport("/message", res); webAppTransports.set(webAppTransport.sessionId, webAppTransport); - - console.log("Created web app transport"); + serverTransports.set(webAppTransport.sessionId, serverTransport); + console.log("Created client/server transports"); await webAppTransport.start(); - (backingServerTransport as StdioClientTransport).stderr!.on( - "data", - (chunk) => { - webAppTransport.send({ - jsonrpc: "2.0", - method: "notifications/stderr", - params: { - content: chunk.toString(), - }, - }); - }, - ); + + (serverTransport as StdioClientTransport).stderr!.on("data", (chunk) => { + webAppTransport.send({ + jsonrpc: "2.0", + method: "notifications/stderr", + params: { + content: chunk.toString(), + }, + }); + }); mcpProxy({ transportToClient: webAppTransport, - transportToServer: backingServerTransport, + transportToServer: serverTransport, }); console.log("Set up MCP proxy"); @@ -284,10 +281,9 @@ app.get("/sse", async (req, res) => { console.log( "New SSE connection. NOTE: The sse transport is deprecated and has been replaced by streamable-http", ); - + let serverTransport: Transport | undefined; try { - await backingServerTransport?.close(); - backingServerTransport = await createTransport(req); + serverTransport = await createTransport(req); } catch (error) { if (error instanceof SseError && error.code === 401) { console.error( @@ -305,13 +301,15 @@ app.get("/sse", async (req, res) => { const webAppTransport = new SSEServerTransport("/message", res); webAppTransports.set(webAppTransport.sessionId, webAppTransport); - console.log("Created web app transport"); + console.log("Created client transport"); + serverTransports.set(webAppTransport.sessionId, serverTransport); + console.log("Created server transport"); await webAppTransport.start(); mcpProxy({ transportToClient: webAppTransport, - transportToServer: backingServerTransport, + transportToServer: serverTransport, }); console.log("Set up MCP proxy");