Skip to content

Commit 1f214de

Browse files
authored
Merge branch 'main' into handle-empty-json-fields
2 parents 1ff410c + c772529 commit 1f214de

File tree

11 files changed

+169
-30
lines changed

11 files changed

+169
-30
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ CLIENT_PORT=8080 SERVER_PORT=9000 npx @modelcontextprotocol/inspector node build
3838

3939
For more details on ways to use the inspector, see the [Inspector section of the MCP docs site](https://modelcontextprotocol.io/docs/tools/inspector). For help with debugging, see the [Debugging guide](https://modelcontextprotocol.io/docs/tools/debugging).
4040

41+
### Authentication
42+
43+
The inspector supports bearer token authentication for SSE connections. Enter your token in the UI when connecting to an MCP server, and it will be sent in the Authorization header.
44+
4145
### From this repository
4246

4347
If you're working on the inspector itself:

client/package.json

Lines changed: 1 addition & 1 deletion
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.6.0",
44
"description": "Client-side application for the Model Context Protocol inspector",
55
"license": "MIT",
66
"author": "Anthropic, PBC (https://anthropic.com)",

client/src/App.tsx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ const App = () => {
9797
>([]);
9898
const [roots, setRoots] = useState<Root[]>([]);
9999
const [env, setEnv] = useState<Record<string, string>>({});
100+
const [bearerToken, setBearerToken] = useState<string>(() => {
101+
return localStorage.getItem("lastBearerToken") || "";
102+
});
100103

101104
const [pendingSampleRequests, setPendingSampleRequests] = useState<
102105
Array<
@@ -128,6 +131,10 @@ const App = () => {
128131
const [selectedResource, setSelectedResource] = useState<Resource | null>(
129132
null,
130133
);
134+
const [resourceSubscriptions, setResourceSubscriptions] = useState<
135+
Set<string>
136+
>(new Set<string>());
137+
131138
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
132139
const [selectedTool, setSelectedTool] = useState<Tool | null>(null);
133140
const [nextResourceCursor, setNextResourceCursor] = useState<
@@ -160,6 +167,7 @@ const App = () => {
160167
args,
161168
sseUrl,
162169
env,
170+
bearerToken,
163171
proxyServerUrl: PROXY_SERVER_URL,
164172
onNotification: (notification) => {
165173
setNotifications((prev) => [...prev, notification as ServerNotification]);
@@ -195,6 +203,10 @@ const App = () => {
195203
localStorage.setItem("lastTransportType", transportType);
196204
}, [transportType]);
197205

206+
useEffect(() => {
207+
localStorage.setItem("lastBearerToken", bearerToken);
208+
}, [bearerToken]);
209+
198210
// Auto-connect if serverUrl is provided in URL params (e.g. after OAuth callback)
199211
useEffect(() => {
200212
const serverUrl = params.get("serverUrl");
@@ -308,6 +320,38 @@ const App = () => {
308320
setResourceContent(JSON.stringify(response, null, 2));
309321
};
310322

323+
const subscribeToResource = async (uri: string) => {
324+
if (!resourceSubscriptions.has(uri)) {
325+
await makeRequest(
326+
{
327+
method: "resources/subscribe" as const,
328+
params: { uri },
329+
},
330+
z.object({}),
331+
"resources",
332+
);
333+
const clone = new Set(resourceSubscriptions);
334+
clone.add(uri);
335+
setResourceSubscriptions(clone);
336+
}
337+
};
338+
339+
const unsubscribeFromResource = async (uri: string) => {
340+
if (resourceSubscriptions.has(uri)) {
341+
await makeRequest(
342+
{
343+
method: "resources/unsubscribe" as const,
344+
params: { uri },
345+
},
346+
z.object({}),
347+
"resources",
348+
);
349+
const clone = new Set(resourceSubscriptions);
350+
clone.delete(uri);
351+
setResourceSubscriptions(clone);
352+
}
353+
};
354+
311355
const listPrompts = async () => {
312356
const response = await makeRequest(
313357
{
@@ -382,6 +426,8 @@ const App = () => {
382426
setSseUrl={setSseUrl}
383427
env={env}
384428
setEnv={setEnv}
429+
bearerToken={bearerToken}
430+
setBearerToken={setBearerToken}
385431
onConnect={connectMcpServer}
386432
stdErrNotifications={stdErrNotifications}
387433
/>
@@ -485,6 +531,18 @@ const App = () => {
485531
clearError("resources");
486532
setSelectedResource(resource);
487533
}}
534+
resourceSubscriptionsSupported={
535+
serverCapabilities?.resources?.subscribe || false
536+
}
537+
resourceSubscriptions={resourceSubscriptions}
538+
subscribeToResource={(uri) => {
539+
clearError("resources");
540+
subscribeToResource(uri);
541+
}}
542+
unsubscribeFromResource={(uri) => {
543+
clearError("resources");
544+
unsubscribeFromResource(uri);
545+
}}
488546
handleCompletion={handleCompletion}
489547
completionsSupported={completionsSupported}
490548
resourceContent={resourceContent}

client/src/components/ResourcesTab.tsx

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ const ResourcesTab = ({
2626
readResource,
2727
selectedResource,
2828
setSelectedResource,
29+
resourceSubscriptionsSupported,
30+
resourceSubscriptions,
31+
subscribeToResource,
32+
unsubscribeFromResource,
2933
handleCompletion,
3034
completionsSupported,
3135
resourceContent,
@@ -52,6 +56,10 @@ const ResourcesTab = ({
5256
nextCursor: ListResourcesResult["nextCursor"];
5357
nextTemplateCursor: ListResourceTemplatesResult["nextCursor"];
5458
error: string | null;
59+
resourceSubscriptionsSupported: boolean;
60+
resourceSubscriptions: Set<string>;
61+
subscribeToResource: (uri: string) => void;
62+
unsubscribeFromResource: (uri: string) => void;
5563
}) => {
5664
const [selectedTemplate, setSelectedTemplate] =
5765
useState<ResourceTemplate | null>(null);
@@ -164,14 +172,38 @@ const ResourcesTab = ({
164172
: "Select a resource or template"}
165173
</h3>
166174
{selectedResource && (
167-
<Button
168-
variant="outline"
169-
size="sm"
170-
onClick={() => readResource(selectedResource.uri)}
171-
>
172-
<RefreshCw className="w-4 h-4 mr-2" />
173-
Refresh
174-
</Button>
175+
<div className="flex row-auto gap-1 justify-end w-2/5">
176+
{resourceSubscriptionsSupported &&
177+
!resourceSubscriptions.has(selectedResource.uri) && (
178+
<Button
179+
variant="outline"
180+
size="sm"
181+
onClick={() => subscribeToResource(selectedResource.uri)}
182+
>
183+
Subscribe
184+
</Button>
185+
)}
186+
{resourceSubscriptionsSupported &&
187+
resourceSubscriptions.has(selectedResource.uri) && (
188+
<Button
189+
variant="outline"
190+
size="sm"
191+
onClick={() =>
192+
unsubscribeFromResource(selectedResource.uri)
193+
}
194+
>
195+
Unsubscribe
196+
</Button>
197+
)}
198+
<Button
199+
variant="outline"
200+
size="sm"
201+
onClick={() => readResource(selectedResource.uri)}
202+
>
203+
<RefreshCw className="w-4 h-4 mr-2" />
204+
Refresh
205+
</Button>
206+
</div>
175207
)}
176208
</div>
177209
<div className="p-4">

client/src/components/SamplingTab.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const SamplingTab = ({ pendingRequests, onApprove, onReject }: Props) => {
4343
<h3 className="text-lg font-semibold">Recent Requests</h3>
4444
{pendingRequests.map((request) => (
4545
<div key={request.id} className="p-4 border rounded-lg space-y-4">
46-
<pre className="bg-gray-50 p-2 rounded">
46+
<pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-2 rounded">
4747
{JSON.stringify(request.request, null, 2)}
4848
</pre>
4949
<div className="flex space-x-2">

client/src/components/Sidebar.tsx

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ interface SidebarProps {
3535
setSseUrl: (url: string) => void;
3636
env: Record<string, string>;
3737
setEnv: (env: Record<string, string>) => void;
38+
bearerToken: string;
39+
setBearerToken: (token: string) => void;
3840
onConnect: () => void;
3941
stdErrNotifications: StdErrNotification[];
4042
}
@@ -51,11 +53,14 @@ const Sidebar = ({
5153
setSseUrl,
5254
env,
5355
setEnv,
56+
bearerToken,
57+
setBearerToken,
5458
onConnect,
5559
stdErrNotifications,
5660
}: SidebarProps) => {
5761
const [theme, setTheme] = useTheme();
5862
const [showEnvVars, setShowEnvVars] = useState(false);
63+
const [showBearerToken, setShowBearerToken] = useState(false);
5964
const [shownEnvVars, setShownEnvVars] = useState<Set<string>>(new Set());
6065

6166
return (
@@ -110,15 +115,43 @@ const Sidebar = ({
110115
</div>
111116
</>
112117
) : (
113-
<div className="space-y-2">
114-
<label className="text-sm font-medium">URL</label>
115-
<Input
116-
placeholder="URL"
117-
value={sseUrl}
118-
onChange={(e) => setSseUrl(e.target.value)}
119-
className="font-mono"
120-
/>
121-
</div>
118+
<>
119+
<div className="space-y-2">
120+
<label className="text-sm font-medium">URL</label>
121+
<Input
122+
placeholder="URL"
123+
value={sseUrl}
124+
onChange={(e) => setSseUrl(e.target.value)}
125+
className="font-mono"
126+
/>
127+
</div>
128+
<div className="space-y-2">
129+
<Button
130+
variant="outline"
131+
onClick={() => setShowBearerToken(!showBearerToken)}
132+
className="flex items-center w-full"
133+
>
134+
{showBearerToken ? (
135+
<ChevronDown className="w-4 h-4 mr-2" />
136+
) : (
137+
<ChevronRight className="w-4 h-4 mr-2" />
138+
)}
139+
Authentication
140+
</Button>
141+
{showBearerToken && (
142+
<div className="space-y-2">
143+
<label className="text-sm font-medium">Bearer Token</label>
144+
<Input
145+
placeholder="Bearer Token"
146+
value={bearerToken}
147+
onChange={(e) => setBearerToken(e.target.value)}
148+
className="font-mono"
149+
type="password"
150+
/>
151+
</div>
152+
)}
153+
</div>
154+
</>
122155
)}
123156
{transportType === "stdio" && (
124157
<div className="space-y-2">

client/src/lib/hooks/useConnection.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
CreateMessageRequestSchema,
1010
ListRootsRequestSchema,
1111
ProgressNotificationSchema,
12+
ResourceUpdatedNotificationSchema,
1213
Request,
1314
Result,
1415
ServerCapabilities,
@@ -25,6 +26,7 @@ import { SESSION_KEYS } from "../constants";
2526
import { Notification, StdErrNotificationSchema } from "../notificationTypes";
2627
import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
2728
import { authProvider } from "../auth";
29+
import packageJson from "../../../package.json";
2830

2931
const params = new URLSearchParams(window.location.search);
3032
const DEFAULT_REQUEST_TIMEOUT_MSEC =
@@ -37,6 +39,7 @@ interface UseConnectionOptions {
3739
sseUrl: string;
3840
env: Record<string, string>;
3941
proxyServerUrl: string;
42+
bearerToken?: string;
4043
requestTimeout?: number;
4144
onNotification?: (notification: Notification) => void;
4245
onStdErrNotification?: (notification: Notification) => void;
@@ -57,6 +60,7 @@ export function useConnection({
5760
sseUrl,
5861
env,
5962
proxyServerUrl,
63+
bearerToken,
6064
requestTimeout = DEFAULT_REQUEST_TIMEOUT_MSEC,
6165
onNotification,
6266
onStdErrNotification,
@@ -202,7 +206,7 @@ export function useConnection({
202206
const client = new Client<Request, Notification, Result>(
203207
{
204208
name: "mcp-inspector",
205-
version: "0.0.1",
209+
version: packageJson.version,
206210
},
207211
{
208212
capabilities: {
@@ -228,9 +232,11 @@ export function useConnection({
228232
// Inject auth manually instead of using SSEClientTransport, because we're
229233
// proxying through the inspector server first.
230234
const headers: HeadersInit = {};
231-
const tokens = await authProvider.tokens();
232-
if (tokens) {
233-
headers["Authorization"] = `Bearer ${tokens.access_token}`;
235+
236+
// Use manually provided bearer token if available, otherwise use OAuth tokens
237+
const token = bearerToken || (await authProvider.tokens())?.access_token;
238+
if (token) {
239+
headers["Authorization"] = `Bearer ${token}`;
234240
}
235241

236242
const clientTransport = new SSEClientTransport(backendUrl, {
@@ -247,6 +253,11 @@ export function useConnection({
247253
ProgressNotificationSchema,
248254
onNotification,
249255
);
256+
257+
client.setNotificationHandler(
258+
ResourceUpdatedNotificationSchema,
259+
onNotification,
260+
);
250261
}
251262

252263
if (onStdErrNotification) {

client/src/lib/notificationTypes.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
NotificationSchema as BaseNotificationSchema,
33
ClientNotificationSchema,
4+
ServerNotificationSchema,
45
} from "@modelcontextprotocol/sdk/types.js";
56
import { z } from "zod";
67

@@ -13,7 +14,7 @@ export const StdErrNotificationSchema = BaseNotificationSchema.extend({
1314

1415
export const NotificationSchema = ClientNotificationSchema.or(
1516
StdErrNotificationSchema,
16-
);
17+
).or(ServerNotificationSchema);
1718

1819
export type StdErrNotification = z.infer<typeof StdErrNotificationSchema>;
1920
export type Notification = z.infer<typeof NotificationSchema>;

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@modelcontextprotocol/inspector",
3-
"version": "0.5.1",
3+
"version": "0.6.0",
44
"description": "Model Context Protocol inspector",
55
"license": "MIT",
66
"author": "Anthropic, PBC (https://anthropic.com)",
@@ -34,8 +34,8 @@
3434
"publish-all": "npm publish --workspaces --access public && npm publish --access public"
3535
},
3636
"dependencies": {
37-
"@modelcontextprotocol/inspector-client": "0.4.1",
38-
"@modelcontextprotocol/inspector-server": "0.4.1",
37+
"@modelcontextprotocol/inspector-client": "^0.6.0",
38+
"@modelcontextprotocol/inspector-server": "^0.6.0",
3939
"concurrently": "^9.0.1",
4040
"shell-quote": "^1.8.2",
4141
"spawn-rx": "^5.1.2",

0 commit comments

Comments
 (0)