Compare commits

...

28 Commits

Author SHA1 Message Date
Justin Spahr-Summers
6fdb2903a4 Merge pull request #47 from modelcontextprotocol/justin/pull-from-npm
Pull SDK from npm
2024-11-11 16:33:22 +00:00
Justin Spahr-Summers
b9d6695fbf Revert "Try restoring npm ci"
This reverts commit 3789ef984a.
2024-11-11 16:31:24 +00:00
Justin Spahr-Summers
3789ef984a Try restoring npm ci 2024-11-11 16:29:09 +00:00
Justin Spahr-Summers
e1aa419332 Pull SDK from npm 2024-11-11 16:28:51 +00:00
David Soria Parra
94e4e92618 Merge pull request #45 from modelcontextprotocol/davidsp/fix-capture 2024-11-11 12:33:27 +00:00
Justin Spahr-Summers
148e84388a Merge pull request #46 from modelcontextprotocol/justin/oss-prep
Add license and package metadata
2024-11-11 12:22:32 +00:00
Justin Spahr-Summers
6147640656 Remove client prepare script 2024-11-11 12:19:14 +00:00
Justin Spahr-Summers
1ddd2d7aaa Disallow publishing root package for now 2024-11-11 12:15:52 +00:00
Justin Spahr-Summers
94ddfed1ac Add concurrently dependency 2024-11-11 11:24:21 +00:00
Justin Spahr-Summers
448910d986 Make nested packages private, just install root package 2024-11-11 11:20:53 +00:00
Justin Spahr-Summers
3a9b08bd37 Pick non-conflicting default client port 2024-11-11 11:11:27 +00:00
Justin Spahr-Summers
bce3a7b8d6 Make root package installable/executable 2024-11-11 11:10:42 +00:00
Justin Spahr-Summers
196f2f801d Use serve to create npm package for the client 2024-11-11 11:04:42 +00:00
Justin Spahr-Summers
a0d8ec1e7e Make server executable on its own 2024-11-11 10:41:07 +00:00
Justin Spahr-Summers
6759c461f9 Add license and more package metadata 2024-11-11 10:37:09 +00:00
David Soria Parra
ea4484cc04 Fix bug that roots returned by roots/list are never updated
The roots state is capture at the initialization of the connectMcpServer.
This causes roots/list to always return empty roots despite updates
to the roots. We are now using a ref to reference it and avoid the closure
issue. roots/list now returns correctly the roots.
2024-11-10 23:45:38 +00:00
Justin Spahr-Summers
171b59bec6 Merge pull request #44 from modelcontextprotocol/justin/update-sdk
Update SDK; add UI for roots, resource templates, and env vars
2024-11-08 12:06:06 +00:00
Justin Spahr-Summers
2867173e7b Record 'roots list changed' notifications in history sidebar 2024-11-08 12:04:45 +00:00
Justin Spahr-Summers
57f0c49154 Remove redundant useCallbacks 2024-11-08 12:00:46 +00:00
Justin Spahr-Summers
5337baa116 Fix type error 2024-11-07 15:35:55 +00:00
Justin Spahr-Summers
afefcb3fa5 Make env vars UI toggleable 2024-11-07 15:28:21 +00:00
Justin Spahr-Summers
76e2cf6fdc Add UI for viewing and configuring environment variables 2024-11-07 15:28:21 +00:00
Justin Spahr-Summers
193032533b Support structured tool results 2024-11-07 15:28:21 +00:00
Justin Spahr-Summers
f3406ca43d Basic support for roots 2024-11-07 14:27:46 +00:00
Justin Spahr-Summers
645f2e942e Add support for listing and filling resource templates 2024-11-07 14:02:53 +00:00
Justin Spahr-Summers
d80214d0a2 Update SDK to 0.3.2 2024-11-07 13:46:21 +00:00
David Soria Parra
97b67ca1ae Merge pull request #43 from modelcontextprotocol/davidsp/prettier
run prettier
2024-11-05 10:40:07 +00:00
David Soria Parra
216c8a15d5 run prettier 2024-11-04 21:29:59 +00:00
26 changed files with 802 additions and 261 deletions

View File

@@ -17,20 +17,6 @@ jobs:
node-version: 18
cache: npm
- uses: actions/checkout@v4
with:
repository: modelcontextprotocol/typescript-sdk
ssh-key: ${{ secrets.TYPESCRIPT_SDK_KEY }}
path: packages/@modelcontextprotocol/sdk
- run: npm ci
working-directory: packages/@modelcontextprotocol/sdk
- run: npm pack
working-directory: packages/@modelcontextprotocol/sdk
- run: npm install --save packages/@modelcontextprotocol/sdk/modelcontextprotocol-sdk-*.tgz
# Working around https://github.com/npm/cli/issues/4828
# - run: npm ci
- run: npm install --no-package-lock

3
.npmrc
View File

@@ -1 +1,2 @@
registry = "https://registry.npmjs.org/"
registry="https://registry.npmjs.org/"
@modelcontextprotocol:registry="https://registry.npmjs.org/"

7
LICENSE Normal file
View File

@@ -0,0 +1,7 @@
Copyright (c) 2024 Anthropic, PBC.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

36
bin/cli.js Executable file
View File

@@ -0,0 +1,36 @@
#!/usr/bin/env node
import { join, dirname } from "path";
import { fileURLToPath } from "url";
import concurrently from "concurrently";
const __dirname = dirname(fileURLToPath(import.meta.url));
// Paths to the server and client entry points
const serverPath = join(__dirname, "../server/build/index.js");
const clientPath = join(__dirname, "../client/bin/cli.js");
console.log("Starting MCP inspector...");
const { result } = concurrently(
[
{
command: `node ${serverPath}`,
name: "server",
},
{
command: `node ${clientPath}`,
name: "client",
},
],
{
prefix: "name",
killOthers: ["failure", "success"],
restartTries: 3,
},
);
result.catch((err) => {
console.error("An error occurred:", err);
process.exit(1);
});

18
client/bin/cli.js Executable file
View File

@@ -0,0 +1,18 @@
#!/usr/bin/env node
import { join, dirname } from "path";
import { fileURLToPath } from "url";
import handler from "serve-handler";
import http from "http";
const __dirname = dirname(fileURLToPath(import.meta.url));
const distPath = join(__dirname, "../dist");
const server = http.createServer((request, response) => {
return handler(request, response, { public: distPath });
});
const port = process.env.PORT || 5173;
server.listen(port, () => {
console.log(`MCP inspector client running at http://localhost:${port}`);
});

View File

@@ -17,4 +17,4 @@
"lib": "@/lib",
"hooks": "@/hooks"
}
}
}

View File

@@ -1,8 +1,20 @@
{
"name": "client",
"name": "@modelcontextprotocol/inspector-client",
"version": "0.1.0",
"private": true,
"version": "0.0.0",
"description": "Client-side application for the Model Context Protocol inspector",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
"homepage": "https://modelcontextprotocol.github.io",
"bugs": "https://github.com/modelcontextprotocol/inspector/issues",
"type": "module",
"bin": {
"mcp-inspector-client": "./bin/cli.js"
},
"files": [
"bin",
"dist"
],
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
@@ -21,6 +33,7 @@
"lucide-react": "^0.447.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"serve-handler": "^6.1.6",
"tailwind-merge": "^2.5.3",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8"
@@ -30,6 +43,7 @@
"@types/node": "^22.7.5",
"@types/react": "^18.3.10",
"@types/react-dom": "^18.3.0",
"@types/serve-handler": "^6.1.4",
"@vitejs/plugin-react": "^4.3.2",
"autoprefixer": "^10.4.20",
"eslint": "^9.11.1",

View File

@@ -3,4 +3,4 @@ export default {
tailwindcss: {},
autoprefixer: {},
},
}
};

View File

@@ -1,7 +1,7 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import {
CallToolResultSchema,
CompatibilityCallToolResultSchema,
ClientRequest,
CreateMessageRequestSchema,
CreateMessageResult,
@@ -9,12 +9,18 @@ import {
GetPromptResultSchema,
ListPromptsResultSchema,
ListResourcesResultSchema,
ListResourceTemplatesResultSchema,
ListRootsRequestSchema,
ListToolsResultSchema,
ProgressNotificationSchema,
ReadResourceResultSchema,
Resource,
ResourceTemplate,
Root,
ServerNotification,
Tool,
CompatibilityCallToolResult,
ClientNotification,
} from "@modelcontextprotocol/sdk/types.js";
import { useEffect, useRef, useState } from "react";
@@ -37,9 +43,12 @@ import {
Play,
Send,
Terminal,
FolderTree,
ChevronDown,
ChevronRight,
} from "lucide-react";
import { AnyZodObject } from "zod";
import { ZodType } from "zod";
import "./App.css";
import ConsoleTab from "./components/ConsoleTab";
import HistoryAndNotifications from "./components/History";
@@ -47,6 +56,7 @@ import PingTab from "./components/PingTab";
import PromptsTab, { Prompt } from "./components/PromptsTab";
import RequestsTab from "./components/RequestsTabs";
import ResourcesTab from "./components/ResourcesTab";
import RootsTab from "./components/RootsTab";
import SamplingTab, { PendingRequest } from "./components/SamplingTab";
import Sidebar from "./components/Sidebar";
import ToolsTab from "./components/ToolsTab";
@@ -56,11 +66,15 @@ const App = () => {
"disconnected" | "connected" | "error"
>("disconnected");
const [resources, setResources] = useState<Resource[]>([]);
const [resourceTemplates, setResourceTemplates] = useState<
ResourceTemplate[]
>([]);
const [resourceContent, setResourceContent] = useState<string>("");
const [prompts, setPrompts] = useState<Prompt[]>([]);
const [promptContent, setPromptContent] = useState<string>("");
const [tools, setTools] = useState<Tool[]>([]);
const [toolResult, setToolResult] = useState<string>("");
const [toolResult, setToolResult] =
useState<CompatibilityCallToolResult | null>(null);
const [error, setError] = useState<string | null>(null);
const [command, setCommand] = useState<string>(() => {
return (
@@ -77,10 +91,13 @@ const App = () => {
const [url, setUrl] = useState<string>("http://localhost:3001/sse");
const [transportType, setTransportType] = useState<"stdio" | "sse">("stdio");
const [requestHistory, setRequestHistory] = useState<
{ request: string; response: string }[]
{ request: string; response?: string }[]
>([]);
const [mcpClient, setMcpClient] = useState<Client | null>(null);
const [notifications, setNotifications] = useState<ServerNotification[]>([]);
const [roots, setRoots] = useState<Root[]>([]);
const [env, setEnv] = useState<Record<string, string>>({});
const [showEnvVars, setShowEnvVars] = useState(false);
const [pendingSampleRequests, setPendingSampleRequests] = useState<
Array<
@@ -91,6 +108,7 @@ const App = () => {
>
>([]);
const nextRequestId = useRef(0);
const rootsRef = useRef<Root[]>([]);
const handleApproveSampling = (id: number, result: CreateMessageResult) => {
setPendingSampleRequests((prev) => {
@@ -116,6 +134,9 @@ const App = () => {
const [nextResourceCursor, setNextResourceCursor] = useState<
string | undefined
>();
const [nextResourceTemplateCursor, setNextResourceTemplateCursor] = useState<
string | undefined
>();
const [nextPromptCursor, setNextPromptCursor] = useState<
string | undefined
>();
@@ -130,14 +151,30 @@ const App = () => {
localStorage.setItem("lastArgs", args);
}, [args]);
const pushHistory = (request: object, response: object) => {
useEffect(() => {
fetch("http://localhost:3000/default-environment")
.then((response) => response.json())
.then((data) => setEnv(data))
.catch((error) =>
console.error("Error fetching default environment:", error),
);
}, []);
useEffect(() => {
rootsRef.current = roots;
}, [roots]);
const pushHistory = (request: object, response?: object) => {
setRequestHistory((prev) => [
...prev,
{ request: JSON.stringify(request), response: JSON.stringify(response) },
{
request: JSON.stringify(request),
response: response !== undefined ? JSON.stringify(response) : undefined,
},
]);
};
const makeRequest = async <T extends AnyZodObject>(
const makeRequest = async <T extends ZodType<object>>(
request: ClientRequest,
schema: T,
) => {
@@ -155,6 +192,20 @@ const App = () => {
}
};
const sendNotification = async (notification: ClientNotification) => {
if (!mcpClient) {
throw new Error("MCP client not connected");
}
try {
await mcpClient.notification(notification);
pushHistory(notification);
} catch (e: unknown) {
setError((e as Error).message);
throw e;
}
};
const listResources = async () => {
const response = await makeRequest(
{
@@ -167,6 +218,22 @@ const App = () => {
setNextResourceCursor(response.nextCursor);
};
const listResourceTemplates = async () => {
const response = await makeRequest(
{
method: "resources/templates/list" as const,
params: nextResourceTemplateCursor
? { cursor: nextResourceTemplateCursor }
: {},
},
ListResourceTemplatesResultSchema,
);
setResourceTemplates(
resourceTemplates.concat(response.resourceTemplates ?? []),
);
setNextResourceTemplateCursor(response.nextCursor);
};
const readResource = async (uri: string) => {
const response = await makeRequest(
{
@@ -225,9 +292,13 @@ const App = () => {
},
},
},
CallToolResultSchema,
CompatibilityCallToolResultSchema,
);
setToolResult(JSON.stringify(response.toolResult, null, 2));
setToolResult(response);
};
const handleRootsChange = async () => {
await sendNotification({ method: "notifications/roots/list_changed" });
};
const connectMcpServer = async () => {
@@ -243,6 +314,7 @@ const App = () => {
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);
}
@@ -269,6 +341,10 @@ const App = () => {
});
});
client.setRequestHandler(ListRootsRequestSchema, async () => {
return { roots: rootsRef.current };
});
setMcpClient(client);
setConnectionStatus("connected");
} catch (e) {
@@ -326,6 +402,66 @@ const App = () => {
Connect
</Button>
</div>
{transportType === "stdio" && (
<div className="mt-4">
<Button
variant="outline"
onClick={() => setShowEnvVars(!showEnvVars)}
className="flex items-center"
>
{showEnvVars ? (
<ChevronDown className="w-4 h-4 mr-2" />
) : (
<ChevronRight className="w-4 h-4 mr-2" />
)}
Environment Variables
</Button>
{showEnvVars && (
<div className="mt-2">
{Object.entries(env).map(([key, value]) => (
<div key={key} className="flex space-x-2 mb-2">
<Input
placeholder="Key"
value={key}
onChange={(e) =>
setEnv((prev) => ({
...prev,
[e.target.value]: value,
}))
}
/>
<Input
placeholder="Value"
value={value}
onChange={(e) =>
setEnv((prev) => ({
...prev,
[key]: e.target.value,
}))
}
/>
<Button
onClick={() =>
setEnv((prev) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [key]: _, ...rest } = prev;
return rest;
})
}
>
Remove
</Button>
</div>
))}
<Button
onClick={() => setEnv((prev) => ({ ...prev, "": "" }))}
>
Add Environment Variable
</Button>
</div>
)}
</div>
)}
</div>
{mcpClient ? (
<Tabs defaultValue="resources" className="w-full p-4">
@@ -363,17 +499,24 @@ const App = () => {
</span>
)}
</TabsTrigger>
<TabsTrigger value="roots">
<FolderTree className="w-4 h-4 mr-2" />
Roots
</TabsTrigger>
</TabsList>
<div className="w-full">
<ResourcesTab
resources={resources}
resourceTemplates={resourceTemplates}
listResources={listResources}
listResourceTemplates={listResourceTemplates}
readResource={readResource}
selectedResource={selectedResource}
setSelectedResource={setSelectedResource}
resourceContent={resourceContent}
nextCursor={nextResourceCursor}
nextTemplateCursor={nextResourceTemplateCursor}
error={error}
/>
<PromptsTab
@@ -394,7 +537,7 @@ const App = () => {
selectedTool={selectedTool}
setSelectedTool={(tool) => {
setSelectedTool(tool);
setToolResult("");
setToolResult(null);
}}
toolResult={toolResult}
nextCursor={nextToolCursor}
@@ -416,6 +559,11 @@ const App = () => {
onApprove={handleApproveSampling}
onReject={handleRejectSampling}
/>
<RootsTab
roots={roots}
setRoots={setRoots}
onRootsChange={handleRootsChange}
/>
</div>
</Tabs>
) : (

View File

@@ -6,7 +6,7 @@ const HistoryAndNotifications = ({
requestHistory,
serverNotifications,
}: {
requestHistory: Array<{ request: string; response: string | null }>;
requestHistory: Array<{ request: string; response?: string }>;
serverNotifications: ServerNotification[];
}) => {
const [expandedRequests, setExpandedRequests] = useState<{

View File

@@ -1,88 +1,199 @@
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { TabsContent } from "@/components/ui/tabs";
import { ListResourcesResult, Resource } from "@modelcontextprotocol/sdk/types.js";
import {
ListResourcesResult,
Resource,
ResourceTemplate,
ListResourceTemplatesResult,
} from "@modelcontextprotocol/sdk/types.js";
import { AlertCircle, ChevronRight, FileText, RefreshCw } from "lucide-react";
import ListPane from "./ListPane";
import { useState } from "react";
const ResourcesTab = ({
resources,
resourceTemplates,
listResources,
listResourceTemplates,
readResource,
selectedResource,
setSelectedResource,
resourceContent,
nextCursor,
nextTemplateCursor,
error,
}: {
resources: Resource[];
resourceTemplates: ResourceTemplate[];
listResources: () => void;
listResourceTemplates: () => void;
readResource: (uri: string) => void;
selectedResource: Resource | null;
setSelectedResource: (resource: Resource) => void;
setSelectedResource: (resource: Resource | null) => void;
resourceContent: string;
nextCursor: ListResourcesResult["nextCursor"];
nextTemplateCursor: ListResourceTemplatesResult["nextCursor"];
error: string | null;
}) => (
<TabsContent value="resources" className="grid grid-cols-2 gap-4">
<ListPane
items={resources}
listItems={listResources}
setSelectedItem={(resource) => {
setSelectedResource(resource);
readResource(resource.uri);
}}
renderItem={(resource) => (
<div className="flex items-center w-full">
<FileText className="w-4 h-4 mr-2 flex-shrink-0 text-gray-500" />
<span className="flex-1 truncate" title={resource.uri.toString()}>
{resource.name}
</span>
<ChevronRight className="w-4 h-4 flex-shrink-0 text-gray-400" />
</div>
)}
title="Resources"
buttonText={nextCursor ? "List More Resources" : "List Resources"}
isButtonDisabled={!nextCursor && resources.length > 0}
/>
}) => {
const [selectedTemplate, setSelectedTemplate] =
useState<ResourceTemplate | null>(null);
const [templateValues, setTemplateValues] = useState<Record<string, string>>(
{},
);
<div className="bg-white rounded-lg shadow">
<div className="p-4 border-b border-gray-200 flex justify-between items-center">
<h3 className="font-semibold truncate" title={selectedResource?.name}>
{selectedResource ? selectedResource.name : "Select a resource"}
</h3>
{selectedResource && (
<Button
variant="outline"
size="sm"
onClick={() => readResource(selectedResource.uri)}
const fillTemplate = (
template: string,
values: Record<string, string>,
): string => {
return template.replace(
/{([^}]+)}/g,
(_, key) => values[key] || `{${key}}`,
);
};
const handleReadTemplateResource = () => {
if (selectedTemplate) {
const uri = fillTemplate(selectedTemplate.uriTemplate, templateValues);
readResource(uri);
setSelectedTemplate(null);
// We don't have the full Resource object here, so we create a partial one
setSelectedResource({ uri, name: uri } as Resource);
}
};
return (
<TabsContent value="resources" className="grid grid-cols-3 gap-4">
<ListPane
items={resources}
listItems={listResources}
setSelectedItem={(resource) => {
setSelectedResource(resource);
readResource(resource.uri);
setSelectedTemplate(null);
}}
renderItem={(resource) => (
<div className="flex items-center w-full">
<FileText className="w-4 h-4 mr-2 flex-shrink-0 text-gray-500" />
<span className="flex-1 truncate" title={resource.uri.toString()}>
{resource.name}
</span>
<ChevronRight className="w-4 h-4 flex-shrink-0 text-gray-400" />
</div>
)}
title="Resources"
buttonText={nextCursor ? "List More Resources" : "List Resources"}
isButtonDisabled={!nextCursor && resources.length > 0}
/>
<ListPane
items={resourceTemplates}
listItems={listResourceTemplates}
setSelectedItem={(template) => {
setSelectedTemplate(template);
setSelectedResource(null);
setTemplateValues({});
}}
renderItem={(template) => (
<div className="flex items-center w-full">
<FileText className="w-4 h-4 mr-2 flex-shrink-0 text-gray-500" />
<span className="flex-1 truncate" title={template.uriTemplate}>
{template.name}
</span>
<ChevronRight className="w-4 h-4 flex-shrink-0 text-gray-400" />
</div>
)}
title="Resource Templates"
buttonText={
nextTemplateCursor ? "List More Templates" : "List Templates"
}
isButtonDisabled={!nextTemplateCursor && resourceTemplates.length > 0}
/>
<div className="bg-white rounded-lg shadow">
<div className="p-4 border-b border-gray-200 flex justify-between items-center">
<h3
className="font-semibold truncate"
title={selectedResource?.name || selectedTemplate?.name}
>
<RefreshCw className="w-4 h-4 mr-2" />
Refresh
</Button>
)}
{selectedResource
? selectedResource.name
: selectedTemplate
? selectedTemplate.name
: "Select a resource or template"}
</h3>
{selectedResource && (
<Button
variant="outline"
size="sm"
onClick={() => readResource(selectedResource.uri)}
>
<RefreshCw className="w-4 h-4 mr-2" />
Refresh
</Button>
)}
</div>
<div className="p-4">
{error ? (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
) : selectedResource ? (
<pre className="bg-gray-50 p-4 rounded text-sm overflow-auto max-h-96 whitespace-pre-wrap break-words">
{resourceContent}
</pre>
) : selectedTemplate ? (
<div className="space-y-4">
<p className="text-sm text-gray-600">
{selectedTemplate.description}
</p>
{selectedTemplate.uriTemplate
.match(/{([^}]+)}/g)
?.map((param) => {
const key = param.slice(1, -1);
return (
<div key={key}>
<label
htmlFor={key}
className="block text-sm font-medium text-gray-700"
>
{key}
</label>
<Input
id={key}
value={templateValues[key] || ""}
onChange={(e) =>
setTemplateValues({
...templateValues,
[key]: e.target.value,
})
}
className="mt-1"
/>
</div>
);
})}
<Button
onClick={handleReadTemplateResource}
disabled={Object.keys(templateValues).length === 0}
>
Read Resource
</Button>
</div>
) : (
<Alert>
<AlertDescription>
Select a resource or template from the list to view its contents
</AlertDescription>
</Alert>
)}
</div>
</div>
<div className="p-4">
{error ? (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
) : selectedResource ? (
<pre className="bg-gray-50 p-4 rounded text-sm overflow-auto max-h-96 whitespace-pre-wrap break-words">
{resourceContent}
</pre>
) : (
<Alert>
<AlertDescription>
Select a resource from the list to view its contents
</AlertDescription>
</Alert>
)}
</div>
</div>
</TabsContent>
);
</TabsContent>
);
};
export default ResourcesTab;

View File

@@ -0,0 +1,77 @@
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { TabsContent } from "@/components/ui/tabs";
import { Root } from "@modelcontextprotocol/sdk/types.js";
import { Plus, Minus, Save } from "lucide-react";
const RootsTab = ({
roots,
setRoots,
onRootsChange,
}: {
roots: Root[];
setRoots: React.Dispatch<React.SetStateAction<Root[]>>;
onRootsChange: () => void;
}) => {
const addRoot = () => {
setRoots((currentRoots) => [...currentRoots, { uri: "file://", name: "" }]);
};
const removeRoot = (index: number) => {
setRoots((currentRoots) => currentRoots.filter((_, i) => i !== index));
};
const updateRoot = (index: number, field: keyof Root, value: string) => {
setRoots((currentRoots) =>
currentRoots.map((root, i) =>
i === index ? { ...root, [field]: value } : root,
),
);
};
const handleSave = () => {
onRootsChange();
};
return (
<TabsContent value="roots" className="space-y-4">
<Alert>
<AlertDescription>
Configure the root directories that the server can access
</AlertDescription>
</Alert>
{roots.map((root, index) => (
<div key={index} className="flex gap-2 items-center">
<Input
placeholder="file:// URI"
value={root.uri}
onChange={(e) => updateRoot(index, "uri", e.target.value)}
className="flex-1"
/>
<Button
variant="destructive"
size="sm"
onClick={() => removeRoot(index)}
>
<Minus className="h-4 w-4" />
</Button>
</div>
))}
<div className="flex gap-2">
<Button variant="outline" onClick={addRoot}>
<Plus className="h-4 w-4 mr-2" />
Add Root
</Button>
<Button onClick={handleSave}>
<Save className="h-4 w-4 mr-2" />
Save Changes
</Button>
</div>
</TabsContent>
);
};
export default RootsTab;

View File

@@ -3,11 +3,13 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { TabsContent } from "@/components/ui/tabs";
import { ListToolsResult, Tool } from "@modelcontextprotocol/sdk/types.js";
import { CallToolResult, ListToolsResult, Tool } from "@modelcontextprotocol/sdk/types.js";
import { AlertCircle, Send } from "lucide-react";
import { useState } from "react";
import ListPane from "./ListPane";
import { CompatibilityCallToolResult } from "@modelcontextprotocol/sdk/types.js";
const ToolsTab = ({
tools,
listTools,
@@ -23,12 +25,58 @@ const ToolsTab = ({
callTool: (name: string, params: Record<string, unknown>) => void;
selectedTool: Tool | null;
setSelectedTool: (tool: Tool) => void;
toolResult: string;
toolResult: CompatibilityCallToolResult | null;
nextCursor: ListToolsResult["nextCursor"];
error: string | null;
}) => {
const [params, setParams] = useState<Record<string, unknown>>({});
const renderToolResult = () => {
if (!toolResult) return null;
if ("content" in toolResult) {
const structuredResult = toolResult as CallToolResult;
return (
<>
<h4 className="font-semibold mb-2">
Tool Result: {structuredResult.isError ? "Error" : "Success"}
</h4>
{structuredResult.content.map((item, index) => (
<div key={index} className="mb-2">
{item.type === "text" && (
<pre className="bg-gray-50 p-4 rounded text-sm overflow-auto max-h-64">
{item.text}
</pre>
)}
{item.type === "image" && (
<img
src={`data:${item.mimeType};base64,${item.data}`}
alt="Tool result image"
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>
)}
</div>
))}
</>
);
} else if ("toolResult" in toolResult) {
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">
{JSON.stringify(toolResult.toolResult, null, 2)}
</pre>
</>
);
}
};
return (
<TabsContent value="tools" className="grid grid-cols-2 gap-4">
<ListPane
@@ -100,14 +148,7 @@ const ToolsTab = ({
<Send className="w-4 h-4 mr-2" />
Run Tool
</Button>
{toolResult && (
<>
<h4 className="font-semibold mb-2">Tool Result:</h4>
<pre className="bg-gray-50 p-4 rounded text-sm overflow-auto max-h-64">
{toolResult}
</pre>
</>
)}
{toolResult && renderToolResult()}
</div>
) : (
<Alert>

View File

@@ -1,7 +1,7 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
@@ -16,8 +16,8 @@ const alertVariants = cva(
defaultVariants: {
variant: "default",
},
}
)
},
);
const Alert = React.forwardRef<
HTMLDivElement,
@@ -29,8 +29,8 @@ const Alert = React.forwardRef<
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
));
Alert.displayName = "Alert";
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
@@ -41,8 +41,8 @@ const AlertTitle = React.forwardRef<
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
));
AlertTitle.displayName = "AlertTitle";
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
@@ -53,7 +53,7 @@ const AlertDescription = React.forwardRef<
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
));
AlertDescription.displayName = "AlertDescription";
export { Alert, AlertTitle, AlertDescription }
export { Alert, AlertTitle, AlertDescription };

View File

@@ -1,8 +1,8 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
@@ -31,27 +31,27 @@ const buttonVariants = cva(
variant: "default",
size: "default",
},
}
)
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants }
export { Button, buttonVariants };

View File

@@ -1,6 +1,6 @@
import * as React from "react"
import * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
@@ -12,14 +12,14 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
className,
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
);
},
);
Input.displayName = "Input";
export { Input }
export { Input };

View File

@@ -1,12 +1,12 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
);
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
@@ -18,7 +18,7 @@ const Label = React.forwardRef<
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label }
export { Label };

View File

@@ -1,19 +1,19 @@
import * as React from "react"
import * as React from "react";
import {
CaretSortIcon,
CheckIcon,
ChevronDownIcon,
ChevronUpIcon,
} from "@radix-ui/react-icons"
import * as SelectPrimitive from "@radix-ui/react-select"
} from "@radix-ui/react-icons";
import * as SelectPrimitive from "@radix-ui/react-select";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const Select = SelectPrimitive.Root
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
@@ -23,7 +23,7 @@ const SelectTrigger = React.forwardRef<
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
className,
)}
{...props}
>
@@ -32,8 +32,8 @@ const SelectTrigger = React.forwardRef<
<CaretSortIcon className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
@@ -43,14 +43,14 @@ const SelectScrollUpButton = React.forwardRef<
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
className,
)}
{...props}
>
<ChevronUpIcon />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
@@ -60,15 +60,15 @@ const SelectScrollDownButton = React.forwardRef<
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
className,
)}
{...props}
>
<ChevronDownIcon />
</SelectPrimitive.ScrollDownButton>
))
));
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
@@ -81,7 +81,7 @@ const SelectContent = React.forwardRef<
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
className,
)}
position={position}
{...props}
@@ -91,7 +91,7 @@ const SelectContent = React.forwardRef<
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
@@ -99,8 +99,8 @@ const SelectContent = React.forwardRef<
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
@@ -111,8 +111,8 @@ const SelectLabel = React.forwardRef<
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
@@ -122,7 +122,7 @@ const SelectItem = React.forwardRef<
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
className,
)}
{...props}
>
@@ -133,8 +133,8 @@ const SelectItem = React.forwardRef<
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
@@ -145,8 +145,8 @@ const SelectSeparator = React.forwardRef<
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
@@ -159,4 +159,4 @@ export {
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}
};

View File

@@ -1,9 +1,9 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const Tabs = TabsPrimitive.Root
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
@@ -13,12 +13,12 @@ const TabsList = React.forwardRef<
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className
className,
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
@@ -28,12 +28,12 @@ const TabsTrigger = React.forwardRef<
ref={ref}
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",
className
className,
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
@@ -43,11 +43,11 @@ const TabsContent = React.forwardRef<
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
className,
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent }
export { Tabs, TabsList, TabsTrigger, TabsContent };

View File

@@ -1,6 +1,6 @@
import * as React from "react"
import * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
@@ -11,14 +11,14 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
<textarea
className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
className,
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
);
},
);
Textarea.displayName = "Textarea";
export { Textarea }
export { Textarea };

View File

@@ -1,6 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
return twMerge(clsx(inputs));
}

View File

@@ -2,9 +2,7 @@
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
"@/*": ["./src/*"]
},
"target": "ES2020",

167
package-lock.json generated
View File

@@ -1,23 +1,32 @@
{
"name": "mcp-inspector",
"version": "0.0.1",
"name": "@modelcontextprotocol/inspector",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mcp-inspector",
"version": "0.0.1",
"name": "@modelcontextprotocol/inspector",
"version": "0.1.0",
"license": "MIT",
"workspaces": [
"client",
"server"
],
"dependencies": {
"concurrently": "^9.0.1"
},
"bin": {
"mcp-inspector": "bin/cli.js"
},
"devDependencies": {
"concurrently": "^9.0.1",
"@types/node": "^22.7.5",
"prettier": "3.3.3"
}
},
"client": {
"version": "0.0.0",
"name": "@modelcontextprotocol/inspector-client",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "*",
"@radix-ui/react-icons": "^1.3.0",
@@ -30,15 +39,20 @@
"lucide-react": "^0.447.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"serve-handler": "^6.1.6",
"tailwind-merge": "^2.5.3",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8"
},
"bin": {
"mcp-inspector-client": "bin/cli.js"
},
"devDependencies": {
"@eslint/js": "^9.11.1",
"@types/node": "^22.7.5",
"@types/react": "^18.3.10",
"@types/react-dom": "^18.3.0",
"@types/serve-handler": "^6.1.4",
"@vitejs/plugin-react": "^4.3.2",
"autoprefixer": "^10.4.20",
"eslint": "^9.11.1",
@@ -840,10 +854,18 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@modelcontextprotocol/inspector-client": {
"resolved": "client",
"link": true
},
"node_modules/@modelcontextprotocol/inspector-server": {
"resolved": "server",
"link": true
},
"node_modules/@modelcontextprotocol/sdk": {
"version": "0.1.0",
"resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-internal/@modelcontextprotocol/sdk/-/@modelcontextprotocol/sdk-0.1.0.tgz",
"integrity": "sha512-46/FTHNZWUWbdFspKsCIixhCKwi9Hub5+HWNXC1DRIL2TSV8cdx5sTsg+Jy6I4Uc8/rd7tLZcVQ6IasIY1g4zg==",
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-0.4.0.tgz",
"integrity": "sha512-79gx8xh4o9YzdbtqMukOe5WKzvEZpvBA1x8PAgJWL7J5k06+vJx8NK2kWzOazPgqnfDego7cNEO8tjai/nOPAA==",
"dependencies": {
"content-type": "^1.0.5",
"raw-body": "^3.0.0",
@@ -1733,6 +1755,16 @@
"@types/node": "*"
}
},
"node_modules/@types/serve-handler": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/@types/serve-handler/-/serve-handler-6.1.4.tgz",
"integrity": "sha512-aXy58tNie0NkuSCY291xUxl0X+kGYy986l4kqW6Gi4kEXgr6Tx0fpSH7YwUSa5usPpG3s9DBeIR6hHcDtL2IvQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/serve-static": {
"version": "1.15.7",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
@@ -2245,7 +2277,6 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@@ -2369,7 +2400,6 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
@@ -2427,15 +2457,10 @@
"node": ">=6"
}
},
"node_modules/client": {
"resolved": "client",
"link": true
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
@@ -2486,14 +2511,12 @@
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true,
"license": "MIT"
},
"node_modules/concurrently": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.0.1.tgz",
"integrity": "sha512-wYKvCd/f54sTXJMSfV6Ln/B8UrfLBKOYa+lzc6CHay3Qek+LorVSBdMVfyewFhRbH0Rbabsk4D+3PL/VjQ5gzg==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "^4.1.2",
@@ -2519,7 +2542,6 @@
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
@@ -2797,7 +2819,6 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -3340,7 +3361,6 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
@@ -3479,7 +3499,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -3843,7 +3862,6 @@
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.merge": {
@@ -3884,10 +3902,6 @@
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
}
},
"node_modules/mcp-inspector": {
"resolved": "server",
"link": true
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -3974,7 +3988,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@@ -4199,6 +4212,11 @@
"node": ">=8"
}
},
"node_modules/path-is-inside": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
"integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w=="
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -4635,12 +4653,6 @@
}
}
},
"node_modules/react-remove-scroll/node_modules/tslib": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
"license": "0BSD"
},
"node_modules/react-style-singleton": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
@@ -4689,7 +4701,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -4805,19 +4816,11 @@
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/rxjs/node_modules/tslib": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
"dev": true,
"license": "0BSD"
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -4911,6 +4914,69 @@
"node": ">= 0.8"
}
},
"node_modules/serve-handler": {
"version": "6.1.6",
"resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz",
"integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==",
"license": "MIT",
"dependencies": {
"bytes": "3.0.0",
"content-disposition": "0.5.2",
"mime-types": "2.1.18",
"minimatch": "3.1.2",
"path-is-inside": "1.0.2",
"path-to-regexp": "3.3.0",
"range-parser": "1.2.0"
}
},
"node_modules/serve-handler/node_modules/bytes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
"integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/serve-handler/node_modules/content-disposition": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
"integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/serve-handler/node_modules/mime-db": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
"integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/serve-handler/node_modules/mime-types": {
"version": "2.1.18",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz",
"integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==",
"dependencies": {
"mime-db": "~1.33.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/serve-handler/node_modules/path-to-regexp": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz",
"integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw=="
},
"node_modules/serve-handler/node_modules/range-parser": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
"integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/serve-static": {
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
@@ -4974,7 +5040,6 @@
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz",
"integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -5121,7 +5186,6 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
@@ -5273,7 +5337,6 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
"dev": true,
"license": "MIT",
"bin": {
"tree-kill": "cli.js"
@@ -5658,7 +5721,6 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
@@ -5715,7 +5777,6 @@
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=10"
@@ -5744,7 +5805,6 @@
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"cliui": "^8.0.1",
@@ -5763,7 +5823,6 @@
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=12"
@@ -5792,8 +5851,9 @@
}
},
"server": {
"name": "mcp-inspector",
"version": "0.0.1",
"name": "@modelcontextprotocol/inspector-server",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "*",
"cors": "^2.8.5",
@@ -5802,6 +5862,9 @@
"ws": "^8.18.0",
"zod": "^3.23.8"
},
"bin": {
"mcp-inspector-server": "build/index.js"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/eventsource": "^1.1.15",

View File

@@ -1,8 +1,22 @@
{
"name": "mcp-inspector",
"name": "@modelcontextprotocol/inspector",
"version": "0.1.0",
"private": true,
"version": "0.0.1",
"description": "Model Context Protocol inspector",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
"homepage": "https://modelcontextprotocol.github.io",
"bugs": "https://github.com/modelcontextprotocol/inspector/issues",
"type": "module",
"bin": {
"mcp-inspector": "./bin/cli.js"
},
"files": [
"bin",
"client/bin",
"client/dist",
"server/build"
],
"workspaces": [
"client",
"server"
@@ -14,11 +28,15 @@
"build": "npm run build-server && npm run build-client",
"start-server": "cd server && npm run start",
"start-client": "cd client && npm run preview",
"start": "concurrently \"npm run start-server\" \"npm run start-client\"",
"start": "./bin/cli.js",
"prepare": "npm run build",
"prettier-fix": "prettier --write ."
},
"dependencies": {
"concurrently": "^9.0.1"
},
"devDependencies": {
"concurrently": "^9.0.1",
"prettier": "3.3.3"
"prettier": "3.3.3",
"@types/node": "^22.7.5"
}
}

View File

@@ -1,8 +1,19 @@
{
"name": "mcp-inspector",
"version": "0.0.1",
"main": "build/index.js",
"name": "@modelcontextprotocol/inspector-server",
"version": "0.1.0",
"private": true,
"description": "Server-side application for the Model Context Protocol inspector",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
"homepage": "https://modelcontextprotocol.github.io",
"bugs": "https://github.com/modelcontextprotocol/inspector/issues",
"type": "module",
"bin": {
"mcp-inspector-server": "build/index.js"
},
"files": [
"build"
],
"scripts": {
"build": "tsc",
"start": "node build/index.js",

View File

@@ -1,8 +1,13 @@
#!/usr/bin/env node
import cors from "cors";
import EventSource from "eventsource";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import {
StdioClientTransport,
getDefaultEnvironment,
} from "@modelcontextprotocol/sdk/client/stdio.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";
import mcpProxy from "./mcpProxy.js";
@@ -24,8 +29,11 @@ const createTransport = async (query: express.Request["query"]) => {
if (transportType === "stdio") {
const command = query.command as string;
const args = (query.args as string).split(/\s+/);
console.log(`Stdio transport: command=${command}, args=${args}`);
const transport = new StdioClientTransport({ command, args });
const env = query.env ? JSON.parse(query.env as string) : undefined;
console.log(
`Stdio transport: command=${command}, args=${args}, env=${JSON.stringify(env)}`,
);
const transport = new StdioClientTransport({ command, args, env });
await transport.start();
console.log("Spawned stdio transport");
return transport;
@@ -79,6 +87,10 @@ app.post("/message", async (req, res) => {
await transport.handlePostMessage(req, res);
});
app.get("/default-environment", (req, res) => {
res.json(getDefaultEnvironment());
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);