Skip to content

Commit 1dfe10b

Browse files
authored
Merge branch 'main' into cli-and-config-file-support
2 parents 65c589c + 539f32b commit 1dfe10b

38 files changed

+5836
-466
lines changed

.github/workflows/main.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ jobs:
2525
# Working around https://github.com/npm/cli/issues/4828
2626
# - run: npm ci
2727
- run: npm install --no-package-lock
28+
29+
- name: Run client tests
30+
working-directory: ./client
31+
run: npm test
32+
2833
- run: npm run build
2934

3035
publish:

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ client/tsconfig.node.tsbuildinfo
88
.vscode
99
bin/build
1010
cli/build
11-
test-output
11+
test-output

CONTRIBUTING.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ Thanks for your interest in contributing! This guide explains how to get involve
77
1. Fork the repository and clone it locally
88
2. Install dependencies with `npm install`
99
3. Run `npm run dev` to start both client and server in development mode
10-
4. Use the web UI at http://localhost:5173 to interact with the inspector
10+
4. Use the web UI at http://127.0.0.1:5173 to interact with the inspector
1111

1212
## Development Process & Pull Requests
1313

1414
1. Create a new branch for your changes
15-
2. Make your changes following existing code style and conventions
16-
3. Test changes locally
15+
2. Make your changes following existing code style and conventions. You can run `npm run prettier-check` and `npm run prettier-fix` as applicable.
16+
3. Test changes locally by running `npm test`
1717
4. Update documentation as needed
1818
5. Use clear commit messages explaining your changes
1919
6. Verify all changes work as expected

client/jest.config.cjs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
module.exports = {
2+
preset: "ts-jest",
3+
testEnvironment: "jsdom",
4+
moduleNameMapper: {
5+
"^@/(.*)$": "<rootDir>/src/$1",
6+
"\\.css$": "<rootDir>/src/__mocks__/styleMock.js",
7+
},
8+
transform: {
9+
"^.+\\.tsx?$": [
10+
"ts-jest",
11+
{
12+
jsx: "react-jsx",
13+
tsconfig: "tsconfig.jest.json",
14+
},
15+
],
16+
},
17+
extensionsToTreatAsEsm: [".ts", ".tsx"],
18+
testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
19+
// Exclude directories and files that don't need to be tested
20+
testPathIgnorePatterns: [
21+
"/node_modules/",
22+
"/dist/",
23+
"/bin/",
24+
"\\.config\\.(js|ts|cjs|mjs)$",
25+
],
26+
// Exclude the same patterns from coverage reports
27+
coveragePathIgnorePatterns: [
28+
"/node_modules/",
29+
"/dist/",
30+
"/bin/",
31+
"\\.config\\.(js|ts|cjs|mjs)$",
32+
],
33+
};

client/package.json

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@modelcontextprotocol/inspector-client",
3-
"version": "0.5.1",
3+
"version": "0.7.0",
44
"description": "Client-side application for the Model Context Protocol inspector",
55
"license": "MIT",
66
"author": "Anthropic, PBC (https://anthropic.com)",
@@ -18,12 +18,14 @@
1818
"dev": "vite",
1919
"build": "tsc -b && vite build",
2020
"lint": "eslint .",
21-
"preview": "vite preview"
21+
"preview": "vite preview",
22+
"test": "jest --config jest.config.cjs",
23+
"test:watch": "jest --config jest.config.cjs --watch"
2224
},
2325
"dependencies": {
2426
"@modelcontextprotocol/sdk": "^1.6.1",
25-
"@radix-ui/react-dialog": "^1.1.3",
2627
"@radix-ui/react-checkbox": "^1.1.4",
28+
"@radix-ui/react-dialog": "^1.1.3",
2729
"@radix-ui/react-icons": "^1.3.0",
2830
"@radix-ui/react-label": "^2.1.0",
2931
"@radix-ui/react-popover": "^1.1.3",
@@ -35,8 +37,8 @@
3537
"clsx": "^2.1.1",
3638
"cmdk": "^1.0.4",
3739
"lucide-react": "^0.447.0",
38-
"prismjs": "^1.29.0",
3940
"pkce-challenge": "^4.1.0",
41+
"prismjs": "^1.30.0",
4042
"react": "^18.3.1",
4143
"react-dom": "^18.3.1",
4244
"react-simple-code-editor": "^0.14.1",
@@ -48,18 +50,25 @@
4850
},
4951
"devDependencies": {
5052
"@eslint/js": "^9.11.1",
53+
"@testing-library/jest-dom": "^6.6.3",
54+
"@testing-library/react": "^16.2.0",
55+
"@types/jest": "^29.5.14",
5156
"@types/node": "^22.7.5",
5257
"@types/react": "^18.3.10",
5358
"@types/react-dom": "^18.3.0",
5459
"@types/serve-handler": "^6.1.4",
5560
"@vitejs/plugin-react": "^4.3.2",
5661
"autoprefixer": "^10.4.20",
62+
"co": "^4.6.0",
5763
"eslint": "^9.11.1",
5864
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
5965
"eslint-plugin-react-refresh": "^0.4.12",
6066
"globals": "^15.9.0",
67+
"jest": "^29.7.0",
68+
"jest-environment-jsdom": "^29.7.0",
6169
"postcss": "^8.4.47",
6270
"tailwindcss": "^3.4.13",
71+
"ts-jest": "^29.2.6",
6372
"typescript": "^5.5.3",
6473
"typescript-eslint": "^8.7.0",
6574
"vite": "^5.4.8"

client/src/App.tsx

Lines changed: 118 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
Root,
1616
ServerNotification,
1717
Tool,
18+
LoggingLevel,
1819
} from "@modelcontextprotocol/sdk/types.js";
1920
import React, { Suspense, useEffect, useRef, useState } from "react";
2021
import { useConnection } from "./lib/hooks/useConnection";
@@ -44,23 +45,16 @@ import RootsTab from "./components/RootsTab";
4445
import SamplingTab, { PendingRequest } from "./components/SamplingTab";
4546
import Sidebar from "./components/Sidebar";
4647
import ToolsTab from "./components/ToolsTab";
48+
import { DEFAULT_INSPECTOR_CONFIG } from "./lib/constants";
49+
import { InspectorConfig } from "./lib/configurationTypes";
4750

4851
const params = new URLSearchParams(window.location.search);
4952
const PROXY_PORT = params.get("proxyPort") ?? "3000";
50-
const PROXY_SERVER_URL = `http://localhost:${PROXY_PORT}`;
53+
const PROXY_SERVER_URL = `http://${window.location.hostname}:${PROXY_PORT}`;
54+
const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1";
5155

5256
const App = () => {
5357
// Handle OAuth callback route
54-
if (window.location.pathname === "/oauth/callback") {
55-
const OAuthCallback = React.lazy(
56-
() => import("./components/OAuthCallback"),
57-
);
58-
return (
59-
<Suspense fallback={<div>Loading...</div>}>
60-
<OAuthCallback />
61-
</Suspense>
62-
);
63-
}
6458
const [resources, setResources] = useState<Resource[]>([]);
6559
const [resourceTemplates, setResourceTemplates] = useState<
6660
ResourceTemplate[]
@@ -91,13 +85,22 @@ const App = () => {
9185
(localStorage.getItem("lastTransportType") as "stdio" | "sse") || "stdio"
9286
);
9387
});
88+
const [logLevel, setLogLevel] = useState<LoggingLevel>("debug");
9489
const [notifications, setNotifications] = useState<ServerNotification[]>([]);
9590
const [stdErrNotifications, setStdErrNotifications] = useState<
9691
StdErrNotification[]
9792
>([]);
9893
const [roots, setRoots] = useState<Root[]>([]);
9994
const [env, setEnv] = useState<Record<string, string>>({});
10095

96+
const [config, setConfig] = useState<InspectorConfig>(() => {
97+
const savedConfig = localStorage.getItem(CONFIG_LOCAL_STORAGE_KEY);
98+
return savedConfig ? JSON.parse(savedConfig) : DEFAULT_INSPECTOR_CONFIG;
99+
});
100+
const [bearerToken, setBearerToken] = useState<string>(() => {
101+
return localStorage.getItem("lastBearerToken") || "";
102+
});
103+
101104
const [pendingSampleRequests, setPendingSampleRequests] = useState<
102105
Array<
103106
PendingRequest & {
@@ -109,25 +112,13 @@ const App = () => {
109112
const nextRequestId = useRef(0);
110113
const rootsRef = useRef<Root[]>([]);
111114

112-
const handleApproveSampling = (id: number, result: CreateMessageResult) => {
113-
setPendingSampleRequests((prev) => {
114-
const request = prev.find((r) => r.id === id);
115-
request?.resolve(result);
116-
return prev.filter((r) => r.id !== id);
117-
});
118-
};
119-
120-
const handleRejectSampling = (id: number) => {
121-
setPendingSampleRequests((prev) => {
122-
const request = prev.find((r) => r.id === id);
123-
request?.reject(new Error("Sampling request rejected"));
124-
return prev.filter((r) => r.id !== id);
125-
});
126-
};
127-
128115
const [selectedResource, setSelectedResource] = useState<Resource | null>(
129116
null,
130117
);
118+
const [resourceSubscriptions, setResourceSubscriptions] = useState<
119+
Set<string>
120+
>(new Set<string>());
121+
131122
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
132123
const [selectedTool, setSelectedTool] = useState<Tool | null>(null);
133124
const [nextResourceCursor, setNextResourceCursor] = useState<
@@ -160,7 +151,9 @@ const App = () => {
160151
args,
161152
sseUrl,
162153
env,
154+
bearerToken,
163155
proxyServerUrl: PROXY_SERVER_URL,
156+
requestTimeout: config.MCP_SERVER_REQUEST_TIMEOUT.value as number,
164157
onNotification: (notification) => {
165158
setNotifications((prev) => [...prev, notification as ServerNotification]);
166159
},
@@ -195,6 +188,14 @@ const App = () => {
195188
localStorage.setItem("lastTransportType", transportType);
196189
}, [transportType]);
197190

191+
useEffect(() => {
192+
localStorage.setItem("lastBearerToken", bearerToken);
193+
}, [bearerToken]);
194+
195+
useEffect(() => {
196+
localStorage.setItem(CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config));
197+
}, [config]);
198+
198199
// Auto-connect if serverUrl is provided in URL params (e.g. after OAuth callback)
199200
useEffect(() => {
200201
const serverUrl = params.get("serverUrl");
@@ -210,7 +211,7 @@ const App = () => {
210211
// Connect to the server
211212
connectMcpServer();
212213
}
213-
}, []);
214+
}, [connectMcpServer]);
214215

215216
useEffect(() => {
216217
fetch(`${PROXY_SERVER_URL}/config`)
@@ -239,6 +240,22 @@ const App = () => {
239240
}
240241
}, []);
241242

243+
const handleApproveSampling = (id: number, result: CreateMessageResult) => {
244+
setPendingSampleRequests((prev) => {
245+
const request = prev.find((r) => r.id === id);
246+
request?.resolve(result);
247+
return prev.filter((r) => r.id !== id);
248+
});
249+
};
250+
251+
const handleRejectSampling = (id: number) => {
252+
setPendingSampleRequests((prev) => {
253+
const request = prev.find((r) => r.id === id);
254+
request?.reject(new Error("Sampling request rejected"));
255+
return prev.filter((r) => r.id !== id);
256+
});
257+
};
258+
242259
const clearError = (tabKey: keyof typeof errors) => {
243260
setErrors((prev) => ({ ...prev, [tabKey]: null }));
244261
};
@@ -308,6 +325,38 @@ const App = () => {
308325
setResourceContent(JSON.stringify(response, null, 2));
309326
};
310327

328+
const subscribeToResource = async (uri: string) => {
329+
if (!resourceSubscriptions.has(uri)) {
330+
await makeRequest(
331+
{
332+
method: "resources/subscribe" as const,
333+
params: { uri },
334+
},
335+
z.object({}),
336+
"resources",
337+
);
338+
const clone = new Set(resourceSubscriptions);
339+
clone.add(uri);
340+
setResourceSubscriptions(clone);
341+
}
342+
};
343+
344+
const unsubscribeFromResource = async (uri: string) => {
345+
if (resourceSubscriptions.has(uri)) {
346+
await makeRequest(
347+
{
348+
method: "resources/unsubscribe" as const,
349+
params: { uri },
350+
},
351+
z.object({}),
352+
"resources",
353+
);
354+
const clone = new Set(resourceSubscriptions);
355+
clone.delete(uri);
356+
setResourceSubscriptions(clone);
357+
}
358+
};
359+
311360
const listPrompts = async () => {
312361
const response = await makeRequest(
313362
{
@@ -368,6 +417,28 @@ const App = () => {
368417
await sendNotification({ method: "notifications/roots/list_changed" });
369418
};
370419

420+
const sendLogLevelRequest = async (level: LoggingLevel) => {
421+
await makeRequest(
422+
{
423+
method: "logging/setLevel" as const,
424+
params: { level },
425+
},
426+
z.object({}),
427+
);
428+
setLogLevel(level);
429+
};
430+
431+
if (window.location.pathname === "/oauth/callback") {
432+
const OAuthCallback = React.lazy(
433+
() => import("./components/OAuthCallback"),
434+
);
435+
return (
436+
<Suspense fallback={<div>Loading...</div>}>
437+
<OAuthCallback />
438+
</Suspense>
439+
);
440+
}
441+
371442
return (
372443
<div className="flex h-screen bg-background">
373444
<Sidebar
@@ -382,8 +453,15 @@ const App = () => {
382453
setSseUrl={setSseUrl}
383454
env={env}
384455
setEnv={setEnv}
456+
config={config}
457+
setConfig={setConfig}
458+
bearerToken={bearerToken}
459+
setBearerToken={setBearerToken}
385460
onConnect={connectMcpServer}
386461
stdErrNotifications={stdErrNotifications}
462+
logLevel={logLevel}
463+
sendLogLevelRequest={sendLogLevelRequest}
464+
loggingSupported={!!serverCapabilities?.logging || false}
387465
/>
388466
<div className="flex-1 flex flex-col overflow-hidden">
389467
<div className="flex-1 overflow-auto">
@@ -485,6 +563,18 @@ const App = () => {
485563
clearError("resources");
486564
setSelectedResource(resource);
487565
}}
566+
resourceSubscriptionsSupported={
567+
serverCapabilities?.resources?.subscribe || false
568+
}
569+
resourceSubscriptions={resourceSubscriptions}
570+
subscribeToResource={(uri) => {
571+
clearError("resources");
572+
subscribeToResource(uri);
573+
}}
574+
unsubscribeFromResource={(uri) => {
575+
clearError("resources");
576+
unsubscribeFromResource(uri);
577+
}}
488578
handleCompletion={handleCompletion}
489579
completionsSupported={completionsSupported}
490580
resourceContent={resourceContent}

client/src/__mocks__/styleMock.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = {};

0 commit comments

Comments
 (0)