Compare commits
9 Commits
ashwin/tes
...
devin/1737
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7e1e9ac06 | ||
|
|
f398d39f4b | ||
|
|
98e6f0e5ec | ||
|
|
ec150eb8b4 | ||
|
|
052de8690d | ||
|
|
a976aefb39 | ||
|
|
5a5873277c | ||
|
|
715936d747 | ||
|
|
d973f58bef |
3
.github/workflows/main.yml
vendored
3
.github/workflows/main.yml
vendored
@@ -14,6 +14,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Check formatting
|
||||||
|
run: npx prettier --check .
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,5 @@
|
|||||||
node_modules
|
node_modules
|
||||||
server/build
|
server/build
|
||||||
client/dist
|
client/dist
|
||||||
client/coverage
|
|
||||||
client/tsconfig.app.tsbuildinfo
|
client/tsconfig.app.tsbuildinfo
|
||||||
client/tsconfig.node.tsbuildinfo
|
client/tsconfig.node.tsbuildinfo
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
packages
|
packages
|
||||||
server/build
|
server/build
|
||||||
|
CODE_OF_CONDUCT.md
|
||||||
|
SECURITY.md
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -14,10 +14,20 @@ To inspect an MCP server implementation, there's no need to clone this repo. Ins
|
|||||||
npx @modelcontextprotocol/inspector build/index.js
|
npx @modelcontextprotocol/inspector build/index.js
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also pass arguments along which will get passed as arguments to your MCP server:
|
You can pass both arguments and environment variables to your MCP server. Arguments are passed directly to your server, while environment variables can be set using the `-e` flag:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
npx @modelcontextprotocol/inspector build/index.js arg1 arg2 ...
|
# Pass arguments only
|
||||||
|
npx @modelcontextprotocol/inspector build/index.js arg1 arg2
|
||||||
|
|
||||||
|
# Pass environment variables only
|
||||||
|
npx @modelcontextprotocol/inspector -e KEY=value -e KEY2=$VALUE2 build/index.js
|
||||||
|
|
||||||
|
# Pass both environment variables and arguments
|
||||||
|
npx @modelcontextprotocol/inspector -e KEY=value -e KEY2=$VALUE2 build/index.js arg1 arg2
|
||||||
|
|
||||||
|
# Use -- to separate inspector flags from server arguments
|
||||||
|
npx @modelcontextprotocol/inspector -e KEY=$VALUE -- build/index.js -e server-flag
|
||||||
```
|
```
|
||||||
|
|
||||||
The inspector runs both a client UI (default port 5173) and an MCP proxy server (default port 3000). Open the client UI in your browser to use the inspector. You can customize the ports if needed:
|
The inspector runs both a client UI (default port 5173) and an MCP proxy server (default port 3000). Open the client UI in your browser to use the inspector. You can customize the ports if needed:
|
||||||
|
|||||||
34
bin/cli.js
34
bin/cli.js
@@ -11,8 +11,32 @@ function delay(ms) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
// Get command line arguments
|
// Parse command line arguments
|
||||||
const [, , command, ...mcpServerArgs] = process.argv;
|
const args = process.argv.slice(2);
|
||||||
|
const envVars = {};
|
||||||
|
const mcpServerArgs = [];
|
||||||
|
let command = null;
|
||||||
|
let parsingFlags = true;
|
||||||
|
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i];
|
||||||
|
|
||||||
|
if (parsingFlags && arg === "--") {
|
||||||
|
parsingFlags = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsingFlags && arg === "-e" && i + 1 < args.length) {
|
||||||
|
const [key, value] = args[++i].split("=");
|
||||||
|
if (key && value) {
|
||||||
|
envVars[key] = value;
|
||||||
|
}
|
||||||
|
} else if (!command) {
|
||||||
|
command = arg;
|
||||||
|
} else {
|
||||||
|
mcpServerArgs.push(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const inspectorServerPath = resolve(
|
const inspectorServerPath = resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
@@ -52,7 +76,11 @@ async function main() {
|
|||||||
...(mcpServerArgs ? [`--args=${mcpServerArgs.join(" ")}`] : []),
|
...(mcpServerArgs ? [`--args=${mcpServerArgs.join(" ")}`] : []),
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
env: { ...process.env, PORT: SERVER_PORT },
|
env: {
|
||||||
|
...process.env,
|
||||||
|
PORT: SERVER_PORT,
|
||||||
|
MCP_ENV_VARS: JSON.stringify(envVars),
|
||||||
|
},
|
||||||
signal: abort.signal,
|
signal: abort.signal,
|
||||||
echoOutput: true,
|
echoOutput: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,9 +18,7 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview"
|
||||||
"test": "vitest",
|
|
||||||
"test:coverage": "vitest run --coverage"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.0.3",
|
"@modelcontextprotocol/sdk": "^1.0.3",
|
||||||
@@ -42,25 +40,20 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.11.1",
|
"@eslint/js": "^9.11.1",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
|
||||||
"@testing-library/react": "^16.1.0",
|
|
||||||
"@types/node": "^22.7.5",
|
"@types/node": "^22.7.5",
|
||||||
"@types/react": "^18.3.10",
|
"@types/react": "^18.3.10",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@types/serve-handler": "^6.1.4",
|
"@types/serve-handler": "^6.1.4",
|
||||||
"@vitejs/plugin-react": "^4.3.2",
|
"@vitejs/plugin-react": "^4.3.2",
|
||||||
"@vitest/coverage-v8": "^2.1.8",
|
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^9.11.1",
|
"eslint": "^9.11.1",
|
||||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.12",
|
"eslint-plugin-react-refresh": "^0.4.12",
|
||||||
"globals": "^15.9.0",
|
"globals": "^15.9.0",
|
||||||
"jsdom": "^25.0.1",
|
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"tailwindcss": "^3.4.13",
|
"tailwindcss": "^3.4.13",
|
||||||
"typescript": "^5.5.3",
|
"typescript": "^5.5.3",
|
||||||
"typescript-eslint": "^8.7.0",
|
"typescript-eslint": "^8.7.0",
|
||||||
"vite": "^5.4.8",
|
"vite": "^5.4.8"
|
||||||
"vitest": "^2.1.8"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,220 +0,0 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
||||||
import { render, screen, fireEvent, act } from "@testing-library/react";
|
|
||||||
import App from "../App";
|
|
||||||
import { useConnection } from "../lib/hooks/useConnection";
|
|
||||||
import { useDraggablePane } from "../lib/hooks/useDraggablePane";
|
|
||||||
|
|
||||||
// Mock URL params
|
|
||||||
const mockURLSearchParams = vi.fn();
|
|
||||||
vi.stubGlobal("URLSearchParams", mockURLSearchParams);
|
|
||||||
|
|
||||||
// Mock the hooks
|
|
||||||
vi.mock("../lib/hooks/useConnection", () => ({
|
|
||||||
useConnection: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../lib/hooks/useDraggablePane", () => ({
|
|
||||||
useDraggablePane: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock fetch for config
|
|
||||||
const mockFetch = vi.fn();
|
|
||||||
global.fetch = mockFetch;
|
|
||||||
|
|
||||||
describe("App", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
// Reset all mocks
|
|
||||||
vi.clearAllMocks();
|
|
||||||
|
|
||||||
// Mock URL params
|
|
||||||
mockURLSearchParams.mockReturnValue({
|
|
||||||
get: () => "3000",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mock fetch response
|
|
||||||
mockFetch.mockResolvedValue({
|
|
||||||
json: () =>
|
|
||||||
Promise.resolve({
|
|
||||||
defaultEnvironment: {},
|
|
||||||
defaultCommand: "test-command",
|
|
||||||
defaultArgs: "--test",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mock useConnection hook
|
|
||||||
const mockUseConnection = useConnection as jest.Mock;
|
|
||||||
mockUseConnection.mockReturnValue({
|
|
||||||
connectionStatus: "disconnected",
|
|
||||||
serverCapabilities: null,
|
|
||||||
mcpClient: null,
|
|
||||||
requestHistory: [],
|
|
||||||
makeRequest: vi.fn(),
|
|
||||||
sendNotification: vi.fn(),
|
|
||||||
connect: vi.fn(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mock useDraggablePane hook
|
|
||||||
const mockUseDraggablePane = useDraggablePane as jest.Mock;
|
|
||||||
mockUseDraggablePane.mockReturnValue({
|
|
||||||
height: 300,
|
|
||||||
handleDragStart: vi.fn(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders initial disconnected state", async () => {
|
|
||||||
await act(async () => {
|
|
||||||
render(<App />);
|
|
||||||
});
|
|
||||||
expect(
|
|
||||||
screen.getByText("Connect to an MCP server to start inspecting"),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("loads config on mount", async () => {
|
|
||||||
await act(async () => {
|
|
||||||
render(<App />);
|
|
||||||
});
|
|
||||||
expect(mockFetch).toHaveBeenCalledWith("http://localhost:3000/config");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows connected interface when mcpClient is available", async () => {
|
|
||||||
const mockUseConnection = useConnection as jest.Mock;
|
|
||||||
mockUseConnection.mockReturnValue({
|
|
||||||
connectionStatus: "connected",
|
|
||||||
serverCapabilities: {
|
|
||||||
resources: true,
|
|
||||||
prompts: true,
|
|
||||||
tools: true,
|
|
||||||
},
|
|
||||||
mcpClient: {},
|
|
||||||
requestHistory: [],
|
|
||||||
makeRequest: vi.fn(),
|
|
||||||
sendNotification: vi.fn(),
|
|
||||||
connect: vi.fn(),
|
|
||||||
});
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
render(<App />);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use more specific selectors
|
|
||||||
const resourcesTab = screen.getByRole("tab", { name: /resources/i });
|
|
||||||
const promptsTab = screen.getByRole("tab", { name: /prompts/i });
|
|
||||||
const toolsTab = screen.getByRole("tab", { name: /tools/i });
|
|
||||||
|
|
||||||
expect(resourcesTab).toBeInTheDocument();
|
|
||||||
expect(promptsTab).toBeInTheDocument();
|
|
||||||
expect(toolsTab).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("disables tabs based on server capabilities", async () => {
|
|
||||||
const mockUseConnection = useConnection as jest.Mock;
|
|
||||||
mockUseConnection.mockReturnValue({
|
|
||||||
connectionStatus: "connected",
|
|
||||||
serverCapabilities: {
|
|
||||||
resources: false,
|
|
||||||
prompts: true,
|
|
||||||
tools: false,
|
|
||||||
},
|
|
||||||
mcpClient: {},
|
|
||||||
requestHistory: [],
|
|
||||||
makeRequest: vi.fn(),
|
|
||||||
sendNotification: vi.fn(),
|
|
||||||
connect: vi.fn(),
|
|
||||||
});
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
render(<App />);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Resources tab should be disabled
|
|
||||||
const resourcesTab = screen.getByRole("tab", { name: /resources/i });
|
|
||||||
expect(resourcesTab).toHaveAttribute("disabled");
|
|
||||||
|
|
||||||
// Prompts tab should be enabled
|
|
||||||
const promptsTab = screen.getByRole("tab", { name: /prompts/i });
|
|
||||||
expect(promptsTab).not.toHaveAttribute("disabled");
|
|
||||||
|
|
||||||
// Tools tab should be disabled
|
|
||||||
const toolsTab = screen.getByRole("tab", { name: /tools/i });
|
|
||||||
expect(toolsTab).toHaveAttribute("disabled");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows notification count in sampling tab", async () => {
|
|
||||||
const mockUseConnection = useConnection as jest.Mock;
|
|
||||||
mockUseConnection.mockReturnValue({
|
|
||||||
connectionStatus: "connected",
|
|
||||||
serverCapabilities: { sampling: true },
|
|
||||||
mcpClient: {},
|
|
||||||
requestHistory: [],
|
|
||||||
makeRequest: vi.fn(),
|
|
||||||
sendNotification: vi.fn(),
|
|
||||||
connect: vi.fn(),
|
|
||||||
onPendingRequest: (request, resolve, reject) => {
|
|
||||||
// Simulate a pending request
|
|
||||||
setPendingSampleRequests((prev) => [
|
|
||||||
...prev,
|
|
||||||
{ id: 1, request, resolve, reject },
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
render(<App />);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initially no notification count
|
|
||||||
const samplingTab = screen.getByRole("tab", { name: /sampling/i });
|
|
||||||
expect(samplingTab.querySelector(".bg-red-500")).not.toBeInTheDocument();
|
|
||||||
|
|
||||||
// Simulate a pending request
|
|
||||||
await act(async () => {
|
|
||||||
mockUseConnection.mock.calls[0][0].onPendingRequest(
|
|
||||||
{ method: "test", params: {} },
|
|
||||||
() => {},
|
|
||||||
() => {},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should show notification count
|
|
||||||
expect(samplingTab.querySelector(".bg-red-500")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("persists command and args to localStorage", async () => {
|
|
||||||
const setItemSpy = vi.spyOn(Storage.prototype, "setItem");
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
render(<App />);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Simulate command change
|
|
||||||
await act(async () => {
|
|
||||||
const commandInput = screen.getByPlaceholderText(/command/i);
|
|
||||||
fireEvent.change(commandInput, { target: { value: "new-command" } });
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(setItemSpy).toHaveBeenCalledWith("lastCommand", "new-command");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows error message when server has no capabilities", async () => {
|
|
||||||
const mockUseConnection = useConnection as jest.Mock;
|
|
||||||
mockUseConnection.mockReturnValue({
|
|
||||||
connectionStatus: "connected",
|
|
||||||
serverCapabilities: {},
|
|
||||||
mcpClient: {},
|
|
||||||
requestHistory: [],
|
|
||||||
makeRequest: vi.fn(),
|
|
||||||
sendNotification: vi.fn(),
|
|
||||||
connect: vi.fn(),
|
|
||||||
});
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
render(<App />);
|
|
||||||
});
|
|
||||||
expect(
|
|
||||||
screen.getByText(
|
|
||||||
"The connected server does not support any MCP capabilities",
|
|
||||||
),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
|
||||||
import { render, screen, fireEvent } from "@testing-library/react";
|
|
||||||
import HistoryAndNotifications from "../../components/History";
|
|
||||||
|
|
||||||
describe("HistoryAndNotifications", () => {
|
|
||||||
const mockHistory = [
|
|
||||||
{
|
|
||||||
request: JSON.stringify({ method: "test1" }),
|
|
||||||
response: JSON.stringify({ result: "output1" }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
request: JSON.stringify({ method: "test2" }),
|
|
||||||
response: JSON.stringify({ result: "output2" }),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
it("renders history items", () => {
|
|
||||||
render(
|
|
||||||
<HistoryAndNotifications
|
|
||||||
requestHistory={mockHistory}
|
|
||||||
serverNotifications={[]}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
const items = screen.getAllByText(/test[12]/, { exact: false });
|
|
||||||
expect(items).toHaveLength(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("expands history item when clicked", () => {
|
|
||||||
render(
|
|
||||||
<HistoryAndNotifications
|
|
||||||
requestHistory={mockHistory}
|
|
||||||
serverNotifications={[]}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
const firstItem = screen.getByText(/test1/, { exact: false });
|
|
||||||
fireEvent.click(firstItem);
|
|
||||||
expect(screen.getByText("Request:")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText(/output1/, { exact: false })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders and expands server notifications", () => {
|
|
||||||
const notifications = [
|
|
||||||
{ method: "notify1", params: { data: "test data 1" } },
|
|
||||||
{ method: "notify2", params: { data: "test data 2" } },
|
|
||||||
];
|
|
||||||
render(
|
|
||||||
<HistoryAndNotifications
|
|
||||||
requestHistory={[]}
|
|
||||||
serverNotifications={notifications}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
const items = screen.getAllByText(/notify[12]/, { exact: false });
|
|
||||||
expect(items).toHaveLength(2);
|
|
||||||
|
|
||||||
fireEvent.click(items[0]);
|
|
||||||
expect(screen.getByText("Details:")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText(/test data/, { exact: false })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import { describe, it, expect, vi } from "vitest";
|
|
||||||
import { render, screen, fireEvent } from "@testing-library/react";
|
|
||||||
import ListPane from "../../components/ListPane";
|
|
||||||
|
|
||||||
describe("ListPane", () => {
|
|
||||||
type TestItem = {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockItems: TestItem[] = [
|
|
||||||
{ id: 1, name: "Item 1" },
|
|
||||||
{ id: 2, name: "Item 2" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const defaultProps = {
|
|
||||||
items: mockItems,
|
|
||||||
listItems: vi.fn(),
|
|
||||||
clearItems: vi.fn(),
|
|
||||||
setSelectedItem: vi.fn(),
|
|
||||||
renderItem: (item: TestItem) => (
|
|
||||||
<>
|
|
||||||
<span className="flex-1">{item.name}</span>
|
|
||||||
<span className="text-sm text-gray-500">ID: {item.id}</span>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
title: "Test Items",
|
|
||||||
buttonText: "List Items",
|
|
||||||
};
|
|
||||||
|
|
||||||
it("renders title and buttons", () => {
|
|
||||||
render(<ListPane {...defaultProps} />);
|
|
||||||
expect(screen.getByText("Test Items")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("List Items")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Clear")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders list of items using renderItem prop", () => {
|
|
||||||
render(<ListPane {...defaultProps} />);
|
|
||||||
expect(screen.getByText("Item 1")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Item 2")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("ID: 1")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("ID: 2")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("calls listItems when List Items button is clicked", () => {
|
|
||||||
const listItems = vi.fn();
|
|
||||||
render(<ListPane {...defaultProps} listItems={listItems} />);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByText("List Items"));
|
|
||||||
expect(listItems).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("calls clearItems when Clear button is clicked", () => {
|
|
||||||
const clearItems = vi.fn();
|
|
||||||
render(<ListPane {...defaultProps} clearItems={clearItems} />);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByText("Clear"));
|
|
||||||
expect(clearItems).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("calls setSelectedItem when an item is clicked", () => {
|
|
||||||
const setSelectedItem = vi.fn();
|
|
||||||
render(<ListPane {...defaultProps} setSelectedItem={setSelectedItem} />);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByText("Item 1"));
|
|
||||||
expect(setSelectedItem).toHaveBeenCalledWith(mockItems[0]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("disables Clear button when items array is empty", () => {
|
|
||||||
render(<ListPane {...defaultProps} items={[]} />);
|
|
||||||
expect(screen.getByText("Clear")).toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("disables List Items button when isButtonDisabled is true", () => {
|
|
||||||
render(<ListPane {...defaultProps} isButtonDisabled={true} />);
|
|
||||||
expect(screen.getByText("List Items")).toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("enables List Items button when isButtonDisabled is false", () => {
|
|
||||||
render(<ListPane {...defaultProps} isButtonDisabled={false} />);
|
|
||||||
expect(screen.getByText("List Items")).not.toBeDisabled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { describe, it, expect, vi } from "vitest";
|
|
||||||
import { render, screen, fireEvent } from "@testing-library/react";
|
|
||||||
import PingTab from "../../components/PingTab";
|
|
||||||
import { Tabs } from "@/components/ui/tabs";
|
|
||||||
|
|
||||||
describe("PingTab", () => {
|
|
||||||
const renderWithTabs = (component: React.ReactElement) => {
|
|
||||||
return render(<Tabs defaultValue="ping">{component}</Tabs>);
|
|
||||||
};
|
|
||||||
|
|
||||||
it("renders the MEGA PING button", () => {
|
|
||||||
renderWithTabs(<PingTab onPingClick={() => {}} />);
|
|
||||||
const button = screen.getByRole("button", { name: /mega ping/i });
|
|
||||||
expect(button).toBeInTheDocument();
|
|
||||||
expect(button).toHaveClass(
|
|
||||||
"bg-gradient-to-r",
|
|
||||||
"from-purple-500",
|
|
||||||
"to-pink-500",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("includes rocket and explosion emojis", () => {
|
|
||||||
renderWithTabs(<PingTab onPingClick={() => {}} />);
|
|
||||||
expect(screen.getByText("🚀")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("💥")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("calls onPingClick when button is clicked", () => {
|
|
||||||
const onPingClick = vi.fn();
|
|
||||||
renderWithTabs(<PingTab onPingClick={onPingClick} />);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: /mega ping/i }));
|
|
||||||
expect(onPingClick).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("has animation classes for visual feedback", () => {
|
|
||||||
renderWithTabs(<PingTab onPingClick={() => {}} />);
|
|
||||||
const button = screen.getByRole("button", { name: /mega ping/i });
|
|
||||||
expect(button).toHaveClass(
|
|
||||||
"animate-pulse",
|
|
||||||
"hover:scale-110",
|
|
||||||
"transition",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("has focus styles for accessibility", () => {
|
|
||||||
renderWithTabs(<PingTab onPingClick={() => {}} />);
|
|
||||||
const button = screen.getByRole("button", { name: /mega ping/i });
|
|
||||||
expect(button).toHaveClass("focus:outline-none", "focus:ring-4");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import { describe, it, expect, vi } from "vitest";
|
|
||||||
import { render, screen, fireEvent } from "@testing-library/react";
|
|
||||||
import PromptsTab from "../../components/PromptsTab";
|
|
||||||
import type { Prompt } from "../../components/PromptsTab";
|
|
||||||
import { Tabs } from "@/components/ui/tabs";
|
|
||||||
|
|
||||||
describe("PromptsTab", () => {
|
|
||||||
const mockPrompts: Prompt[] = [
|
|
||||||
{
|
|
||||||
name: "test-prompt-1",
|
|
||||||
description: "Test prompt 1 description",
|
|
||||||
arguments: [
|
|
||||||
{ name: "arg1", description: "Argument 1", required: true },
|
|
||||||
{ name: "arg2", description: "Argument 2" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "test-prompt-2",
|
|
||||||
description: "Test prompt 2 description",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const defaultProps = {
|
|
||||||
prompts: mockPrompts,
|
|
||||||
listPrompts: vi.fn(),
|
|
||||||
clearPrompts: vi.fn(),
|
|
||||||
getPrompt: vi.fn(),
|
|
||||||
selectedPrompt: null,
|
|
||||||
setSelectedPrompt: vi.fn(),
|
|
||||||
promptContent: "",
|
|
||||||
nextCursor: null,
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderWithTabs = (component: React.ReactElement) => {
|
|
||||||
return render(<Tabs defaultValue="prompts">{component}</Tabs>);
|
|
||||||
};
|
|
||||||
|
|
||||||
it("renders list of prompts", () => {
|
|
||||||
renderWithTabs(<PromptsTab {...defaultProps} />);
|
|
||||||
expect(screen.getByText("test-prompt-1")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("test-prompt-2")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows prompt details when selected", () => {
|
|
||||||
const props = {
|
|
||||||
...defaultProps,
|
|
||||||
selectedPrompt: mockPrompts[0],
|
|
||||||
};
|
|
||||||
renderWithTabs(<PromptsTab {...props} />);
|
|
||||||
expect(
|
|
||||||
screen.getByText("Test prompt 1 description", {
|
|
||||||
selector: "p.text-sm.text-gray-600",
|
|
||||||
}),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("arg1")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("arg2")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles argument input", () => {
|
|
||||||
const getPrompt = vi.fn();
|
|
||||||
const props = {
|
|
||||||
...defaultProps,
|
|
||||||
selectedPrompt: mockPrompts[0],
|
|
||||||
getPrompt,
|
|
||||||
};
|
|
||||||
renderWithTabs(<PromptsTab {...props} />);
|
|
||||||
|
|
||||||
const arg1Input = screen.getByPlaceholderText("Enter arg1");
|
|
||||||
fireEvent.change(arg1Input, { target: { value: "test value" } });
|
|
||||||
|
|
||||||
const getPromptButton = screen.getByText("Get Prompt");
|
|
||||||
fireEvent.click(getPromptButton);
|
|
||||||
|
|
||||||
expect(getPrompt).toHaveBeenCalledWith("test-prompt-1", {
|
|
||||||
arg1: "test value",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows error message when error prop is provided", () => {
|
|
||||||
const props = {
|
|
||||||
...defaultProps,
|
|
||||||
error: "Test error message",
|
|
||||||
};
|
|
||||||
renderWithTabs(<PromptsTab {...props} />);
|
|
||||||
expect(screen.getByText("Test error message")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows prompt content when provided", () => {
|
|
||||||
const props = {
|
|
||||||
...defaultProps,
|
|
||||||
selectedPrompt: mockPrompts[0],
|
|
||||||
promptContent: "Test prompt content",
|
|
||||||
};
|
|
||||||
renderWithTabs(<PromptsTab {...props} />);
|
|
||||||
expect(screen.getByText("Test prompt content")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
import { describe, it, expect, vi } from "vitest";
|
|
||||||
import { render, screen, fireEvent } from "@testing-library/react";
|
|
||||||
import ResourcesTab from "../../components/ResourcesTab";
|
|
||||||
import { Tabs } from "@/components/ui/tabs";
|
|
||||||
import type {
|
|
||||||
Resource,
|
|
||||||
ResourceTemplate,
|
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
|
||||||
|
|
||||||
describe("ResourcesTab", () => {
|
|
||||||
const mockResources: Resource[] = [
|
|
||||||
{ uri: "file:///test1.txt", name: "Test 1" },
|
|
||||||
{ uri: "file:///test2.txt", name: "Test 2" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const mockTemplates: ResourceTemplate[] = [
|
|
||||||
{
|
|
||||||
name: "Template 1",
|
|
||||||
description: "Test template 1",
|
|
||||||
uriTemplate: "file:///test/{param1}/{param2}.txt",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Template 2",
|
|
||||||
description: "Test template 2",
|
|
||||||
uriTemplate: "file:///other/{name}.txt",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const defaultProps = {
|
|
||||||
resources: mockResources,
|
|
||||||
resourceTemplates: mockTemplates,
|
|
||||||
listResources: vi.fn(),
|
|
||||||
clearResources: vi.fn(),
|
|
||||||
listResourceTemplates: vi.fn(),
|
|
||||||
clearResourceTemplates: vi.fn(),
|
|
||||||
readResource: vi.fn(),
|
|
||||||
selectedResource: null,
|
|
||||||
setSelectedResource: vi.fn(),
|
|
||||||
resourceContent: "",
|
|
||||||
nextCursor: null,
|
|
||||||
nextTemplateCursor: null,
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderWithTabs = (component: React.ReactElement) => {
|
|
||||||
return render(<Tabs defaultValue="resources">{component}</Tabs>);
|
|
||||||
};
|
|
||||||
|
|
||||||
it("renders resources list", () => {
|
|
||||||
renderWithTabs(<ResourcesTab {...defaultProps} />);
|
|
||||||
expect(screen.getByText("Test 1")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Test 2")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders templates list", () => {
|
|
||||||
renderWithTabs(<ResourcesTab {...defaultProps} />);
|
|
||||||
expect(screen.getByText("Template 1")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Template 2")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows resource content when resource is selected", () => {
|
|
||||||
const props = {
|
|
||||||
...defaultProps,
|
|
||||||
selectedResource: mockResources[0],
|
|
||||||
resourceContent: "Test content",
|
|
||||||
};
|
|
||||||
renderWithTabs(<ResourcesTab {...props} />);
|
|
||||||
expect(screen.getByText("Test content")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows template form when template is selected", () => {
|
|
||||||
renderWithTabs(<ResourcesTab {...defaultProps} />);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByText("Template 1"));
|
|
||||||
expect(screen.getByText("Test template 1")).toBeInTheDocument();
|
|
||||||
expect(screen.getByLabelText("param1")).toBeInTheDocument();
|
|
||||||
expect(screen.getByLabelText("param2")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("fills template and reads resource", () => {
|
|
||||||
const readResource = vi.fn();
|
|
||||||
const setSelectedResource = vi.fn();
|
|
||||||
renderWithTabs(
|
|
||||||
<ResourcesTab
|
|
||||||
{...defaultProps}
|
|
||||||
readResource={readResource}
|
|
||||||
setSelectedResource={setSelectedResource}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Select template
|
|
||||||
fireEvent.click(screen.getByText("Template 1"));
|
|
||||||
|
|
||||||
// Fill in template parameters
|
|
||||||
fireEvent.change(screen.getByLabelText("param1"), {
|
|
||||||
target: { value: "value1" },
|
|
||||||
});
|
|
||||||
fireEvent.change(screen.getByLabelText("param2"), {
|
|
||||||
target: { value: "value2" },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Submit form
|
|
||||||
fireEvent.click(screen.getByText("Read Resource"));
|
|
||||||
|
|
||||||
expect(readResource).toHaveBeenCalledWith("file:///test/value1/value2.txt");
|
|
||||||
expect(setSelectedResource).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
uri: "file:///test/value1/value2.txt",
|
|
||||||
name: "file:///test/value1/value2.txt",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows error message when error prop is provided", () => {
|
|
||||||
const props = {
|
|
||||||
...defaultProps,
|
|
||||||
error: "Test error message",
|
|
||||||
};
|
|
||||||
renderWithTabs(<ResourcesTab {...props} />);
|
|
||||||
expect(screen.getByText("Test error message")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("refreshes resource content when refresh button is clicked", () => {
|
|
||||||
const readResource = vi.fn();
|
|
||||||
const props = {
|
|
||||||
...defaultProps,
|
|
||||||
selectedResource: mockResources[0],
|
|
||||||
readResource,
|
|
||||||
};
|
|
||||||
renderWithTabs(<ResourcesTab {...props} />);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByText("Refresh"));
|
|
||||||
expect(readResource).toHaveBeenCalledWith(mockResources[0].uri);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import { describe, it, expect, vi } from "vitest";
|
|
||||||
import { render, screen, fireEvent } from "@testing-library/react";
|
|
||||||
import RootsTab from "../../components/RootsTab";
|
|
||||||
import { Tabs } from "@/components/ui/tabs";
|
|
||||||
import type { Root } from "@modelcontextprotocol/sdk/types.js";
|
|
||||||
|
|
||||||
describe("RootsTab", () => {
|
|
||||||
const mockRoots: Root[] = [
|
|
||||||
{ uri: "file:///test/path1", name: "test1" },
|
|
||||||
{ uri: "file:///test/path2", name: "test2" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const defaultProps = {
|
|
||||||
roots: mockRoots,
|
|
||||||
setRoots: vi.fn(),
|
|
||||||
onRootsChange: vi.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderWithTabs = (component: React.ReactElement) => {
|
|
||||||
return render(<Tabs defaultValue="roots">{component}</Tabs>);
|
|
||||||
};
|
|
||||||
|
|
||||||
it("renders list of roots", () => {
|
|
||||||
renderWithTabs(<RootsTab {...defaultProps} />);
|
|
||||||
expect(screen.getByDisplayValue("file:///test/path1")).toBeInTheDocument();
|
|
||||||
expect(screen.getByDisplayValue("file:///test/path2")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("adds a new root when Add Root button is clicked", () => {
|
|
||||||
const setRoots = vi.fn();
|
|
||||||
renderWithTabs(<RootsTab {...defaultProps} setRoots={setRoots} />);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByText("Add Root"));
|
|
||||||
|
|
||||||
expect(setRoots).toHaveBeenCalled();
|
|
||||||
const updateFn = setRoots.mock.calls[0][0];
|
|
||||||
const result = updateFn(mockRoots);
|
|
||||||
expect(result).toEqual([...mockRoots, { uri: "file://", name: "" }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("removes a root when remove button is clicked", () => {
|
|
||||||
const setRoots = vi.fn();
|
|
||||||
renderWithTabs(<RootsTab {...defaultProps} setRoots={setRoots} />);
|
|
||||||
|
|
||||||
const removeButtons = screen.getAllByRole("button", {
|
|
||||||
name: /remove root/i,
|
|
||||||
});
|
|
||||||
fireEvent.click(removeButtons[0]);
|
|
||||||
|
|
||||||
expect(setRoots).toHaveBeenCalled();
|
|
||||||
const updateFn = setRoots.mock.calls[0][0];
|
|
||||||
const result = updateFn(mockRoots);
|
|
||||||
expect(result).toEqual([mockRoots[1]]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("updates root URI when input changes", () => {
|
|
||||||
const setRoots = vi.fn();
|
|
||||||
renderWithTabs(<RootsTab {...defaultProps} setRoots={setRoots} />);
|
|
||||||
|
|
||||||
const firstInput = screen.getByDisplayValue("file:///test/path1");
|
|
||||||
fireEvent.change(firstInput, { target: { value: "file:///new/path" } });
|
|
||||||
|
|
||||||
expect(setRoots).toHaveBeenCalled();
|
|
||||||
const updateFn = setRoots.mock.calls[0][0];
|
|
||||||
const result = updateFn(mockRoots);
|
|
||||||
expect(result[0].uri).toBe("file:///new/path");
|
|
||||||
expect(result[1]).toEqual(mockRoots[1]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("calls onRootsChange when Save Changes is clicked", () => {
|
|
||||||
const onRootsChange = vi.fn();
|
|
||||||
renderWithTabs(
|
|
||||||
<RootsTab {...defaultProps} onRootsChange={onRootsChange} />,
|
|
||||||
);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByText("Save Changes"));
|
|
||||||
|
|
||||||
expect(onRootsChange).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import { describe, it, expect, vi } from "vitest";
|
|
||||||
import { render, screen, fireEvent } from "@testing-library/react";
|
|
||||||
import SamplingTab from "../../components/SamplingTab";
|
|
||||||
import { Tabs } from "@/components/ui/tabs";
|
|
||||||
import type { CreateMessageRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
||||||
|
|
||||||
describe("SamplingTab", () => {
|
|
||||||
const mockRequest: CreateMessageRequest = {
|
|
||||||
model: "test-model",
|
|
||||||
role: "user",
|
|
||||||
content: {
|
|
||||||
type: "text",
|
|
||||||
text: "Test message",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockPendingRequests = [
|
|
||||||
{ id: 1, request: mockRequest },
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
request: {
|
|
||||||
...mockRequest,
|
|
||||||
content: { type: "text", text: "Another test" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const defaultProps = {
|
|
||||||
pendingRequests: mockPendingRequests,
|
|
||||||
onApprove: vi.fn(),
|
|
||||||
onReject: vi.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderWithTabs = (component: React.ReactElement) => {
|
|
||||||
return render(<Tabs defaultValue="sampling">{component}</Tabs>);
|
|
||||||
};
|
|
||||||
|
|
||||||
it("renders empty state when no requests", () => {
|
|
||||||
renderWithTabs(<SamplingTab {...defaultProps} pendingRequests={[]} />);
|
|
||||||
expect(screen.getByText("No pending requests")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders list of pending requests", () => {
|
|
||||||
renderWithTabs(<SamplingTab {...defaultProps} />);
|
|
||||||
expect(screen.getByText(/Test message/)).toBeInTheDocument();
|
|
||||||
expect(screen.getByText(/Another test/)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows request details in JSON format", () => {
|
|
||||||
renderWithTabs(<SamplingTab {...defaultProps} />);
|
|
||||||
const requestJson = screen.getAllByText((content) =>
|
|
||||||
content.includes('"model": "test-model"'),
|
|
||||||
);
|
|
||||||
expect(requestJson).toHaveLength(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("calls onApprove with stub response when Approve is clicked", () => {
|
|
||||||
const onApprove = vi.fn();
|
|
||||||
renderWithTabs(<SamplingTab {...defaultProps} onApprove={onApprove} />);
|
|
||||||
|
|
||||||
const approveButtons = screen.getAllByText("Approve");
|
|
||||||
fireEvent.click(approveButtons[0]);
|
|
||||||
|
|
||||||
expect(onApprove).toHaveBeenCalledWith(1, {
|
|
||||||
model: "stub-model",
|
|
||||||
stopReason: "endTurn",
|
|
||||||
role: "assistant",
|
|
||||||
content: {
|
|
||||||
type: "text",
|
|
||||||
text: "This is a stub response.",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("calls onReject when Reject is clicked", () => {
|
|
||||||
const onReject = vi.fn();
|
|
||||||
renderWithTabs(<SamplingTab {...defaultProps} onReject={onReject} />);
|
|
||||||
|
|
||||||
const rejectButtons = screen.getAllByText("Reject");
|
|
||||||
fireEvent.click(rejectButtons[0]);
|
|
||||||
|
|
||||||
expect(onReject).toHaveBeenCalledWith(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows informational alert about sampling requests", () => {
|
|
||||||
renderWithTabs(<SamplingTab {...defaultProps} />);
|
|
||||||
expect(
|
|
||||||
screen.getByText(/When the server requests LLM sampling/),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import "@testing-library/jest-dom";
|
|
||||||
import { expect, afterEach, vi } from "vitest";
|
|
||||||
import { cleanup } from "@testing-library/react";
|
|
||||||
import * as matchers from "@testing-library/jest-dom/matchers";
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
expect.extend(matchers);
|
|
||||||
|
|
||||||
// Mock window.matchMedia
|
|
||||||
Object.defineProperty(window, "matchMedia", {
|
|
||||||
writable: true,
|
|
||||||
value: vi.fn().mockImplementation((query) => ({
|
|
||||||
matches: false,
|
|
||||||
media: query,
|
|
||||||
onchange: null,
|
|
||||||
addListener: vi.fn(), // Deprecated
|
|
||||||
removeListener: vi.fn(), // Deprecated
|
|
||||||
addEventListener: vi.fn(),
|
|
||||||
removeEventListener: vi.fn(),
|
|
||||||
dispatchEvent: vi.fn(),
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mock window.location.hash
|
|
||||||
Object.defineProperty(window, "location", {
|
|
||||||
writable: true,
|
|
||||||
value: { hash: "" },
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
cleanup();
|
|
||||||
vi.clearAllMocks();
|
|
||||||
window.location.hash = "";
|
|
||||||
});
|
|
||||||
@@ -54,7 +54,6 @@ const RootsTab = ({
|
|||||||
variant="destructive"
|
variant="destructive"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => removeRoot(index)}
|
onClick={() => removeRoot(index)}
|
||||||
aria-label="Remove root"
|
|
||||||
>
|
>
|
||||||
<Minus className="h-4 w-4" />
|
<Minus className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
CircleHelp,
|
CircleHelp,
|
||||||
Bug,
|
Bug,
|
||||||
Github,
|
Github,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -54,6 +56,7 @@ const Sidebar = ({
|
|||||||
}: SidebarProps) => {
|
}: SidebarProps) => {
|
||||||
const [theme, setTheme] = useTheme();
|
const [theme, setTheme] = useTheme();
|
||||||
const [showEnvVars, setShowEnvVars] = useState(false);
|
const [showEnvVars, setShowEnvVars] = useState(false);
|
||||||
|
const [shownEnvVars, setShownEnvVars] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-80 bg-card border-r border-border flex flex-col h-full">
|
<div className="w-80 bg-card border-r border-border flex flex-col h-full">
|
||||||
@@ -134,20 +137,44 @@ const Sidebar = ({
|
|||||||
{showEnvVars && (
|
{showEnvVars && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{Object.entries(env).map(([key, value], idx) => (
|
{Object.entries(env).map(([key, value], idx) => (
|
||||||
<div key={idx} className="grid grid-cols-[1fr,auto] gap-2">
|
<div key={idx} className="space-y-2 pb-4">
|
||||||
<div className="space-y-1">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Key"
|
placeholder="Key"
|
||||||
value={key}
|
value={key}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
const newKey = e.target.value;
|
||||||
const newEnv = { ...env };
|
const newEnv = { ...env };
|
||||||
delete newEnv[key];
|
delete newEnv[key];
|
||||||
newEnv[e.target.value] = value;
|
newEnv[newKey] = value;
|
||||||
setEnv(newEnv);
|
setEnv(newEnv);
|
||||||
|
setShownEnvVars((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(key)) {
|
||||||
|
next.delete(key);
|
||||||
|
next.add(newKey);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
className="font-mono"
|
className="font-mono"
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="icon"
|
||||||
|
className="h-9 w-9 p-0 shrink-0"
|
||||||
|
onClick={() => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { [key]: _removed, ...rest } = env;
|
||||||
|
setEnv(rest);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
|
type={shownEnvVars.has(key) ? "text" : "password"}
|
||||||
placeholder="Value"
|
placeholder="Value"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -157,24 +184,45 @@ const Sidebar = ({
|
|||||||
}}
|
}}
|
||||||
className="font-mono"
|
className="font-mono"
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="h-9 w-9 p-0 shrink-0"
|
||||||
|
onClick={() => {
|
||||||
|
setShownEnvVars((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(key)) {
|
||||||
|
next.delete(key);
|
||||||
|
} else {
|
||||||
|
next.add(key);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
aria-label={
|
||||||
|
shownEnvVars.has(key) ? "Hide value" : "Show value"
|
||||||
|
}
|
||||||
|
aria-pressed={shownEnvVars.has(key)}
|
||||||
|
title={
|
||||||
|
shownEnvVars.has(key) ? "Hide value" : "Show value"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{shownEnvVars.has(key) ? (
|
||||||
|
<Eye className="h-4 w-4" aria-hidden="true" />
|
||||||
|
) : (
|
||||||
|
<EyeOff className="h-4 w-4" aria-hidden="true" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
onClick={() => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const { [key]: removed, ...rest } = env;
|
|
||||||
setEnv(rest);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
className="w-full mt-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
const key = "";
|
||||||
const newEnv = { ...env };
|
const newEnv = { ...env };
|
||||||
newEnv[""] = "";
|
newEnv[key] = "";
|
||||||
setEnv(newEnv);
|
setEnv(newEnv);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ export default defineConfig({
|
|||||||
minify: false,
|
minify: false,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
manualChunks: undefined
|
manualChunks: undefined,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
/// <reference types="vitest" />
|
|
||||||
import { defineConfig } from 'vite'
|
|
||||||
import react from '@vitejs/plugin-react'
|
|
||||||
import path from 'path'
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [react()],
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@': path.resolve(__dirname, './src'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
test: {
|
|
||||||
environment: 'jsdom',
|
|
||||||
globals: true,
|
|
||||||
setupFiles: ['./src/__tests__/setup/setup.ts'],
|
|
||||||
include: ['src/__tests__/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
1277
package-lock.json
generated
1277
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -30,7 +30,7 @@
|
|||||||
"@modelcontextprotocol/sdk": "^1.0.3",
|
"@modelcontextprotocol/sdk": "^1.0.3",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"eventsource": "^2.0.2",
|
"eventsource": "^2.0.2",
|
||||||
"express": "^4.21.0",
|
"express": "^4.21.2",
|
||||||
"ws": "^8.18.0",
|
"ws": "^8.18.0",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ import express from "express";
|
|||||||
import mcpProxy from "./mcpProxy.js";
|
import mcpProxy from "./mcpProxy.js";
|
||||||
import { findActualExecutable } from "spawn-rx";
|
import { findActualExecutable } from "spawn-rx";
|
||||||
|
|
||||||
|
const defaultEnvironment = {
|
||||||
|
...getDefaultEnvironment(),
|
||||||
|
...(process.env.MCP_ENV_VARS ? JSON.parse(process.env.MCP_ENV_VARS) : {}),
|
||||||
|
};
|
||||||
|
|
||||||
// Polyfill EventSource for an SSE client in Node.js
|
// Polyfill EventSource for an SSE client in Node.js
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
(global as any).EventSource = EventSource;
|
(global as any).EventSource = EventSource;
|
||||||
@@ -40,13 +45,12 @@ const createTransport = async (query: express.Request["query"]) => {
|
|||||||
if (transportType === "stdio") {
|
if (transportType === "stdio") {
|
||||||
const command = query.command as string;
|
const command = query.command as string;
|
||||||
const origArgs = shellParseArgs(query.args as string) as string[];
|
const origArgs = shellParseArgs(query.args as string) as string[];
|
||||||
const env = query.env ? JSON.parse(query.env as string) : undefined;
|
const queryEnv = query.env ? JSON.parse(query.env as string) : {};
|
||||||
|
const env = { ...process.env, ...defaultEnvironment, ...queryEnv };
|
||||||
|
|
||||||
const { cmd, args } = findActualExecutable(command, origArgs);
|
const { cmd, args } = findActualExecutable(command, origArgs);
|
||||||
|
|
||||||
console.log(
|
console.log(`Stdio transport: command=${cmd}, args=${args}`);
|
||||||
`Stdio transport: command=${cmd}, args=${args}, env=${JSON.stringify(env)}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const transport = new StdioClientTransport({
|
const transport = new StdioClientTransport({
|
||||||
command: cmd,
|
command: cmd,
|
||||||
@@ -136,8 +140,6 @@ app.post("/message", async (req, res) => {
|
|||||||
|
|
||||||
app.get("/config", (req, res) => {
|
app.get("/config", (req, res) => {
|
||||||
try {
|
try {
|
||||||
const defaultEnvironment = getDefaultEnvironment();
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
defaultEnvironment,
|
defaultEnvironment,
|
||||||
defaultCommand: values.env,
|
defaultCommand: values.env,
|
||||||
|
|||||||
Reference in New Issue
Block a user