Merge branch 'main' into fix/validate-json-on-tool-execution

This commit is contained in:
Ola Hungerford 2025-07-06 20:34:42 -07:00 • committed by GitHub
commit cbb9331643
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 264 additions and 22 deletions

View file

@ -21,8 +21,10 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
id: setup_node
with:
node-version: 18
node-version-file: package.json
cache: npm
# Cache Playwright browsers
- name: Cache Playwright browsers
@ -69,7 +71,7 @@ jobs:
job-summary: true
icon-style: "emojis"
custom-info: |
**Test Environment:** Ubuntu Latest, Node.js 18
**Test Environment:** Ubuntu Latest, Node.js ${{ steps.setup_node.outputs.node-version }}
**Browsers:** Chromium, Firefox
📊 [View Detailed HTML Report](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) (download artifacts)

View file

@ -19,7 +19,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 18
node-version-file: package.json
cache: npm
# Working around https://github.com/npm/cli/issues/4828
@ -53,7 +53,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
node-version-file: package.json
cache: npm
registry-url: "https://registry.npmjs.org"

1
.node-version Normal file
View file

@ -0,0 +1 @@
22.x.x

View file

@ -7,7 +7,7 @@
<title>MCP Inspector</title>
</head>
<body>
<div id="root"></div>
<div id="root" class="w-full"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View file

@ -1,7 +1,3 @@
#root {
margin: 0 auto;
}
.logo {
height: 6em;
padding: 1.5em;

View file

@ -52,7 +52,7 @@ import { z } from "zod";
import "./App.css";
import AuthDebugger from "./components/AuthDebugger";
import ConsoleTab from "./components/ConsoleTab";
import HistoryAndNotifications from "./components/History";
import HistoryAndNotifications from "./components/HistoryAndNotifications";
import PingTab from "./components/PingTab";
import PromptsTab, { Prompt } from "./components/PromptsTab";
import ResourcesTab from "./components/ResourcesTab";

View file

@ -0,0 +1,155 @@
import { ServerNotification } from "@modelcontextprotocol/sdk/types.js";
import { useState } from "react";
import JsonView from "./JsonView";
const HistoryAndNotifications = ({
requestHistory,
serverNotifications,
}: {
requestHistory: Array<{ request: string; response?: string }>;
serverNotifications: ServerNotification[];
}) => {
const [expandedRequests, setExpandedRequests] = useState<{
[key: number]: boolean;
}>({});
const [expandedNotifications, setExpandedNotifications] = useState<{
[key: number]: boolean;
}>({});
const toggleRequestExpansion = (index: number) => {
setExpandedRequests((prev) => ({ ...prev, [index]: !prev[index] }));
};
const toggleNotificationExpansion = (index: number) => {
setExpandedNotifications((prev) => ({ ...prev, [index]: !prev[index] }));
};
return (
<div className="bg-card overflow-hidden flex h-full">
<div className="flex-1 overflow-y-auto p-4 border-r">
<h2 className="text-lg font-semibold mb-4">History</h2>
{requestHistory.length === 0 ? (
<p className="text-sm text-gray-500 dark:text-gray-400 italic">
No history yet
</p>
) : (
<ul className="space-y-3">
{requestHistory
.slice()
.reverse()
.map((request, index) => (
<li
key={index}
className="text-sm text-foreground bg-secondary py-2 px-3 rounded"
>
<div
className="flex justify-between items-center cursor-pointer"
onClick={() =>
toggleRequestExpansion(requestHistory.length - 1 - index)
}
>
<span className="font-mono">
{requestHistory.length - index}.{" "}
{JSON.parse(request.request).method}
</span>
<span>
{expandedRequests[requestHistory.length - 1 - index]
? "▼"
: "▶"}
</span>
</div>
{expandedRequests[requestHistory.length - 1 - index] && (
<>
<div className="mt-2">
<div className="flex justify-between items-center mb-1">
<span className="font-semibold text-blue-600">
Request:
</span>
</div>
<JsonView
data={request.request}
className="bg-background"
/>
</div>
{request.response && (
<div className="mt-2">
<div className="flex justify-between items-center mb-1">
<span className="font-semibold text-green-600">
Response:
</span>
</div>
<JsonView
data={request.response}
className="bg-background"
/>
</div>
)}
</>
)}
</li>
))}
</ul>
)}
</div>
<div className="flex-1 overflow-y-auto p-4">
<h2 className="text-lg font-semibold mb-4">Server Notifications</h2>
{serverNotifications.length === 0 ? (
<p className="text-sm text-gray-500 dark:text-gray-400 italic">
No notifications yet
</p>
) : (
<ul className="space-y-3">
{serverNotifications
.slice()
.reverse()
.map((notification, index) => (
<li
key={index}
className="text-sm text-foreground bg-secondary py-2 px-3 rounded"
>
<div
className="flex justify-between items-center cursor-pointer"
onClick={() =>
toggleNotificationExpansion(
serverNotifications.length - 1 - index,
)
}
>
<span className="font-mono">
{serverNotifications.length - index}.{" "}
{notification.method}
</span>
<span>
{expandedNotifications[
serverNotifications.length - 1 - index
]
? "▼"
: "▶"}
</span>
</div>
{expandedNotifications[
serverNotifications.length - 1 - index
] && (
<div className="mt-2">
<div className="flex justify-between items-center mb-1">
<span className="font-semibold text-purple-600">
Details:
</span>
</div>
<JsonView
data={JSON.stringify(notification, null, 2)}
className="bg-background"
/>
</div>
)}
</li>
))}
</ul>
)}
</div>
</div>
);
};
export default HistoryAndNotifications;

View file

@ -98,12 +98,12 @@ const PromptsTab = ({
setPromptArgs({});
}}
renderItem={(prompt) => (
<>
<div className="flex flex-col items-start">
<span className="flex-1">{prompt.name}</span>
<span className="text-sm text-gray-500">
<span className="text-sm text-gray-500 text-left">
{prompt.description}
</span>
</>
</div>
)}
title="Prompts"
buttonText={nextCursor ? "List More Prompts" : "List Prompts"}

View file

@ -0,0 +1,226 @@
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, jest } from "@jest/globals";
import HistoryAndNotifications from "../HistoryAndNotifications";
import { ServerNotification } from "@modelcontextprotocol/sdk/types.js";
// Mock JsonView component
jest.mock("../JsonView", () => {
return function JsonView({ data }: { data: string }) {
return <div data-testid="json-view">{data}</div>;
};
});
describe("HistoryAndNotifications", () => {
const mockRequestHistory = [
{
request: JSON.stringify({ method: "test/method1", params: {} }),
response: JSON.stringify({ result: "success" }),
},
{
request: JSON.stringify({ method: "test/method2", params: {} }),
response: JSON.stringify({ result: "success" }),
},
];
const mockNotifications: ServerNotification[] = [
{
method: "notifications/message",
params: {
level: "info" as const,
message: "First notification",
},
},
{
method: "notifications/progress",
params: {
progressToken: "test-token",
progress: 50,
message: "Second notification",
},
},
];
it("renders history and notifications sections", () => {
render(
<HistoryAndNotifications
requestHistory={mockRequestHistory}
serverNotifications={mockNotifications}
/>,
);
expect(screen.getByText("History")).toBeTruthy();
expect(screen.getByText("Server Notifications")).toBeTruthy();
});
it("displays request history items with correct numbering", () => {
render(
<HistoryAndNotifications
requestHistory={mockRequestHistory}
serverNotifications={[]}
/>,
);
// Items should be numbered in reverse order (newest first)
expect(screen.getByText("2. test/method2")).toBeTruthy();
expect(screen.getByText("1. test/method1")).toBeTruthy();
});
it("displays server notifications with correct numbering", () => {
render(
<HistoryAndNotifications
requestHistory={[]}
serverNotifications={mockNotifications}
/>,
);
// Items should be numbered in reverse order (newest first)
expect(screen.getByText("2. notifications/progress")).toBeTruthy();
expect(screen.getByText("1. notifications/message")).toBeTruthy();
});
it("expands and collapses request items when clicked", () => {
render(
<HistoryAndNotifications
requestHistory={mockRequestHistory}
serverNotifications={[]}
/>,
);
const firstRequestHeader = screen.getByText("2. test/method2");
// Initially collapsed - should show ▶ arrows (there are multiple)
expect(screen.getAllByText("▶")).toHaveLength(2);
expect(screen.queryByText("Request:")).toBeNull();
// Click to expand
fireEvent.click(firstRequestHeader);
// Should now be expanded - one ▼ and one ▶
expect(screen.getByText("▼")).toBeTruthy();
expect(screen.getAllByText("▶")).toHaveLength(1);
expect(screen.getByText("Request:")).toBeTruthy();
expect(screen.getByText("Response:")).toBeTruthy();
});
it("expands and collapses notification items when clicked", () => {
render(
<HistoryAndNotifications
requestHistory={[]}
serverNotifications={mockNotifications}
/>,
);
const firstNotificationHeader = screen.getByText(
"2. notifications/progress",
);
// Initially collapsed
expect(screen.getAllByText("▶")).toHaveLength(2);
expect(screen.queryByText("Details:")).toBeNull();
// Click to expand
fireEvent.click(firstNotificationHeader);
// Should now be expanded
expect(screen.getByText("▼")).toBeTruthy();
expect(screen.getAllByText("▶")).toHaveLength(1);
expect(screen.getByText("Details:")).toBeTruthy();
});
it("maintains expanded state when new notifications are added", () => {
const { rerender } = render(
<HistoryAndNotifications
requestHistory={[]}
serverNotifications={mockNotifications}
/>,
);
// Find and expand the older notification (should be "1. notifications/message")
const olderNotificationHeader = screen.getByText(
"1. notifications/message",
);
fireEvent.click(olderNotificationHeader);
// Verify it's expanded
expect(screen.getByText("Details:")).toBeTruthy();
// Add a new notification at the beginning (simulating real behavior)
const newNotifications: ServerNotification[] = [
{
method: "notifications/resources/updated",
params: { uri: "file://test.txt" },
},
...mockNotifications,
];
// Re-render with new notifications
rerender(
<HistoryAndNotifications
requestHistory={[]}
serverNotifications={newNotifications}
/>,
);
// The original notification should still be expanded
// It's now numbered as "2. notifications/message" due to the new item
expect(screen.getByText("3. notifications/progress")).toBeTruthy();
expect(screen.getByText("2. notifications/message")).toBeTruthy();
expect(screen.getByText("1. notifications/resources/updated")).toBeTruthy();
// The originally expanded notification should still show its details
expect(screen.getByText("Details:")).toBeTruthy();
});
it("maintains expanded state when new requests are added", () => {
const { rerender } = render(
<HistoryAndNotifications
requestHistory={mockRequestHistory}
serverNotifications={[]}
/>,
);
// Find and expand the older request (should be "1. test/method1")
const olderRequestHeader = screen.getByText("1. test/method1");
fireEvent.click(olderRequestHeader);
// Verify it's expanded
expect(screen.getByText("Request:")).toBeTruthy();
expect(screen.getByText("Response:")).toBeTruthy();
// Add a new request at the beginning
const newRequestHistory = [
{
request: JSON.stringify({ method: "test/new_method", params: {} }),
response: JSON.stringify({ result: "new success" }),
},
...mockRequestHistory,
];
// Re-render with new request history
rerender(
<HistoryAndNotifications
requestHistory={newRequestHistory}
serverNotifications={[]}
/>,
);
// The original request should still be expanded
// It's now numbered as "2. test/method1" due to the new item
expect(screen.getByText("3. test/method2")).toBeTruthy();
expect(screen.getByText("2. test/method1")).toBeTruthy();
expect(screen.getByText("1. test/new_method")).toBeTruthy();
// The originally expanded request should still show its details
expect(screen.getByText("Request:")).toBeTruthy();
expect(screen.getByText("Response:")).toBeTruthy();
});
it("displays empty state messages when no data is available", () => {
render(
<HistoryAndNotifications requestHistory={[]} serverNotifications={[]} />,
);
expect(screen.getByText("No history yet")).toBeTruthy();
expect(screen.getByText("No notifications yet")).toBeTruthy();
});
});

View file

@ -14,8 +14,9 @@ const getSearchParam = (key: string): string | null => {
};
export const getMCPProxyAddress = (config: InspectorConfig): string => {
const proxyFullAddress = config.MCP_PROXY_FULL_ADDRESS.value as string;
let proxyFullAddress = config.MCP_PROXY_FULL_ADDRESS.value as string;
if (proxyFullAddress) {
proxyFullAddress = proxyFullAddress.replace(/\/+$/, "");
return proxyFullAddress;
}

3
package-lock.json generated
View file

@ -37,6 +37,9 @@
"prettier": "3.3.3",
"rimraf": "^6.0.1",
"typescript": "^5.4.2"
},
"engines": {
"node": ">=22.7.5"
}
},
"cli": {

View file

@ -64,5 +64,8 @@
"prettier": "3.3.3",
"rimraf": "^6.0.1",
"typescript": "^5.4.2"
},
"engines": {
"node": ">=22.7.5"
}
}

View file

@ -6,12 +6,10 @@ function onClientError(error: Error) {
}
function onServerError(error: Error) {
if (
(error?.message &&
error.message.includes("Error POSTing to endpoint (HTTP 404)")) ||
(error?.cause && JSON.stringify(error.cause).includes("ECONNREFUSED"))
) {
if (error?.cause && JSON.stringify(error.cause).includes("ECONNREFUSED")) {
console.error("Connection refused. Is the MCP server running?");
} else if (error.message && error.message.includes("404")) {
console.error("Error accessing endpoint (HTTP 404)");
} else {
console.error("Error from MCP server:", error);
}