Compare commits
28 Commits
0.2.2
...
ashwin/ver
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f62066d34 | ||
|
|
c770d217e7 | ||
|
|
98470a12f9 | ||
|
|
a00564fafa | ||
|
|
62546dec58 | ||
|
|
886ac5fc7b | ||
|
|
722df4d798 | ||
|
|
407e304585 | ||
|
|
60578314aa | ||
|
|
3c4cb17d09 | ||
|
|
fbac5b78bc | ||
|
|
f876b1ec0d | ||
|
|
aecfa21d47 | ||
|
|
a3d542c0a3 | ||
|
|
2b79b6ffd4 | ||
|
|
1f28b4474c | ||
|
|
d69d67cb64 | ||
|
|
7792070d81 | ||
|
|
34a2843756 | ||
|
|
2a34770959 | ||
|
|
6b674b0827 | ||
|
|
ca8db1f417 | ||
|
|
eb4456d1e3 | ||
|
|
780b92274d | ||
|
|
b825784b8f | ||
|
|
52c7e98055 | ||
|
|
4862aa7c1d | ||
|
|
561ea91504 |
35
README.md
35
README.md
@@ -2,18 +2,47 @@
|
||||
|
||||
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`:
|
||||

|
||||
|
||||
```
|
||||
## 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
|
||||
```
|
||||
|
||||
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 ...
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
This project is licensed under the MIT License—see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
125
bin/cli.js
125
bin/cli.js
@@ -1,65 +1,88 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { join, dirname } from "path";
|
||||
import { resolve, dirname } from "path";
|
||||
import { spawnPromise } from "spawn-rx";
|
||||
import { fileURLToPath } from "url";
|
||||
import concurrently from "concurrently";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Get command line arguments
|
||||
const [, , command, ...mcpServerArgs] = process.argv;
|
||||
|
||||
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;
|
||||
function delay(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
const serverCommand = [
|
||||
`node`,
|
||||
inspectorServerPath,
|
||||
command ? `--env ${escapeArg(command)}` : "",
|
||||
mcpServerArgs.length
|
||||
? `--args="${mcpServerArgs.map(escapeArg).join(" ")}"`
|
||||
: "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
async function main() {
|
||||
// Get command line arguments
|
||||
const [, , command, ...mcpServerArgs] = process.argv;
|
||||
|
||||
const CLIENT_PORT = process.env.CLIENT_PORT ?? "";
|
||||
const SERVER_PORT = process.env.SERVER_PORT ?? "";
|
||||
const inspectorServerPath = resolve(
|
||||
__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}`,
|
||||
name: "server",
|
||||
env: { ...process.env, PORT: SERVER_PORT },
|
||||
signal: abort.signal,
|
||||
echoOutput: true,
|
||||
},
|
||||
{
|
||||
command: `PORT=${CLIENT_PORT} node ${inspectorClientPath}`,
|
||||
name: "client",
|
||||
},
|
||||
],
|
||||
{
|
||||
prefix: "name",
|
||||
killOthers: ["failure", "success"],
|
||||
restartTries: 3,
|
||||
},
|
||||
);
|
||||
);
|
||||
|
||||
console.log(
|
||||
`\n🔍 MCP Inspector is up and running at http://localhost:${CLIENT_PORT || 5173} 🚀`,
|
||||
);
|
||||
const client = spawnPromise("node", [inspectorClientPath], {
|
||||
env: { ...process.env, PORT: CLIENT_PORT },
|
||||
signal: abort.signal,
|
||||
echoOutput: true,
|
||||
});
|
||||
|
||||
result.catch((err) => {
|
||||
console.error("An error occurred:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
// 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}${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);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@modelcontextprotocol/inspector-client",
|
||||
"version": "0.2.2",
|
||||
"version": "0.2.4",
|
||||
"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": "0.7.0",
|
||||
"@modelcontextprotocol/sdk": "^1.0.1",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-select": "^2.1.2",
|
||||
|
||||
@@ -57,6 +57,10 @@ 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"
|
||||
@@ -82,7 +86,8 @@ const App = () => {
|
||||
const [args, setArgs] = useState<string>(() => {
|
||||
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 [requestHistory, setRequestHistory] = useState<
|
||||
{ request: string; response?: string }[]
|
||||
@@ -191,7 +196,7 @@ const App = () => {
|
||||
}, [args]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("http://localhost:3000/config")
|
||||
fetch(`${PROXY_SERVER_URL}/config`)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
setEnv(data.defaultEnvironment);
|
||||
@@ -404,7 +409,7 @@ const App = () => {
|
||||
},
|
||||
);
|
||||
|
||||
const backendUrl = new URL("http://localhost:3000/sse");
|
||||
const backendUrl = new URL(`${PROXY_SERVER_URL}/sse`);
|
||||
|
||||
backendUrl.searchParams.append("transportType", transportType);
|
||||
if (transportType === "stdio") {
|
||||
@@ -412,7 +417,7 @@ const App = () => {
|
||||
backendUrl.searchParams.append("args", args);
|
||||
backendUrl.searchParams.append("env", JSON.stringify(env));
|
||||
} else {
|
||||
backendUrl.searchParams.append("url", url);
|
||||
backendUrl.searchParams.append("url", sseUrl);
|
||||
}
|
||||
|
||||
const clientTransport = new SSEClientTransport(backendUrl);
|
||||
@@ -469,8 +474,8 @@ const App = () => {
|
||||
setCommand={setCommand}
|
||||
args={args}
|
||||
setArgs={setArgs}
|
||||
url={url}
|
||||
setUrl={setUrl}
|
||||
sseUrl={sseUrl}
|
||||
setSseUrl={setSseUrl}
|
||||
env={env}
|
||||
setEnv={setEnv}
|
||||
onConnect={connectMcpServer}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { Play, ChevronDown, ChevronRight } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -13,6 +12,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 +22,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 +38,8 @@ const Sidebar = ({
|
||||
setCommand,
|
||||
args,
|
||||
setArgs,
|
||||
url,
|
||||
setUrl,
|
||||
sseUrl,
|
||||
setSseUrl,
|
||||
env,
|
||||
setEnv,
|
||||
onConnect,
|
||||
@@ -52,7 +52,9 @@ const Sidebar = ({
|
||||
<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 +77,7 @@ const Sidebar = ({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{transportType === "stdio" ? (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
@@ -99,8 +102,8 @@ 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)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -5,9 +5,9 @@ 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";
|
||||
@@ -40,7 +40,27 @@ const ToolsTab = ({
|
||||
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 (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
import animate from "tailwindcss-animate";
|
||||
export default {
|
||||
darkMode: ["class"],
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
@@ -53,5 +54,5 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
plugins: [animate],
|
||||
};
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
@@ -10,4 +10,12 @@ export default defineConfig({
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
minify: false,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
BIN
mcp-inspector.png
Normal file
BIN
mcp-inspector.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 294 KiB |
2425
package-lock.json
generated
2425
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@modelcontextprotocol/inspector",
|
||||
"version": "0.2.2",
|
||||
"version": "0.2.4",
|
||||
"description": "Model Context Protocol inspector",
|
||||
"license": "MIT",
|
||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||
@@ -27,18 +27,20 @@
|
||||
"build": "npm run build-server && npm run build-client",
|
||||
"start-server": "cd server && npm run start",
|
||||
"start-client": "cd client && npm run preview",
|
||||
"start": "./bin/cli.js",
|
||||
"start": "node ./bin/cli.js",
|
||||
"prepare": "npm run build",
|
||||
"prettier-fix": "prettier --write .",
|
||||
"publish-all": "npm publish --workspaces --access public && npm publish --access public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/inspector-client": "0.2.1",
|
||||
"@modelcontextprotocol/inspector-server": "0.2.1",
|
||||
"concurrently": "^9.0.1"
|
||||
"@modelcontextprotocol/inspector-client": "0.2.4",
|
||||
"@modelcontextprotocol/inspector-server": "0.2.4",
|
||||
"concurrently": "^9.0.1",
|
||||
"spawn-rx": "^5.1.0",
|
||||
"ts-node": "^10.9.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "3.3.3",
|
||||
"@types/node": "^22.7.5"
|
||||
"@types/node": "^22.7.5",
|
||||
"prettier": "3.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@modelcontextprotocol/inspector-server",
|
||||
"version": "0.2.2",
|
||||
"version": "0.2.4",
|
||||
"description": "Server-side application for the Model Context Protocol inspector",
|
||||
"license": "MIT",
|
||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||
@@ -27,7 +27,7 @@
|
||||
"typescript": "^5.6.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "0.7.0",
|
||||
"@modelcontextprotocol/sdk": "^1.0.1",
|
||||
"cors": "^2.8.5",
|
||||
"eventsource": "^2.0.2",
|
||||
"express": "^4.21.0",
|
||||
|
||||
@@ -39,6 +39,7 @@ const createTransport = async (query: express.Request["query"]) => {
|
||||
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;
|
||||
|
||||
console.log(
|
||||
`Stdio transport: command=${command}, args=${args}, env=${JSON.stringify(env)}`,
|
||||
);
|
||||
@@ -48,14 +49,18 @@ const createTransport = async (query: express.Request["query"]) => {
|
||||
env,
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
await transport.start();
|
||||
|
||||
console.log("Spawned stdio transport");
|
||||
return transport;
|
||||
} else if (transportType === "sse") {
|
||||
const url = query.url as string;
|
||||
console.log(`SSE transport: url=${url}`);
|
||||
|
||||
const transport = new SSEClientTransport(new URL(url));
|
||||
await transport.start();
|
||||
|
||||
console.log("Connected to SSE transport");
|
||||
return transport;
|
||||
} else {
|
||||
@@ -99,6 +104,7 @@ app.get("/sse", async (req, res) => {
|
||||
console.error(error);
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Set up MCP proxy");
|
||||
} catch (error) {
|
||||
console.error("Error in /sse route:", error);
|
||||
@@ -126,6 +132,7 @@ app.post("/message", async (req, res) => {
|
||||
app.get("/config", (req, res) => {
|
||||
try {
|
||||
const defaultEnvironment = getDefaultEnvironment();
|
||||
|
||||
res.json({
|
||||
defaultEnvironment,
|
||||
defaultCommand: values.env,
|
||||
@@ -138,4 +145,4 @@ app.get("/config", (req, res) => {
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
app.listen(PORT, () => { });
|
||||
app.listen(PORT, () => {});
|
||||
|
||||
Reference in New Issue
Block a user