Merge branch 'main' into patch-1

This commit is contained in:
Cliff Hall 2025-07-11 14:58:06 -04:00 • committed by GitHub
commit 5c15e50caa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 468 additions and 112 deletions

64
.github/workflows/claude.yml vendored Normal file
View file

@ -0,0 +1,64 @@
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
(
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
) &&
(
github.actor == 'ihrpr' ||
github.actor == 'olaservo'
)
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
# model: "claude-opus-4-20250514"
# Optional: Customize the trigger phrase (default: @claude)
# trigger_phrase: "/claude"
# Optional: Trigger when specific user is assigned to an issue
# assignee_trigger: "claude-bot"
# Optional: Allow Claude to run specific commands
# allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
# Optional: Add custom instructions for Claude to customize its behavior for your project
# custom_instructions: |
# Follow our coding standards
# Ensure all new code has tests
# Use TypeScript for new files
# Optional: Custom environment variables for Claude
# claude_env: |
# NODE_ENV: test

3
.gitignore vendored
View file

@ -9,7 +9,8 @@ client/tsconfig.app.tsbuildinfo
client/tsconfig.node.tsbuildinfo client/tsconfig.node.tsbuildinfo
cli/build cli/build
test-output test-output
# symlinked by `npm run link:sdk`:
sdk
client/playwright-report/ client/playwright-report/
client/results.json client/results.json
client/test-results/ client/test-results/

View file

@ -260,6 +260,12 @@ Development mode:
```bash ```bash
npm run dev npm run dev
# To co-develop with the typescript-sdk package (assuming it's cloned in ../typescript-sdk; set MCP_SDK otherwise):
npm run dev:sdk "cd sdk && npm run examples:simple-server:w"
# then open http://localhost:3000/mcp as SHTTP in the inspector.
# To go back to the deployed SDK version:
# npm run unlink:sdk && npm i
``` ```
> **Note for Windows users:** > **Note for Windows users:**

View file

@ -1,6 +1,6 @@
{ {
"name": "@modelcontextprotocol/inspector-cli", "name": "@modelcontextprotocol/inspector-cli",
"version": "0.15.0", "version": "0.16.1",
"description": "CLI for the Model Context Protocol inspector", "description": "CLI for the Model Context Protocol inspector",
"license": "MIT", "license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)", "author": "Anthropic, PBC (https://anthropic.com)",

View file

@ -50,28 +50,15 @@ function delay(ms: number): Promise<void> {
} }
async function runWebClient(args: Args): Promise<void> { async function runWebClient(args: Args): Promise<void> {
const inspectorServerPath = resolve(
__dirname,
"../../",
"server",
"build",
"index.js",
);
// Path to the client entry point // Path to the client entry point
const inspectorClientPath = resolve( const inspectorClientPath = resolve(
__dirname, __dirname,
"../../", "../../",
"client", "client",
"bin", "bin",
"client.js", "start.js",
); );
const CLIENT_PORT: string = process.env.CLIENT_PORT ?? "6274";
const SERVER_PORT: string = process.env.SERVER_PORT ?? "6277";
console.log("Starting MCP inspector...");
const abort = new AbortController(); const abort = new AbortController();
let cancelled: boolean = false; let cancelled: boolean = false;
process.on("SIGINT", () => { process.on("SIGINT", () => {
@ -79,42 +66,13 @@ async function runWebClient(args: Args): Promise<void> {
abort.abort(); abort.abort();
}); });
let server: ReturnType<typeof spawnPromise>;
let serverOk: unknown;
try { try {
server = spawnPromise( await spawnPromise("node", [inspectorClientPath], {
"node", signal: abort.signal,
[ echoOutput: true,
inspectorServerPath, });
...(args.command ? [`--env`, args.command] : []), } catch (e) {
...(args.args ? [`--args=${args.args.join(" ")}`] : []), if (!cancelled || process.env.DEBUG) throw e;
],
{
env: {
...process.env,
PORT: SERVER_PORT,
MCP_ENV_VARS: JSON.stringify(args.envArgs),
},
signal: abort.signal,
echoOutput: true,
},
);
// Make sure server started before starting client
serverOk = await Promise.race([server, delay(2 * 1000)]);
} catch (error) {}
if (serverOk) {
try {
await spawnPromise("node", [inspectorClientPath], {
env: { ...process.env, PORT: CLIENT_PORT },
signal: abort.signal,
echoOutput: true,
});
} catch (e) {
if (!cancelled || process.env.DEBUG) throw e;
}
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "@modelcontextprotocol/inspector-client", "name": "@modelcontextprotocol/inspector-client",
"version": "0.15.0", "version": "0.16.1",
"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)",

View file

@ -80,6 +80,9 @@ const App = () => {
ResourceTemplate[] ResourceTemplate[]
>([]); >([]);
const [resourceContent, setResourceContent] = useState<string>(""); const [resourceContent, setResourceContent] = useState<string>("");
const [resourceContentMap, setResourceContentMap] = useState<
Record<string, string>
>({});
const [prompts, setPrompts] = useState<Prompt[]>([]); const [prompts, setPrompts] = useState<Prompt[]>([]);
const [promptContent, setPromptContent] = useState<string>(""); const [promptContent, setPromptContent] = useState<string>("");
const [tools, setTools] = useState<Tool[]>([]); const [tools, setTools] = useState<Tool[]>([]);
@ -461,7 +464,12 @@ const App = () => {
ReadResourceResultSchema, ReadResourceResultSchema,
"resources", "resources",
); );
setResourceContent(JSON.stringify(response, null, 2)); const content = JSON.stringify(response, null, 2);
setResourceContent(content);
setResourceContentMap((prev) => ({
...prev,
[uri]: content,
}));
}; };
const subscribeToResource = async (uri: string) => { const subscribeToResource = async (uri: string) => {
@ -863,6 +871,11 @@ const App = () => {
toolResult={toolResult} toolResult={toolResult}
nextCursor={nextToolCursor} nextCursor={nextToolCursor}
error={errors.tools} error={errors.tools}
resourceContent={resourceContentMap}
onReadResource={(uri: string) => {
clearError("resources");
readResource(uri);
}}
/> />
<ConsoleTab /> <ConsoleTab />
<PingTab <PingTab

View file

@ -0,0 +1,112 @@
import { useState, useCallback, useMemo, memo } from "react";
import JsonView from "./JsonView";
interface ResourceLinkViewProps {
uri: string;
name?: string;
description?: string;
mimeType?: string;
resourceContent: string;
onReadResource?: (uri: string) => void;
}
const ResourceLinkView = memo(
({
uri,
name,
description,
mimeType,
resourceContent,
onReadResource,
}: ResourceLinkViewProps) => {
const [{ expanded, loading }, setState] = useState({
expanded: false,
loading: false,
});
const expandedContent = useMemo(
() =>
expanded && resourceContent ? (
<div className="mt-2">
<div className="flex justify-between items-center mb-1">
<span className="font-semibold text-green-600">Resource:</span>
</div>
<JsonView data={resourceContent} className="bg-background" />
</div>
) : null,
[expanded, resourceContent],
);
const handleClick = useCallback(() => {
if (!onReadResource) return;
if (!expanded) {
setState((prev) => ({ ...prev, expanded: true, loading: true }));
onReadResource(uri);
setState((prev) => ({ ...prev, loading: false }));
} else {
setState((prev) => ({ ...prev, expanded: false }));
}
}, [expanded, onReadResource, uri]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if ((e.key === "Enter" || e.key === " ") && onReadResource) {
e.preventDefault();
handleClick();
}
},
[handleClick, onReadResource],
);
return (
<div className="text-sm text-foreground bg-secondary py-2 px-3 rounded">
<div
className="flex justify-between items-center cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1 rounded"
onClick={onReadResource ? handleClick : undefined}
onKeyDown={onReadResource ? handleKeyDown : undefined}
tabIndex={onReadResource ? 0 : -1}
role="button"
aria-expanded={expanded}
aria-label={`${expanded ? "Collapse" : "Expand"} resource ${uri}`}
>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2 mb-1">
<span className="text-sm text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:underline px-1 py-0.5 break-all font-mono flex-1 min-w-0">
{uri}
</span>
<div className="flex items-center gap-2 flex-shrink-0">
{mimeType && (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
{mimeType}
</span>
)}
{onReadResource && (
<span className="ml-2 flex-shrink-0" aria-hidden="true">
{loading ? (
<div className="w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
) : (
<span>{expanded ? "▼" : "▶"}</span>
)}
</span>
)}
</div>
</div>
{name && (
<div className="font-semibold text-sm text-gray-900 dark:text-gray-100 mb-1">
{name}
</div>
)}
{description && (
<p className="text-sm text-gray-600 dark:text-gray-300 leading-relaxed">
{description}
</p>
)}
</div>
</div>
{expandedContent}
</div>
);
},
);
export default ResourceLinkView;

View file

@ -1,4 +1,5 @@
import JsonView from "./JsonView"; import JsonView from "./JsonView";
import ResourceLinkView from "./ResourceLinkView";
import { import {
CallToolResultSchema, CallToolResultSchema,
CompatibilityCallToolResult, CompatibilityCallToolResult,
@ -9,6 +10,8 @@ import { validateToolOutput, hasOutputSchema } from "@/utils/schemaUtils";
interface ToolResultsProps { interface ToolResultsProps {
toolResult: CompatibilityCallToolResult | null; toolResult: CompatibilityCallToolResult | null;
selectedTool: Tool | null; selectedTool: Tool | null;
resourceContent: Record<string, string>;
onReadResource?: (uri: string) => void;
} }
const checkContentCompatibility = ( const checkContentCompatibility = (
@ -19,49 +22,54 @@ const checkContentCompatibility = (
[key: string]: unknown; [key: string]: unknown;
}>, }>,
): { isCompatible: boolean; message: string } => { ): { isCompatible: boolean; message: string } => {
if ( // Look for at least one text content block that matches the structured content
unstructuredContent.length !== 1 || const textBlocks = unstructuredContent.filter(
unstructuredContent[0].type !== "text" (block) => block.type === "text",
) { );
if (textBlocks.length === 0) {
return { return {
isCompatible: false, isCompatible: false,
message: "Unstructured content is not a single text block", message: "No text blocks to match structured content",
}; };
} }
const textContent = unstructuredContent[0].text; // Check if any text block contains JSON that matches the structured content
if (!textContent) { for (const textBlock of textBlocks) {
return { const textContent = textBlock.text;
isCompatible: false, if (!textContent) {
message: "Text content is empty", continue;
}; }
}
try {
try { const parsedContent = JSON.parse(textContent);
const parsedContent = JSON.parse(textContent); const isEqual =
const isEqual = JSON.stringify(parsedContent) === JSON.stringify(structuredContent);
JSON.stringify(parsedContent) === JSON.stringify(structuredContent);
if (isEqual) {
if (isEqual) { return {
return { isCompatible: true,
isCompatible: true, message: `Structured content matches text block${textBlocks.length > 1 ? " (multiple blocks)" : ""}${unstructuredContent.length > textBlocks.length ? " + other content" : ""}`,
message: "Unstructured content matches structured content", };
}; }
} else { } catch {
return { // Continue to next text block if this one doesn't parse as JSON
isCompatible: false, continue;
message: "Parsed JSON does not match structured content",
};
} }
} catch {
return {
isCompatible: false,
message: "Unstructured content is not valid JSON",
};
} }
return {
isCompatible: false,
message: "No text block matches structured content",
};
}; };
const ToolResults = ({ toolResult, selectedTool }: ToolResultsProps) => { const ToolResults = ({
toolResult,
selectedTool,
resourceContent,
onReadResource,
}: ToolResultsProps) => {
if (!toolResult) return null; if (!toolResult) return null;
if ("content" in toolResult) { if ("content" in toolResult) {
@ -200,6 +208,16 @@ const ToolResults = ({ toolResult, selectedTool }: ToolResultsProps) => {
) : ( ) : (
<JsonView data={item.resource} /> <JsonView data={item.resource} />
))} ))}
{item.type === "resource_link" && (
<ResourceLinkView
uri={item.uri}
name={item.name}
description={item.description}
mimeType={item.mimeType}
resourceContent={resourceContent[item.uri] || ""}
onReadResource={onReadResource}
/>
)}
</div> </div>
))} ))}
</div> </div>

View file

@ -28,6 +28,8 @@ const ToolsTab = ({
setSelectedTool, setSelectedTool,
toolResult, toolResult,
nextCursor, nextCursor,
resourceContent,
onReadResource,
}: { }: {
tools: Tool[]; tools: Tool[];
listTools: () => void; listTools: () => void;
@ -38,6 +40,8 @@ const ToolsTab = ({
toolResult: CompatibilityCallToolResult | null; toolResult: CompatibilityCallToolResult | null;
nextCursor: ListToolsResult["nextCursor"]; nextCursor: ListToolsResult["nextCursor"];
error: string | null; error: string | null;
resourceContent: Record<string, string>;
onReadResource?: (uri: string) => void;
}) => { }) => {
const [params, setParams] = useState<Record<string, unknown>>({}); const [params, setParams] = useState<Record<string, unknown>>({});
const [isToolRunning, setIsToolRunning] = useState(false); const [isToolRunning, setIsToolRunning] = useState(false);
@ -267,6 +271,8 @@ const ToolsTab = ({
<ToolResults <ToolResults
toolResult={toolResult} toolResult={toolResult}
selectedTool={selectedTool} selectedTool={selectedTool}
resourceContent={resourceContent}
onReadResource={onReadResource}
/> />
</div> </div>
) : ( ) : (

View file

@ -55,6 +55,8 @@ describe("ToolsTab", () => {
toolResult: null, toolResult: null,
nextCursor: "", nextCursor: "",
error: null, error: null,
resourceContent: {},
onReadResource: jest.fn(),
}; };
const renderToolsTab = (props = {}) => { const renderToolsTab = (props = {}) => {
@ -249,10 +251,12 @@ describe("ToolsTab", () => {
}, },
}; };
it("should display structured content when present", () => { beforeEach(() => {
// Cache the tool's output schema so hasOutputSchema returns true // Cache the tool's output schema before each test
cacheToolOutputSchemas([toolWithOutputSchema]); cacheToolOutputSchemas([toolWithOutputSchema]);
});
it("should display structured content when present", () => {
const structuredResult = { const structuredResult = {
content: [], content: [],
structuredContent: { structuredContent: {
@ -261,6 +265,7 @@ describe("ToolsTab", () => {
}; };
renderToolsTab({ renderToolsTab({
tools: [toolWithOutputSchema],
selectedTool: toolWithOutputSchema, selectedTool: toolWithOutputSchema,
toolResult: structuredResult, toolResult: structuredResult,
}); });
@ -272,8 +277,6 @@ describe("ToolsTab", () => {
}); });
it("should show validation error for invalid structured content", () => { it("should show validation error for invalid structured content", () => {
cacheToolOutputSchemas([toolWithOutputSchema]);
const invalidResult = { const invalidResult = {
content: [], content: [],
structuredContent: { structuredContent: {
@ -282,6 +285,7 @@ describe("ToolsTab", () => {
}; };
renderToolsTab({ renderToolsTab({
tools: [toolWithOutputSchema],
selectedTool: toolWithOutputSchema, selectedTool: toolWithOutputSchema,
toolResult: invalidResult, toolResult: invalidResult,
}); });
@ -290,14 +294,13 @@ describe("ToolsTab", () => {
}); });
it("should show error when tool with output schema doesn't return structured content", () => { it("should show error when tool with output schema doesn't return structured content", () => {
cacheToolOutputSchemas([toolWithOutputSchema]);
const resultWithoutStructured = { const resultWithoutStructured = {
content: [{ type: "text", text: "some result" }], content: [{ type: "text", text: "some result" }],
// No structuredContent // No structuredContent
}; };
renderToolsTab({ renderToolsTab({
tools: [toolWithOutputSchema],
selectedTool: toolWithOutputSchema, selectedTool: toolWithOutputSchema,
toolResult: resultWithoutStructured, toolResult: resultWithoutStructured,
}); });
@ -310,14 +313,13 @@ describe("ToolsTab", () => {
}); });
it("should show unstructured content title when both structured and unstructured exist", () => { it("should show unstructured content title when both structured and unstructured exist", () => {
cacheToolOutputSchemas([toolWithOutputSchema]);
const resultWithBoth = { const resultWithBoth = {
content: [{ type: "text", text: '{"temperature": 25}' }], content: [{ type: "text", text: '{"temperature": 25}' }],
structuredContent: { temperature: 25 }, structuredContent: { temperature: 25 },
}; };
renderToolsTab({ renderToolsTab({
tools: [toolWithOutputSchema],
selectedTool: toolWithOutputSchema, selectedTool: toolWithOutputSchema,
toolResult: resultWithBoth, toolResult: resultWithBoth,
}); });
@ -342,26 +344,100 @@ describe("ToolsTab", () => {
}); });
it("should show compatibility check when tool has output schema", () => { it("should show compatibility check when tool has output schema", () => {
cacheToolOutputSchemas([toolWithOutputSchema]);
const compatibleResult = { const compatibleResult = {
content: [{ type: "text", text: '{"temperature": 25}' }], content: [{ type: "text", text: '{"temperature": 25}' }],
structuredContent: { temperature: 25 }, structuredContent: { temperature: 25 },
}; };
renderToolsTab({ renderToolsTab({
tools: [toolWithOutputSchema],
selectedTool: toolWithOutputSchema, selectedTool: toolWithOutputSchema,
toolResult: compatibleResult, toolResult: compatibleResult,
}); });
// Should show compatibility result // Should show compatibility result
expect( expect(
screen.getByText( screen.getByText(/structured content matches/i),
/matches structured content|not a single text block|not valid JSON|does not match/,
),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
it("should accept multiple content blocks with structured output", () => {
const multipleBlocksResult = {
content: [
{ type: "text", text: "Here is the weather data:" },
{ type: "text", text: '{"temperature": 25}' },
{ type: "text", text: "Have a nice day!" },
],
structuredContent: { temperature: 25 },
};
renderToolsTab({
tools: [toolWithOutputSchema],
selectedTool: toolWithOutputSchema,
toolResult: multipleBlocksResult,
});
// Should show compatible result with multiple blocks
expect(
screen.getByText(/structured content matches.*multiple/i),
).toBeInTheDocument();
});
it("should accept mixed content types with structured output", () => {
const mixedContentResult = {
content: [
{ type: "text", text: "Weather report:" },
{ type: "text", text: '{"temperature": 25}' },
{ type: "image", data: "base64data", mimeType: "image/png" },
],
structuredContent: { temperature: 25 },
};
renderToolsTab({
tools: [toolWithOutputSchema],
selectedTool: toolWithOutputSchema,
toolResult: mixedContentResult,
});
// Should render without crashing - the validation logic has been updated
expect(screen.getAllByText("weatherTool")).toHaveLength(2);
});
it("should reject when no text blocks match structured content", () => {
const noMatchResult = {
content: [
{ type: "text", text: "Some text" },
{ type: "text", text: '{"humidity": 60}' }, // Different structure
],
structuredContent: { temperature: 25 },
};
renderToolsTab({
tools: [toolWithOutputSchema],
selectedTool: toolWithOutputSchema,
toolResult: noMatchResult,
});
// Should render without crashing - the validation logic has been updated
expect(screen.getAllByText("weatherTool")).toHaveLength(2);
});
it("should reject when no text blocks are present", () => {
const noTextBlocksResult = {
content: [{ type: "image", data: "base64data", mimeType: "image/png" }],
structuredContent: { temperature: 25 },
};
renderToolsTab({
tools: [toolWithOutputSchema],
selectedTool: toolWithOutputSchema,
toolResult: noTextBlocksResult,
});
// Should render without crashing - the validation logic has been updated
expect(screen.getAllByText("weatherTool")).toHaveLength(2);
});
it("should not show compatibility check when tool has no output schema", () => { it("should not show compatibility check when tool has no output schema", () => {
const resultWithBoth = { const resultWithBoth = {
content: [{ type: "text", text: '{"data": "value"}' }], content: [{ type: "text", text: '{"data": "value"}' }],
@ -376,9 +452,108 @@ describe("ToolsTab", () => {
// Should not show any compatibility messages // Should not show any compatibility messages
expect( expect(
screen.queryByText( screen.queryByText(
/matches structured content|not a single text block|not valid JSON|does not match/, /structured content matches|no text blocks|no.*matches/i,
), ),
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
}); });
}); });
describe("Resource Link Content Type", () => {
it("should render resource_link content type and handle expansion", async () => {
const mockOnReadResource = jest.fn();
const resourceContent = {
"test://static/resource/1": JSON.stringify({
contents: [
{
uri: "test://static/resource/1",
name: "Resource 1",
mimeType: "text/plain",
text: "Resource 1: This is a plaintext resource",
},
],
}),
};
const result = {
content: [
{
type: "resource_link",
uri: "test://static/resource/1",
name: "Resource 1",
description: "Resource 1: plaintext resource",
mimeType: "text/plain",
},
{
type: "resource_link",
uri: "test://static/resource/2",
name: "Resource 2",
description: "Resource 2: binary blob resource",
mimeType: "application/octet-stream",
},
{
type: "resource_link",
uri: "test://static/resource/3",
name: "Resource 3",
description: "Resource 3: plaintext resource",
mimeType: "text/plain",
},
],
};
renderToolsTab({
selectedTool: mockTools[0],
toolResult: result,
resourceContent,
onReadResource: mockOnReadResource,
});
["1", "2", "3"].forEach((id) => {
expect(
screen.getByText(`test://static/resource/${id}`),
).toBeInTheDocument();
expect(screen.getByText(`Resource ${id}`)).toBeInTheDocument();
});
expect(screen.getAllByText("text/plain")).toHaveLength(2);
expect(screen.getByText("application/octet-stream")).toBeInTheDocument();
const expandButtons = screen.getAllByRole("button", {
name: /expand resource/i,
});
expect(expandButtons).toHaveLength(3);
expect(screen.queryByText("Resource:")).not.toBeInTheDocument();
expandButtons.forEach((button) => {
expect(button).toHaveAttribute("aria-expanded", "false");
});
const resource1Button = screen.getByRole("button", {
name: /expand resource test:\/\/static\/resource\/1/i,
});
await act(async () => {
fireEvent.click(resource1Button);
});
expect(mockOnReadResource).toHaveBeenCalledWith(
"test://static/resource/1",
);
expect(screen.getByText("Resource:")).toBeInTheDocument();
expect(document.body).toHaveTextContent("contents:");
expect(document.body).toHaveTextContent('uri:"test://static/resource/1"');
expect(resource1Button).toHaveAttribute("aria-expanded", "true");
await act(async () => {
fireEvent.click(resource1Button);
});
expect(screen.queryByText("Resource:")).not.toBeInTheDocument();
expect(document.body).not.toHaveTextContent("contents:");
expect(document.body).not.toHaveTextContent(
'uri:"test://static/resource/1"',
);
expect(resource1Button).toHaveAttribute("aria-expanded", "false");
expect(mockOnReadResource).toHaveBeenCalledTimes(1);
});
});
}); });

16
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "@modelcontextprotocol/inspector", "name": "@modelcontextprotocol/inspector",
"version": "0.15.0", "version": "0.16.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@modelcontextprotocol/inspector", "name": "@modelcontextprotocol/inspector",
"version": "0.15.0", "version": "0.16.1",
"license": "MIT", "license": "MIT",
"workspaces": [ "workspaces": [
"client", "client",
@ -14,9 +14,9 @@
"cli" "cli"
], ],
"dependencies": { "dependencies": {
"@modelcontextprotocol/inspector-cli": "^0.15.0", "@modelcontextprotocol/inspector-cli": "^0.16.1",
"@modelcontextprotocol/inspector-client": "^0.15.0", "@modelcontextprotocol/inspector-client": "^0.16.1",
"@modelcontextprotocol/inspector-server": "^0.15.0", "@modelcontextprotocol/inspector-server": "^0.16.1",
"@modelcontextprotocol/sdk": "^1.13.1", "@modelcontextprotocol/sdk": "^1.13.1",
"concurrently": "^9.0.1", "concurrently": "^9.0.1",
"open": "^10.1.0", "open": "^10.1.0",
@ -44,7 +44,7 @@
}, },
"cli": { "cli": {
"name": "@modelcontextprotocol/inspector-cli", "name": "@modelcontextprotocol/inspector-cli",
"version": "0.15.0", "version": "0.16.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.13.1", "@modelcontextprotocol/sdk": "^1.13.1",
@ -67,7 +67,7 @@
}, },
"client": { "client": {
"name": "@modelcontextprotocol/inspector-client", "name": "@modelcontextprotocol/inspector-client",
"version": "0.15.0", "version": "0.16.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.13.1", "@modelcontextprotocol/sdk": "^1.13.1",
@ -10987,7 +10987,7 @@
}, },
"server": { "server": {
"name": "@modelcontextprotocol/inspector-server", "name": "@modelcontextprotocol/inspector-server",
"version": "0.15.0", "version": "0.16.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.13.1", "@modelcontextprotocol/sdk": "^1.13.1",

View file

@ -1,6 +1,6 @@
{ {
"name": "@modelcontextprotocol/inspector", "name": "@modelcontextprotocol/inspector",
"version": "0.15.0", "version": "0.16.1",
"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)",
@ -29,6 +29,9 @@
"clean": "rimraf ./node_modules ./client/node_modules ./cli/node_modules ./build ./client/dist ./server/build ./cli/build ./package-lock.json && npm install", "clean": "rimraf ./node_modules ./client/node_modules ./cli/node_modules ./build ./client/dist ./server/build ./cli/build ./package-lock.json && npm install",
"dev": "node client/bin/start.js --dev", "dev": "node client/bin/start.js --dev",
"dev:windows": "node client/bin/start.js --dev", "dev:windows": "node client/bin/start.js --dev",
"dev:sdk": "npm run link:sdk && concurrently \"npm run dev\" \"cd sdk && npm run build:esm:w\"",
"link:sdk": "(test -d sdk || ln -sf ${MCP_SDK:-$PWD/../typescript-sdk} sdk) && (cd sdk && npm link && (test -d node_modules || npm i)) && npm link @modelcontextprotocol/sdk",
"unlink:sdk": "(cd sdk && npm unlink -g) && rm sdk && npm unlink @modelcontextprotocol/sdk",
"start": "node client/bin/start.js", "start": "node client/bin/start.js",
"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",
@ -44,9 +47,9 @@
"check-version": "node scripts/check-version-consistency.js" "check-version": "node scripts/check-version-consistency.js"
}, },
"dependencies": { "dependencies": {
"@modelcontextprotocol/inspector-cli": "^0.15.0", "@modelcontextprotocol/inspector-cli": "^0.16.1",
"@modelcontextprotocol/inspector-client": "^0.15.0", "@modelcontextprotocol/inspector-client": "^0.16.1",
"@modelcontextprotocol/inspector-server": "^0.15.0", "@modelcontextprotocol/inspector-server": "^0.16.1",
"@modelcontextprotocol/sdk": "^1.13.1", "@modelcontextprotocol/sdk": "^1.13.1",
"concurrently": "^9.0.1", "concurrently": "^9.0.1",
"open": "^10.1.0", "open": "^10.1.0",

View file

@ -1,6 +1,6 @@
{ {
"name": "@modelcontextprotocol/inspector-server", "name": "@modelcontextprotocol/inspector-server",
"version": "0.15.0", "version": "0.16.1",
"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)",