Compare commits

...

54 Commits
0.2.0 ... 0.3.0

Author SHA1 Message Date
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
=
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
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
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
Ashwin Bhat
3c4cb17d09 bump to 0.2.3 2024-11-26 14:14:19 -05: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
Ani Betts
2b79b6ffd4 Merge pull request #79 from modelcontextprotocol/ani/fix-windows
Fix launch issues on Windows
2024-11-26 18:12:48 +01:00
Ani Betts
1f28b4474c Don't eat the env, add PORT 2024-11-26 18:08:43 +01:00
Anaïs Betts
d69d67cb64 Fix server args 2024-11-26 17:33:22 +01:00
Anaïs Betts
7792070d81 Add debugging 2024-11-26 17:18:33 +01:00
Anaïs Betts
34a2843756 resolve usually better than join 2024-11-26 17:11:32 +01:00
Anaïs Betts
2a34770959 Don't 🔥 the hello message 2024-11-26 17:06:56 +01:00
Anaïs Betts
6b674b0827 Version bump MCP SDK to latest 2024-11-26 17:02:39 +01:00
Anaïs Betts
ca8db1f417 Handle spawning the client and server on Windows using correct paths 2024-11-26 17:02:39 +01:00
Anaïs Betts
eb4456d1e3 Add spawn-rx 2024-11-26 16:52:22 +01:00
Anaïs Betts
780b92274d Make tailwind config work on Windows 2024-11-26 16:51:50 +01:00
ashwin-ant
b825784b8f Merge pull request #75 from modelcontextprotocol/readmeupdate
clarify readme
2024-11-25 14:01:07 -08:00
Ashwin Bhat
52c7e98055 clarify readme 2024-11-25 17:00:04 -05:00
ashwin-ant
4862aa7c1d Merge pull request #74 from simonw/patch-1
Skip the dist/index.js bit
2024-11-25 13:58:02 -08:00
Simon Willison
561ea91504 Skip the dist/index.js bit
The `dist/index.js` bit is confusing. Running without that gives you a working web UI.
2024-11-25 10:26:35 -08:00
Ashwin Bhat
7c2be8d139 bump version to 0.2.2 2024-11-25 10:35:26 -05:00
ashwin-ant
97d469911e Merge pull request #73 from modelcontextprotocol/ashwin/darkmodetoggle
make theme selectable in UI, store setting in localstorage
2024-11-25 06:24:59 -08:00
Ashwin Bhat
11b891c6ca make theme selectable in UI 2024-11-25 09:23:26 -05:00
Justin Spahr-Summers
5139e723a4 Bump package versions 2024-11-25 07:20:05 -06:00
Justin Spahr-Summers
fc5b79c9a6 Merge pull request #72 from modelcontextprotocol/ashwin/darkmodetool
more dark mode fixes
2024-11-24 16:37:52 -06:00
Ashwin Bhat
47a87e1884 more dark mode fixes 2024-11-24 14:26:39 -08:00
20 changed files with 2054 additions and 810 deletions

View File

@@ -2,18 +2,49 @@
The MCP inspector is a developer tool for testing and debugging MCP servers. The MCP inspector is a developer tool for testing and debugging MCP servers.
It can be run easily from `npx`. For example, in a folder where there's a built JavaScript server at `build/index.js`: ![MCP Inspector Screenshot](mcp-inspector.png)
``` ## Running the Inspector
### 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`:
```bash
npx @modelcontextprotocol/inspector build/index.js npx @modelcontextprotocol/inspector build/index.js
``` ```
You can also pass arguments along to the server: You can also pass arguments along which will get passed as arguments to your MCP server:
``` ```
npx @modelcontextprotocol/inspector build/index.js arg1 arg2 ... npx @modelcontextprotocol/inspector build/index.js arg1 arg2 ...
``` ```
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:
```bash
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).
### From this repository
If you're working on the inspector itself:
Development mode:
```bash
npm run dev
```
Production mode:
```bash
npm run build
npm start
```
## License ## License
This project is licensed under the MIT License—see the [LICENSE](LICENSE) file for details. This project is licensed under the MIT License—see the [LICENSE](LICENSE) file for details.

View File

@@ -1,65 +1,88 @@
#!/usr/bin/env node #!/usr/bin/env node
import { join, dirname } from "path"; import { resolve, dirname } from "path";
import { spawnPromise } from "spawn-rx";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import concurrently from "concurrently";
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
// Get command line arguments function delay(ms) {
const [, , command, ...mcpServerArgs] = process.argv; return new Promise((resolve) => setTimeout(resolve, ms));
const inspectorServerPath = join(__dirname, "../server/build/index.js");
// Path to the client entry point
const inspectorClientPath = join(__dirname, "../client/bin/cli.js");
console.log("Starting MCP inspector...");
function escapeArg(arg) {
if (arg.includes(" ") || arg.includes("'") || arg.includes('"')) {
return `\\"${arg.replace(/"/g, '\\\\\\"')}\\"`;
}
return arg;
} }
const serverCommand = [ async function main() {
`node`, // Get command line arguments
inspectorServerPath, const [, , command, ...mcpServerArgs] = process.argv;
command ? `--env ${escapeArg(command)}` : "",
mcpServerArgs.length
? `--args="${mcpServerArgs.map(escapeArg).join(" ")}"`
: "",
]
.filter(Boolean)
.join(" ");
const CLIENT_PORT = process.env.CLIENT_PORT ?? ""; const inspectorServerPath = resolve(
const SERVER_PORT = process.env.SERVER_PORT ?? ""; __dirname,
"..",
"server",
"build",
"index.js",
);
const { result } = concurrently( // Path to the client entry point
[ const inspectorClientPath = resolve(
__dirname,
"..",
"client",
"bin",
"cli.js",
);
const CLIENT_PORT = process.env.CLIENT_PORT ?? "5173";
const SERVER_PORT = process.env.SERVER_PORT ?? "3000";
console.log("Starting MCP inspector...");
const abort = new AbortController();
let cancelled = false;
process.on("SIGINT", () => {
cancelled = true;
abort.abort();
});
const server = spawnPromise(
"node",
[
inspectorServerPath,
...(command ? [`--env`, command] : []),
...(mcpServerArgs ? [`--args=${mcpServerArgs.join(" ")}`] : []),
],
{ {
command: `PORT=${SERVER_PORT} ${serverCommand}`, env: { ...process.env, PORT: SERVER_PORT },
name: "server", signal: abort.signal,
echoOutput: true,
}, },
{ );
command: `PORT=${CLIENT_PORT} node ${inspectorClientPath}`,
name: "client",
},
],
{
prefix: "name",
killOthers: ["failure", "success"],
restartTries: 3,
},
);
console.log( const client = spawnPromise("node", [inspectorClientPath], {
`\n🔍 MCP Inspector is up and running at http://localhost:${CLIENT_PORT || 5173} 🚀`, env: { ...process.env, PORT: CLIENT_PORT },
); signal: abort.signal,
echoOutput: true,
});
result.catch((err) => { // Make sure our server/client didn't immediately fail
console.error("An error occurred:", err); await Promise.any([server, client, delay(2 * 1000)]);
process.exit(1); const portParam = SERVER_PORT === "3000" ? "" : `?proxyPort=${SERVER_PORT}`;
}); console.log(
`\n🔍 MCP Inspector is up and running at http://localhost:${CLIENT_PORT}${portParam} 🚀`,
);
try {
await Promise.any([server, client]);
} catch (e) {
if (!cancelled || process.env.DEBUG) throw e;
}
return 0;
}
main()
.then((_) => process.exit(0))
.catch((e) => {
console.error(e);
process.exit(1);
});

View File

@@ -1,6 +1,6 @@
{ {
"name": "@modelcontextprotocol/inspector-client", "name": "@modelcontextprotocol/inspector-client",
"version": "0.2.0", "version": "0.3.0",
"description": "Client-side application for the Model Context Protocol inspector", "description": "Client-side application for the Model Context Protocol inspector",
"license": "MIT", "license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)", "author": "Anthropic, PBC (https://anthropic.com)",
@@ -21,7 +21,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "0.7.0", "@modelcontextprotocol/sdk": "^1.0.3",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0", "@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-select": "^2.1.2", "@radix-ui/react-select": "^2.1.2",

View File

@@ -29,7 +29,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { import {
Notification, Notification,
StdErrNotification, StdErrNotification,
StdErrNotificationSchema StdErrNotificationSchema,
} from "./lib/notificationTypes"; } from "./lib/notificationTypes";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
@@ -54,10 +54,14 @@ import RootsTab from "./components/RootsTab";
import SamplingTab, { PendingRequest } from "./components/SamplingTab"; import SamplingTab, { PendingRequest } from "./components/SamplingTab";
import Sidebar from "./components/Sidebar"; import Sidebar from "./components/Sidebar";
import ToolsTab from "./components/ToolsTab"; import ToolsTab from "./components/ToolsTab";
import useDarkModeSync from "./lib/useDarkModeSync";
const DEFAULT_REQUEST_TIMEOUT_MSEC = 10000; const DEFAULT_REQUEST_TIMEOUT_MSEC = 10000;
const params = new URLSearchParams(window.location.search);
const PROXY_PORT = params.get("proxyPort") ?? "3000";
const REQUEST_TIMEOUT = parseInt(params.get("timeout") ?? "") || DEFAULT_REQUEST_TIMEOUT_MSEC;
const PROXY_SERVER_URL = `http://localhost:${PROXY_PORT}`;
const App = () => { const App = () => {
const [connectionStatus, setConnectionStatus] = useState< const [connectionStatus, setConnectionStatus] = useState<
"disconnected" | "connected" | "error" "disconnected" | "connected" | "error"
@@ -83,7 +87,8 @@ const App = () => {
const [args, setArgs] = useState<string>(() => { const [args, setArgs] = useState<string>(() => {
return localStorage.getItem("lastArgs") || ""; return localStorage.getItem("lastArgs") || "";
}); });
const [url, setUrl] = useState<string>("http://localhost:3001/sse");
const [sseUrl, setSseUrl] = useState<string>("http://localhost:3001/sse");
const [transportType, setTransportType] = useState<"stdio" | "sse">("stdio"); const [transportType, setTransportType] = useState<"stdio" | "sse">("stdio");
const [requestHistory, setRequestHistory] = useState< const [requestHistory, setRequestHistory] = useState<
{ request: string; response?: string }[] { request: string; response?: string }[]
@@ -144,8 +149,6 @@ const App = () => {
const dragStartY = useRef<number>(0); const dragStartY = useRef<number>(0);
const dragStartHeight = useRef<number>(0); const dragStartHeight = useRef<number>(0);
useDarkModeSync();
const handleDragStart = useCallback( const handleDragStart = useCallback(
(e: React.MouseEvent) => { (e: React.MouseEvent) => {
setIsDragging(true); setIsDragging(true);
@@ -194,7 +197,7 @@ const App = () => {
}, [args]); }, [args]);
useEffect(() => { useEffect(() => {
fetch("http://localhost:3000/config") fetch(`${PROXY_SERVER_URL}/config`)
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { .then((data) => {
setEnv(data.defaultEnvironment); setEnv(data.defaultEnvironment);
@@ -241,7 +244,7 @@ const App = () => {
const abortController = new AbortController(); const abortController = new AbortController();
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
abortController.abort("Request timed out"); abortController.abort("Request timed out");
}, DEFAULT_REQUEST_TIMEOUT_MSEC); }, REQUEST_TIMEOUT);
let response; let response;
try { try {
@@ -407,7 +410,7 @@ const App = () => {
}, },
); );
const backendUrl = new URL("http://localhost:3000/sse"); const backendUrl = new URL(`${PROXY_SERVER_URL}/sse`);
backendUrl.searchParams.append("transportType", transportType); backendUrl.searchParams.append("transportType", transportType);
if (transportType === "stdio") { if (transportType === "stdio") {
@@ -415,7 +418,7 @@ const App = () => {
backendUrl.searchParams.append("args", args); backendUrl.searchParams.append("args", args);
backendUrl.searchParams.append("env", JSON.stringify(env)); backendUrl.searchParams.append("env", JSON.stringify(env));
} else { } else {
backendUrl.searchParams.append("url", url); backendUrl.searchParams.append("url", sseUrl);
} }
const clientTransport = new SSEClientTransport(backendUrl); const clientTransport = new SSEClientTransport(backendUrl);
@@ -472,8 +475,8 @@ const App = () => {
setCommand={setCommand} setCommand={setCommand}
args={args} args={args}
setArgs={setArgs} setArgs={setArgs}
url={url} sseUrl={sseUrl}
setUrl={setUrl} setSseUrl={setSseUrl}
env={env} env={env}
setEnv={setEnv} setEnv={setEnv}
onConnect={connectMcpServer} onConnect={connectMcpServer}
@@ -523,10 +526,18 @@ const App = () => {
clearError("resources"); clearError("resources");
listResources(); listResources();
}} }}
clearResources={() => {
setResources([]);
setNextResourceCursor(undefined);
}}
listResourceTemplates={() => { listResourceTemplates={() => {
clearError("resources"); clearError("resources");
listResourceTemplates(); listResourceTemplates();
}} }}
clearResourceTemplates={() => {
setResourceTemplates([]);
setNextResourceTemplateCursor(undefined);
}}
readResource={(uri) => { readResource={(uri) => {
clearError("resources"); clearError("resources");
readResource(uri); readResource(uri);
@@ -547,6 +558,10 @@ const App = () => {
clearError("prompts"); clearError("prompts");
listPrompts(); listPrompts();
}} }}
clearPrompts={() => {
setPrompts([]);
setNextPromptCursor(undefined);
}}
getPrompt={(name, args) => { getPrompt={(name, args) => {
clearError("prompts"); clearError("prompts");
getPrompt(name, args); getPrompt(name, args);
@@ -566,6 +581,10 @@ const App = () => {
clearError("tools"); clearError("tools");
listTools(); listTools();
}} }}
clearTools={() => {
setTools([]);
setNextToolCursor(undefined);
}}
callTool={(name, params) => { callTool={(name, params) => {
clearError("tools"); clearError("tools");
callTool(name, params); callTool(name, params);

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { Play, ChevronDown, ChevronRight } from "lucide-react";
import { Play, ChevronDown, ChevronRight, Settings } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { import {
@@ -12,6 +11,9 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { StdErrNotification } from "@/lib/notificationTypes"; import { StdErrNotification } from "@/lib/notificationTypes";
import useTheme from "../lib/useTheme";
import { version } from "../../../package.json";
interface SidebarProps { interface SidebarProps {
connectionStatus: "disconnected" | "connected" | "error"; connectionStatus: "disconnected" | "connected" | "error";
transportType: "stdio" | "sse"; transportType: "stdio" | "sse";
@@ -20,8 +22,8 @@ interface SidebarProps {
setCommand: (command: string) => void; setCommand: (command: string) => void;
args: string; args: string;
setArgs: (args: string) => void; setArgs: (args: string) => void;
url: string; sseUrl: string;
setUrl: (url: string) => void; setSseUrl: (url: string) => void;
env: Record<string, string>; env: Record<string, string>;
setEnv: (env: Record<string, string>) => void; setEnv: (env: Record<string, string>) => void;
onConnect: () => void; onConnect: () => void;
@@ -36,20 +38,24 @@ const Sidebar = ({
setCommand, setCommand,
args, args,
setArgs, setArgs,
url, sseUrl,
setUrl, setSseUrl,
env, env,
setEnv, setEnv,
onConnect, onConnect,
stdErrNotifications, stdErrNotifications,
}: SidebarProps) => { }: SidebarProps) => {
const [theme, setTheme] = useTheme();
const [showEnvVars, setShowEnvVars] = useState(false); const [showEnvVars, setShowEnvVars] = useState(false);
return ( return (
<div className="w-80 bg-card border-r border-border flex flex-col h-full"> <div className="w-80 bg-card border-r border-border flex flex-col h-full">
<div className="flex items-center p-4 border-b border-gray-200"> <div className="flex items-center justify-between p-4 border-b border-gray-200">
<Settings className="w-6 h-6 text-gray-500" /> <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> </div>
<div className="p-4 flex-1 overflow-auto"> <div className="p-4 flex-1 overflow-auto">
@@ -71,6 +77,7 @@ const Sidebar = ({
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
{transportType === "stdio" ? ( {transportType === "stdio" ? (
<> <>
<div className="space-y-2"> <div className="space-y-2">
@@ -95,8 +102,8 @@ const Sidebar = ({
<label className="text-sm font-medium">URL</label> <label className="text-sm font-medium">URL</label>
<Input <Input
placeholder="URL" placeholder="URL"
value={url} value={sseUrl}
onChange={(e) => setUrl(e.target.value)} onChange={(e) => setSseUrl(e.target.value)}
/> />
</div> </div>
)} )}
@@ -212,6 +219,25 @@ const Sidebar = ({
</div> </div>
</div> </div>
</div> </div>
<div className="p-4 border-t">
<div className="flex items-center space-x-2">
<Select
value={theme}
onValueChange={(value: string) =>
setTheme(value as "system" | "light" | "dark")
}
>
<SelectTrigger className="w-[120px]" id="theme-select">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="system">System</SelectItem>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div> </div>
); );
}; };

View File

@@ -5,9 +5,9 @@ import { Label } from "@/components/ui/label";
import { TabsContent } from "@/components/ui/tabs"; import { TabsContent } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { import {
CallToolResult,
ListToolsResult, ListToolsResult,
Tool, Tool,
CallToolResultSchema,
} from "@modelcontextprotocol/sdk/types.js"; } from "@modelcontextprotocol/sdk/types.js";
import { AlertCircle, Send } from "lucide-react"; import { AlertCircle, Send } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
@@ -18,6 +18,7 @@ import { CompatibilityCallToolResult } from "@modelcontextprotocol/sdk/types.js"
const ToolsTab = ({ const ToolsTab = ({
tools, tools,
listTools, listTools,
clearTools,
callTool, callTool,
selectedTool, selectedTool,
setSelectedTool, setSelectedTool,
@@ -27,6 +28,7 @@ const ToolsTab = ({
}: { }: {
tools: Tool[]; tools: Tool[];
listTools: () => void; listTools: () => void;
clearTools: () => void;
callTool: (name: string, params: Record<string, unknown>) => void; callTool: (name: string, params: Record<string, unknown>) => void;
selectedTool: Tool | null; selectedTool: Tool | null;
setSelectedTool: (tool: Tool) => void; setSelectedTool: (tool: Tool) => void;
@@ -40,7 +42,27 @@ const ToolsTab = ({
if (!toolResult) return null; if (!toolResult) return null;
if ("content" in toolResult) { 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; const isError = structuredResult.isError ?? false;
return ( return (
@@ -51,7 +73,7 @@ const ToolsTab = ({
{structuredResult.content.map((item, index) => ( {structuredResult.content.map((item, index) => (
<div key={index} className="mb-2"> <div key={index} className="mb-2">
{item.type === "text" && ( {item.type === "text" && (
<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">
{item.text} {item.text}
</pre> </pre>
)} )}
@@ -63,7 +85,7 @@ const ToolsTab = ({
/> />
)} )}
{item.type === "resource" && ( {item.type === "resource" && (
<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 whitespace-pre-wrap break-words p-4 rounded text-sm overflow-auto max-h-64">
{JSON.stringify(item.resource, null, 2)} {JSON.stringify(item.resource, null, 2)}
</pre> </pre>
)} )}
@@ -88,6 +110,7 @@ const ToolsTab = ({
<ListPane <ListPane
items={tools} items={tools}
listItems={listTools} listItems={listTools}
clearItems={clearTools}
setSelectedItem={setSelectedTool} setSelectedItem={setSelectedTool}
renderItem={(tool) => ( renderItem={(tool) => (
<> <>

View File

@@ -27,7 +27,7 @@ const TabsTrigger = React.forwardRef<
<TabsPrimitive.Trigger <TabsPrimitive.Trigger
ref={ref} ref={ref}
className={cn( className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow", "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-muted data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className, className,
)} )}
{...props} {...props}

View File

@@ -1,29 +0,0 @@
import { useEffect } from "react";
// Listen for changes to the user's preferred color scheme
const useDarkModeSync = () => {
useEffect(() => {
const darkModeMediaQuery = window.matchMedia(
"(prefers-color-scheme: dark)",
);
const handleDarkModeChange = (e: MediaQueryListEvent) => {
if (e.matches) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
};
if (darkModeMediaQuery.matches) {
document.documentElement.classList.add("dark");
}
darkModeMediaQuery.addEventListener("change", handleDarkModeChange);
return () => {
darkModeMediaQuery.removeEventListener("change", handleDarkModeChange);
};
}, []);
};
export default useDarkModeSync;

View File

@@ -0,0 +1,51 @@
import { useCallback, useEffect, useState } from "react";
type Theme = "light" | "dark" | "system";
const useTheme = (): [Theme, (mode: Theme) => void] => {
const [theme, setTheme] = useState<Theme>(() => {
const savedTheme = localStorage.getItem("theme") as Theme;
return savedTheme || "system";
});
useEffect(() => {
const darkModeMediaQuery = window.matchMedia(
"(prefers-color-scheme: dark)",
);
const handleDarkModeChange = (e: MediaQueryListEvent) => {
if (theme === "system") {
updateDocumentTheme(e.matches ? "dark" : "light");
}
};
const updateDocumentTheme = (newTheme: "light" | "dark") => {
document.documentElement.classList.toggle("dark", newTheme === "dark");
};
// Set initial theme based on current mode
if (theme === "system") {
updateDocumentTheme(darkModeMediaQuery.matches ? "dark" : "light");
} else {
updateDocumentTheme(theme);
}
darkModeMediaQuery.addEventListener("change", handleDarkModeChange);
return () => {
darkModeMediaQuery.removeEventListener("change", handleDarkModeChange);
};
}, [theme]);
return [
theme,
useCallback((newTheme: Theme) => {
setTheme(newTheme);
localStorage.setItem("theme", newTheme);
if (newTheme !== "system") {
document.documentElement.classList.toggle("dark", newTheme === "dark");
}
}, []),
];
};
export default useTheme;

View File

@@ -1,4 +1,5 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
import animate from "tailwindcss-animate";
export default { export default {
darkMode: ["class"], darkMode: ["class"],
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
@@ -53,5 +54,5 @@ export default {
}, },
}, },
}, },
plugins: [require("tailwindcss-animate")], plugins: [animate],
}; };

View File

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

View File

@@ -10,4 +10,12 @@ export default defineConfig({
"@": path.resolve(__dirname, "./src"), "@": path.resolve(__dirname, "./src"),
}, },
}, },
build: {
minify: false,
rollupOptions: {
output: {
manualChunks: undefined
}
}
}
}); });

BIN
mcp-inspector.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

2424
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "@modelcontextprotocol/inspector", "name": "@modelcontextprotocol/inspector",
"version": "0.2.0", "version": "0.3.0",
"description": "Model Context Protocol inspector", "description": "Model Context Protocol inspector",
"license": "MIT", "license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)", "author": "Anthropic, PBC (https://anthropic.com)",
@@ -27,18 +27,20 @@
"build": "npm run build-server && npm run build-client", "build": "npm run build-server && npm run build-client",
"start-server": "cd server && npm run start", "start-server": "cd server && npm run start",
"start-client": "cd client && npm run preview", "start-client": "cd client && npm run preview",
"start": "./bin/cli.js", "start": "node ./bin/cli.js",
"prepare": "npm run build", "prepare": "npm run build",
"prettier-fix": "prettier --write .", "prettier-fix": "prettier --write .",
"publish-all": "npm publish --workspaces --access public && npm publish --access public" "publish-all": "npm publish --workspaces --access public && npm publish --access public"
}, },
"dependencies": { "dependencies": {
"@modelcontextprotocol/inspector-client": "0.2.0", "@modelcontextprotocol/inspector-client": "0.3.0",
"@modelcontextprotocol/inspector-server": "0.2.0", "@modelcontextprotocol/inspector-server": "0.3.0",
"concurrently": "^9.0.1" "concurrently": "^9.0.1",
"spawn-rx": "^5.1.0",
"ts-node": "^10.9.2"
}, },
"devDependencies": { "devDependencies": {
"prettier": "3.3.3", "@types/node": "^22.7.5",
"@types/node": "^22.7.5" "prettier": "3.3.3"
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@modelcontextprotocol/inspector-server", "name": "@modelcontextprotocol/inspector-server",
"version": "0.2.0", "version": "0.3.0",
"description": "Server-side application for the Model Context Protocol inspector", "description": "Server-side application for the Model Context Protocol inspector",
"license": "MIT", "license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)", "author": "Anthropic, PBC (https://anthropic.com)",
@@ -27,7 +27,7 @@
"typescript": "^5.6.2" "typescript": "^5.6.2"
}, },
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "0.7.0", "@modelcontextprotocol/sdk": "^1.0.3",
"cors": "^2.8.5", "cors": "^2.8.5",
"eventsource": "^2.0.2", "eventsource": "^2.0.2",
"express": "^4.21.0", "express": "^4.21.0",

View File

@@ -12,6 +12,7 @@ import {
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express"; import express from "express";
import mcpProxy from "./mcpProxy.js"; import mcpProxy from "./mcpProxy.js";
import { findActualExecutable } from "spawn-rx";
// Polyfill EventSource for an SSE client in Node.js // Polyfill EventSource for an SSE client in Node.js
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -37,25 +38,33 @@ const createTransport = async (query: express.Request["query"]) => {
if (transportType === "stdio") { if (transportType === "stdio") {
const command = query.command as string; const command = query.command as string;
const args = (query.args as string).split(/\s+/); const origArgs = (query.args as string).split(/\s+/);
const env = query.env ? JSON.parse(query.env as string) : undefined; const env = query.env ? JSON.parse(query.env as string) : undefined;
const { cmd, args } = findActualExecutable(command, origArgs);
console.log( console.log(
`Stdio transport: command=${command}, args=${args}, env=${JSON.stringify(env)}`, `Stdio transport: command=${cmd}, args=${args}, env=${JSON.stringify(env)}`,
); );
const transport = new StdioClientTransport({ const transport = new StdioClientTransport({
command, command: cmd,
args, args,
env, env,
stderr: "pipe", stderr: "pipe",
}); });
await transport.start(); await transport.start();
console.log("Spawned stdio transport"); console.log("Spawned stdio transport");
return transport; return transport;
} else if (transportType === "sse") { } else if (transportType === "sse") {
const url = query.url as string; const url = query.url as string;
console.log(`SSE transport: url=${url}`); console.log(`SSE transport: url=${url}`);
const transport = new SSEClientTransport(new URL(url)); const transport = new SSEClientTransport(new URL(url));
await transport.start(); await transport.start();
console.log("Connected to SSE transport"); console.log("Connected to SSE transport");
return transport; return transport;
} else { } else {
@@ -99,6 +108,7 @@ app.get("/sse", async (req, res) => {
console.error(error); console.error(error);
}, },
}); });
console.log("Set up MCP proxy"); console.log("Set up MCP proxy");
} catch (error) { } catch (error) {
console.error("Error in /sse route:", error); console.error("Error in /sse route:", error);
@@ -126,6 +136,7 @@ app.post("/message", async (req, res) => {
app.get("/config", (req, res) => { app.get("/config", (req, res) => {
try { try {
const defaultEnvironment = getDefaultEnvironment(); const defaultEnvironment = getDefaultEnvironment();
res.json({ res.json({
defaultEnvironment, defaultEnvironment,
defaultCommand: values.env, defaultCommand: values.env,
@@ -138,4 +149,4 @@ app.get("/config", (req, res) => {
}); });
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
app.listen(PORT, () => { }); app.listen(PORT, () => {});