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
|
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/
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:**
|
||||||
|
|
|
||||||
|
|
@ -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)",
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)",
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
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 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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -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
16
package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
11
package.json
11
package.json
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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)",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue