Compare commits

..

109 Commits
0.2.3 ... 0.4.0

Author SHA1 Message Date
Jerome
d438760e36 Merge pull request #99 from evalstate/feature/audio-rendering
Render Audio Player if Tool Result resource mime type is audio.
2025-02-03 12:28:41 +13:00
Jerome
d0ad677784 Update ToolsTab.tsx
Linting on originally approved commit
2025-02-03 12:24:52 +13:00
Justin Spahr-Summers
1d4e8885db Merge pull request #131 from modelcontextprotocol/justin/sse-auth
OAuth support for SSE
2025-01-29 11:20:48 +00:00
=
a87bd17f51 ❯ npx prettier --check .
Checking formatting...
[warn] client/src/components/ToolsTab.tsx
[warn] Code style issues found in the above file. Run Prettier with --write to fix.

inspector on  feature/audio-rendering [$⇡] is 📦 v0.3.0 via  v22.11.0
❯ npx prettier --write client/src/components/ToolsTab.tsx
client/src/components/ToolsTab.tsx 109ms
2025-01-28 08:28:25 +00:00
=
afe14bc883 Merge branch 'feature/audio-rendering' of https://github.com/evalstate/inspector into feature/audio-rendering 2025-01-28 08:18:24 +00:00
Jerome
04faff4757 Merge branch 'main' into feature/audio-rendering 2025-01-28 02:15:55 -05:00
Justin Spahr-Summers
0882a3e0e5 Formatting 2025-01-24 15:23:24 +00:00
Justin Spahr-Summers
fce6644e30 Fix double fetching 2025-01-24 15:22:40 +00:00
Justin Spahr-Summers
51ea4bc6ac Add toast when OAuth succeeds 2025-01-24 15:19:41 +00:00
Justin Spahr-Summers
0648ba44e3 Auto-reconnect after OAuth 2025-01-24 15:17:03 +00:00
Justin Spahr-Summers
c22f91858c Remember last selected transport and SSE URL 2025-01-24 15:04:22 +00:00
Justin Spahr-Summers
99d7592ac9 Fix error state being briefly shown before OAuth 2025-01-24 15:02:34 +00:00
Justin Spahr-Summers
3bc776f7cd Fix Vite config 2025-01-24 14:55:10 +00:00
Justin Spahr-Summers
a6d22cf1e4 Bump SDK version 2025-01-24 14:54:46 +00:00
Justin Spahr-Summers
731ee588c2 Fix Authorization header passthrough
Node.js headers are lowercase
2025-01-24 13:55:43 +00:00
Justin Spahr-Summers
af8877064e Set Authorization header from client 2025-01-24 13:55:32 +00:00
Justin Spahr-Summers
874320ebe6 Token exchange body needs to be JSON 2025-01-24 13:44:26 +00:00
Justin Spahr-Summers
e470eb5c51 Fix React import 2025-01-24 13:27:20 +00:00
Justin Spahr-Summers
02cfb47c83 Extract session storage keys into constants 2025-01-24 13:09:58 +00:00
Justin Spahr-Summers
23f89e49b8 Implement OAuth callback 2025-01-24 13:08:39 +00:00
Justin Spahr-Summers
16cb59670c OAuth callback handler (not yet attached) 2025-01-24 11:37:35 +00:00
Justin Spahr-Summers
1c4ad60354 Redirect into OAuth flow upon receiving 401 2025-01-24 11:34:07 +00:00
Justin Spahr-Summers
8a20f7711a Use new SseError class from SDK 2025-01-24 11:27:40 +00:00
Justin Spahr-Summers
8bb5308797 Report SSE 401 errors to the client 2025-01-24 11:04:44 +00:00
Justin Spahr-Summers
14db05c2a2 Clarify inspector-server error logging 2025-01-23 17:19:39 +00:00
Justin Spahr-Summers
e7697eb5cd Pass through Authorization headers sent to inspector server 2025-01-23 16:45:37 +00:00
Justin Spahr-Summers
c1e06c4af0 Server doesn't need to inject eventsource anymore 2025-01-23 16:45:12 +00:00
Justin Spahr-Summers
60b8892dd3 Pre-emptively bump npm package versions
Before I forget!
2025-01-23 16:30:19 +00:00
Justin Spahr-Summers
2b53a8399c Bump SDK 2025-01-23 16:29:43 +00:00
Justin Spahr-Summers
361f9d109b Merge pull request #128 from modelcontextprotocol/dependabot/npm_and_yarn/npm_and_yarn-f25c717a0f
Bump vite from 5.4.11 to 5.4.12 in the npm_and_yarn group across 1 directory
2025-01-22 12:02:57 +00:00
dependabot[bot]
7ec661e8bd Bump vite in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 5.4.11 to 5.4.12
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.12/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.12/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-22 04:50:31 +00:00
Ashwin Bhat
98e6f0e5ec Merge pull request #124 from modelcontextprotocol/ashwin/envvar
allow passing env vars to server from command line
2025-01-13 12:11:25 -08:00
Ashwin Bhat
ec150eb8b4 prettier 2025-01-10 07:53:55 -08:00
Ashwin Bhat
052de8690d respond to PR feedback 2025-01-10 07:51:55 -08:00
Ashwin Bhat
a976aefb39 allow passing env vars to server from command line 2025-01-10 07:51:54 -08:00
Ashwin Bhat
5a5873277c Merge pull request #123 from modelcontextprotocol/ashwin/prettier
enforce prettier formatting
2025-01-10 07:28:29 -08:00
Ashwin Bhat
715936d747 run prettier 2025-01-09 11:01:35 -08:00
Ashwin Bhat
d973f58bef run prettier check in CI 2025-01-09 11:01:28 -08:00
David Soria Parra
1797fbfba8 Merge pull request #118 from modelcontextprotocol/ashwin/githublink
feat: add GitHub link to sidebar for bug reports and contributions
2024-12-20 10:57:01 +00:00
David Soria Parra
8f4013c42c Merge pull request #117 from modelcontextprotocol/ashwin/state
refactor: extract draggable pane and connection logic into hooks
2024-12-20 10:56:33 +00:00
David Soria Parra
1abb7ca59c Merge pull request #119 from modelcontextprotocol/ashwin/font
feat: use monospace font for all input fields in sidebar
2024-12-20 10:56:05 +00:00
Ashwin Bhat
dfb36e1792 feat: use monospace font for all input fields in sidebar
Makes command, arguments, URL and environment variables easier to read and edit.
2024-12-19 13:18:52 -08:00
Ashwin Bhat
ffc29663c8 fix: reduce theme selector width to 100px to prevent crowding with icon buttons 2024-12-19 13:17:16 -08:00
Ashwin Bhat
53226dd391 feat: add GitHub link to sidebar for bug reports and contributions 2024-12-19 13:06:00 -08:00
Ashwin Bhat
dc49d46baa refactor: extract draggable pane and connection logic into hooks
- Create useDraggablePane hook for history pane drag behavior
- Create useConnection hook for MCP client connection and requests
- Update App.tsx to use both hooks
2024-12-18 12:54:24 -08:00
Ashwin Bhat
ef32a8f289 Merge pull request #116 from modelcontextprotocol/claude/update-readme
feat: add help and debug links to sidebar
2024-12-18 09:14:31 -08:00
David Soria Parra
54e9957ec5 feat: add help and debug links to sidebar 2024-12-18 11:39:24 +00:00
Ashwin Bhat
7edde5001b Merge pull request #112 from modelcontextprotocol/devin/1734088716-fix-object-params
fix: properly handle object type parameters in tools
2024-12-17 14:50:23 -08:00
Devin AI
14bda1f030 chore: revert unrelated changes to TypeScript comments and formatting
- Reverted @ts-expect-error messages back to original text
- Removed unnecessary line breaks in placeholder and type properties
- Kept object parameter handling functionality intact

Co-Authored-By: ashwin@anthropic.com <ashwin@anthropic.com>
2024-12-13 12:12:33 +00:00
Devin AI
1f4d35f8a3 fix: properly handle object type parameters in tools
- Add special handling for object type parameters
- Parse JSON input for object parameters
- Maintain raw input if JSON parsing fails
- Fixes #110

Co-Authored-By: ashwin@anthropic.com <ashwin@anthropic.com>
2024-12-13 11:21:35 +00:00
Ashwin Bhat
eb70539958 Merge pull request #105 from devin-open-source/devin/1733551277-capability-negotiation
feat: implement capability negotiation for UI tabs
2024-12-09 03:56:58 -08:00
Jeffrey Ling
7878e1764a address comment 2024-12-09 04:52:15 -07:00
Jeffrey Ling
26f0cb3c8b merge conflict 2024-12-09 04:43:48 -07:00
Jeffrey Ling
8f40e052c1 Merge remote-tracking branch 'origin/main' into devin/1733551277-capability-negotiation 2024-12-09 04:31:45 -07:00
Jeffrey Ling
024f06c1b7 cleanup diffs 2024-12-09 04:29:30 -07:00
Devin AI
1ddc63b330 refactor: remove disabled state from Sampling and Roots tabs 2024-12-09 11:07:42 +00:00
Devin AI
27bd503240 fix: remove duplicate ServerCapabilities type declarations 2024-12-09 10:37:56 +00:00
Devin AI
b39c96de7c refactor: revert tab files to main and restore tab disabling 2024-12-09 10:36:56 +00:00
Devin AI
d857e1462b refactor: simplify capability handling and remove context provider
- Remove redundant useEffect for capability checking
- Remove CapabilityContext provider pattern
- Set default tab to first supported capability
- Add fallback UI for unsupported capabilities
- Delete unused contexts.ts file
2024-12-09 10:11:03 +00:00
Ashwin Bhat
2eae823d65 Merge pull request #107 from JensWallgren/tool-result-legacy-dark-fix
Add dark mode support for the legacy Tool Result display
2024-12-09 02:03:34 -08:00
David Soria Parra
79ba164fda Merge pull request #108 from 8enmann/ben/persist
feat: improve request history and tab persistence
2024-12-09 09:27:19 +00:00
Ben Mann
2f513df6c1 feat: improve request history and tab persistence
- Add failed requests to history with error messages for better debugging
- Persist selected tab in URL hash and restore on page load
- Fix formatting of timeout parameter parsing

🤖 Generated with Claude CLI.

Co-Authored-By: Claude <noreply@anthropic.com>
2024-12-08 21:15:34 +02:00
Jens Wallgren
ce68085e77 Add dark mode support for the legacy Tool Result display 2024-12-08 14:14:40 +01:00
devin-ai-integration[bot]
e96b3be159 feat: implement capability negotiation for UI tabs
- Add CapabilityContext to manage server capabilities
- Disable tabs when server doesn't support feature
- Show error message in tab content when capability missing
- Implements #85
2024-12-07 06:15:21 +00:00
Ashwin Bhat
034699524a Merge pull request #104 from devin-open-source/fix-shell-quote
Fix shell argument parsing issue: #96
2024-12-06 17:07:50 -08:00
Jeffrey Ling
fdc521646f no need to prettier format everything right now 2024-12-06 12:48:48 -07:00
devin-ai-integration[bot]
bd6586bbad style: apply prettier formatting 2024-12-06 19:43:48 +00:00
devin-ai-integration[bot]
c340e5f1ed chore: move shell-quote to main dependencies 2024-12-06 19:43:11 +00:00
devin-ai-integration[bot]
cc1ae05f9d fix: use shell-quote for proper argument parsing 2024-12-06 19:34:03 +00:00
devin-ai-integration[bot]
9ea77a729c chore: add shell-quote package and types 2024-12-06 19:33:08 +00:00
Ashwin Bhat
8c7b0c360e Merge pull request #103 from evalstate/fix/tool-tab-parameters
Tool Tab - Parameter Handling Fixes.
2024-12-05 17:51:05 -08:00
evalstate
35effc4d16 Merge branch 'main' into feature/audio-rendering 2024-12-05 19:50:21 +00:00
Ashwin Bhat
576ff0043a bump version to 0.3.0 2024-12-05 08:01:57 -08:00
Ashwin Bhat
18dc4d0a99 Merge pull request #100 from evalstate/fix/tool-timeout
Allow setting the timeout with the "timeout" URL parameter
2024-12-05 07:59:55 -08:00
evalstate
ed5017d73e Two fixes to the Tools Tab:
1) Tool Parameters were stale when switching between Tools causing incorrect messages to be sent.
2) Tool List is emptied when "Clear" is selected, so invalid messages can't be sent.
2024-12-05 15:32:55 +00:00
=
f04b161411 Allow setting timeout via "timeout" URL parameter 2024-12-05 08:11:35 +00:00
Ashwin Bhat
bd6a63603a Merge pull request #102 from modelcontextprotocol/ashwin/sdk
update sdk to 1.0.3
2024-12-04 14:20:09 -08:00
Ashwin Bhat
b845444fab update sdk to 1.0.3 2024-12-04 09:56:04 -08:00
evalstate
14802b8043 Merge branch 'main' into feature/audio-rendering 2024-12-03 17:11:47 +00:00
Justin Spahr-Summers
ace94c4d37 Merge pull request #95 from modelcontextprotocol/ashwin-ant-patch-1
link to MCP docs site in readme
2024-12-02 06:56:07 -06:00
Ashwin Bhat
50640bc9cc Merge pull request #98 from heuperman/add-button-to-clear-items
Add button to clear loaded items
2024-12-01 11:08:32 -05:00
=
068d21387a extended type for "audio" update to spec 2024-12-01 12:47:50 +00:00
=
66b1b73448 Render HTML5 Audio Player if Tool Result resource mimetype is audio. 2024-12-01 10:24:12 +00:00
Kees Heuperman
cc17ba8d56 feat: Add button to clear loaded items
Add a button to the ListPane component that clears loaded items. This
will allow the user to clear and reload resources, resource templates,
prompts or tools when they expect the available items to have changed.
2024-12-01 09:50:53 +01:00
Ashwin Bhat
764f02310d link to MCP docs site in readme 2024-11-29 16:26:56 -05:00
Ashwin Bhat
945299181d bump version to 0.2.7 2024-11-29 08:44:12 -05:00
David Soria Parra
79344bd495 Merge pull request #91 from modelcontextprotocol/ashwin/spawnfix
fix arg passing
2024-11-29 11:34:25 +00:00
Ashwin Bhat
295ccac27e fix arg passing 2024-11-27 19:17:47 -05:00
Ashwin Bhat
f3f424f21e bump version 2024-11-27 17:29:02 -05:00
Ashwin Bhat
6b6eeb8dcd bump version to 0.2.5 2024-11-27 17:24:19 -05:00
Ashwin Bhat
3110cf9343 Merge pull request #88 from modelcontextprotocol/ani/fix-npx
Enable using 'npx' as your command on Windows
2024-11-27 16:03:37 -05:00
Ani Betts
2c04fa31e8 Merge branch 'main' into ani/fix-npx 2024-11-27 21:57:44 +01:00
Ashwin Bhat
e700bc713a Merge pull request #87 from modelcontextprotocol/ashwin/versiondisplay
display inspector version in UI
2024-11-27 13:06:52 -05:00
Ashwin Bhat
bea86af65b Merge pull request #89 from evalstate/main
Dark Mode and Word Wrap for Resource Viewer
2024-11-27 13:06:06 -05:00
evalstate
68a6130b17 fix dark mode styling and add word wrap for resource viewer. 2024-11-27 17:56:22 +00:00
Ani Betts
853a3b4faf Enable using 'npx' as your command on Windows 2024-11-27 17:04:52 +01:00
Ashwin Bhat
6f62066d34 display inspector version in UI 2024-11-27 10:52:38 -05:00
ashwin-ant
c770d217e7 Merge pull request #86 from modelcontextprotocol/ani/debuggability
Make debugging Inspector easier for users
2024-11-27 10:04:00 -05:00
Ani Betts
98470a12f9 Make stdout/error echo for client and server 2024-11-27 15:57:02 +01:00
Ani Betts
a00564fafa Disable minification on production build, we don't need it here and it makes debugging annoying 2024-11-27 15:55:11 +01:00
Ashwin Bhat
62546dec58 bump version to 0.2.4 2024-11-27 09:33:15 -05:00
ashwin-ant
886ac5fc7b Merge pull request #81 from modelcontextprotocol/ashwin/serverport
Respect custom server port
2024-11-27 08:59:59 -05:00
ashwin-ant
722df4d798 Merge pull request #82 from jacksteamdev/fix-1
Add Runtime Type Validation for Tool Results
2024-11-26 15:32:58 -05:00
Jack Steam
407e304585 Merge branch 'main' into fix-1 2024-11-26 13:31:12 -07:00
Jack Steam
60578314aa Update client/src/components/ToolsTab.tsx
Co-authored-by: ashwin-ant <ashwin@anthropic.com>
2024-11-26 13:30:43 -07:00
Jack Steam
fbac5b78bc feat: add data validation message 2024-11-26 12:47:39 -06:00
Ashwin Bhat
f876b1ec0d consolidate server URL configuration 2024-11-26 13:40:28 -05:00
Jack Steam
aecfa21d47 fix: add static type validation 2024-11-26 11:14:55 -07:00
Ashwin Bhat
a3d542c0a3 make server port configurable via URL query param 2024-11-26 13:12:45 -05:00
24 changed files with 1131 additions and 422 deletions

View File

@@ -14,6 +14,9 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Check formatting
run: npx prettier --check .
- uses: actions/setup-node@v4
with:
node-version: 18

View File

@@ -1,2 +1,4 @@
packages
server/build
CODE_OF_CONDUCT.md
SECURITY.md

View File

@@ -14,10 +14,20 @@ To inspect an MCP server implementation, there's no need to clone this repo. Ins
npx @modelcontextprotocol/inspector build/index.js
```
You can also pass arguments along which will get passed as arguments to your MCP server:
You can pass both arguments and environment variables to your MCP server. Arguments are passed directly to your server, while environment variables can be set using the `-e` flag:
```
npx @modelcontextprotocol/inspector build/index.js arg1 arg2 ...
```bash
# Pass arguments only
npx @modelcontextprotocol/inspector build/index.js arg1 arg2
# Pass environment variables only
npx @modelcontextprotocol/inspector -e KEY=value -e KEY2=$VALUE2 build/index.js
# Pass both environment variables and arguments
npx @modelcontextprotocol/inspector -e KEY=value -e KEY2=$VALUE2 build/index.js arg1 arg2
# Use -- to separate inspector flags from server arguments
npx @modelcontextprotocol/inspector -e KEY=$VALUE -- build/index.js -e server-flag
```
The inspector runs both a client UI (default port 5173) and an MCP proxy server (default port 3000). Open the client UI in your browser to use the inspector. You can customize the ports if needed:
@@ -26,6 +36,8 @@ The inspector runs both a client UI (default port 5173) and an MCP proxy server
CLIENT_PORT=8080 SERVER_PORT=9000 npx @modelcontextprotocol/inspector build/index.js
```
For more details on ways to use the inspector, see the [Inspector section of the MCP docs site](https://modelcontextprotocol.io/docs/tools/inspector). For help with debugging, see the [Debugging guide](https://modelcontextprotocol.io/docs/tools/debugging).
### From this repository
If you're working on the inspector itself:

View File

@@ -11,8 +11,32 @@ function delay(ms) {
}
async function main() {
// Get command line arguments
const [, , command, ...mcpServerArgs] = process.argv;
// Parse command line arguments
const args = process.argv.slice(2);
const envVars = {};
const mcpServerArgs = [];
let command = null;
let parsingFlags = true;
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (parsingFlags && arg === "--") {
parsingFlags = false;
continue;
}
if (parsingFlags && arg === "-e" && i + 1 < args.length) {
const [key, value] = args[++i].split("=");
if (key && value) {
envVars[key] = value;
}
} else if (!command) {
command = arg;
} else {
mcpServerArgs.push(arg);
}
}
const inspectorServerPath = resolve(
__dirname,
@@ -49,20 +73,30 @@ async function main() {
[
inspectorServerPath,
...(command ? [`--env`, command] : []),
...(mcpServerArgs ? ["--args", mcpServerArgs.join(" ")] : []),
...(mcpServerArgs ? [`--args=${mcpServerArgs.join(" ")}`] : []),
],
{ env: { ...process.env, PORT: SERVER_PORT }, signal: abort.signal },
{
env: {
...process.env,
PORT: SERVER_PORT,
MCP_ENV_VARS: JSON.stringify(envVars),
},
signal: abort.signal,
echoOutput: true,
},
);
const client = spawnPromise("node", [inspectorClientPath], {
env: { ...process.env, PORT: CLIENT_PORT },
signal: abort.signal,
echoOutput: true,
});
// Make sure our server/client didn't immediately fail
await Promise.any([server, client, delay(2 * 1000)]);
const portParam = SERVER_PORT === "3000" ? "" : `?proxyPort=${SERVER_PORT}`;
console.log(
`\n🔍 MCP Inspector is up and running at http://localhost:${CLIENT_PORT} 🚀`,
`\n🔍 MCP Inspector is up and running at http://localhost:${CLIENT_PORT}${portParam} 🚀`,
);
try {

View File

@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/inspector-client",
"version": "0.2.3",
"version": "0.4.0",
"description": "Client-side application for the Model Context Protocol inspector",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
@@ -21,7 +21,7 @@
"preview": "vite preview"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.1",
"@modelcontextprotocol/sdk": "^1.4.1",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-select": "^2.1.2",
@@ -30,6 +30,7 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"lucide-react": "^0.447.0",
"pkce-challenge": "^4.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-toastify": "^10.0.6",

View File

@@ -1,36 +1,26 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import {
ClientNotification,
ClientRequest,
CompatibilityCallToolResult,
CompatibilityCallToolResultSchema,
CreateMessageRequestSchema,
CreateMessageResult,
EmptyResultSchema,
GetPromptResultSchema,
ListPromptsResultSchema,
ListResourcesResultSchema,
ListResourceTemplatesResultSchema,
ListRootsRequestSchema,
ListToolsResultSchema,
ProgressNotificationSchema,
ReadResourceResultSchema,
Request,
Resource,
ResourceTemplate,
Result,
Root,
ServerNotification,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import { useCallback, useEffect, useRef, useState } from "react";
import React, { Suspense, useEffect, useRef, useState } from "react";
import { useConnection } from "./lib/hooks/useConnection";
import { useDraggablePane } from "./lib/hooks/useDraggablePane";
import {
Notification,
StdErrNotification,
StdErrNotificationSchema,
} from "./lib/notificationTypes";
import { StdErrNotification } from "./lib/notificationTypes";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
@@ -43,7 +33,7 @@ import {
} from "lucide-react";
import { toast } from "react-toastify";
import { ZodType } from "zod";
import { z } from "zod";
import "./App.css";
import ConsoleTab from "./components/ConsoleTab";
import HistoryAndNotifications from "./components/History";
@@ -55,12 +45,22 @@ import SamplingTab, { PendingRequest } from "./components/SamplingTab";
import Sidebar from "./components/Sidebar";
import ToolsTab from "./components/ToolsTab";
const DEFAULT_REQUEST_TIMEOUT_MSEC = 10000;
const params = new URLSearchParams(window.location.search);
const PROXY_PORT = params.get("proxyPort") ?? "3000";
const PROXY_SERVER_URL = `http://localhost:${PROXY_PORT}`;
const App = () => {
const [connectionStatus, setConnectionStatus] = useState<
"disconnected" | "connected" | "error"
>("disconnected");
// Handle OAuth callback route
if (window.location.pathname === "/oauth/callback") {
const OAuthCallback = React.lazy(
() => import("./components/OAuthCallback"),
);
return (
<Suspense fallback={<div>Loading...</div>}>
<OAuthCallback />
</Suspense>
);
}
const [resources, setResources] = useState<Resource[]>([]);
const [resourceTemplates, setResourceTemplates] = useState<
ResourceTemplate[]
@@ -82,12 +82,15 @@ const App = () => {
const [args, setArgs] = useState<string>(() => {
return localStorage.getItem("lastArgs") || "";
});
const [url, setUrl] = useState<string>("http://localhost:3001/sse");
const [transportType, setTransportType] = useState<"stdio" | "sse">("stdio");
const [requestHistory, setRequestHistory] = useState<
{ request: string; response?: string }[]
>([]);
const [mcpClient, setMcpClient] = useState<Client | null>(null);
const [sseUrl, setSseUrl] = useState<string>(() => {
return localStorage.getItem("lastSseUrl") || "http://localhost:3001/sse";
});
const [transportType, setTransportType] = useState<"stdio" | "sse">(() => {
return (
(localStorage.getItem("lastTransportType") as "stdio" | "sse") || "stdio"
);
});
const [notifications, setNotifications] = useState<ServerNotification[]>([]);
const [stdErrNotifications, setStdErrNotifications] = useState<
StdErrNotification[]
@@ -138,49 +141,64 @@ const App = () => {
>();
const [nextToolCursor, setNextToolCursor] = useState<string | undefined>();
const progressTokenRef = useRef(0);
const [historyPaneHeight, setHistoryPaneHeight] = useState<number>(300);
const [isDragging, setIsDragging] = useState(false);
const dragStartY = useRef<number>(0);
const dragStartHeight = useRef<number>(0);
const handleDragStart = useCallback(
(e: React.MouseEvent) => {
setIsDragging(true);
dragStartY.current = e.clientY;
dragStartHeight.current = historyPaneHeight;
document.body.style.userSelect = "none";
const { height: historyPaneHeight, handleDragStart } = useDraggablePane(300);
const {
connectionStatus,
serverCapabilities,
mcpClient,
requestHistory,
makeRequest: makeConnectionRequest,
sendNotification,
connect: connectMcpServer,
} = useConnection({
transportType,
command,
args,
sseUrl,
env,
proxyServerUrl: PROXY_SERVER_URL,
onNotification: (notification) => {
setNotifications((prev) => [...prev, notification as ServerNotification]);
},
[historyPaneHeight],
);
const handleDragMove = useCallback(
(e: MouseEvent) => {
if (!isDragging) return;
const deltaY = dragStartY.current - e.clientY;
const newHeight = Math.max(
100,
Math.min(800, dragStartHeight.current + deltaY),
);
setHistoryPaneHeight(newHeight);
onStdErrNotification: (notification) => {
setStdErrNotifications((prev) => [
...prev,
notification as StdErrNotification,
]);
},
[isDragging],
);
onPendingRequest: (request, resolve, reject) => {
setPendingSampleRequests((prev) => [
...prev,
{ id: nextRequestId.current++, request, resolve, reject },
]);
},
getRoots: () => rootsRef.current,
});
const handleDragEnd = useCallback(() => {
setIsDragging(false);
document.body.style.userSelect = "";
}, []);
useEffect(() => {
if (isDragging) {
window.addEventListener("mousemove", handleDragMove);
window.addEventListener("mouseup", handleDragEnd);
return () => {
window.removeEventListener("mousemove", handleDragMove);
window.removeEventListener("mouseup", handleDragEnd);
};
const makeRequest = async <T extends z.ZodType>(
request: ClientRequest,
schema: T,
tabKey?: keyof typeof errors,
) => {
try {
const response = await makeConnectionRequest(request, schema);
if (tabKey !== undefined) {
clearError(tabKey);
}
return response;
} catch (e) {
const errorString = (e as Error).message ?? String(e);
if (tabKey !== undefined) {
setErrors((prev) => ({
...prev,
[tabKey]: errorString,
}));
}
throw e;
}
}, [isDragging, handleDragMove, handleDragEnd]);
};
useEffect(() => {
localStorage.setItem("lastCommand", command);
@@ -191,7 +209,32 @@ const App = () => {
}, [args]);
useEffect(() => {
fetch("http://localhost:3000/config")
localStorage.setItem("lastSseUrl", sseUrl);
}, [sseUrl]);
useEffect(() => {
localStorage.setItem("lastTransportType", transportType);
}, [transportType]);
// Auto-connect if serverUrl is provided in URL params (e.g. after OAuth callback)
useEffect(() => {
const serverUrl = params.get("serverUrl");
if (serverUrl) {
setSseUrl(serverUrl);
setTransportType("sse");
// Remove serverUrl from URL without reloading the page
const newUrl = new URL(window.location.href);
newUrl.searchParams.delete("serverUrl");
window.history.replaceState({}, "", newUrl.toString());
// Show success toast for OAuth
toast.success("Successfully authenticated with OAuth");
// Connect to the server
connectMcpServer();
}
}, []);
useEffect(() => {
fetch(`${PROXY_SERVER_URL}/config`)
.then((response) => response.json())
.then((data) => {
setEnv(data.defaultEnvironment);
@@ -211,79 +254,16 @@ const App = () => {
rootsRef.current = roots;
}, [roots]);
const pushHistory = (request: object, response?: object) => {
setRequestHistory((prev) => [
...prev,
{
request: JSON.stringify(request),
response: response !== undefined ? JSON.stringify(response) : undefined,
},
]);
};
useEffect(() => {
if (!window.location.hash) {
window.location.hash = "resources";
}
}, []);
const clearError = (tabKey: keyof typeof errors) => {
setErrors((prev) => ({ ...prev, [tabKey]: null }));
};
const makeRequest = async <T extends ZodType<object>>(
request: ClientRequest,
schema: T,
tabKey?: keyof typeof errors,
) => {
if (!mcpClient) {
throw new Error("MCP client not connected");
}
try {
const abortController = new AbortController();
const timeoutId = setTimeout(() => {
abortController.abort("Request timed out");
}, DEFAULT_REQUEST_TIMEOUT_MSEC);
let response;
try {
response = await mcpClient.request(request, schema, {
signal: abortController.signal,
});
} finally {
clearTimeout(timeoutId);
}
pushHistory(request, response);
if (tabKey !== undefined) {
clearError(tabKey);
}
return response;
} catch (e: unknown) {
const errorString = (e as Error).message ?? String(e);
if (tabKey === undefined) {
toast.error(errorString);
} else {
setErrors((prev) => ({
...prev,
[tabKey]: errorString,
}));
}
throw e;
}
};
const sendNotification = async (notification: ClientNotification) => {
if (!mcpClient) {
throw new Error("MCP client not connected");
}
try {
await mcpClient.notification(notification);
pushHistory(notification);
} catch (e: unknown) {
toast.error((e as Error).message ?? String(e));
throw e;
}
};
const listResources = async () => {
const response = await makeRequest(
{
@@ -386,79 +366,6 @@ const App = () => {
await sendNotification({ method: "notifications/roots/list_changed" });
};
const connectMcpServer = async () => {
try {
const client = new Client<Request, Notification, Result>(
{
name: "mcp-inspector",
version: "0.0.1",
},
{
capabilities: {
// Support all client capabilities since we're an inspector tool
sampling: {},
roots: {
listChanged: true,
},
},
},
);
const backendUrl = new URL("http://localhost:3000/sse");
backendUrl.searchParams.append("transportType", transportType);
if (transportType === "stdio") {
backendUrl.searchParams.append("command", command);
backendUrl.searchParams.append("args", args);
backendUrl.searchParams.append("env", JSON.stringify(env));
} else {
backendUrl.searchParams.append("url", url);
}
const clientTransport = new SSEClientTransport(backendUrl);
client.setNotificationHandler(
ProgressNotificationSchema,
(notification) => {
setNotifications((prevNotifications) => [
...prevNotifications,
notification,
]);
},
);
client.setNotificationHandler(
StdErrNotificationSchema,
(notification) => {
setStdErrNotifications((prevErrorNotifications) => [
...prevErrorNotifications,
notification,
]);
},
);
await client.connect(clientTransport);
client.setRequestHandler(CreateMessageRequestSchema, (request) => {
return new Promise<CreateMessageResult>((resolve, reject) => {
setPendingSampleRequests((prev) => [
...prev,
{ id: nextRequestId.current++, request, resolve, reject },
]);
});
});
client.setRequestHandler(ListRootsRequestSchema, async () => {
return { roots: rootsRef.current };
});
setMcpClient(client);
setConnectionStatus("connected");
} catch (e) {
console.error(e);
setConnectionStatus("error");
}
};
return (
<div className="flex h-screen bg-background">
<Sidebar
@@ -469,8 +376,8 @@ const App = () => {
setCommand={setCommand}
args={args}
setArgs={setArgs}
url={url}
setUrl={setUrl}
sseUrl={sseUrl}
setSseUrl={setSseUrl}
env={env}
setEnv={setEnv}
onConnect={connectMcpServer}
@@ -479,17 +386,42 @@ const App = () => {
<div className="flex-1 flex flex-col overflow-hidden">
<div className="flex-1 overflow-auto">
{mcpClient ? (
<Tabs defaultValue="resources" className="w-full p-4">
<Tabs
defaultValue={
Object.keys(serverCapabilities ?? {}).includes(
window.location.hash.slice(1),
)
? window.location.hash.slice(1)
: serverCapabilities?.resources
? "resources"
: serverCapabilities?.prompts
? "prompts"
: serverCapabilities?.tools
? "tools"
: "ping"
}
className="w-full p-4"
onValueChange={(value) => (window.location.hash = value)}
>
<TabsList className="mb-4 p-0">
<TabsTrigger value="resources">
<TabsTrigger
value="resources"
disabled={!serverCapabilities?.resources}
>
<Files className="w-4 h-4 mr-2" />
Resources
</TabsTrigger>
<TabsTrigger value="prompts">
<TabsTrigger
value="prompts"
disabled={!serverCapabilities?.prompts}
>
<MessageSquare className="w-4 h-4 mr-2" />
Prompts
</TabsTrigger>
<TabsTrigger value="tools">
<TabsTrigger
value="tools"
disabled={!serverCapabilities?.tools}
>
<Hammer className="w-4 h-4 mr-2" />
Tools
</TabsTrigger>
@@ -513,91 +445,119 @@ const App = () => {
</TabsList>
<div className="w-full">
<ResourcesTab
resources={resources}
resourceTemplates={resourceTemplates}
listResources={() => {
clearError("resources");
listResources();
}}
listResourceTemplates={() => {
clearError("resources");
listResourceTemplates();
}}
readResource={(uri) => {
clearError("resources");
readResource(uri);
}}
selectedResource={selectedResource}
setSelectedResource={(resource) => {
clearError("resources");
setSelectedResource(resource);
}}
resourceContent={resourceContent}
nextCursor={nextResourceCursor}
nextTemplateCursor={nextResourceTemplateCursor}
error={errors.resources}
/>
<PromptsTab
prompts={prompts}
listPrompts={() => {
clearError("prompts");
listPrompts();
}}
getPrompt={(name, args) => {
clearError("prompts");
getPrompt(name, args);
}}
selectedPrompt={selectedPrompt}
setSelectedPrompt={(prompt) => {
clearError("prompts");
setSelectedPrompt(prompt);
}}
promptContent={promptContent}
nextCursor={nextPromptCursor}
error={errors.prompts}
/>
<ToolsTab
tools={tools}
listTools={() => {
clearError("tools");
listTools();
}}
callTool={(name, params) => {
clearError("tools");
callTool(name, params);
}}
selectedTool={selectedTool}
setSelectedTool={(tool) => {
clearError("tools");
setSelectedTool(tool);
setToolResult(null);
}}
toolResult={toolResult}
nextCursor={nextToolCursor}
error={errors.tools}
/>
<ConsoleTab />
<PingTab
onPingClick={() => {
void makeRequest(
{
method: "ping" as const,
},
EmptyResultSchema,
);
}}
/>
<SamplingTab
pendingRequests={pendingSampleRequests}
onApprove={handleApproveSampling}
onReject={handleRejectSampling}
/>
<RootsTab
roots={roots}
setRoots={setRoots}
onRootsChange={handleRootsChange}
/>
{!serverCapabilities?.resources &&
!serverCapabilities?.prompts &&
!serverCapabilities?.tools ? (
<div className="flex items-center justify-center p-4">
<p className="text-lg text-gray-500">
The connected server does not support any MCP capabilities
</p>
</div>
) : (
<>
<ResourcesTab
resources={resources}
resourceTemplates={resourceTemplates}
listResources={() => {
clearError("resources");
listResources();
}}
clearResources={() => {
setResources([]);
setNextResourceCursor(undefined);
}}
listResourceTemplates={() => {
clearError("resources");
listResourceTemplates();
}}
clearResourceTemplates={() => {
setResourceTemplates([]);
setNextResourceTemplateCursor(undefined);
}}
readResource={(uri) => {
clearError("resources");
readResource(uri);
}}
selectedResource={selectedResource}
setSelectedResource={(resource) => {
clearError("resources");
setSelectedResource(resource);
}}
resourceContent={resourceContent}
nextCursor={nextResourceCursor}
nextTemplateCursor={nextResourceTemplateCursor}
error={errors.resources}
/>
<PromptsTab
prompts={prompts}
listPrompts={() => {
clearError("prompts");
listPrompts();
}}
clearPrompts={() => {
setPrompts([]);
setNextPromptCursor(undefined);
}}
getPrompt={(name, args) => {
clearError("prompts");
getPrompt(name, args);
}}
selectedPrompt={selectedPrompt}
setSelectedPrompt={(prompt) => {
clearError("prompts");
setSelectedPrompt(prompt);
}}
promptContent={promptContent}
nextCursor={nextPromptCursor}
error={errors.prompts}
/>
<ToolsTab
tools={tools}
listTools={() => {
clearError("tools");
listTools();
}}
clearTools={() => {
setTools([]);
setNextToolCursor(undefined);
}}
callTool={(name, params) => {
clearError("tools");
callTool(name, params);
}}
selectedTool={selectedTool}
setSelectedTool={(tool) => {
clearError("tools");
setSelectedTool(tool);
setToolResult(null);
}}
toolResult={toolResult}
nextCursor={nextToolCursor}
error={errors.tools}
/>
<ConsoleTab />
<PingTab
onPingClick={() => {
void makeRequest(
{
method: "ping" as const,
},
EmptyResultSchema,
);
}}
/>
<SamplingTab
pendingRequests={pendingSampleRequests}
onApprove={handleApproveSampling}
onReject={handleRejectSampling}
/>
<RootsTab
roots={roots}
setRoots={setRoots}
onRootsChange={handleRootsChange}
/>
</>
)}
</div>
</Tabs>
) : (

View File

@@ -3,6 +3,7 @@ import { Button } from "./ui/button";
type ListPaneProps<T> = {
items: T[];
listItems: () => void;
clearItems: () => void;
setSelectedItem: (item: T) => void;
renderItem: (item: T) => React.ReactNode;
title: string;
@@ -13,6 +14,7 @@ type ListPaneProps<T> = {
const ListPane = <T extends object>({
items,
listItems,
clearItems,
setSelectedItem,
renderItem,
title,
@@ -32,6 +34,14 @@ const ListPane = <T extends object>({
>
{buttonText}
</Button>
<Button
variant="outline"
className="w-full mb-4"
onClick={clearItems}
disabled={items.length === 0}
>
Clear
</Button>
<div className="space-y-2 overflow-y-auto max-h-96">
{items.map((item, index) => (
<div

View File

@@ -0,0 +1,48 @@
import { useEffect, useRef } from "react";
import { handleOAuthCallback } from "../lib/auth";
import { SESSION_KEYS } from "../lib/constants";
const OAuthCallback = () => {
const hasProcessedRef = useRef(false);
useEffect(() => {
const handleCallback = async () => {
// Skip if we've already processed this callback
if (hasProcessedRef.current) {
return;
}
hasProcessedRef.current = true;
const params = new URLSearchParams(window.location.search);
const code = params.get("code");
const serverUrl = sessionStorage.getItem(SESSION_KEYS.SERVER_URL);
if (!code || !serverUrl) {
console.error("Missing code or server URL");
window.location.href = "/";
return;
}
try {
const accessToken = await handleOAuthCallback(serverUrl, code);
// Store the access token for future use
sessionStorage.setItem(SESSION_KEYS.ACCESS_TOKEN, accessToken);
// Redirect back to the main app with server URL to trigger auto-connect
window.location.href = `/?serverUrl=${encodeURIComponent(serverUrl)}`;
} catch (error) {
console.error("OAuth callback error:", error);
window.location.href = "/";
}
};
void handleCallback();
}, []);
return (
<div className="flex items-center justify-center h-screen">
<p className="text-lg text-gray-500">Processing OAuth callback...</p>
</div>
);
};
export default OAuthCallback;

View File

@@ -22,6 +22,7 @@ export type Prompt = {
const PromptsTab = ({
prompts,
listPrompts,
clearPrompts,
getPrompt,
selectedPrompt,
setSelectedPrompt,
@@ -31,6 +32,7 @@ const PromptsTab = ({
}: {
prompts: Prompt[];
listPrompts: () => void;
clearPrompts: () => void;
getPrompt: (name: string, args: Record<string, string>) => void;
selectedPrompt: Prompt | null;
setSelectedPrompt: (prompt: Prompt) => void;
@@ -55,6 +57,7 @@ const PromptsTab = ({
<ListPane
items={prompts}
listItems={listPrompts}
clearItems={clearPrompts}
setSelectedItem={(prompt) => {
setSelectedPrompt(prompt);
setPromptArgs({});

View File

@@ -16,7 +16,9 @@ const ResourcesTab = ({
resources,
resourceTemplates,
listResources,
clearResources,
listResourceTemplates,
clearResourceTemplates,
readResource,
selectedResource,
setSelectedResource,
@@ -28,7 +30,9 @@ const ResourcesTab = ({
resources: Resource[];
resourceTemplates: ResourceTemplate[];
listResources: () => void;
clearResources: () => void;
listResourceTemplates: () => void;
clearResourceTemplates: () => void;
readResource: (uri: string) => void;
selectedResource: Resource | null;
setSelectedResource: (resource: Resource | null) => void;
@@ -68,6 +72,7 @@ const ResourcesTab = ({
<ListPane
items={resources}
listItems={listResources}
clearItems={clearResources}
setSelectedItem={(resource) => {
setSelectedResource(resource);
readResource(resource.uri);
@@ -90,6 +95,7 @@ const ResourcesTab = ({
<ListPane
items={resourceTemplates}
listItems={listResourceTemplates}
clearItems={clearResourceTemplates}
setSelectedItem={(template) => {
setSelectedTemplate(template);
setSelectedResource(null);

View File

@@ -1,6 +1,14 @@
import { useState } from "react";
import { Play, ChevronDown, ChevronRight } from "lucide-react";
import {
Play,
ChevronDown,
ChevronRight,
CircleHelp,
Bug,
Github,
Eye,
EyeOff,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
@@ -13,6 +21,7 @@ import {
import { StdErrNotification } from "@/lib/notificationTypes";
import useTheme from "../lib/useTheme";
import { version } from "../../../package.json";
interface SidebarProps {
connectionStatus: "disconnected" | "connected" | "error";
@@ -22,8 +31,8 @@ interface SidebarProps {
setCommand: (command: string) => void;
args: string;
setArgs: (args: string) => void;
url: string;
setUrl: (url: string) => void;
sseUrl: string;
setSseUrl: (url: string) => void;
env: Record<string, string>;
setEnv: (env: Record<string, string>) => void;
onConnect: () => void;
@@ -38,8 +47,8 @@ const Sidebar = ({
setCommand,
args,
setArgs,
url,
setUrl,
sseUrl,
setSseUrl,
env,
setEnv,
onConnect,
@@ -47,12 +56,15 @@ const Sidebar = ({
}: SidebarProps) => {
const [theme, setTheme] = useTheme();
const [showEnvVars, setShowEnvVars] = useState(false);
const [shownEnvVars, setShownEnvVars] = useState<Set<string>>(new Set());
return (
<div className="w-80 bg-card border-r border-border flex flex-col h-full">
<div className="flex items-center justify-between p-4 border-b border-gray-200">
<div className="flex items-center">
<h1 className="ml-2 text-lg font-semibold">MCP Inspector</h1>
<h1 className="ml-2 text-lg font-semibold">
MCP Inspector v{version}
</h1>
</div>
</div>
@@ -75,6 +87,7 @@ const Sidebar = ({
</SelectContent>
</Select>
</div>
{transportType === "stdio" ? (
<>
<div className="space-y-2">
@@ -83,6 +96,7 @@ const Sidebar = ({
placeholder="Command"
value={command}
onChange={(e) => setCommand(e.target.value)}
className="font-mono"
/>
</div>
<div className="space-y-2">
@@ -91,6 +105,7 @@ const Sidebar = ({
placeholder="Arguments (space-separated)"
value={args}
onChange={(e) => setArgs(e.target.value)}
className="font-mono"
/>
</div>
</>
@@ -99,8 +114,9 @@ const Sidebar = ({
<label className="text-sm font-medium">URL</label>
<Input
placeholder="URL"
value={url}
onChange={(e) => setUrl(e.target.value)}
value={sseUrl}
onChange={(e) => setSseUrl(e.target.value)}
className="font-mono"
/>
</div>
)}
@@ -121,19 +137,44 @@ const Sidebar = ({
{showEnvVars && (
<div className="space-y-2">
{Object.entries(env).map(([key, value], idx) => (
<div key={idx} className="grid grid-cols-[1fr,auto] gap-2">
<div className="space-y-1">
<div key={idx} className="space-y-2 pb-4">
<div className="flex gap-2">
<Input
placeholder="Key"
value={key}
onChange={(e) => {
const newKey = e.target.value;
const newEnv = { ...env };
delete newEnv[key];
newEnv[e.target.value] = value;
newEnv[newKey] = value;
setEnv(newEnv);
setShownEnvVars((prev) => {
const next = new Set(prev);
if (next.has(key)) {
next.delete(key);
next.add(newKey);
}
return next;
});
}}
className="font-mono"
/>
<Button
variant="destructive"
size="icon"
className="h-9 w-9 p-0 shrink-0"
onClick={() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [key]: _removed, ...rest } = env;
setEnv(rest);
}}
>
×
</Button>
</div>
<div className="flex gap-2">
<Input
type={shownEnvVars.has(key) ? "text" : "password"}
placeholder="Value"
value={value}
onChange={(e) => {
@@ -141,25 +182,47 @@ const Sidebar = ({
newEnv[key] = e.target.value;
setEnv(newEnv);
}}
className="font-mono"
/>
<Button
variant="outline"
size="icon"
className="h-9 w-9 p-0 shrink-0"
onClick={() => {
setShownEnvVars((prev) => {
const next = new Set(prev);
if (next.has(key)) {
next.delete(key);
} else {
next.add(key);
}
return next;
});
}}
aria-label={
shownEnvVars.has(key) ? "Hide value" : "Show value"
}
aria-pressed={shownEnvVars.has(key)}
title={
shownEnvVars.has(key) ? "Hide value" : "Show value"
}
>
{shownEnvVars.has(key) ? (
<Eye className="h-4 w-4" aria-hidden="true" />
) : (
<EyeOff className="h-4 w-4" aria-hidden="true" />
)}
</Button>
</div>
<Button
variant="destructive"
onClick={() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [key]: removed, ...rest } = env;
setEnv(rest);
}}
>
Remove
</Button>
</div>
))}
<Button
variant="outline"
className="w-full mt-2"
onClick={() => {
const key = "";
const newEnv = { ...env };
newEnv[""] = "";
newEnv[key] = "";
setEnv(newEnv);
}}
>
@@ -217,14 +280,14 @@ const Sidebar = ({
</div>
</div>
<div className="p-4 border-t">
<div className="flex items-center space-x-2">
<div className="flex items-center justify-between">
<Select
value={theme}
onValueChange={(value: string) =>
setTheme(value as "system" | "light" | "dark")
}
>
<SelectTrigger className="w-[120px]" id="theme-select">
<SelectTrigger className="w-[100px]" id="theme-select">
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -233,6 +296,39 @@ const Sidebar = ({
<SelectItem value="dark">Dark</SelectItem>
</SelectContent>
</Select>
<div className="flex items-center space-x-2">
<a
href="https://modelcontextprotocol.io/docs/tools/inspector"
target="_blank"
rel="noopener noreferrer"
>
<Button variant="ghost" title="Inspector Documentation">
<CircleHelp className="w-4 h-4 text-gray-800" />
</Button>
</a>
<a
href="https://modelcontextprotocol.io/docs/tools/debugging"
target="_blank"
rel="noopener noreferrer"
>
<Button variant="ghost" title="Debugging Guide">
<Bug className="w-4 h-4 text-gray-800" />
</Button>
</a>
<a
href="https://github.com/modelcontextprotocol/inspector"
target="_blank"
rel="noopener noreferrer"
>
<Button
variant="ghost"
title="Report bugs or contribute on GitHub"
>
<Github className="w-4 h-4 text-gray-800" />
</Button>
</a>
</div>
</div>
</div>
</div>

View File

@@ -5,12 +5,12 @@ import { Label } from "@/components/ui/label";
import { TabsContent } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";
import {
CallToolResult,
ListToolsResult,
Tool,
CallToolResultSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { AlertCircle, Send } from "lucide-react";
import { useState } from "react";
import { useEffect, useState } from "react";
import ListPane from "./ListPane";
import { CompatibilityCallToolResult } from "@modelcontextprotocol/sdk/types.js";
@@ -18,6 +18,7 @@ import { CompatibilityCallToolResult } from "@modelcontextprotocol/sdk/types.js"
const ToolsTab = ({
tools,
listTools,
clearTools,
callTool,
selectedTool,
setSelectedTool,
@@ -27,20 +28,44 @@ const ToolsTab = ({
}: {
tools: Tool[];
listTools: () => void;
clearTools: () => void;
callTool: (name: string, params: Record<string, unknown>) => void;
selectedTool: Tool | null;
setSelectedTool: (tool: Tool) => void;
setSelectedTool: (tool: Tool | null) => void;
toolResult: CompatibilityCallToolResult | null;
nextCursor: ListToolsResult["nextCursor"];
error: string | null;
}) => {
const [params, setParams] = useState<Record<string, unknown>>({});
useEffect(() => {
setParams({});
}, [selectedTool]);
const renderToolResult = () => {
if (!toolResult) return null;
if ("content" in toolResult) {
const structuredResult = toolResult as CallToolResult;
const parsedResult = CallToolResultSchema.safeParse(toolResult);
if (!parsedResult.success) {
return (
<>
<h4 className="font-semibold mb-2">Invalid Tool Result:</h4>
<pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-4 rounded text-sm overflow-auto max-h-64">
{JSON.stringify(toolResult, null, 2)}
</pre>
<h4 className="font-semibold mb-2">Errors:</h4>
{parsedResult.error.errors.map((error, idx) => (
<pre
key={idx}
className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-4 rounded text-sm overflow-auto max-h-64"
>
{JSON.stringify(error, null, 2)}
</pre>
))}
</>
);
}
const structuredResult = parsedResult.data;
const isError = structuredResult.isError ?? false;
return (
@@ -62,11 +87,20 @@ const ToolsTab = ({
className="max-w-full h-auto"
/>
)}
{item.type === "resource" && (
<pre className="bg-gray-50 p-4 rounded text-sm overflow-auto max-h-64">
{JSON.stringify(item.resource, null, 2)}
</pre>
)}
{item.type === "resource" &&
(item.resource?.mimeType?.startsWith("audio/") ? (
<audio
controls
src={`data:${item.resource.mimeType};base64,${item.resource.blob}`}
className="w-full"
>
<p>Your browser does not support audio playback</p>
</audio>
) : (
<pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 whitespace-pre-wrap break-words p-4 rounded text-sm overflow-auto max-h-64">
{JSON.stringify(item.resource, null, 2)}
</pre>
))}
</div>
))}
</>
@@ -75,7 +109,7 @@ const ToolsTab = ({
return (
<>
<h4 className="font-semibold mb-2">Tool Result (Legacy):</h4>
<pre className="bg-gray-50 p-4 rounded text-sm overflow-auto max-h-64">
<pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-4 rounded text-sm overflow-auto max-h-64">
{JSON.stringify(toolResult.toolResult, null, 2)}
</pre>
</>
@@ -88,6 +122,10 @@ const ToolsTab = ({
<ListPane
items={tools}
listItems={listTools}
clearItems={() => {
clearTools();
setSelectedTool(null);
}}
setSelectedItem={setSelectedTool}
renderItem={(tool) => (
<>
@@ -145,6 +183,30 @@ const ToolsTab = ({
}
className="mt-1"
/>
) : /* @ts-expect-error value type is currently unknown */
value.type === "object" ? (
<Textarea
id={key}
name={key}
// @ts-expect-error value type is currently unknown
placeholder={value.description}
onChange={(e) => {
try {
const parsed = JSON.parse(e.target.value);
setParams({
...params,
[key]: parsed,
});
} catch (err) {
// If invalid JSON, store as string - will be validated on submit
setParams({
...params,
[key]: e.target.value,
});
}
}}
className="mt-1"
/>
) : (
<Input
// @ts-expect-error value type is currently unknown

93
client/src/lib/auth.ts Normal file
View File

@@ -0,0 +1,93 @@
import pkceChallenge from "pkce-challenge";
import { SESSION_KEYS } from "./constants";
export interface OAuthMetadata {
authorization_endpoint: string;
token_endpoint: string;
}
export async function discoverOAuthMetadata(
serverUrl: string,
): Promise<OAuthMetadata> {
try {
const url = new URL("/.well-known/oauth-authorization-server", serverUrl);
const response = await fetch(url.toString());
if (response.ok) {
const metadata = await response.json();
return {
authorization_endpoint: metadata.authorization_endpoint,
token_endpoint: metadata.token_endpoint,
};
}
} catch (error) {
console.warn("OAuth metadata discovery failed:", error);
}
// Fall back to default endpoints
const baseUrl = new URL(serverUrl);
return {
authorization_endpoint: new URL("/authorize", baseUrl).toString(),
token_endpoint: new URL("/token", baseUrl).toString(),
};
}
export async function startOAuthFlow(serverUrl: string): Promise<string> {
// Generate PKCE challenge
const challenge = await pkceChallenge();
const codeVerifier = challenge.code_verifier;
const codeChallenge = challenge.code_challenge;
// Store code verifier for later use
sessionStorage.setItem(SESSION_KEYS.CODE_VERIFIER, codeVerifier);
// Discover OAuth endpoints
const metadata = await discoverOAuthMetadata(serverUrl);
// Build authorization URL
const authUrl = new URL(metadata.authorization_endpoint);
authUrl.searchParams.set("response_type", "code");
authUrl.searchParams.set("code_challenge", codeChallenge);
authUrl.searchParams.set("code_challenge_method", "S256");
authUrl.searchParams.set(
"redirect_uri",
window.location.origin + "/oauth/callback",
);
return authUrl.toString();
}
export async function handleOAuthCallback(
serverUrl: string,
code: string,
): Promise<string> {
// Get stored code verifier
const codeVerifier = sessionStorage.getItem(SESSION_KEYS.CODE_VERIFIER);
if (!codeVerifier) {
throw new Error("No code verifier found");
}
// Discover OAuth endpoints
const metadata = await discoverOAuthMetadata(serverUrl);
// Exchange code for tokens
const response = await fetch(metadata.token_endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
grant_type: "authorization_code",
code,
code_verifier: codeVerifier,
redirect_uri: window.location.origin + "/oauth/callback",
}),
});
if (!response.ok) {
throw new Error("Token exchange failed");
}
const data = await response.json();
return data.access_token;
}

View File

@@ -0,0 +1,6 @@
// OAuth-related session storage keys
export const SESSION_KEYS = {
CODE_VERIFIER: "mcp_code_verifier",
SERVER_URL: "mcp_server_url",
ACCESS_TOKEN: "mcp_access_token",
} as const;

View File

@@ -0,0 +1,230 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import {
SSEClientTransport,
SseError,
} from "@modelcontextprotocol/sdk/client/sse.js";
import {
ClientNotification,
ClientRequest,
CreateMessageRequestSchema,
ListRootsRequestSchema,
ProgressNotificationSchema,
Request,
Result,
ServerCapabilities,
} from "@modelcontextprotocol/sdk/types.js";
import { useState } from "react";
import { toast } from "react-toastify";
import { z } from "zod";
import { startOAuthFlow } from "../auth";
import { SESSION_KEYS } from "../constants";
import { Notification, StdErrNotificationSchema } from "../notificationTypes";
const DEFAULT_REQUEST_TIMEOUT_MSEC = 10000;
interface UseConnectionOptions {
transportType: "stdio" | "sse";
command: string;
args: string;
sseUrl: string;
env: Record<string, string>;
proxyServerUrl: string;
requestTimeout?: number;
onNotification?: (notification: Notification) => void;
onStdErrNotification?: (notification: Notification) => void;
onPendingRequest?: (request: any, resolve: any, reject: any) => void;
getRoots?: () => any[];
}
export function useConnection({
transportType,
command,
args,
sseUrl,
env,
proxyServerUrl,
requestTimeout = DEFAULT_REQUEST_TIMEOUT_MSEC,
onNotification,
onStdErrNotification,
onPendingRequest,
getRoots,
}: UseConnectionOptions) {
const [connectionStatus, setConnectionStatus] = useState<
"disconnected" | "connected" | "error"
>("disconnected");
const [serverCapabilities, setServerCapabilities] =
useState<ServerCapabilities | null>(null);
const [mcpClient, setMcpClient] = useState<Client | null>(null);
const [requestHistory, setRequestHistory] = useState<
{ request: string; response?: string }[]
>([]);
const pushHistory = (request: object, response?: object) => {
setRequestHistory((prev) => [
...prev,
{
request: JSON.stringify(request),
response: response !== undefined ? JSON.stringify(response) : undefined,
},
]);
};
const makeRequest = async <T extends z.ZodType>(
request: ClientRequest,
schema: T,
) => {
if (!mcpClient) {
throw new Error("MCP client not connected");
}
try {
const abortController = new AbortController();
const timeoutId = setTimeout(() => {
abortController.abort("Request timed out");
}, requestTimeout);
let response;
try {
response = await mcpClient.request(request, schema, {
signal: abortController.signal,
});
pushHistory(request, response);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
pushHistory(request, { error: errorMessage });
throw error;
} finally {
clearTimeout(timeoutId);
}
return response;
} catch (e: unknown) {
const errorString = (e as Error).message ?? String(e);
toast.error(errorString);
throw e;
}
};
const sendNotification = async (notification: ClientNotification) => {
if (!mcpClient) {
throw new Error("MCP client not connected");
}
try {
await mcpClient.notification(notification);
pushHistory(notification);
} catch (e: unknown) {
toast.error((e as Error).message ?? String(e));
throw e;
}
};
const connect = async () => {
try {
const client = new Client<Request, Notification, Result>(
{
name: "mcp-inspector",
version: "0.0.1",
},
{
capabilities: {
sampling: {},
roots: {
listChanged: true,
},
},
},
);
const backendUrl = new URL(`${proxyServerUrl}/sse`);
backendUrl.searchParams.append("transportType", transportType);
if (transportType === "stdio") {
backendUrl.searchParams.append("command", command);
backendUrl.searchParams.append("args", args);
backendUrl.searchParams.append("env", JSON.stringify(env));
} else {
backendUrl.searchParams.append("url", sseUrl);
}
const headers: HeadersInit = {};
const accessToken = sessionStorage.getItem(SESSION_KEYS.ACCESS_TOKEN);
if (accessToken) {
headers["Authorization"] = `Bearer ${accessToken}`;
}
const clientTransport = new SSEClientTransport(backendUrl, {
eventSourceInit: {
fetch: (url, init) => fetch(url, { ...init, headers }),
},
requestInit: {
headers,
},
});
if (onNotification) {
client.setNotificationHandler(
ProgressNotificationSchema,
onNotification,
);
}
if (onStdErrNotification) {
client.setNotificationHandler(
StdErrNotificationSchema,
onStdErrNotification,
);
}
try {
await client.connect(clientTransport);
} catch (error) {
console.error("Failed to connect to MCP server:", error);
if (error instanceof SseError && error.code === 401) {
// Store the server URL for the callback handler
sessionStorage.setItem(SESSION_KEYS.SERVER_URL, sseUrl);
const redirectUrl = await startOAuthFlow(sseUrl);
window.location.href = redirectUrl;
return;
}
throw error;
}
const capabilities = client.getServerCapabilities();
setServerCapabilities(capabilities ?? null);
if (onPendingRequest) {
client.setRequestHandler(CreateMessageRequestSchema, (request) => {
return new Promise((resolve, reject) => {
onPendingRequest(request, resolve, reject);
});
});
}
if (getRoots) {
client.setRequestHandler(ListRootsRequestSchema, async () => {
return { roots: getRoots() };
});
}
setMcpClient(client);
setConnectionStatus("connected");
} catch (e) {
console.error(e);
setConnectionStatus("error");
}
};
return {
connectionStatus,
serverCapabilities,
mcpClient,
requestHistory,
makeRequest,
sendNotification,
connect,
};
}

View File

@@ -0,0 +1,53 @@
import { useCallback, useEffect, useRef, useState } from "react";
export function useDraggablePane(initialHeight: number) {
const [height, setHeight] = useState(initialHeight);
const [isDragging, setIsDragging] = useState(false);
const dragStartY = useRef<number>(0);
const dragStartHeight = useRef<number>(0);
const handleDragStart = useCallback(
(e: React.MouseEvent) => {
setIsDragging(true);
dragStartY.current = e.clientY;
dragStartHeight.current = height;
document.body.style.userSelect = "none";
},
[height],
);
const handleDragMove = useCallback(
(e: MouseEvent) => {
if (!isDragging) return;
const deltaY = dragStartY.current - e.clientY;
const newHeight = Math.max(
100,
Math.min(800, dragStartHeight.current + deltaY),
);
setHeight(newHeight);
},
[isDragging],
);
const handleDragEnd = useCallback(() => {
setIsDragging(false);
document.body.style.userSelect = "";
}, []);
useEffect(() => {
if (isDragging) {
window.addEventListener("mousemove", handleDragMove);
window.addEventListener("mouseup", handleDragEnd);
return () => {
window.removeEventListener("mousemove", handleDragMove);
window.removeEventListener("mouseup", handleDragEnd);
};
}
}, [isDragging, handleDragMove, handleDragEnd]);
return {
height,
isDragging,
handleDragStart,
};
}

View File

@@ -1,7 +1,7 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import App from "./App.tsx";
import "./index.css";

View File

@@ -23,7 +23,8 @@
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
"noFallthroughCasesInSwitch": true,
"resolveJsonModule": true
},
"include": ["src"]
}

View File

@@ -1,13 +1,22 @@
import react from "@vitejs/plugin-react";
import path from "path";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {},
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
build: {
minify: false,
rollupOptions: {
output: {
manualChunks: undefined,
},
},
},
});

117
package-lock.json generated
View File

@@ -1,22 +1,23 @@
{
"name": "@modelcontextprotocol/inspector",
"version": "0.2.3",
"version": "0.4.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@modelcontextprotocol/inspector",
"version": "0.2.3",
"version": "0.4.0",
"license": "MIT",
"workspaces": [
"client",
"server"
],
"dependencies": {
"@modelcontextprotocol/inspector-client": "0.2.3",
"@modelcontextprotocol/inspector-server": "0.2.3",
"@modelcontextprotocol/inspector-client": "0.3.0",
"@modelcontextprotocol/inspector-server": "0.3.0",
"concurrently": "^9.0.1",
"spawn-rx": "^5.0.4",
"shell-quote": "^1.8.2",
"spawn-rx": "^5.1.0",
"ts-node": "^10.9.2"
},
"bin": {
@@ -24,15 +25,16 @@
},
"devDependencies": {
"@types/node": "^22.7.5",
"@types/shell-quote": "^1.7.5",
"prettier": "3.3.3"
}
},
"client": {
"name": "@modelcontextprotocol/inspector-client",
"version": "0.2.3",
"version": "0.4.0",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.1",
"@modelcontextprotocol/sdk": "^1.4.1",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-select": "^2.1.2",
@@ -41,6 +43,7 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"lucide-react": "^0.447.0",
"pkce-challenge": "^4.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-toastify": "^10.0.6",
@@ -1203,14 +1206,31 @@
"link": true
},
"node_modules/@modelcontextprotocol/sdk": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.0.1.tgz",
"integrity": "sha512-slLdFaxQJ9AlRg+hw28iiTtGvShAOgOKXcD0F91nUcRYiOMuS9ZBYjcdNZRXW9G5JQ511GRTdUy1zQVZDpJ+4w==",
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.4.1.tgz",
"integrity": "sha512-wS6YC4lkUZ9QpP+/7NBTlVNiEvsnyl0xF7rRusLF+RsG0xDPc/zWR7fEEyhKnnNutGsDAZh59l/AeoWGwIb1+g==",
"license": "MIT",
"dependencies": {
"content-type": "^1.0.5",
"eventsource": "^3.0.2",
"raw-body": "^3.0.0",
"zod": "^3.23.8"
"zod": "^3.23.8",
"zod-to-json-schema": "^3.24.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@modelcontextprotocol/sdk/node_modules/eventsource": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.2.tgz",
"integrity": "sha512-YolzkJNxsTL3tCJMWFxpxtG2sCjbZ4LQUBUrkdaJK0ub0p6lmJt+2+1SwhKjLc652lpH9L/79Ptez972H9tphw==",
"license": "MIT",
"dependencies": {
"eventsource-parser": "^3.0.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@nodelib/fs.scandir": {
@@ -2256,13 +2276,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/eventsource": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/@types/eventsource/-/eventsource-1.1.15.tgz",
"integrity": "sha512-XQmGcbnxUNa06HR3VBVkc9+A2Vpi9ZyLJcdS5dwaQQ/4ZMWFO+5c90FnMUpbtMZwB/FChoYHwuVg8TvkECacTA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/express": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
@@ -2394,6 +2407,13 @@
"@types/send": "*"
}
},
"node_modules/@types/shell-quote": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@types/shell-quote/-/shell-quote-1.7.5.tgz",
"integrity": "sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/ws": {
"version": "8.5.13",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz",
@@ -3680,13 +3700,13 @@
"node": ">= 0.6"
}
},
"node_modules/eventsource": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz",
"integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==",
"node_modules/eventsource-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.0.tgz",
"integrity": "sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==",
"license": "MIT",
"engines": {
"node": ">=12.0.0"
"node": ">=18.0.0"
}
},
"node_modules/express": {
@@ -4927,6 +4947,15 @@
"node": ">= 6"
}
},
"node_modules/pkce-challenge": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-4.1.0.tgz",
"integrity": "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ==",
"license": "MIT",
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/postcss": {
"version": "8.4.49",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
@@ -5689,10 +5718,13 @@
}
},
"node_modules/shell-quote": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz",
"integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==",
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz",
"integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -5737,9 +5769,9 @@
}
},
"node_modules/spawn-rx": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/spawn-rx/-/spawn-rx-5.0.4.tgz",
"integrity": "sha512-Do11ahkHLlqN9G/J6fs10gdx25BU33NrpkyN3/DFXIIUVojBiJysl12nC0iGUkE+msJAPflzyfpLWWHGHw/6Xg==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/spawn-rx/-/spawn-rx-5.1.0.tgz",
"integrity": "sha512-b4HX44hI0usMiHu6LNaZUVg0BGqHuBcl+81iEhZwhvKHz1efTqD/CHBcUbm/uIe5TARy9pJolxU2NMfh6GuQBA==",
"license": "MIT",
"dependencies": {
"debug": "^4.3.7",
@@ -6268,9 +6300,9 @@
}
},
"node_modules/vite": {
"version": "5.4.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",
"integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==",
"version": "5.4.12",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.12.tgz",
"integrity": "sha512-KwUaKB27TvWwDJr1GjjWthLMATbGEbeWYZIbGZ5qFIsgPP3vWzLu4cVooqhm5/Z2SPDUMjyPVjTztm5tYKwQxA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6916,22 +6948,30 @@
}
},
"node_modules/zod": {
"version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zod-to-json-schema": {
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.1.tgz",
"integrity": "sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==",
"license": "ISC",
"peerDependencies": {
"zod": "^3.24.1"
}
},
"server": {
"name": "@modelcontextprotocol/inspector-server",
"version": "0.2.3",
"version": "0.4.0",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.1",
"@modelcontextprotocol/sdk": "^1.4.1",
"cors": "^2.8.5",
"eventsource": "^2.0.2",
"express": "^4.21.0",
"ws": "^8.18.0",
"zod": "^3.23.8"
@@ -6941,7 +6981,6 @@
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/eventsource": "^1.1.15",
"@types/express": "^4.17.21",
"@types/ws": "^8.5.12",
"tsx": "^4.19.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/inspector",
"version": "0.2.3",
"version": "0.4.0",
"description": "Model Context Protocol inspector",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
@@ -33,14 +33,16 @@
"publish-all": "npm publish --workspaces --access public && npm publish --access public"
},
"dependencies": {
"@modelcontextprotocol/inspector-client": "0.2.3",
"@modelcontextprotocol/inspector-server": "0.2.3",
"@modelcontextprotocol/inspector-client": "0.3.0",
"@modelcontextprotocol/inspector-server": "0.3.0",
"concurrently": "^9.0.1",
"spawn-rx": "^5.0.4",
"shell-quote": "^1.8.2",
"spawn-rx": "^5.1.0",
"ts-node": "^10.9.2"
},
"devDependencies": {
"@types/node": "^22.7.5",
"@types/shell-quote": "^1.7.5",
"prettier": "3.3.3"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/inspector-server",
"version": "0.2.3",
"version": "0.4.0",
"description": "Server-side application for the Model Context Protocol inspector",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
@@ -20,16 +20,14 @@
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/eventsource": "^1.1.15",
"@types/express": "^4.17.21",
"@types/ws": "^8.5.12",
"tsx": "^4.19.0",
"typescript": "^5.6.2"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.1",
"@modelcontextprotocol/sdk": "^1.4.1",
"cors": "^2.8.5",
"eventsource": "^2.0.2",
"express": "^4.21.0",
"ws": "^8.18.0",
"zod": "^3.23.8"

View File

@@ -1,21 +1,28 @@
#!/usr/bin/env node
import cors from "cors";
import EventSource from "eventsource";
import { parseArgs } from "node:util";
import { parse as shellParseArgs } from "shell-quote";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import {
SSEClientTransport,
SseError,
} from "@modelcontextprotocol/sdk/client/sse.js";
import {
StdioClientTransport,
getDefaultEnvironment,
} from "@modelcontextprotocol/sdk/client/stdio.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";
import { findActualExecutable } from "spawn-rx";
import mcpProxy from "./mcpProxy.js";
// Polyfill EventSource for an SSE client in Node.js
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(global as any).EventSource = EventSource;
const SSE_HEADERS_PASSTHROUGH = ["authorization"];
const defaultEnvironment = {
...getDefaultEnvironment(),
...(process.env.MCP_ENV_VARS ? JSON.parse(process.env.MCP_ENV_VARS) : {}),
};
const { values } = parseArgs({
args: process.argv.slice(2),
@@ -30,21 +37,24 @@ app.use(cors());
let webAppTransports: SSEServerTransport[] = [];
const createTransport = async (query: express.Request["query"]) => {
const createTransport = async (req: express.Request) => {
const query = req.query;
console.log("Query parameters:", query);
const transportType = query.transportType as string;
if (transportType === "stdio") {
const command = query.command as string;
const args = (query.args as string).split(/\s+/);
const env = query.env ? JSON.parse(query.env as string) : undefined;
const origArgs = shellParseArgs(query.args as string) as string[];
const queryEnv = query.env ? JSON.parse(query.env as string) : {};
const env = { ...process.env, ...defaultEnvironment, ...queryEnv };
const { cmd, args } = findActualExecutable(command, origArgs);
console.log(`Stdio transport: command=${cmd}, args=${args}`);
console.log(
`Stdio transport: command=${command}, args=${args}, env=${JSON.stringify(env)}`,
);
const transport = new StdioClientTransport({
command,
command: cmd,
args,
env,
stderr: "pipe",
@@ -56,9 +66,26 @@ const createTransport = async (query: express.Request["query"]) => {
return transport;
} else if (transportType === "sse") {
const url = query.url as string;
console.log(`SSE transport: url=${url}`);
const headers: HeadersInit = {};
for (const key of SSE_HEADERS_PASSTHROUGH) {
if (req.headers[key] === undefined) {
continue;
}
const transport = new SSEClientTransport(new URL(url));
const value = req.headers[key];
headers[key] = Array.isArray(value) ? value[value.length - 1] : value;
}
console.log(`SSE transport: url=${url}, headers=${Object.keys(headers)}`);
const transport = new SSEClientTransport(new URL(url), {
eventSourceInit: {
fetch: (url, init) => fetch(url, { ...init, headers }),
},
requestInit: {
headers,
},
});
await transport.start();
console.log("Connected to SSE transport");
@@ -73,7 +100,21 @@ app.get("/sse", async (req, res) => {
try {
console.log("New SSE connection");
const backingServerTransport = await createTransport(req.query);
let backingServerTransport;
try {
backingServerTransport = 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("Connected MCP client to backing server transport");
@@ -100,9 +141,6 @@ app.get("/sse", async (req, res) => {
mcpProxy({
transportToClient: webAppTransport,
transportToServer: backingServerTransport,
onerror: (error) => {
console.error(error);
},
});
console.log("Set up MCP proxy");
@@ -131,8 +169,6 @@ app.post("/message", async (req, res) => {
app.get("/config", (req, res) => {
try {
const defaultEnvironment = getDefaultEnvironment();
res.json({
defaultEnvironment,
defaultCommand: values.env,

View File

@@ -1,23 +1,29 @@
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
function onClientError(error: Error) {
console.error("Error from inspector client:", error);
}
function onServerError(error: Error) {
console.error("Error from MCP server:", error);
}
export default function mcpProxy({
transportToClient,
transportToServer,
onerror,
}: {
transportToClient: Transport;
transportToServer: Transport;
onerror: (error: Error) => void;
}) {
let transportToClientClosed = false;
let transportToServerClosed = false;
transportToClient.onmessage = (message) => {
transportToServer.send(message).catch(onerror);
transportToServer.send(message).catch(onServerError);
};
transportToServer.onmessage = (message) => {
transportToClient.send(message).catch(onerror);
transportToClient.send(message).catch(onClientError);
};
transportToClient.onclose = () => {
@@ -26,7 +32,7 @@ export default function mcpProxy({
}
transportToClientClosed = true;
transportToServer.close().catch(onerror);
transportToServer.close().catch(onServerError);
};
transportToServer.onclose = () => {
@@ -34,10 +40,9 @@ export default function mcpProxy({
return;
}
transportToServerClosed = true;
transportToClient.close().catch(onerror);
transportToClient.close().catch(onClientError);
};
transportToClient.onerror = onerror;
transportToServer.onerror = onerror;
transportToClient.onerror = onClientError;
transportToServer.onerror = onServerError;
}