Compare commits
9 Commits
ashwin/tes
...
devin/1737
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb6ab5a85a | ||
|
|
ce7f65b5be | ||
|
|
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:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Check formatting
|
||||
run: npx prettier --check .
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
packages
|
||||
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
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
npx @modelcontextprotocol/inspector build/index.js arg1 arg2 ...
|
||||
```bash
|
||||
# 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:
|
||||
|
||||
34
bin/cli.js
34
bin/cli.js
@@ -11,8 +11,32 @@ function delay(ms) {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Get command line arguments
|
||||
const [, , command, ...mcpServerArgs] = process.argv;
|
||||
// Parse command line arguments
|
||||
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(
|
||||
__dirname,
|
||||
@@ -52,7 +76,11 @@ async function main() {
|
||||
...(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,
|
||||
echoOutput: true,
|
||||
},
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.3",
|
||||
@@ -40,6 +41,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.11.1",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@types/node": "^22.7.5",
|
||||
"@types/react": "^18.3.10",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
@@ -50,10 +53,12 @@
|
||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.12",
|
||||
"globals": "^15.9.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"postcss": "^8.4.47",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"typescript": "^5.5.3",
|
||||
"typescript-eslint": "^8.7.0",
|
||||
"vite": "^5.4.8"
|
||||
"vite": "^5.4.8",
|
||||
"vitest": "^3.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
ResourceTemplate,
|
||||
Root,
|
||||
ServerNotification,
|
||||
Tool
|
||||
Tool,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
@@ -124,10 +124,7 @@ const App = () => {
|
||||
const [nextToolCursor, setNextToolCursor] = useState<string | undefined>();
|
||||
const progressTokenRef = useRef(0);
|
||||
|
||||
const {
|
||||
height: historyPaneHeight,
|
||||
handleDragStart
|
||||
} = useDraggablePane(300);
|
||||
const { height: historyPaneHeight, handleDragStart } = useDraggablePane(300);
|
||||
|
||||
const {
|
||||
connectionStatus,
|
||||
@@ -136,7 +133,7 @@ const App = () => {
|
||||
requestHistory,
|
||||
makeRequest: makeConnectionRequest,
|
||||
sendNotification,
|
||||
connect: connectMcpServer
|
||||
connect: connectMcpServer,
|
||||
} = useConnection({
|
||||
transportType,
|
||||
command,
|
||||
@@ -145,18 +142,21 @@ const App = () => {
|
||||
env,
|
||||
proxyServerUrl: PROXY_SERVER_URL,
|
||||
onNotification: (notification) => {
|
||||
setNotifications(prev => [...prev, notification as ServerNotification]);
|
||||
setNotifications((prev) => [...prev, notification as ServerNotification]);
|
||||
},
|
||||
onStdErrNotification: (notification) => {
|
||||
setStdErrNotifications(prev => [...prev, notification as StdErrNotification]);
|
||||
},
|
||||
onPendingRequest: (request, resolve, reject) => {
|
||||
setPendingSampleRequests(prev => [
|
||||
setStdErrNotifications((prev) => [
|
||||
...prev,
|
||||
{ id: nextRequestId.current++, request, resolve, reject }
|
||||
notification as StdErrNotification,
|
||||
]);
|
||||
},
|
||||
getRoots: () => rootsRef.current
|
||||
onPendingRequest: (request, resolve, reject) => {
|
||||
setPendingSampleRequests((prev) => [
|
||||
...prev,
|
||||
{ id: nextRequestId.current++, request, resolve, reject },
|
||||
]);
|
||||
},
|
||||
getRoots: () => rootsRef.current,
|
||||
});
|
||||
|
||||
const makeRequest = async <T extends z.ZodType>(
|
||||
@@ -345,26 +345,40 @@ const App = () => {
|
||||
{mcpClient ? (
|
||||
<Tabs
|
||||
defaultValue={
|
||||
Object.keys(serverCapabilities ?? {}).includes(window.location.hash.slice(1)) ?
|
||||
window.location.hash.slice(1) :
|
||||
serverCapabilities?.resources ? "resources" :
|
||||
serverCapabilities?.prompts ? "prompts" :
|
||||
serverCapabilities?.tools ? "tools" :
|
||||
"ping"
|
||||
Object.keys(serverCapabilities ?? {}).includes(
|
||||
window.location.hash.slice(1),
|
||||
)
|
||||
? window.location.hash.slice(1)
|
||||
: serverCapabilities?.resources
|
||||
? "resources"
|
||||
: serverCapabilities?.prompts
|
||||
? "prompts"
|
||||
: serverCapabilities?.tools
|
||||
? "tools"
|
||||
: "ping"
|
||||
}
|
||||
className="w-full p-4"
|
||||
onValueChange={(value) => (window.location.hash = value)}
|
||||
>
|
||||
<TabsList className="mb-4 p-0">
|
||||
<TabsTrigger value="resources" disabled={!serverCapabilities?.resources}>
|
||||
<TabsTrigger
|
||||
value="resources"
|
||||
disabled={!serverCapabilities?.resources}
|
||||
>
|
||||
<Files className="w-4 h-4 mr-2" />
|
||||
Resources
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="prompts" disabled={!serverCapabilities?.prompts}>
|
||||
<TabsTrigger
|
||||
value="prompts"
|
||||
disabled={!serverCapabilities?.prompts}
|
||||
>
|
||||
<MessageSquare className="w-4 h-4 mr-2" />
|
||||
Prompts
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="tools" disabled={!serverCapabilities?.tools}>
|
||||
<TabsTrigger
|
||||
value="tools"
|
||||
disabled={!serverCapabilities?.tools}
|
||||
>
|
||||
<Hammer className="w-4 h-4 mr-2" />
|
||||
Tools
|
||||
</TabsTrigger>
|
||||
@@ -388,7 +402,9 @@ const App = () => {
|
||||
</TabsList>
|
||||
|
||||
<div className="w-full">
|
||||
{!serverCapabilities?.resources && !serverCapabilities?.prompts && !serverCapabilities?.tools ? (
|
||||
{!serverCapabilities?.resources &&
|
||||
!serverCapabilities?.prompts &&
|
||||
!serverCapabilities?.tools ? (
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<p className="text-lg text-gray-500">
|
||||
The connected server does not support any MCP capabilities
|
||||
|
||||
59
client/src/components/ListPane.test.tsx
Normal file
59
client/src/components/ListPane.test.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import ListPane from "./ListPane";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
|
||||
describe("ListPane", () => {
|
||||
const defaultProps = {
|
||||
items: [
|
||||
{ id: 1, name: "Item 1" },
|
||||
{ id: 2, name: "Item 2" },
|
||||
],
|
||||
listItems: vi.fn(),
|
||||
clearItems: vi.fn(),
|
||||
setSelectedItem: vi.fn(),
|
||||
renderItem: (item: { name: string }) => <span>{item.name}</span>,
|
||||
title: "Test List",
|
||||
buttonText: "List Items",
|
||||
};
|
||||
|
||||
it("renders title correctly", () => {
|
||||
render(<ListPane {...defaultProps} />);
|
||||
expect(screen.getByText("Test List")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders list items using renderItem prop", () => {
|
||||
render(<ListPane {...defaultProps} />);
|
||||
expect(screen.getByText("Item 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Item 2")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls listItems when List Items button is clicked", () => {
|
||||
render(<ListPane {...defaultProps} />);
|
||||
fireEvent.click(screen.getByText("List Items"));
|
||||
expect(defaultProps.listItems).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("calls clearItems when Clear button is clicked", () => {
|
||||
render(<ListPane {...defaultProps} />);
|
||||
fireEvent.click(screen.getByText("Clear"));
|
||||
expect(defaultProps.clearItems).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("calls setSelectedItem when an item is clicked", () => {
|
||||
render(<ListPane {...defaultProps} />);
|
||||
fireEvent.click(screen.getByText("Item 1"));
|
||||
expect(defaultProps.setSelectedItem).toHaveBeenCalledWith(
|
||||
defaultProps.items[0],
|
||||
);
|
||||
});
|
||||
|
||||
it("disables Clear button when items array is empty", () => {
|
||||
render(<ListPane {...defaultProps} items={[]} />);
|
||||
expect(screen.getByText("Clear")).toBeDisabled();
|
||||
});
|
||||
|
||||
it("respects isButtonDisabled prop for List Items button", () => {
|
||||
render(<ListPane {...defaultProps} isButtonDisabled={true} />);
|
||||
expect(screen.getByText("List Items")).toBeDisabled();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,14 @@
|
||||
import { useState } from "react";
|
||||
import { Play, ChevronDown, ChevronRight, CircleHelp, Bug, Github } from "lucide-react";
|
||||
import {
|
||||
Play,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
CircleHelp,
|
||||
Bug,
|
||||
Github,
|
||||
Eye,
|
||||
EyeOff,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
@@ -47,6 +56,7 @@ const Sidebar = ({
|
||||
}: SidebarProps) => {
|
||||
const [theme, setTheme] = useTheme();
|
||||
const [showEnvVars, setShowEnvVars] = useState(false);
|
||||
const [shownEnvVars, setShownEnvVars] = useState<Set<string>>(new Set());
|
||||
|
||||
return (
|
||||
<div className="w-80 bg-card border-r border-border flex flex-col h-full">
|
||||
@@ -127,20 +137,44 @@ const Sidebar = ({
|
||||
{showEnvVars && (
|
||||
<div className="space-y-2">
|
||||
{Object.entries(env).map(([key, value], idx) => (
|
||||
<div key={idx} className="grid grid-cols-[1fr,auto] gap-2">
|
||||
<div className="space-y-1">
|
||||
<div key={idx} className="space-y-2 pb-4">
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="Key"
|
||||
value={key}
|
||||
onChange={(e) => {
|
||||
const newKey = e.target.value;
|
||||
const newEnv = { ...env };
|
||||
delete newEnv[key];
|
||||
newEnv[e.target.value] = value;
|
||||
newEnv[newKey] = value;
|
||||
setEnv(newEnv);
|
||||
setShownEnvVars((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(key)) {
|
||||
next.delete(key);
|
||||
next.add(newKey);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
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
|
||||
type={shownEnvVars.has(key) ? "text" : "password"}
|
||||
placeholder="Value"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
@@ -150,24 +184,45 @@ const Sidebar = ({
|
||||
}}
|
||||
className="font-mono"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="destructive"
|
||||
variant="outline"
|
||||
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);
|
||||
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"
|
||||
}
|
||||
>
|
||||
Remove
|
||||
{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="outline"
|
||||
className="w-full mt-2"
|
||||
onClick={() => {
|
||||
const key = "";
|
||||
const newEnv = { ...env };
|
||||
newEnv[""] = "";
|
||||
newEnv[key] = "";
|
||||
setEnv(newEnv);
|
||||
}}
|
||||
>
|
||||
@@ -243,18 +298,33 @@ const Sidebar = ({
|
||||
</Select>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<a href="https://modelcontextprotocol.io/docs/tools/inspector" target="_blank" rel="noopener noreferrer">
|
||||
<a
|
||||
href="https://modelcontextprotocol.io/docs/tools/inspector"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button variant="ghost" title="Inspector Documentation">
|
||||
<CircleHelp className="w-4 h-4 text-gray-800" />
|
||||
</Button>
|
||||
</a>
|
||||
<a href="https://modelcontextprotocol.io/docs/tools/debugging" target="_blank" rel="noopener noreferrer">
|
||||
<a
|
||||
href="https://modelcontextprotocol.io/docs/tools/debugging"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button variant="ghost" title="Debugging Guide">
|
||||
<Bug className="w-4 h-4 text-gray-800" />
|
||||
</Button>
|
||||
</a>
|
||||
<a href="https://github.com/modelcontextprotocol/inspector" target="_blank" rel="noopener noreferrer">
|
||||
<Button variant="ghost" title="Report bugs or contribute on GitHub">
|
||||
<a
|
||||
href="https://github.com/modelcontextprotocol/inspector"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
title="Report bugs or contribute on GitHub"
|
||||
>
|
||||
<Github className="w-4 h-4 text-gray-800" />
|
||||
</Button>
|
||||
</a>
|
||||
|
||||
@@ -174,8 +174,7 @@ const ToolsTab = ({
|
||||
}
|
||||
className="mt-1"
|
||||
/>
|
||||
) :
|
||||
/* @ts-expect-error value type is currently unknown */
|
||||
) : /* @ts-expect-error value type is currently unknown */
|
||||
value.type === "object" ? (
|
||||
<Textarea
|
||||
id={key}
|
||||
|
||||
55
client/src/components/ui/Button.test.tsx
Normal file
55
client/src/components/ui/Button.test.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { Button } from "./button";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { createRef } from "react";
|
||||
|
||||
describe("Button", () => {
|
||||
it("renders children correctly", () => {
|
||||
render(<Button>Click me</Button>);
|
||||
expect(screen.getByText("Click me")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("handles click events", () => {
|
||||
const handleClick = vi.fn();
|
||||
render(<Button onClick={handleClick}>Click me</Button>);
|
||||
fireEvent.click(screen.getByText("Click me"));
|
||||
expect(handleClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("applies different variants correctly", () => {
|
||||
const { rerender } = render(<Button variant="default">Default</Button>);
|
||||
expect(screen.getByText("Default")).toHaveClass("bg-primary");
|
||||
|
||||
rerender(<Button variant="outline">Outline</Button>);
|
||||
expect(screen.getByText("Outline")).toHaveClass("border-input");
|
||||
|
||||
rerender(<Button variant="secondary">Secondary</Button>);
|
||||
expect(screen.getByText("Secondary")).toHaveClass("bg-secondary");
|
||||
});
|
||||
|
||||
it("applies different sizes correctly", () => {
|
||||
const { rerender } = render(<Button size="default">Default</Button>);
|
||||
expect(screen.getByText("Default")).toHaveClass("h-9");
|
||||
|
||||
rerender(<Button size="sm">Small</Button>);
|
||||
expect(screen.getByText("Small")).toHaveClass("h-8");
|
||||
|
||||
rerender(<Button size="lg">Large</Button>);
|
||||
expect(screen.getByText("Large")).toHaveClass("h-10");
|
||||
});
|
||||
|
||||
it("forwards ref correctly", () => {
|
||||
const ref = createRef<HTMLButtonElement>();
|
||||
render(<Button ref={ref}>Button with ref</Button>);
|
||||
expect(ref.current).toBeInstanceOf(HTMLButtonElement);
|
||||
});
|
||||
|
||||
it("renders as a different element when asChild is true", () => {
|
||||
render(
|
||||
<Button asChild>
|
||||
<a href="#">Link Button</a>
|
||||
</Button>,
|
||||
);
|
||||
expect(screen.getByText("Link Button").tagName).toBe("A");
|
||||
});
|
||||
});
|
||||
@@ -44,10 +44,15 @@ export function useConnection({
|
||||
onPendingRequest,
|
||||
getRoots,
|
||||
}: UseConnectionOptions) {
|
||||
const [connectionStatus, setConnectionStatus] = useState<"disconnected" | "connected" | "error">("disconnected");
|
||||
const [serverCapabilities, setServerCapabilities] = useState<ServerCapabilities | null>(null);
|
||||
const [connectionStatus, setConnectionStatus] = useState<
|
||||
"disconnected" | "connected" | "error"
|
||||
>("disconnected");
|
||||
const [serverCapabilities, setServerCapabilities] =
|
||||
useState<ServerCapabilities | null>(null);
|
||||
const [mcpClient, setMcpClient] = useState<Client | null>(null);
|
||||
const [requestHistory, setRequestHistory] = useState<{ request: string; response?: string }[]>([]);
|
||||
const [requestHistory, setRequestHistory] = useState<
|
||||
{ request: string; response?: string }[]
|
||||
>([]);
|
||||
|
||||
const pushHistory = (request: object, response?: object) => {
|
||||
setRequestHistory((prev) => [
|
||||
@@ -61,7 +66,7 @@ export function useConnection({
|
||||
|
||||
const makeRequest = async <T extends z.ZodType>(
|
||||
request: ClientRequest,
|
||||
schema: T
|
||||
schema: T,
|
||||
) => {
|
||||
if (!mcpClient) {
|
||||
throw new Error("MCP client not connected");
|
||||
@@ -80,14 +85,14 @@ export function useConnection({
|
||||
});
|
||||
pushHistory(request, response);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
pushHistory(request, { error: errorMessage });
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
|
||||
return response;
|
||||
} catch (e: unknown) {
|
||||
const errorString = (e as Error).message ?? String(e);
|
||||
@@ -142,11 +147,17 @@ export function useConnection({
|
||||
const clientTransport = new SSEClientTransport(backendUrl);
|
||||
|
||||
if (onNotification) {
|
||||
client.setNotificationHandler(ProgressNotificationSchema, onNotification);
|
||||
client.setNotificationHandler(
|
||||
ProgressNotificationSchema,
|
||||
onNotification,
|
||||
);
|
||||
}
|
||||
|
||||
if (onStdErrNotification) {
|
||||
client.setNotificationHandler(StdErrNotificationSchema, onStdErrNotification);
|
||||
client.setNotificationHandler(
|
||||
StdErrNotificationSchema,
|
||||
onStdErrNotification,
|
||||
);
|
||||
}
|
||||
|
||||
await client.connect(clientTransport);
|
||||
@@ -183,6 +194,6 @@ export function useConnection({
|
||||
requestHistory,
|
||||
makeRequest,
|
||||
sendNotification,
|
||||
connect
|
||||
connect,
|
||||
};
|
||||
}
|
||||
@@ -6,19 +6,28 @@ export function useDraggablePane(initialHeight: number) {
|
||||
const dragStartY = useRef<number>(0);
|
||||
const dragStartHeight = useRef<number>(0);
|
||||
|
||||
const handleDragStart = useCallback((e: React.MouseEvent) => {
|
||||
const handleDragStart = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
setIsDragging(true);
|
||||
dragStartY.current = e.clientY;
|
||||
dragStartHeight.current = height;
|
||||
document.body.style.userSelect = "none";
|
||||
}, [height]);
|
||||
},
|
||||
[height],
|
||||
);
|
||||
|
||||
const handleDragMove = useCallback((e: MouseEvent) => {
|
||||
const handleDragMove = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (!isDragging) return;
|
||||
const deltaY = dragStartY.current - e.clientY;
|
||||
const newHeight = Math.max(100, Math.min(800, dragStartHeight.current + deltaY));
|
||||
const newHeight = Math.max(
|
||||
100,
|
||||
Math.min(800, dragStartHeight.current + deltaY),
|
||||
);
|
||||
setHeight(newHeight);
|
||||
}, [isDragging]);
|
||||
},
|
||||
[isDragging],
|
||||
);
|
||||
|
||||
const handleDragEnd = useCallback(() => {
|
||||
setIsDragging(false);
|
||||
@@ -39,6 +48,6 @@ export function useDraggablePane(initialHeight: number) {
|
||||
return {
|
||||
height,
|
||||
isDragging,
|
||||
handleDragStart
|
||||
handleDragStart,
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
import App from "./App.tsx";
|
||||
import "./index.css";
|
||||
|
||||
|
||||
12
client/src/test.d.ts
vendored
Normal file
12
client/src/test.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
/// <reference types="vitest/globals" />
|
||||
/// <reference types="@testing-library/jest-dom" />
|
||||
|
||||
import "@testing-library/jest-dom";
|
||||
|
||||
declare global {
|
||||
namespace Vi {
|
||||
interface JestAssertion<T = any> extends jest.Matchers<void, T> {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
6
client/test/setupTests.ts
Normal file
6
client/test/setupTests.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/// <reference types="vitest/globals" />
|
||||
/// <reference types="@testing-library/jest-dom" />
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
|
||||
// Add any additional test setup, custom matchers, or global mocks here
|
||||
// This file runs before each test file
|
||||
@@ -4,6 +4,7 @@
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"types": ["vitest/globals", "@testing-library/jest-dom"],
|
||||
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
@@ -26,5 +27,5 @@
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src"]
|
||||
"include": ["src", "test"]
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
{ "path": "./tsconfig.node.json" },
|
||||
{ "path": "./tsconfig.test.json" }
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
|
||||
7
client/tsconfig.test.json
Normal file
7
client/tsconfig.test.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.app.json",
|
||||
"compilerOptions": {
|
||||
"types": ["vitest/globals", "@testing-library/jest-dom"]
|
||||
},
|
||||
"include": ["src/**/*.test.tsx", "src/**/*.test.ts", "test/**/*.ts"]
|
||||
}
|
||||
@@ -14,8 +14,8 @@ export default defineConfig({
|
||||
minify: false,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
manualChunks: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
20
client/vitest.config.ts
Normal file
20
client/vitest.config.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import path from "path";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
globals: true,
|
||||
setupFiles: ["./test/setupTests.ts"],
|
||||
typecheck: {
|
||||
tsconfig: "./tsconfig.test.json",
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -15,6 +15,11 @@ import express from "express";
|
||||
import mcpProxy from "./mcpProxy.js";
|
||||
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
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(global as any).EventSource = EventSource;
|
||||
@@ -40,13 +45,12 @@ const createTransport = async (query: express.Request["query"]) => {
|
||||
if (transportType === "stdio") {
|
||||
const command = query.command 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);
|
||||
|
||||
console.log(
|
||||
`Stdio transport: command=${cmd}, args=${args}, env=${JSON.stringify(env)}`,
|
||||
);
|
||||
console.log(`Stdio transport: command=${cmd}, args=${args}`);
|
||||
|
||||
const transport = new StdioClientTransport({
|
||||
command: cmd,
|
||||
@@ -136,8 +140,6 @@ app.post("/message", async (req, res) => {
|
||||
|
||||
app.get("/config", (req, res) => {
|
||||
try {
|
||||
const defaultEnvironment = getDefaultEnvironment();
|
||||
|
||||
res.json({
|
||||
defaultEnvironment,
|
||||
defaultCommand: values.env,
|
||||
|
||||
Reference in New Issue
Block a user