Merge branch 'main' into patch-1
This commit is contained in:
commit
5c15e50caa
14 changed files with 468 additions and 112 deletions
64
.github/workflows/claude.yml
vendored
Normal file
64
.github/workflows/claude.yml
vendored
Normal 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
3
.gitignore
vendored
|
|
@ -9,7 +9,8 @@ client/tsconfig.app.tsbuildinfo
|
|||
client/tsconfig.node.tsbuildinfo
|
||||
cli/build
|
||||
test-output
|
||||
# symlinked by `npm run link:sdk`:
|
||||
sdk
|
||||
client/playwright-report/
|
||||
client/results.json
|
||||
client/test-results/
|
||||
|
||||
|
|
|
|||
|
|
@ -260,6 +260,12 @@ Development mode:
|
|||
|
||||
```bash
|
||||
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:**
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@modelcontextprotocol/inspector-cli",
|
||||
"version": "0.15.0",
|
||||
"version": "0.16.1",
|
||||
"description": "CLI for the Model Context Protocol inspector",
|
||||
"license": "MIT",
|
||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||
|
|
|
|||
|
|
@ -50,28 +50,15 @@ function delay(ms: number): Promise<void> {
|
|||
}
|
||||
|
||||
async function runWebClient(args: Args): Promise<void> {
|
||||
const inspectorServerPath = resolve(
|
||||
__dirname,
|
||||
"../../",
|
||||
"server",
|
||||
"build",
|
||||
"index.js",
|
||||
);
|
||||
|
||||
// Path to the client entry point
|
||||
const inspectorClientPath = resolve(
|
||||
__dirname,
|
||||
"../../",
|
||||
"client",
|
||||
"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();
|
||||
let cancelled: boolean = false;
|
||||
process.on("SIGINT", () => {
|
||||
|
|
@ -79,42 +66,13 @@ async function runWebClient(args: Args): Promise<void> {
|
|||
abort.abort();
|
||||
});
|
||||
|
||||
let server: ReturnType<typeof spawnPromise>;
|
||||
let serverOk: unknown;
|
||||
|
||||
try {
|
||||
server = spawnPromise(
|
||||
"node",
|
||||
[
|
||||
inspectorServerPath,
|
||||
...(args.command ? [`--env`, args.command] : []),
|
||||
...(args.args ? [`--args=${args.args.join(" ")}`] : []),
|
||||
],
|
||||
{
|
||||
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;
|
||||
}
|
||||
await spawnPromise("node", [inspectorClientPath], {
|
||||
signal: abort.signal,
|
||||
echoOutput: true,
|
||||
});
|
||||
} catch (e) {
|
||||
if (!cancelled || process.env.DEBUG) throw e;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@modelcontextprotocol/inspector-client",
|
||||
"version": "0.15.0",
|
||||
"version": "0.16.1",
|
||||
"description": "Client-side application for the Model Context Protocol inspector",
|
||||
"license": "MIT",
|
||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||
|
|
|
|||
|
|
@ -80,6 +80,9 @@ const App = () => {
|
|||
ResourceTemplate[]
|
||||
>([]);
|
||||
const [resourceContent, setResourceContent] = useState<string>("");
|
||||
const [resourceContentMap, setResourceContentMap] = useState<
|
||||
Record<string, string>
|
||||
>({});
|
||||
const [prompts, setPrompts] = useState<Prompt[]>([]);
|
||||
const [promptContent, setPromptContent] = useState<string>("");
|
||||
const [tools, setTools] = useState<Tool[]>([]);
|
||||
|
|
@ -461,7 +464,12 @@ const App = () => {
|
|||
ReadResourceResultSchema,
|
||||
"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) => {
|
||||
|
|
@ -863,6 +871,11 @@ const App = () => {
|
|||
toolResult={toolResult}
|
||||
nextCursor={nextToolCursor}
|
||||
error={errors.tools}
|
||||
resourceContent={resourceContentMap}
|
||||
onReadResource={(uri: string) => {
|
||||
clearError("resources");
|
||||
readResource(uri);
|
||||
}}
|
||||
/>
|
||||
<ConsoleTab />
|
||||
<PingTab
|
||||
|
|
|
|||
112
client/src/components/ResourceLinkView.tsx
Normal file
112
client/src/components/ResourceLinkView.tsx
Normal 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;
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import JsonView from "./JsonView";
|
||||
import ResourceLinkView from "./ResourceLinkView";
|
||||
import {
|
||||
CallToolResultSchema,
|
||||
CompatibilityCallToolResult,
|
||||
|
|
@ -9,6 +10,8 @@ import { validateToolOutput, hasOutputSchema } from "@/utils/schemaUtils";
|
|||
interface ToolResultsProps {
|
||||
toolResult: CompatibilityCallToolResult | null;
|
||||
selectedTool: Tool | null;
|
||||
resourceContent: Record<string, string>;
|
||||
onReadResource?: (uri: string) => void;
|
||||
}
|
||||
|
||||
const checkContentCompatibility = (
|
||||
|
|
@ -19,49 +22,54 @@ const checkContentCompatibility = (
|
|||
[key: string]: unknown;
|
||||
}>,
|
||||
): { isCompatible: boolean; message: string } => {
|
||||
if (
|
||||
unstructuredContent.length !== 1 ||
|
||||
unstructuredContent[0].type !== "text"
|
||||
) {
|
||||
// Look for at least one text content block that matches the structured content
|
||||
const textBlocks = unstructuredContent.filter(
|
||||
(block) => block.type === "text",
|
||||
);
|
||||
|
||||
if (textBlocks.length === 0) {
|
||||
return {
|
||||
isCompatible: false,
|
||||
message: "Unstructured content is not a single text block",
|
||||
message: "No text blocks to match structured content",
|
||||
};
|
||||
}
|
||||
|
||||
const textContent = unstructuredContent[0].text;
|
||||
if (!textContent) {
|
||||
return {
|
||||
isCompatible: false,
|
||||
message: "Text content is empty",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedContent = JSON.parse(textContent);
|
||||
const isEqual =
|
||||
JSON.stringify(parsedContent) === JSON.stringify(structuredContent);
|
||||
|
||||
if (isEqual) {
|
||||
return {
|
||||
isCompatible: true,
|
||||
message: "Unstructured content matches structured content",
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
isCompatible: false,
|
||||
message: "Parsed JSON does not match structured content",
|
||||
};
|
||||
// Check if any text block contains JSON that matches the structured content
|
||||
for (const textBlock of textBlocks) {
|
||||
const textContent = textBlock.text;
|
||||
if (!textContent) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedContent = JSON.parse(textContent);
|
||||
const isEqual =
|
||||
JSON.stringify(parsedContent) === JSON.stringify(structuredContent);
|
||||
|
||||
if (isEqual) {
|
||||
return {
|
||||
isCompatible: true,
|
||||
message: `Structured content matches text block${textBlocks.length > 1 ? " (multiple blocks)" : ""}${unstructuredContent.length > textBlocks.length ? " + other content" : ""}`,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Continue to next text block if this one doesn't parse as JSON
|
||||
continue;
|
||||
}
|
||||
} 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 ("content" in toolResult) {
|
||||
|
|
@ -200,6 +208,16 @@ const ToolResults = ({ toolResult, selectedTool }: ToolResultsProps) => {
|
|||
) : (
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ const ToolsTab = ({
|
|||
setSelectedTool,
|
||||
toolResult,
|
||||
nextCursor,
|
||||
resourceContent,
|
||||
onReadResource,
|
||||
}: {
|
||||
tools: Tool[];
|
||||
listTools: () => void;
|
||||
|
|
@ -38,6 +40,8 @@ const ToolsTab = ({
|
|||
toolResult: CompatibilityCallToolResult | null;
|
||||
nextCursor: ListToolsResult["nextCursor"];
|
||||
error: string | null;
|
||||
resourceContent: Record<string, string>;
|
||||
onReadResource?: (uri: string) => void;
|
||||
}) => {
|
||||
const [params, setParams] = useState<Record<string, unknown>>({});
|
||||
const [isToolRunning, setIsToolRunning] = useState(false);
|
||||
|
|
@ -267,6 +271,8 @@ const ToolsTab = ({
|
|||
<ToolResults
|
||||
toolResult={toolResult}
|
||||
selectedTool={selectedTool}
|
||||
resourceContent={resourceContent}
|
||||
onReadResource={onReadResource}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ describe("ToolsTab", () => {
|
|||
toolResult: null,
|
||||
nextCursor: "",
|
||||
error: null,
|
||||
resourceContent: {},
|
||||
onReadResource: jest.fn(),
|
||||
};
|
||||
|
||||
const renderToolsTab = (props = {}) => {
|
||||
|
|
@ -249,10 +251,12 @@ describe("ToolsTab", () => {
|
|||
},
|
||||
};
|
||||
|
||||
it("should display structured content when present", () => {
|
||||
// Cache the tool's output schema so hasOutputSchema returns true
|
||||
beforeEach(() => {
|
||||
// Cache the tool's output schema before each test
|
||||
cacheToolOutputSchemas([toolWithOutputSchema]);
|
||||
});
|
||||
|
||||
it("should display structured content when present", () => {
|
||||
const structuredResult = {
|
||||
content: [],
|
||||
structuredContent: {
|
||||
|
|
@ -261,6 +265,7 @@ describe("ToolsTab", () => {
|
|||
};
|
||||
|
||||
renderToolsTab({
|
||||
tools: [toolWithOutputSchema],
|
||||
selectedTool: toolWithOutputSchema,
|
||||
toolResult: structuredResult,
|
||||
});
|
||||
|
|
@ -272,8 +277,6 @@ describe("ToolsTab", () => {
|
|||
});
|
||||
|
||||
it("should show validation error for invalid structured content", () => {
|
||||
cacheToolOutputSchemas([toolWithOutputSchema]);
|
||||
|
||||
const invalidResult = {
|
||||
content: [],
|
||||
structuredContent: {
|
||||
|
|
@ -282,6 +285,7 @@ describe("ToolsTab", () => {
|
|||
};
|
||||
|
||||
renderToolsTab({
|
||||
tools: [toolWithOutputSchema],
|
||||
selectedTool: toolWithOutputSchema,
|
||||
toolResult: invalidResult,
|
||||
});
|
||||
|
|
@ -290,14 +294,13 @@ describe("ToolsTab", () => {
|
|||
});
|
||||
|
||||
it("should show error when tool with output schema doesn't return structured content", () => {
|
||||
cacheToolOutputSchemas([toolWithOutputSchema]);
|
||||
|
||||
const resultWithoutStructured = {
|
||||
content: [{ type: "text", text: "some result" }],
|
||||
// No structuredContent
|
||||
};
|
||||
|
||||
renderToolsTab({
|
||||
tools: [toolWithOutputSchema],
|
||||
selectedTool: toolWithOutputSchema,
|
||||
toolResult: resultWithoutStructured,
|
||||
});
|
||||
|
|
@ -310,14 +313,13 @@ describe("ToolsTab", () => {
|
|||
});
|
||||
|
||||
it("should show unstructured content title when both structured and unstructured exist", () => {
|
||||
cacheToolOutputSchemas([toolWithOutputSchema]);
|
||||
|
||||
const resultWithBoth = {
|
||||
content: [{ type: "text", text: '{"temperature": 25}' }],
|
||||
structuredContent: { temperature: 25 },
|
||||
};
|
||||
|
||||
renderToolsTab({
|
||||
tools: [toolWithOutputSchema],
|
||||
selectedTool: toolWithOutputSchema,
|
||||
toolResult: resultWithBoth,
|
||||
});
|
||||
|
|
@ -342,26 +344,100 @@ describe("ToolsTab", () => {
|
|||
});
|
||||
|
||||
it("should show compatibility check when tool has output schema", () => {
|
||||
cacheToolOutputSchemas([toolWithOutputSchema]);
|
||||
|
||||
const compatibleResult = {
|
||||
content: [{ type: "text", text: '{"temperature": 25}' }],
|
||||
structuredContent: { temperature: 25 },
|
||||
};
|
||||
|
||||
renderToolsTab({
|
||||
tools: [toolWithOutputSchema],
|
||||
selectedTool: toolWithOutputSchema,
|
||||
toolResult: compatibleResult,
|
||||
});
|
||||
|
||||
// Should show compatibility result
|
||||
expect(
|
||||
screen.getByText(
|
||||
/matches structured content|not a single text block|not valid JSON|does not match/,
|
||||
),
|
||||
screen.getByText(/structured content matches/i),
|
||||
).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", () => {
|
||||
const resultWithBoth = {
|
||||
content: [{ type: "text", text: '{"data": "value"}' }],
|
||||
|
|
@ -376,9 +452,108 @@ describe("ToolsTab", () => {
|
|||
// Should not show any compatibility messages
|
||||
expect(
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
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
16
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "@modelcontextprotocol/inspector",
|
||||
"version": "0.15.0",
|
||||
"version": "0.16.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@modelcontextprotocol/inspector",
|
||||
"version": "0.15.0",
|
||||
"version": "0.16.1",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"client",
|
||||
|
|
@ -14,9 +14,9 @@
|
|||
"cli"
|
||||
],
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/inspector-cli": "^0.15.0",
|
||||
"@modelcontextprotocol/inspector-client": "^0.15.0",
|
||||
"@modelcontextprotocol/inspector-server": "^0.15.0",
|
||||
"@modelcontextprotocol/inspector-cli": "^0.16.1",
|
||||
"@modelcontextprotocol/inspector-client": "^0.16.1",
|
||||
"@modelcontextprotocol/inspector-server": "^0.16.1",
|
||||
"@modelcontextprotocol/sdk": "^1.13.1",
|
||||
"concurrently": "^9.0.1",
|
||||
"open": "^10.1.0",
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
},
|
||||
"cli": {
|
||||
"name": "@modelcontextprotocol/inspector-cli",
|
||||
"version": "0.15.0",
|
||||
"version": "0.16.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.13.1",
|
||||
|
|
@ -67,7 +67,7 @@
|
|||
},
|
||||
"client": {
|
||||
"name": "@modelcontextprotocol/inspector-client",
|
||||
"version": "0.15.0",
|
||||
"version": "0.16.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.13.1",
|
||||
|
|
@ -10987,7 +10987,7 @@
|
|||
},
|
||||
"server": {
|
||||
"name": "@modelcontextprotocol/inspector-server",
|
||||
"version": "0.15.0",
|
||||
"version": "0.16.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.13.1",
|
||||
|
|
|
|||
11
package.json
11
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@modelcontextprotocol/inspector",
|
||||
"version": "0.15.0",
|
||||
"version": "0.16.1",
|
||||
"description": "Model Context Protocol inspector",
|
||||
"license": "MIT",
|
||||
"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",
|
||||
"dev": "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-server": "cd server && npm run start",
|
||||
"start-client": "cd client && npm run preview",
|
||||
|
|
@ -44,9 +47,9 @@
|
|||
"check-version": "node scripts/check-version-consistency.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/inspector-cli": "^0.15.0",
|
||||
"@modelcontextprotocol/inspector-client": "^0.15.0",
|
||||
"@modelcontextprotocol/inspector-server": "^0.15.0",
|
||||
"@modelcontextprotocol/inspector-cli": "^0.16.1",
|
||||
"@modelcontextprotocol/inspector-client": "^0.16.1",
|
||||
"@modelcontextprotocol/inspector-server": "^0.16.1",
|
||||
"@modelcontextprotocol/sdk": "^1.13.1",
|
||||
"concurrently": "^9.0.1",
|
||||
"open": "^10.1.0",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@modelcontextprotocol/inspector-server",
|
||||
"version": "0.15.0",
|
||||
"version": "0.16.1",
|
||||
"description": "Server-side application for the Model Context Protocol inspector",
|
||||
"license": "MIT",
|
||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||
|
|
|
|||
Loading…
Reference in a new issue