Skip to content

Commit beee383

Browse files
committed
2 parents 98b26e9 + 7b3dff6 commit beee383

File tree

10 files changed

+149
-37
lines changed

10 files changed

+149
-37
lines changed

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ The MCP inspector is a developer tool for testing and debugging MCP servers.
1111
To inspect an MCP server implementation, there's no need to clone this repo. Instead, use `npx`. For example, if your server is built at `build/index.js`:
1212

1313
```bash
14-
npx @modelcontextprotocol/inspector build/index.js
14+
npx @modelcontextprotocol/inspector node build/index.js
1515
```
1616

1717
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:
@@ -21,19 +21,19 @@ You can pass both arguments and environment variables to your MCP server. Argume
2121
npx @modelcontextprotocol/inspector build/index.js arg1 arg2
2222

2323
# Pass environment variables only
24-
npx @modelcontextprotocol/inspector -e KEY=value -e KEY2=$VALUE2 build/index.js
24+
npx @modelcontextprotocol/inspector -e KEY=value -e KEY2=$VALUE2 node build/index.js
2525

2626
# Pass both environment variables and arguments
27-
npx @modelcontextprotocol/inspector -e KEY=value -e KEY2=$VALUE2 build/index.js arg1 arg2
27+
npx @modelcontextprotocol/inspector -e KEY=value -e KEY2=$VALUE2 node build/index.js arg1 arg2
2828

2929
# Use -- to separate inspector flags from server arguments
30-
npx @modelcontextprotocol/inspector -e KEY=$VALUE -- build/index.js -e server-flag
30+
npx @modelcontextprotocol/inspector -e KEY=$VALUE -- node build/index.js -e server-flag
3131
```
3232

3333
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:
3434

3535
```bash
36-
CLIENT_PORT=8080 SERVER_PORT=9000 npx @modelcontextprotocol/inspector build/index.js
36+
CLIENT_PORT=8080 SERVER_PORT=9000 npx @modelcontextprotocol/inspector node build/index.js
3737
```
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).

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.4.0",
3+
"version": "0.4.1",
44
"description": "Client-side application for the Model Context Protocol inspector",
55
"license": "MIT",
66
"author": "Anthropic, PBC (https://anthropic.com)",

client/src/components/OAuthCallback.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,15 @@ const OAuthCallback = () => {
2424
}
2525

2626
try {
27-
const accessToken = await handleOAuthCallback(serverUrl, code);
28-
// Store the access token for future use
29-
sessionStorage.setItem(SESSION_KEYS.ACCESS_TOKEN, accessToken);
27+
const tokens = await handleOAuthCallback(serverUrl, code);
28+
// Store both access and refresh tokens
29+
sessionStorage.setItem(SESSION_KEYS.ACCESS_TOKEN, tokens.access_token);
30+
if (tokens.refresh_token) {
31+
sessionStorage.setItem(
32+
SESSION_KEYS.REFRESH_TOKEN,
33+
tokens.refresh_token,
34+
);
35+
}
3036
// Redirect back to the main app with server URL to trigger auto-connect
3137
window.location.href = `/?serverUrl=${encodeURIComponent(serverUrl)}`;
3238
} catch (error) {

client/src/components/ToolsTab.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,20 @@ const ToolsTab = ({
8787
className="max-w-full h-auto"
8888
/>
8989
)}
90-
{item.type === "resource" && (
91-
<pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 whitespace-pre-wrap break-words p-4 rounded text-sm overflow-auto max-h-64">
92-
{JSON.stringify(item.resource, null, 2)}
93-
</pre>
94-
)}
90+
{item.type === "resource" &&
91+
(item.resource?.mimeType?.startsWith("audio/") ? (
92+
<audio
93+
controls
94+
src={`data:${item.resource.mimeType};base64,${item.resource.blob}`}
95+
className="w-full"
96+
>
97+
<p>Your browser does not support audio playback</p>
98+
</audio>
99+
) : (
100+
<pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 whitespace-pre-wrap break-words p-4 rounded text-sm overflow-auto max-h-64">
101+
{JSON.stringify(item.resource, null, 2)}
102+
</pre>
103+
))}
95104
</div>
96105
))}
97106
</>

client/src/lib/auth.ts

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
import pkceChallenge from "pkce-challenge";
22
import { SESSION_KEYS } from "./constants";
3+
import { z } from "zod";
34

4-
export interface OAuthMetadata {
5-
authorization_endpoint: string;
6-
token_endpoint: string;
7-
}
5+
export const OAuthMetadataSchema = z.object({
6+
authorization_endpoint: z.string(),
7+
token_endpoint: z.string(),
8+
});
9+
10+
export type OAuthMetadata = z.infer<typeof OAuthMetadataSchema>;
11+
12+
export const OAuthTokensSchema = z.object({
13+
access_token: z.string(),
14+
refresh_token: z.string().optional(),
15+
expires_in: z.number().optional(),
16+
});
17+
18+
export type OAuthTokens = z.infer<typeof OAuthTokensSchema>;
819

920
export async function discoverOAuthMetadata(
1021
serverUrl: string,
@@ -15,21 +26,23 @@ export async function discoverOAuthMetadata(
1526

1627
if (response.ok) {
1728
const metadata = await response.json();
18-
return {
29+
const validatedMetadata = OAuthMetadataSchema.parse({
1930
authorization_endpoint: metadata.authorization_endpoint,
2031
token_endpoint: metadata.token_endpoint,
21-
};
32+
});
33+
return validatedMetadata;
2234
}
2335
} catch (error) {
2436
console.warn("OAuth metadata discovery failed:", error);
2537
}
2638

2739
// Fall back to default endpoints
2840
const baseUrl = new URL(serverUrl);
29-
return {
41+
const defaultMetadata = {
3042
authorization_endpoint: new URL("/authorize", baseUrl).toString(),
3143
token_endpoint: new URL("/token", baseUrl).toString(),
3244
};
45+
return OAuthMetadataSchema.parse(defaultMetadata);
3346
}
3447

3548
export async function startOAuthFlow(serverUrl: string): Promise<string> {
@@ -60,7 +73,7 @@ export async function startOAuthFlow(serverUrl: string): Promise<string> {
6073
export async function handleOAuthCallback(
6174
serverUrl: string,
6275
code: string,
63-
): Promise<string> {
76+
): Promise<OAuthTokens> {
6477
// Get stored code verifier
6578
const codeVerifier = sessionStorage.getItem(SESSION_KEYS.CODE_VERIFIER);
6679
if (!codeVerifier) {
@@ -69,7 +82,6 @@ export async function handleOAuthCallback(
6982

7083
// Discover OAuth endpoints
7184
const metadata = await discoverOAuthMetadata(serverUrl);
72-
7385
// Exchange code for tokens
7486
const response = await fetch(metadata.token_endpoint, {
7587
method: "POST",
@@ -88,6 +100,35 @@ export async function handleOAuthCallback(
88100
throw new Error("Token exchange failed");
89101
}
90102

91-
const data = await response.json();
92-
return data.access_token;
103+
const tokens = await response.json();
104+
return OAuthTokensSchema.parse(tokens);
105+
}
106+
107+
export async function refreshAccessToken(
108+
serverUrl: string,
109+
): Promise<OAuthTokens> {
110+
const refreshToken = sessionStorage.getItem(SESSION_KEYS.REFRESH_TOKEN);
111+
if (!refreshToken) {
112+
throw new Error("No refresh token available");
113+
}
114+
115+
const metadata = await discoverOAuthMetadata(serverUrl);
116+
117+
const response = await fetch(metadata.token_endpoint, {
118+
method: "POST",
119+
headers: {
120+
"Content-Type": "application/json",
121+
},
122+
body: JSON.stringify({
123+
grant_type: "refresh_token",
124+
refresh_token: refreshToken,
125+
}),
126+
});
127+
128+
if (!response.ok) {
129+
throw new Error("Token refresh failed");
130+
}
131+
132+
const tokens = await response.json();
133+
return OAuthTokensSchema.parse(tokens);
93134
}

client/src/lib/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ export const SESSION_KEYS = {
33
CODE_VERIFIER: "mcp_code_verifier",
44
SERVER_URL: "mcp_server_url",
55
ACCESS_TOKEN: "mcp_access_token",
6+
REFRESH_TOKEN: "mcp_refresh_token",
67
} as const;

client/src/lib/hooks/useConnection.ts

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
import { useState } from "react";
1717
import { toast } from "react-toastify";
1818
import { z } from "zod";
19-
import { startOAuthFlow } from "../auth";
19+
import { startOAuthFlow, refreshAccessToken } from "../auth";
2020
import { SESSION_KEYS } from "../constants";
2121
import { Notification, StdErrNotificationSchema } from "../notificationTypes";
2222

@@ -121,7 +121,49 @@ export function useConnection({
121121
}
122122
};
123123

124-
const connect = async () => {
124+
const initiateOAuthFlow = async () => {
125+
sessionStorage.removeItem(SESSION_KEYS.ACCESS_TOKEN);
126+
sessionStorage.removeItem(SESSION_KEYS.REFRESH_TOKEN);
127+
sessionStorage.setItem(SESSION_KEYS.SERVER_URL, sseUrl);
128+
const redirectUrl = await startOAuthFlow(sseUrl);
129+
window.location.href = redirectUrl;
130+
};
131+
132+
const handleTokenRefresh = async () => {
133+
try {
134+
const tokens = await refreshAccessToken(sseUrl);
135+
sessionStorage.setItem(SESSION_KEYS.ACCESS_TOKEN, tokens.access_token);
136+
if (tokens.refresh_token) {
137+
sessionStorage.setItem(
138+
SESSION_KEYS.REFRESH_TOKEN,
139+
tokens.refresh_token,
140+
);
141+
}
142+
return tokens.access_token;
143+
} catch (error) {
144+
console.error("Token refresh failed:", error);
145+
await initiateOAuthFlow();
146+
throw error;
147+
}
148+
};
149+
150+
const handleAuthError = async (error: unknown) => {
151+
if (error instanceof SseError && error.code === 401) {
152+
if (sessionStorage.getItem(SESSION_KEYS.REFRESH_TOKEN)) {
153+
try {
154+
await handleTokenRefresh();
155+
return true;
156+
} catch (error) {
157+
console.error("Token refresh failed:", error);
158+
}
159+
} else {
160+
await initiateOAuthFlow();
161+
}
162+
}
163+
return false;
164+
};
165+
166+
const connect = async (_e?: unknown, retryCount: number = 0) => {
125167
try {
126168
const client = new Client<Request, Notification, Result>(
127169
{
@@ -182,14 +224,15 @@ export function useConnection({
182224
await client.connect(clientTransport);
183225
} catch (error) {
184226
console.error("Failed to connect to MCP server:", error);
227+
const shouldRetry = await handleAuthError(error);
228+
if (shouldRetry) {
229+
return connect(undefined, retryCount + 1);
230+
}
231+
185232
if (error instanceof SseError && error.code === 401) {
186-
// Store the server URL for the callback handler
187-
sessionStorage.setItem(SESSION_KEYS.SERVER_URL, sseUrl);
188-
const redirectUrl = await startOAuthFlow(sseUrl);
189-
window.location.href = redirectUrl;
233+
// Don't set error state if we're about to redirect for auth
190234
return;
191235
}
192-
193236
throw error;
194237
}
195238

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.4.0",
3+
"version": "0.4.1",
44
"description": "Model Context Protocol inspector",
55
"license": "MIT",
66
"author": "Anthropic, PBC (https://anthropic.com)",
@@ -33,8 +33,8 @@
3333
"publish-all": "npm publish --workspaces --access public && npm publish --access public"
3434
},
3535
"dependencies": {
36-
"@modelcontextprotocol/inspector-client": "0.3.0",
37-
"@modelcontextprotocol/inspector-server": "0.3.0",
36+
"@modelcontextprotocol/inspector-client": "0.4.1",
37+
"@modelcontextprotocol/inspector-server": "0.4.1",
3838
"concurrently": "^9.0.1",
3939
"shell-quote": "^1.8.2",
4040
"spawn-rx": "^5.1.0",

server/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-server",
3-
"version": "0.4.0",
3+
"version": "0.4.1",
44
"description": "Server-side application for the Model Context Protocol inspector",
55
"license": "MIT",
66
"author": "Anthropic, PBC (https://anthropic.com)",

server/src/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,4 +181,16 @@ app.get("/config", (req, res) => {
181181
});
182182

183183
const PORT = process.env.PORT || 3000;
184-
app.listen(PORT, () => {});
184+
185+
try {
186+
const server = app.listen(PORT);
187+
188+
server.on("listening", () => {
189+
const addr = server.address();
190+
const port = typeof addr === "string" ? addr : addr?.port;
191+
console.log(`Proxy server listening on port ${port}`);
192+
});
193+
} catch (error) {
194+
console.error("Failed to start server:", error);
195+
process.exit(1);
196+
}

0 commit comments

Comments
 (0)