Merge branch 'main' into feat-support-enter-oauth-client-information-manually
This commit is contained in:
commit
91aacba85d
17 changed files with 483 additions and 120 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/
|
||||
|
||||
|
|
|
|||
14
README.md
14
README.md
|
|
@ -166,6 +166,12 @@ If you need to disable authentication (NOT RECOMMENDED), you can set the `DANGER
|
|||
DANGEROUSLY_OMIT_AUTH=true npm start
|
||||
```
|
||||
|
||||
You can also set the token via the `MCP_PROXY_AUTH_TOKEN` environment variable when starting the server:
|
||||
|
||||
```bash
|
||||
MCP_PROXY_AUTH_TOKEN=$(openssl rand -hex 32) npm start
|
||||
```
|
||||
|
||||
#### Local-only Binding
|
||||
|
||||
By default, both the MCP Inspector proxy server and client bind only to `localhost` to prevent network access. This ensures they are not accessible from other devices on the network. If you need to bind to all interfaces for development purposes, you can override this with the `HOST` environment variable:
|
||||
|
|
@ -254,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:**
|
||||
|
|
@ -303,7 +315,7 @@ npx @modelcontextprotocol/inspector --cli node build/index.js --method prompts/l
|
|||
npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com
|
||||
|
||||
# Connect to a remote MCP server (with Streamable HTTP transport)
|
||||
npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --transport http
|
||||
npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --transport http --method tools/list
|
||||
|
||||
# Call a tool on a remote server
|
||||
npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --method tools/call --tool-name remotetool --tool-arg param=value
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@modelcontextprotocol/inspector-cli",
|
||||
"version": "0.16.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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ async function startDevServer(serverOptions) {
|
|||
...process.env,
|
||||
SERVER_PORT,
|
||||
CLIENT_PORT,
|
||||
MCP_PROXY_TOKEN: sessionToken,
|
||||
MCP_PROXY_AUTH_TOKEN: sessionToken,
|
||||
MCP_ENV_VARS: JSON.stringify(envVars),
|
||||
},
|
||||
signal: abort.signal,
|
||||
|
|
@ -99,7 +99,7 @@ async function startProdServer(serverOptions) {
|
|||
...process.env,
|
||||
SERVER_PORT,
|
||||
CLIENT_PORT,
|
||||
MCP_PROXY_TOKEN: sessionToken,
|
||||
MCP_PROXY_AUTH_TOKEN: sessionToken,
|
||||
MCP_ENV_VARS: JSON.stringify(envVars),
|
||||
},
|
||||
signal: abort.signal,
|
||||
|
|
@ -247,8 +247,9 @@ async function main() {
|
|||
: "Starting MCP inspector...",
|
||||
);
|
||||
|
||||
// Generate session token for authentication
|
||||
const sessionToken = randomBytes(32).toString("hex");
|
||||
// Use provided token from environment or generate a new one
|
||||
const sessionToken =
|
||||
process.env.MCP_PROXY_AUTH_TOKEN || randomBytes(32).toString("hex");
|
||||
const authDisabled = !!process.env.DANGEROUSLY_OMIT_AUTH;
|
||||
|
||||
const abort = new AbortController();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@modelcontextprotocol/inspector-client",
|
||||
"version": "0.16.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[]>([]);
|
||||
|
|
@ -479,7 +482,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) => {
|
||||
|
|
@ -885,6 +893,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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ export function useConnection({
|
|||
const [requestHistory, setRequestHistory] = useState<
|
||||
{ request: string; response?: string }[]
|
||||
>([]);
|
||||
const [completionsSupported, setCompletionsSupported] = useState(true);
|
||||
const [completionsSupported, setCompletionsSupported] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!oauthClientId) {
|
||||
|
|
@ -530,7 +530,7 @@ export function useConnection({
|
|||
throw error;
|
||||
}
|
||||
setServerCapabilities(capabilities ?? null);
|
||||
setCompletionsSupported(true); // Reset completions support on new connection
|
||||
setCompletionsSupported(capabilities?.completions !== undefined);
|
||||
|
||||
if (onPendingRequest) {
|
||||
client.setRequestHandler(CreateMessageRequestSchema, (request) => {
|
||||
|
|
|
|||
16
package-lock.json
generated
16
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "@modelcontextprotocol/inspector",
|
||||
"version": "0.16.0",
|
||||
"version": "0.16.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@modelcontextprotocol/inspector",
|
||||
"version": "0.16.0",
|
||||
"version": "0.16.1",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"client",
|
||||
|
|
@ -14,9 +14,9 @@
|
|||
"cli"
|
||||
],
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/inspector-cli": "^0.16.0",
|
||||
"@modelcontextprotocol/inspector-client": "^0.16.0",
|
||||
"@modelcontextprotocol/inspector-server": "^0.16.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.16.0",
|
||||
"version": "0.16.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.13.1",
|
||||
|
|
@ -67,7 +67,7 @@
|
|||
},
|
||||
"client": {
|
||||
"name": "@modelcontextprotocol/inspector-client",
|
||||
"version": "0.16.0",
|
||||
"version": "0.16.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.13.1",
|
||||
|
|
@ -10987,7 +10987,7 @@
|
|||
},
|
||||
"server": {
|
||||
"name": "@modelcontextprotocol/inspector-server",
|
||||
"version": "0.16.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.16.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.16.0",
|
||||
"@modelcontextprotocol/inspector-client": "^0.16.0",
|
||||
"@modelcontextprotocol/inspector-server": "^0.16.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.16.0",
|
||||
"version": "0.16.1",
|
||||
"description": "Server-side application for the Model Context Protocol inspector",
|
||||
"license": "MIT",
|
||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ const serverTransports: Map<string, Transport> = new Map<string, Transport>(); /
|
|||
|
||||
// Use provided token from environment or generate a new one
|
||||
const sessionToken =
|
||||
process.env.MCP_PROXY_TOKEN || randomBytes(32).toString("hex");
|
||||
process.env.MCP_PROXY_AUTH_TOKEN || randomBytes(32).toString("hex");
|
||||
const authDisabled = !!process.env.DANGEROUSLY_OMIT_AUTH;
|
||||
|
||||
// Origin validation middleware to prevent DNS rebinding attacks
|
||||
|
|
|
|||
Loading…
Reference in a new issue