Merge branch 'main' of https://github.com/olaservo/inspector
This commit is contained in:
10
README.md
10
README.md
@@ -11,7 +11,7 @@ The MCP inspector is a developer tool for testing and debugging MCP servers.
|
||||
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`:
|
||||
|
||||
```bash
|
||||
npx @modelcontextprotocol/inspector build/index.js
|
||||
npx @modelcontextprotocol/inspector node build/index.js
|
||||
```
|
||||
|
||||
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
|
||||
npx @modelcontextprotocol/inspector build/index.js arg1 arg2
|
||||
|
||||
# Pass environment variables only
|
||||
npx @modelcontextprotocol/inspector -e KEY=value -e KEY2=$VALUE2 build/index.js
|
||||
npx @modelcontextprotocol/inspector -e KEY=value -e KEY2=$VALUE2 node build/index.js
|
||||
|
||||
# Pass both environment variables and arguments
|
||||
npx @modelcontextprotocol/inspector -e KEY=value -e KEY2=$VALUE2 build/index.js arg1 arg2
|
||||
npx @modelcontextprotocol/inspector -e KEY=value -e KEY2=$VALUE2 node 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
|
||||
npx @modelcontextprotocol/inspector -e KEY=$VALUE -- node 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:
|
||||
|
||||
```bash
|
||||
CLIENT_PORT=8080 SERVER_PORT=9000 npx @modelcontextprotocol/inspector build/index.js
|
||||
CLIENT_PORT=8080 SERVER_PORT=9000 npx @modelcontextprotocol/inspector node build/index.js
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@modelcontextprotocol/inspector-client",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.1",
|
||||
"description": "Client-side application for the Model Context Protocol inspector",
|
||||
"license": "MIT",
|
||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||
|
||||
@@ -24,9 +24,15 @@ const OAuthCallback = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
const accessToken = await handleOAuthCallback(serverUrl, code);
|
||||
// Store the access token for future use
|
||||
sessionStorage.setItem(SESSION_KEYS.ACCESS_TOKEN, accessToken);
|
||||
const tokens = await handleOAuthCallback(serverUrl, code);
|
||||
// Store both access and refresh tokens
|
||||
sessionStorage.setItem(SESSION_KEYS.ACCESS_TOKEN, tokens.access_token);
|
||||
if (tokens.refresh_token) {
|
||||
sessionStorage.setItem(
|
||||
SESSION_KEYS.REFRESH_TOKEN,
|
||||
tokens.refresh_token,
|
||||
);
|
||||
}
|
||||
// Redirect back to the main app with server URL to trigger auto-connect
|
||||
window.location.href = `/?serverUrl=${encodeURIComponent(serverUrl)}`;
|
||||
} catch (error) {
|
||||
|
||||
@@ -87,11 +87,20 @@ const ToolsTab = ({
|
||||
className="max-w-full h-auto"
|
||||
/>
|
||||
)}
|
||||
{item.type === "resource" && (
|
||||
<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">
|
||||
{JSON.stringify(item.resource, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
{item.type === "resource" &&
|
||||
(item.resource?.mimeType?.startsWith("audio/") ? (
|
||||
<audio
|
||||
controls
|
||||
src={`data:${item.resource.mimeType};base64,${item.resource.blob}`}
|
||||
className="w-full"
|
||||
>
|
||||
<p>Your browser does not support audio playback</p>
|
||||
</audio>
|
||||
) : (
|
||||
<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">
|
||||
{JSON.stringify(item.resource, null, 2)}
|
||||
</pre>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
import pkceChallenge from "pkce-challenge";
|
||||
import { SESSION_KEYS } from "./constants";
|
||||
import { z } from "zod";
|
||||
|
||||
export interface OAuthMetadata {
|
||||
authorization_endpoint: string;
|
||||
token_endpoint: string;
|
||||
}
|
||||
export const OAuthMetadataSchema = z.object({
|
||||
authorization_endpoint: z.string(),
|
||||
token_endpoint: z.string(),
|
||||
});
|
||||
|
||||
export type OAuthMetadata = z.infer<typeof OAuthMetadataSchema>;
|
||||
|
||||
export const OAuthTokensSchema = z.object({
|
||||
access_token: z.string(),
|
||||
refresh_token: z.string().optional(),
|
||||
expires_in: z.number().optional(),
|
||||
});
|
||||
|
||||
export type OAuthTokens = z.infer<typeof OAuthTokensSchema>;
|
||||
|
||||
export async function discoverOAuthMetadata(
|
||||
serverUrl: string,
|
||||
@@ -15,10 +26,11 @@ export async function discoverOAuthMetadata(
|
||||
|
||||
if (response.ok) {
|
||||
const metadata = await response.json();
|
||||
return {
|
||||
const validatedMetadata = OAuthMetadataSchema.parse({
|
||||
authorization_endpoint: metadata.authorization_endpoint,
|
||||
token_endpoint: metadata.token_endpoint,
|
||||
};
|
||||
});
|
||||
return validatedMetadata;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("OAuth metadata discovery failed:", error);
|
||||
@@ -26,10 +38,11 @@ export async function discoverOAuthMetadata(
|
||||
|
||||
// Fall back to default endpoints
|
||||
const baseUrl = new URL(serverUrl);
|
||||
return {
|
||||
const defaultMetadata = {
|
||||
authorization_endpoint: new URL("/authorize", baseUrl).toString(),
|
||||
token_endpoint: new URL("/token", baseUrl).toString(),
|
||||
};
|
||||
return OAuthMetadataSchema.parse(defaultMetadata);
|
||||
}
|
||||
|
||||
export async function startOAuthFlow(serverUrl: string): Promise<string> {
|
||||
@@ -60,7 +73,7 @@ export async function startOAuthFlow(serverUrl: string): Promise<string> {
|
||||
export async function handleOAuthCallback(
|
||||
serverUrl: string,
|
||||
code: string,
|
||||
): Promise<string> {
|
||||
): Promise<OAuthTokens> {
|
||||
// Get stored code verifier
|
||||
const codeVerifier = sessionStorage.getItem(SESSION_KEYS.CODE_VERIFIER);
|
||||
if (!codeVerifier) {
|
||||
@@ -69,7 +82,6 @@ export async function handleOAuthCallback(
|
||||
|
||||
// Discover OAuth endpoints
|
||||
const metadata = await discoverOAuthMetadata(serverUrl);
|
||||
|
||||
// Exchange code for tokens
|
||||
const response = await fetch(metadata.token_endpoint, {
|
||||
method: "POST",
|
||||
@@ -88,6 +100,35 @@ export async function handleOAuthCallback(
|
||||
throw new Error("Token exchange failed");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.access_token;
|
||||
const tokens = await response.json();
|
||||
return OAuthTokensSchema.parse(tokens);
|
||||
}
|
||||
|
||||
export async function refreshAccessToken(
|
||||
serverUrl: string,
|
||||
): Promise<OAuthTokens> {
|
||||
const refreshToken = sessionStorage.getItem(SESSION_KEYS.REFRESH_TOKEN);
|
||||
if (!refreshToken) {
|
||||
throw new Error("No refresh token available");
|
||||
}
|
||||
|
||||
const metadata = await discoverOAuthMetadata(serverUrl);
|
||||
|
||||
const response = await fetch(metadata.token_endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: refreshToken,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Token refresh failed");
|
||||
}
|
||||
|
||||
const tokens = await response.json();
|
||||
return OAuthTokensSchema.parse(tokens);
|
||||
}
|
||||
|
||||
@@ -3,4 +3,5 @@ export const SESSION_KEYS = {
|
||||
CODE_VERIFIER: "mcp_code_verifier",
|
||||
SERVER_URL: "mcp_server_url",
|
||||
ACCESS_TOKEN: "mcp_access_token",
|
||||
REFRESH_TOKEN: "mcp_refresh_token",
|
||||
} as const;
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import { useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import { z } from "zod";
|
||||
import { startOAuthFlow } from "../auth";
|
||||
import { startOAuthFlow, refreshAccessToken } from "../auth";
|
||||
import { SESSION_KEYS } from "../constants";
|
||||
import { Notification, StdErrNotificationSchema } from "../notificationTypes";
|
||||
|
||||
@@ -121,7 +121,49 @@ export function useConnection({
|
||||
}
|
||||
};
|
||||
|
||||
const connect = async () => {
|
||||
const initiateOAuthFlow = async () => {
|
||||
sessionStorage.removeItem(SESSION_KEYS.ACCESS_TOKEN);
|
||||
sessionStorage.removeItem(SESSION_KEYS.REFRESH_TOKEN);
|
||||
sessionStorage.setItem(SESSION_KEYS.SERVER_URL, sseUrl);
|
||||
const redirectUrl = await startOAuthFlow(sseUrl);
|
||||
window.location.href = redirectUrl;
|
||||
};
|
||||
|
||||
const handleTokenRefresh = async () => {
|
||||
try {
|
||||
const tokens = await refreshAccessToken(sseUrl);
|
||||
sessionStorage.setItem(SESSION_KEYS.ACCESS_TOKEN, tokens.access_token);
|
||||
if (tokens.refresh_token) {
|
||||
sessionStorage.setItem(
|
||||
SESSION_KEYS.REFRESH_TOKEN,
|
||||
tokens.refresh_token,
|
||||
);
|
||||
}
|
||||
return tokens.access_token;
|
||||
} catch (error) {
|
||||
console.error("Token refresh failed:", error);
|
||||
await initiateOAuthFlow();
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const handleAuthError = async (error: unknown) => {
|
||||
if (error instanceof SseError && error.code === 401) {
|
||||
if (sessionStorage.getItem(SESSION_KEYS.REFRESH_TOKEN)) {
|
||||
try {
|
||||
await handleTokenRefresh();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Token refresh failed:", error);
|
||||
}
|
||||
} else {
|
||||
await initiateOAuthFlow();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const connect = async (_e?: unknown, retryCount: number = 0) => {
|
||||
try {
|
||||
const client = new Client<Request, Notification, Result>(
|
||||
{
|
||||
@@ -182,14 +224,15 @@ export function useConnection({
|
||||
await client.connect(clientTransport);
|
||||
} catch (error) {
|
||||
console.error("Failed to connect to MCP server:", error);
|
||||
if (error instanceof SseError && error.code === 401) {
|
||||
// Store the server URL for the callback handler
|
||||
sessionStorage.setItem(SESSION_KEYS.SERVER_URL, sseUrl);
|
||||
const redirectUrl = await startOAuthFlow(sseUrl);
|
||||
window.location.href = redirectUrl;
|
||||
return;
|
||||
const shouldRetry = await handleAuthError(error);
|
||||
if (shouldRetry) {
|
||||
return connect(undefined, retryCount + 1);
|
||||
}
|
||||
|
||||
if (error instanceof SseError && error.code === 401) {
|
||||
// Don't set error state if we're about to redirect for auth
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@modelcontextprotocol/inspector",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.1",
|
||||
"description": "Model Context Protocol inspector",
|
||||
"license": "MIT",
|
||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||
@@ -33,8 +33,8 @@
|
||||
"publish-all": "npm publish --workspaces --access public && npm publish --access public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/inspector-client": "0.3.0",
|
||||
"@modelcontextprotocol/inspector-server": "0.3.0",
|
||||
"@modelcontextprotocol/inspector-client": "0.4.1",
|
||||
"@modelcontextprotocol/inspector-server": "0.4.1",
|
||||
"concurrently": "^9.0.1",
|
||||
"shell-quote": "^1.8.2",
|
||||
"spawn-rx": "^5.1.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@modelcontextprotocol/inspector-server",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.1",
|
||||
"description": "Server-side application for the Model Context Protocol inspector",
|
||||
"license": "MIT",
|
||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||
|
||||
@@ -181,4 +181,16 @@ app.get("/config", (req, res) => {
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
app.listen(PORT, () => {});
|
||||
|
||||
try {
|
||||
const server = app.listen(PORT);
|
||||
|
||||
server.on("listening", () => {
|
||||
const addr = server.address();
|
||||
const port = typeof addr === "string" ? addr : addr?.port;
|
||||
console.log(`Proxy server listening on port ${port}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to start server:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user