Merge branch 'main' into docs/utils-jsdoc

This commit is contained in:
Ola Hungerford 2025-08-27 20:13:01 -07:00 • committed by GitHub
commit 73873e2b77
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 1520 additions and 665 deletions

View file

@ -18,10 +18,6 @@ jobs:
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
) &&
(
github.actor == 'ihrpr' ||
github.actor == 'olaservo'
) )
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
@ -29,6 +25,7 @@ jobs:
pull-requests: read pull-requests: read
issues: read issues: read
id-token: write id-token: write
actions: read
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -41,24 +38,16 @@ jobs:
with: with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) # Allow Claude to read CI results on PRs
# model: "claude-opus-4-20250514" additional_permissions: |
actions: read
# Optional: Customize the trigger phrase (default: @claude) # Trigger when assigned to an issue
# trigger_phrase: "/claude" assignee_trigger: "claude"
# Optional: Trigger when specific user is assigned to an issue # Allow Claude to run bash
# assignee_trigger: "claude-bot" # This should be safe given the repo is already public
allowed_tools: "Bash"
# Optional: Allow Claude to run specific commands custom_instructions: |
# allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)" If posting a comment to GitHub, give a concise summary of the comment at the top and put all the details in a <details> block.
# Optional: Add custom instructions for Claude to customize its behavior for your project
# custom_instructions: |
# Follow our coding standards
# Ensure all new code has tests
# Use TypeScript for new files
# Optional: Custom environment variables for Claude
# claude_env: |
# NODE_ENV: test

2
.husky/pre-commit Normal file
View file

@ -0,0 +1,2 @@
npx lint-staged
git update-index --again

View file

@ -1,5 +1,5 @@
# Build stage # Build stage
FROM node:24-slim AS builder FROM node:current-alpine3.22 AS builder
# Set working directory # Set working directory
WORKDIR /app WORKDIR /app

View file

@ -29,6 +29,14 @@ npx @modelcontextprotocol/inspector
The server will start up and the UI will be accessible at `http://localhost:6274`. The server will start up and the UI will be accessible at `http://localhost:6274`.
### Docker Container
You can also start it in a Docker container with the following command:
```bash
docker run --rm --network host -p 6274:6274 -p 6277:6277 ghcr.io/modelcontextprotocol/inspector:latest
```
### From an MCP server repository ### From an MCP server repository
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`: 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`:
@ -166,6 +174,16 @@ If you need to disable authentication (NOT RECOMMENDED), you can set the `DANGER
DANGEROUSLY_OMIT_AUTH=true npm start DANGEROUSLY_OMIT_AUTH=true npm start
``` ```
---
**🚨 WARNING 🚨**
Disabling authentication with `DANGEROUSLY_OMIT_AUTH` is incredibly dangerous! Disabling auth leaves your machine open to attack not just when exposed to the public internet, but also **via your web browser**. Meaning, visiting a malicious website OR viewing a malicious advertizement could allow an attacker to remotely compromise your computer. Do not disable this feature unless you truly understand the risks.
Read more about the risks of this vulnerability on Oligo's blog: [Critical RCE Vulnerability in Anthropic MCP Inspector - CVE-2025-49596](https://www.oligo.security/blog/critical-rce-vulnerability-in-anthropic-mcp-inspector-cve-2025-49596)
---
You can also set the token via the `MCP_PROXY_AUTH_TOKEN` environment variable when starting the server: You can also set the token via the `MCP_PROXY_AUTH_TOKEN` environment variable when starting the server:
```bash ```bash

View file

@ -1,6 +1,6 @@
{ {
"name": "@modelcontextprotocol/inspector-cli", "name": "@modelcontextprotocol/inspector-cli",
"version": "0.16.4", "version": "0.16.5",
"description": "CLI for the Model Context Protocol inspector", "description": "CLI for the Model Context Protocol inspector",
"license": "MIT", "license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)", "author": "Anthropic, PBC (https://anthropic.com)",
@ -21,7 +21,7 @@
}, },
"devDependencies": {}, "devDependencies": {},
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.17.2", "@modelcontextprotocol/sdk": "^1.17.3",
"commander": "^13.1.0", "commander": "^13.1.0",
"spawn-rx": "^5.1.2" "spawn-rx": "^5.1.2"
} }

View file

@ -32,8 +32,8 @@ function createStdioTransport(options: TransportOptions): Transport {
const defaultEnv = getDefaultEnvironment(); const defaultEnv = getDefaultEnvironment();
const env: Record<string, string> = { const env: Record<string, string> = {
...processEnv,
...defaultEnv, ...defaultEnv,
...processEnv,
}; };
const { cmd: actualCommand, args: actualArgs } = findActualExecutable( const { cmd: actualCommand, args: actualArgs } = findActualExecutable(

View file

@ -1,6 +1,6 @@
{ {
"name": "@modelcontextprotocol/inspector-client", "name": "@modelcontextprotocol/inspector-client",
"version": "0.16.4", "version": "0.16.5",
"description": "Client-side application for the Model Context Protocol inspector", "description": "Client-side application for the Model Context Protocol inspector",
"license": "MIT", "license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)", "author": "Anthropic, PBC (https://anthropic.com)",
@ -25,7 +25,7 @@
"cleanup:e2e": "node e2e/global-teardown.js" "cleanup:e2e": "node e2e/global-teardown.js"
}, },
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.17.2", "@modelcontextprotocol/sdk": "^1.17.3",
"@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-dialog": "^1.1.3", "@radix-ui/react-dialog": "^1.1.3",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",

View file

@ -34,7 +34,6 @@ import {
useDraggablePane, useDraggablePane,
useDraggableSidebar, useDraggableSidebar,
} from "./lib/hooks/useDraggablePane"; } from "./lib/hooks/useDraggablePane";
import { StdErrNotification } from "./lib/notificationTypes";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -106,9 +105,6 @@ const App = () => {
>(getInitialTransportType); >(getInitialTransportType);
const [logLevel, setLogLevel] = useState<LoggingLevel>("debug"); const [logLevel, setLogLevel] = useState<LoggingLevel>("debug");
const [notifications, setNotifications] = useState<ServerNotification[]>([]); const [notifications, setNotifications] = useState<ServerNotification[]>([]);
const [stdErrNotifications, setStdErrNotifications] = useState<
StdErrNotification[]
>([]);
const [roots, setRoots] = useState<Root[]>([]); const [roots, setRoots] = useState<Root[]>([]);
const [env, setEnv] = useState<Record<string, string>>({}); const [env, setEnv] = useState<Record<string, string>>({});
@ -224,12 +220,6 @@ const App = () => {
onNotification: (notification) => { 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) => { onPendingRequest: (request, resolve, reject) => {
setPendingSampleRequests((prev) => [ setPendingSampleRequests((prev) => [
...prev, ...prev,
@ -757,10 +747,6 @@ const App = () => {
setLogLevel(level); setLogLevel(level);
}; };
const clearStdErrNotifications = () => {
setStdErrNotifications([]);
};
const AuthDebuggerWrapper = () => ( const AuthDebuggerWrapper = () => (
<TabsContent value="auth"> <TabsContent value="auth">
<AuthDebugger <AuthDebugger
@ -829,11 +815,9 @@ const App = () => {
setOauthScope={setOauthScope} setOauthScope={setOauthScope}
onConnect={connectMcpServer} onConnect={connectMcpServer}
onDisconnect={disconnectMcpServer} onDisconnect={disconnectMcpServer}
stdErrNotifications={stdErrNotifications}
logLevel={logLevel} logLevel={logLevel}
sendLogLevelRequest={sendLogLevelRequest} sendLogLevelRequest={sendLogLevelRequest}
loggingSupported={!!serverCapabilities?.logging || false} loggingSupported={!!serverCapabilities?.logging || false}
clearStdErrNotifications={clearStdErrNotifications}
/> />
<div <div
onMouseDown={handleSidebarDragStart} onMouseDown={handleSidebarDragStart}

View file

@ -6,6 +6,7 @@ import { AuthDebuggerState, EMPTY_DEBUGGER_STATE } from "../lib/auth-types";
import { OAuthFlowProgress } from "./OAuthFlowProgress"; import { OAuthFlowProgress } from "./OAuthFlowProgress";
import { OAuthStateMachine } from "../lib/oauth-state-machine"; import { OAuthStateMachine } from "../lib/oauth-state-machine";
import { SESSION_KEYS } from "../lib/constants"; import { SESSION_KEYS } from "../lib/constants";
import { validateRedirectUrl } from "@/utils/urlValidation";
export interface AuthDebuggerProps { export interface AuthDebuggerProps {
serverUrl: string; serverUrl: string;
@ -163,6 +164,23 @@ const AuthDebugger = ({
currentState.oauthStep === "authorization_code" && currentState.oauthStep === "authorization_code" &&
currentState.authorizationUrl currentState.authorizationUrl
) { ) {
// Validate the URL before redirecting
try {
validateRedirectUrl(currentState.authorizationUrl);
} catch (error) {
updateAuthState({
...currentState,
isInitiatingAuth: false,
latestError:
error instanceof Error ? error : new Error(String(error)),
statusMessage: {
type: "error",
message: `Invalid authorization URL: ${error instanceof Error ? error.message : String(error)}`,
},
});
return;
}
// Store the current auth state before redirecting // Store the current auth state before redirecting
sessionStorage.setItem( sessionStorage.setItem(
SESSION_KEYS.AUTH_DEBUGGER_STATE, SESSION_KEYS.AUTH_DEBUGGER_STATE,

View file

@ -46,6 +46,17 @@ const OAuthDebugCallback = ({ onConnect }: OAuthCallbackProps) => {
if (storedState) { if (storedState) {
try { try {
restoredState = JSON.parse(storedState); restoredState = JSON.parse(storedState);
if (restoredState && typeof restoredState.resource === "string") {
restoredState.resource = new URL(restoredState.resource);
}
if (
restoredState &&
typeof restoredState.authorizationUrl === "string"
) {
restoredState.authorizationUrl = new URL(
restoredState.authorizationUrl,
);
}
// Clean up the stored state // Clean up the stored state
sessionStorage.removeItem(SESSION_KEYS.AUTH_DEBUGGER_STATE); sessionStorage.removeItem(SESSION_KEYS.AUTH_DEBUGGER_STATE);
} catch (e) { } catch (e) {

View file

@ -4,6 +4,8 @@ import { Button } from "./ui/button";
import { DebugInspectorOAuthClientProvider } from "@/lib/auth"; import { DebugInspectorOAuthClientProvider } from "@/lib/auth";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { OAuthClientInformation } from "@modelcontextprotocol/sdk/shared/auth.js"; import { OAuthClientInformation } from "@modelcontextprotocol/sdk/shared/auth.js";
import { validateRedirectUrl } from "@/utils/urlValidation";
import { useToast } from "@/lib/hooks/useToast";
interface OAuthStepProps { interface OAuthStepProps {
label: string; label: string;
@ -71,6 +73,7 @@ export const OAuthFlowProgress = ({
updateAuthState, updateAuthState,
proceedToNextStep, proceedToNextStep,
}: OAuthFlowProgressProps) => { }: OAuthFlowProgressProps) => {
const { toast } = useToast();
const provider = useMemo( const provider = useMemo(
() => new DebugInspectorOAuthClientProvider(serverUrl), () => new DebugInspectorOAuthClientProvider(serverUrl),
[serverUrl], [serverUrl],
@ -239,16 +242,32 @@ export const OAuthFlowProgress = ({
<p className="text-xs break-all"> <p className="text-xs break-all">
{authState.authorizationUrl} {authState.authorizationUrl}
</p> </p>
<a <button
href={authState.authorizationUrl} onClick={() => {
target="_blank" try {
rel="noopener noreferrer" validateRedirectUrl(authState.authorizationUrl!);
window.open(
authState.authorizationUrl!,
"_blank",
"noopener noreferrer",
);
} catch (error) {
toast({
title: "Invalid URL",
description:
error instanceof Error
? error.message
: "The authorization URL is not valid",
variant: "destructive",
});
}
}}
className="flex items-center text-blue-500 hover:text-blue-700" className="flex items-center text-blue-500 hover:text-blue-700"
aria-label="Open authorization URL in new tab" aria-label="Open authorization URL in new tab"
title="Open authorization URL" title="Open authorization URL"
> >
<ExternalLink className="h-4 w-4" /> <ExternalLink className="h-4 w-4" />
</a> </button>
</div> </div>
<p className="text-xs text-muted-foreground mt-2"> <p className="text-xs text-muted-foreground mt-2">
Click the link to authorize in your browser. After Click the link to authorize in your browser. After
@ -354,7 +373,21 @@ export const OAuthFlowProgress = ({
authState.authorizationUrl && ( authState.authorizationUrl && (
<Button <Button
variant="outline" variant="outline"
onClick={() => window.open(authState.authorizationUrl!, "_blank")} onClick={() => {
try {
validateRedirectUrl(authState.authorizationUrl!);
window.open(authState.authorizationUrl!, "_blank");
} catch (error) {
toast({
title: "Invalid URL",
description:
error instanceof Error
? error.message
: "The authorization URL is not valid",
variant: "destructive",
});
}
}}
> >
Open in New Tab Open in New Tab
</Button> </Button>

View file

@ -24,7 +24,6 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { StdErrNotification } from "@/lib/notificationTypes";
import { import {
LoggingLevel, LoggingLevel,
LoggingLevelSchema, LoggingLevelSchema,
@ -62,8 +61,6 @@ interface SidebarProps {
setOauthScope: (scope: string) => void; setOauthScope: (scope: string) => void;
onConnect: () => void; onConnect: () => void;
onDisconnect: () => void; onDisconnect: () => void;
stdErrNotifications: StdErrNotification[];
clearStdErrNotifications: () => void;
logLevel: LoggingLevel; logLevel: LoggingLevel;
sendLogLevelRequest: (level: LoggingLevel) => void; sendLogLevelRequest: (level: LoggingLevel) => void;
loggingSupported: boolean; loggingSupported: boolean;
@ -93,8 +90,6 @@ const Sidebar = ({
setOauthScope, setOauthScope,
onConnect, onConnect,
onDisconnect, onDisconnect,
stdErrNotifications,
clearStdErrNotifications,
logLevel, logLevel,
sendLogLevelRequest, sendLogLevelRequest,
loggingSupported, loggingSupported,
@ -268,6 +263,7 @@ const Sidebar = ({
placeholder="Command" placeholder="Command"
value={command} value={command}
onChange={(e) => setCommand(e.target.value)} onChange={(e) => setCommand(e.target.value)}
onBlur={(e) => setCommand(e.target.value.trim())}
className="font-mono" className="font-mono"
/> />
</div> </div>
@ -760,36 +756,6 @@ const Sidebar = ({
</Select> </Select>
</div> </div>
)} )}
{stdErrNotifications.length > 0 && (
<>
<div className="mt-4 border-t border-gray-200 pt-4">
<div className="flex justify-between items-center">
<h3 className="text-sm font-medium">
Error output from MCP server
</h3>
<Button
variant="outline"
size="sm"
onClick={clearStdErrNotifications}
className="h-8 px-2"
>
Clear
</Button>
</div>
<div className="mt-2 max-h-80 overflow-y-auto">
{stdErrNotifications.map((notification, index) => (
<div
key={index}
className="text-sm text-red-500 font-mono py-2 border-b border-gray-200 last:border-b-0"
>
{notification.params.content}
</div>
))}
</div>
</div>
</>
)}
</div> </div>
</div> </div>
</div> </div>

View file

@ -31,7 +31,7 @@ Object.defineProperty(navigator, "clipboard", {
// Setup fake timers // Setup fake timers
jest.useFakeTimers(); jest.useFakeTimers();
describe("Sidebar Environment Variables", () => { describe("Sidebar", () => {
const defaultProps = { const defaultProps = {
connectionStatus: "disconnected" as const, connectionStatus: "disconnected" as const,
transportType: "stdio" as const, transportType: "stdio" as const,
@ -69,16 +69,49 @@ describe("Sidebar Environment Variables", () => {
); );
}; };
const openEnvVarsSection = () => {
const button = screen.getByTestId("env-vars-button");
fireEvent.click(button);
};
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
jest.clearAllTimers(); jest.clearAllTimers();
}); });
describe("Command and arguments", () => {
it("should trim whitespace from command input on blur", () => {
const setCommand = jest.fn();
renderSidebar({ command: " node server.js ", setCommand });
const commandInput = screen.getByLabelText("Command");
fireEvent.blur(commandInput);
expect(setCommand).toHaveBeenLastCalledWith("node server.js");
});
it("should handle whitespace-only command input on blur", () => {
const setCommand = jest.fn();
renderSidebar({ command: " ", setCommand });
const commandInput = screen.getByLabelText("Command");
fireEvent.blur(commandInput);
expect(setCommand).toHaveBeenLastCalledWith("");
});
it("should not affect command without surrounding whitespace", () => {
const setCommand = jest.fn();
renderSidebar({ command: "node", setCommand });
const commandInput = screen.getByLabelText("Command");
fireEvent.blur(commandInput);
expect(setCommand).toHaveBeenLastCalledWith("node");
});
});
describe("Environment Variables", () => {
const openEnvVarsSection = () => {
const button = screen.getByTestId("env-vars-button");
fireEvent.click(button);
};
describe("Basic Operations", () => { describe("Basic Operations", () => {
it("should add a new environment variable", () => { it("should add a new environment variable", () => {
const setEnv = jest.fn(); const setEnv = jest.fn();
@ -127,164 +160,15 @@ describe("Sidebar Environment Variables", () => {
const valueInput = screen.getByDisplayValue("test_value"); const valueInput = screen.getByDisplayValue("test_value");
expect(valueInput).toHaveProperty("type", "password"); expect(valueInput).toHaveProperty("type", "password");
const toggleButton = screen.getByRole("button", { name: /show value/i }); const toggleButton = screen.getByRole("button", {
name: /show value/i,
});
fireEvent.click(toggleButton); fireEvent.click(toggleButton);
expect(valueInput).toHaveProperty("type", "text"); expect(valueInput).toHaveProperty("type", "text");
}); });
}); });
describe("Authentication", () => {
const openAuthSection = () => {
const button = screen.getByTestId("auth-button");
fireEvent.click(button);
};
it("should update bearer token", () => {
const setBearerToken = jest.fn();
renderSidebar({
bearerToken: "",
setBearerToken,
transportType: "sse", // Set transport type to SSE
});
openAuthSection();
const tokenInput = screen.getByTestId("bearer-token-input");
fireEvent.change(tokenInput, { target: { value: "new_token" } });
expect(setBearerToken).toHaveBeenCalledWith("new_token");
});
it("should update header name", () => {
const setHeaderName = jest.fn();
renderSidebar({
headerName: "Authorization",
setHeaderName,
transportType: "sse",
});
openAuthSection();
const headerInput = screen.getByTestId("header-input");
fireEvent.change(headerInput, { target: { value: "X-Custom-Auth" } });
expect(setHeaderName).toHaveBeenCalledWith("X-Custom-Auth");
});
it("should clear bearer token", () => {
const setBearerToken = jest.fn();
renderSidebar({
bearerToken: "existing_token",
setBearerToken,
transportType: "sse", // Set transport type to SSE
});
openAuthSection();
const tokenInput = screen.getByTestId("bearer-token-input");
fireEvent.change(tokenInput, { target: { value: "" } });
expect(setBearerToken).toHaveBeenCalledWith("");
});
it("should properly render bearer token input", () => {
const { rerender } = renderSidebar({
bearerToken: "existing_token",
transportType: "sse", // Set transport type to SSE
});
openAuthSection();
// Token input should be a password field
const tokenInput = screen.getByTestId("bearer-token-input");
expect(tokenInput).toHaveProperty("type", "password");
// Update the token
fireEvent.change(tokenInput, { target: { value: "new_token" } });
// Rerender with updated token
rerender(
<TooltipProvider>
<Sidebar
{...defaultProps}
bearerToken="new_token"
transportType="sse"
/>
</TooltipProvider>,
);
// Token input should still exist after update
expect(screen.getByTestId("bearer-token-input")).toBeInTheDocument();
});
it("should maintain token visibility state after update", () => {
const { rerender } = renderSidebar({
bearerToken: "existing_token",
transportType: "sse", // Set transport type to SSE
});
openAuthSection();
// Token input should be a password field
const tokenInput = screen.getByTestId("bearer-token-input");
expect(tokenInput).toHaveProperty("type", "password");
// Update the token
fireEvent.change(tokenInput, { target: { value: "new_token" } });
// Rerender with updated token
rerender(
<TooltipProvider>
<Sidebar
{...defaultProps}
bearerToken="new_token"
transportType="sse"
/>
</TooltipProvider>,
);
// Token input should still exist after update
expect(screen.getByTestId("bearer-token-input")).toBeInTheDocument();
});
it("should maintain header name when toggling auth section", () => {
renderSidebar({
headerName: "X-API-Key",
transportType: "sse",
});
// Open auth section
openAuthSection();
// Verify header name is displayed
const headerInput = screen.getByTestId("header-input");
expect(headerInput).toHaveValue("X-API-Key");
// Close auth section
const authButton = screen.getByTestId("auth-button");
fireEvent.click(authButton);
// Reopen auth section
fireEvent.click(authButton);
// Verify header name is still preserved
expect(screen.getByTestId("header-input")).toHaveValue("X-API-Key");
});
it("should display default header name when not specified", () => {
renderSidebar({
headerName: undefined,
transportType: "sse",
});
openAuthSection();
const headerInput = screen.getByTestId("header-input");
expect(headerInput).toHaveAttribute("placeholder", "Authorization");
});
});
describe("Key Editing", () => { describe("Key Editing", () => {
it("should maintain order when editing first key", () => { it("should maintain order when editing first key", () => {
const setEnv = jest.fn(); const setEnv = jest.fn();
@ -319,7 +203,9 @@ describe("Sidebar Environment Variables", () => {
openEnvVarsSection(); openEnvVarsSection();
const middleKeyInput = screen.getByDisplayValue("SECOND_KEY"); const middleKeyInput = screen.getByDisplayValue("SECOND_KEY");
fireEvent.change(middleKeyInput, { target: { value: "NEW_SECOND_KEY" } }); fireEvent.change(middleKeyInput, {
target: { value: "NEW_SECOND_KEY" },
});
expect(setEnv).toHaveBeenCalledWith({ expect(setEnv).toHaveBeenCalledWith({
FIRST_KEY: "first_value", FIRST_KEY: "first_value",
@ -406,7 +292,9 @@ describe("Sidebar Environment Variables", () => {
// Second key edit // Second key edit
const secondKeyInput = screen.getByDisplayValue("SECOND_KEY"); const secondKeyInput = screen.getByDisplayValue("SECOND_KEY");
fireEvent.change(secondKeyInput, { target: { value: "NEW_SECOND_KEY" } }); fireEvent.change(secondKeyInput, {
target: { value: "NEW_SECOND_KEY" },
});
// Verify the final state matches what we expect // Verify the final state matches what we expect
expect(setEnv).toHaveBeenLastCalledWith({ expect(setEnv).toHaveBeenLastCalledWith({
@ -422,7 +310,9 @@ describe("Sidebar Environment Variables", () => {
openEnvVarsSection(); openEnvVarsSection();
// Show the value // Show the value
const toggleButton = screen.getByRole("button", { name: /show value/i }); const toggleButton = screen.getByRole("button", {
name: /show value/i,
});
fireEvent.click(toggleButton); fireEvent.click(toggleButton);
const valueInput = screen.getByDisplayValue("test_value"); const valueInput = screen.getByDisplayValue("test_value");
@ -472,7 +362,7 @@ describe("Sidebar Environment Variables", () => {
expect(setEnv).toHaveBeenCalledWith({ "TEST-KEY@123": "test_value" }); expect(setEnv).toHaveBeenCalledWith({ "TEST-KEY@123": "test_value" });
}); });
it("should handle unicode characters", () => { it("should handle unicode characters in key", () => {
const setEnv = jest.fn(); const setEnv = jest.fn();
const initialEnv = { TEST_KEY: "test_value" }; const initialEnv = { TEST_KEY: "test_value" };
renderSidebar({ env: initialEnv, setEnv }); renderSidebar({ env: initialEnv, setEnv });
@ -485,7 +375,7 @@ describe("Sidebar Environment Variables", () => {
expect(setEnv).toHaveBeenCalledWith({ "TEST_🔑": "test_value" }); expect(setEnv).toHaveBeenCalledWith({ "TEST_🔑": "test_value" });
}); });
it("should handle very long key names", () => { it("should handle a very long key name", () => {
const setEnv = jest.fn(); const setEnv = jest.fn();
const initialEnv = { TEST_KEY: "test_value" }; const initialEnv = { TEST_KEY: "test_value" };
renderSidebar({ env: initialEnv, setEnv }); renderSidebar({ env: initialEnv, setEnv });
@ -499,165 +389,9 @@ describe("Sidebar Environment Variables", () => {
expect(setEnv).toHaveBeenCalledWith({ [longKey]: "test_value" }); expect(setEnv).toHaveBeenCalledWith({ [longKey]: "test_value" });
}); });
}); });
describe("Configuration Operations", () => {
const openConfigSection = () => {
const button = screen.getByTestId("config-button");
fireEvent.click(button);
};
it("should update MCP server request timeout", () => {
const setConfig = jest.fn();
renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
openConfigSection();
const timeoutInput = screen.getByTestId(
"MCP_SERVER_REQUEST_TIMEOUT-input",
);
fireEvent.change(timeoutInput, { target: { value: "5000" } });
expect(setConfig).toHaveBeenCalledWith(
expect.objectContaining({
MCP_SERVER_REQUEST_TIMEOUT: {
label: "Request Timeout",
description: "Timeout for requests to the MCP server (ms)",
value: 5000,
is_session_item: false,
},
}),
);
});
it("should update MCP server proxy address", () => {
const setConfig = jest.fn();
renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
openConfigSection();
const proxyAddressInput = screen.getByTestId(
"MCP_PROXY_FULL_ADDRESS-input",
);
fireEvent.change(proxyAddressInput, {
target: { value: "http://localhost:8080" },
});
expect(setConfig).toHaveBeenCalledWith(
expect.objectContaining({
MCP_PROXY_FULL_ADDRESS: {
label: "Inspector Proxy Address",
description:
"Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577",
value: "http://localhost:8080",
is_session_item: false,
},
}),
);
});
it("should update max total timeout", () => {
const setConfig = jest.fn();
renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
openConfigSection();
const maxTotalTimeoutInput = screen.getByTestId(
"MCP_REQUEST_MAX_TOTAL_TIMEOUT-input",
);
fireEvent.change(maxTotalTimeoutInput, {
target: { value: "10000" },
});
expect(setConfig).toHaveBeenCalledWith(
expect.objectContaining({
MCP_REQUEST_MAX_TOTAL_TIMEOUT: {
label: "Maximum Total Timeout",
description:
"Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications)",
value: 10000,
is_session_item: false,
},
}),
);
});
it("should handle invalid timeout values entered by user", () => {
const setConfig = jest.fn();
renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
openConfigSection();
const timeoutInput = screen.getByTestId(
"MCP_SERVER_REQUEST_TIMEOUT-input",
);
fireEvent.change(timeoutInput, { target: { value: "abc1" } });
expect(setConfig).toHaveBeenCalledWith(
expect.objectContaining({
MCP_SERVER_REQUEST_TIMEOUT: {
label: "Request Timeout",
description: "Timeout for requests to the MCP server (ms)",
value: 0,
is_session_item: false,
},
}),
);
});
it("should maintain configuration state after multiple updates", () => {
const setConfig = jest.fn();
const { rerender } = renderSidebar({
config: DEFAULT_INSPECTOR_CONFIG,
setConfig,
});
openConfigSection();
// First update
const timeoutInput = screen.getByTestId(
"MCP_SERVER_REQUEST_TIMEOUT-input",
);
fireEvent.change(timeoutInput, { target: { value: "5000" } });
// Get the updated config from the first setConfig call
const updatedConfig = setConfig.mock.calls[0][0] as InspectorConfig;
// Rerender with the updated config
rerender(
<TooltipProvider>
<Sidebar
{...defaultProps}
config={updatedConfig}
setConfig={setConfig}
/>
</TooltipProvider>,
);
// Second update
const updatedTimeoutInput = screen.getByTestId(
"MCP_SERVER_REQUEST_TIMEOUT-input",
);
fireEvent.change(updatedTimeoutInput, { target: { value: "3000" } });
// Verify the final state matches what we expect
expect(setConfig).toHaveBeenLastCalledWith(
expect.objectContaining({
MCP_SERVER_REQUEST_TIMEOUT: {
label: "Request Timeout",
description: "Timeout for requests to the MCP server (ms)",
value: 3000,
is_session_item: false,
},
}),
);
});
});
describe("Copy Configuration Features", () => {
beforeEach(() => {
jest.clearAllMocks();
jest.clearAllTimers();
}); });
describe("Copy Server Features", () => {
const getCopyButtons = () => { const getCopyButtons = () => {
return { return {
serverEntry: screen.getByRole("button", { name: /server entry/i }), serverEntry: screen.getByRole("button", { name: /server entry/i }),
@ -878,4 +612,307 @@ describe("Sidebar Environment Variables", () => {
expect(mockClipboardWrite).toHaveBeenCalledWith(expectedConfig); expect(mockClipboardWrite).toHaveBeenCalledWith(expectedConfig);
}); });
}); });
describe("Authentication", () => {
const openAuthSection = () => {
const button = screen.getByTestId("auth-button");
fireEvent.click(button);
};
it("should update bearer token", () => {
const setBearerToken = jest.fn();
renderSidebar({
bearerToken: "",
setBearerToken,
transportType: "sse", // Set transport type to SSE
});
openAuthSection();
const tokenInput = screen.getByTestId("bearer-token-input");
fireEvent.change(tokenInput, { target: { value: "new_token" } });
expect(setBearerToken).toHaveBeenCalledWith("new_token");
});
it("should update header name", () => {
const setHeaderName = jest.fn();
renderSidebar({
headerName: "Authorization",
setHeaderName,
transportType: "sse",
});
openAuthSection();
const headerInput = screen.getByTestId("header-input");
fireEvent.change(headerInput, { target: { value: "X-Custom-Auth" } });
expect(setHeaderName).toHaveBeenCalledWith("X-Custom-Auth");
});
it("should clear bearer token", () => {
const setBearerToken = jest.fn();
renderSidebar({
bearerToken: "existing_token",
setBearerToken,
transportType: "sse", // Set transport type to SSE
});
openAuthSection();
const tokenInput = screen.getByTestId("bearer-token-input");
fireEvent.change(tokenInput, { target: { value: "" } });
expect(setBearerToken).toHaveBeenCalledWith("");
});
it("should properly render bearer token input", () => {
const { rerender } = renderSidebar({
bearerToken: "existing_token",
transportType: "sse", // Set transport type to SSE
});
openAuthSection();
// Token input should be a password field
const tokenInput = screen.getByTestId("bearer-token-input");
expect(tokenInput).toHaveProperty("type", "password");
// Update the token
fireEvent.change(tokenInput, { target: { value: "new_token" } });
// Rerender with updated token
rerender(
<TooltipProvider>
<Sidebar
{...defaultProps}
bearerToken="new_token"
transportType="sse"
/>
</TooltipProvider>,
);
// Token input should still exist after update
expect(screen.getByTestId("bearer-token-input")).toBeInTheDocument();
});
it("should maintain token visibility state after update", () => {
const { rerender } = renderSidebar({
bearerToken: "existing_token",
transportType: "sse", // Set transport type to SSE
});
openAuthSection();
// Token input should be a password field
const tokenInput = screen.getByTestId("bearer-token-input");
expect(tokenInput).toHaveProperty("type", "password");
// Update the token
fireEvent.change(tokenInput, { target: { value: "new_token" } });
// Rerender with updated token
rerender(
<TooltipProvider>
<Sidebar
{...defaultProps}
bearerToken="new_token"
transportType="sse"
/>
</TooltipProvider>,
);
// Token input should still exist after update
expect(screen.getByTestId("bearer-token-input")).toBeInTheDocument();
});
it("should maintain header name when toggling auth section", () => {
renderSidebar({
headerName: "X-API-Key",
transportType: "sse",
});
// Open auth section
openAuthSection();
// Verify header name is displayed
const headerInput = screen.getByTestId("header-input");
expect(headerInput).toHaveValue("X-API-Key");
// Close auth section
const authButton = screen.getByTestId("auth-button");
fireEvent.click(authButton);
// Reopen auth section
fireEvent.click(authButton);
// Verify header name is still preserved
expect(screen.getByTestId("header-input")).toHaveValue("X-API-Key");
});
it("should display default header name when not specified", () => {
renderSidebar({
headerName: undefined,
transportType: "sse",
});
openAuthSection();
const headerInput = screen.getByTestId("header-input");
expect(headerInput).toHaveAttribute("placeholder", "Authorization");
});
});
describe("Configuration", () => {
const openConfigSection = () => {
const button = screen.getByTestId("config-button");
fireEvent.click(button);
};
it("should update MCP server request timeout", () => {
const setConfig = jest.fn();
renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
openConfigSection();
const timeoutInput = screen.getByTestId(
"MCP_SERVER_REQUEST_TIMEOUT-input",
);
fireEvent.change(timeoutInput, { target: { value: "5000" } });
expect(setConfig).toHaveBeenCalledWith(
expect.objectContaining({
MCP_SERVER_REQUEST_TIMEOUT: {
label: "Request Timeout",
description: "Timeout for requests to the MCP server (ms)",
value: 5000,
is_session_item: false,
},
}),
);
});
it("should update MCP server proxy address", () => {
const setConfig = jest.fn();
renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
openConfigSection();
const proxyAddressInput = screen.getByTestId(
"MCP_PROXY_FULL_ADDRESS-input",
);
fireEvent.change(proxyAddressInput, {
target: { value: "http://localhost:8080" },
});
expect(setConfig).toHaveBeenCalledWith(
expect.objectContaining({
MCP_PROXY_FULL_ADDRESS: {
label: "Inspector Proxy Address",
description:
"Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577",
value: "http://localhost:8080",
is_session_item: false,
},
}),
);
});
it("should update max total timeout", () => {
const setConfig = jest.fn();
renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
openConfigSection();
const maxTotalTimeoutInput = screen.getByTestId(
"MCP_REQUEST_MAX_TOTAL_TIMEOUT-input",
);
fireEvent.change(maxTotalTimeoutInput, {
target: { value: "10000" },
});
expect(setConfig).toHaveBeenCalledWith(
expect.objectContaining({
MCP_REQUEST_MAX_TOTAL_TIMEOUT: {
label: "Maximum Total Timeout",
description:
"Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications)",
value: 10000,
is_session_item: false,
},
}),
);
});
it("should handle invalid timeout values entered by user", () => {
const setConfig = jest.fn();
renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
openConfigSection();
const timeoutInput = screen.getByTestId(
"MCP_SERVER_REQUEST_TIMEOUT-input",
);
fireEvent.change(timeoutInput, { target: { value: "abc1" } });
expect(setConfig).toHaveBeenCalledWith(
expect.objectContaining({
MCP_SERVER_REQUEST_TIMEOUT: {
label: "Request Timeout",
description: "Timeout for requests to the MCP server (ms)",
value: 0,
is_session_item: false,
},
}),
);
});
it("should maintain configuration state after multiple updates", () => {
const setConfig = jest.fn();
const { rerender } = renderSidebar({
config: DEFAULT_INSPECTOR_CONFIG,
setConfig,
});
openConfigSection();
// First update
const timeoutInput = screen.getByTestId(
"MCP_SERVER_REQUEST_TIMEOUT-input",
);
fireEvent.change(timeoutInput, { target: { value: "5000" } });
// Get the updated config from the first setConfig call
const updatedConfig = setConfig.mock.calls[0][0] as InspectorConfig;
// Rerender with the updated config
rerender(
<TooltipProvider>
<Sidebar
{...defaultProps}
config={updatedConfig}
setConfig={setConfig}
/>
</TooltipProvider>,
);
// Second update
const updatedTimeoutInput = screen.getByTestId(
"MCP_SERVER_REQUEST_TIMEOUT-input",
);
fireEvent.change(updatedTimeoutInput, { target: { value: "3000" } });
// Verify the final state matches what we expect
expect(setConfig).toHaveBeenLastCalledWith(
expect.objectContaining({
MCP_SERVER_REQUEST_TIMEOUT: {
label: "Request Timeout",
description: "Timeout for requests to the MCP server (ms)",
value: 3000,
is_session_item: false,
},
}),
);
});
});
}); });

View file

@ -11,6 +11,7 @@ import {
import { discoverAuthorizationServerMetadata } from "@modelcontextprotocol/sdk/client/auth.js"; import { discoverAuthorizationServerMetadata } from "@modelcontextprotocol/sdk/client/auth.js";
import { SESSION_KEYS, getServerSpecificKey } from "./constants"; import { SESSION_KEYS, getServerSpecificKey } from "./constants";
import { generateOAuthState } from "@/utils/oauthUtils"; import { generateOAuthState } from "@/utils/oauthUtils";
import { validateRedirectUrl } from "@/utils/urlValidation";
/** /**
* Discovers OAuth scopes from server metadata, with preference for resource metadata scopes * Discovers OAuth scopes from server metadata, with preference for resource metadata scopes
@ -102,10 +103,15 @@ export const clearClientInformationFromSessionStorage = ({
}; };
export class InspectorOAuthClientProvider implements OAuthClientProvider { export class InspectorOAuthClientProvider implements OAuthClientProvider {
constructor(protected serverUrl: string) { constructor(
protected serverUrl: string,
scope?: string,
) {
this.scope = scope;
// Save the server URL to session storage // Save the server URL to session storage
sessionStorage.setItem(SESSION_KEYS.SERVER_URL, serverUrl); sessionStorage.setItem(SESSION_KEYS.SERVER_URL, serverUrl);
} }
scope: string | undefined;
get redirectUrl() { get redirectUrl() {
return window.location.origin + "/oauth/callback"; return window.location.origin + "/oauth/callback";
@ -119,6 +125,7 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider {
response_types: ["code"], response_types: ["code"],
client_name: "MCP Inspector", client_name: "MCP Inspector",
client_uri: "https://github.com/modelcontextprotocol/inspector", client_uri: "https://github.com/modelcontextprotocol/inspector",
scope: this.scope ?? "",
}; };
} }
@ -176,12 +183,8 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider {
} }
redirectToAuthorization(authorizationUrl: URL) { redirectToAuthorization(authorizationUrl: URL) {
if ( // Validate the URL using the shared utility
authorizationUrl.protocol !== "http:" && validateRedirectUrl(authorizationUrl.href);
authorizationUrl.protocol !== "https:"
) {
throw new Error("Authorization URL must be HTTP or HTTPS");
}
window.location.href = authorizationUrl.href; window.location.href = authorizationUrl.href;
} }

View file

@ -36,7 +36,7 @@ import { useEffect, useState } from "react";
import { useToast } from "@/lib/hooks/useToast"; import { useToast } from "@/lib/hooks/useToast";
import { z } from "zod"; import { z } from "zod";
import { ConnectionStatus } from "../constants"; import { ConnectionStatus } from "../constants";
import { Notification, StdErrNotificationSchema } from "../notificationTypes"; import { Notification } from "../notificationTypes";
import { import {
auth, auth,
discoverOAuthProtectedResourceMetadata, discoverOAuthProtectedResourceMetadata,
@ -92,7 +92,6 @@ export function useConnection({
oauthScope, oauthScope,
config, config,
onNotification, onNotification,
onStdErrNotification,
onPendingRequest, onPendingRequest,
onElicitationRequest, onElicitationRequest,
getRoots, getRoots,
@ -319,8 +318,6 @@ export function useConnection({
const handleAuthError = async (error: unknown) => { const handleAuthError = async (error: unknown) => {
if (is401Error(error)) { if (is401Error(error)) {
const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl);
let scope = oauthScope?.trim(); let scope = oauthScope?.trim();
if (!scope) { if (!scope) {
// Only discover resource metadata when we need to discover scopes // Only discover resource metadata when we need to discover scopes
@ -334,6 +331,10 @@ export function useConnection({
} }
scope = await discoverScopes(sseUrl, resourceMetadata); scope = await discoverScopes(sseUrl, resourceMetadata);
} }
const serverAuthProvider = new InspectorOAuthClientProvider(
sseUrl,
scope,
);
const result = await auth(serverAuthProvider, { const result = await auth(serverAuthProvider, {
serverUrl: sseUrl, serverUrl: sseUrl,
@ -503,13 +504,6 @@ export function useConnection({
}; };
} }
if (onStdErrNotification) {
client.setNotificationHandler(
StdErrNotificationSchema,
onStdErrNotification,
);
}
let capabilities; let capabilities;
try { try {
const transport = const transport =

View file

@ -5,18 +5,8 @@ import {
} from "@modelcontextprotocol/sdk/types.js"; } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod"; import { z } from "zod";
export const StdErrNotificationSchema = BaseNotificationSchema.extend({
method: z.literal("notifications/stderr"),
params: z.object({
content: z.string(),
}),
});
export const NotificationSchema = ClientNotificationSchema.or( export const NotificationSchema = ClientNotificationSchema.or(
StdErrNotificationSchema, ServerNotificationSchema,
) ).or(BaseNotificationSchema);
.or(ServerNotificationSchema)
.or(BaseNotificationSchema);
export type StdErrNotification = z.infer<typeof StdErrNotificationSchema>;
export type Notification = z.infer<typeof NotificationSchema>; export type Notification = z.infer<typeof NotificationSchema>;

View file

@ -177,7 +177,11 @@ export const oauthTransitions: Record<OAuthStep, StateTransition> = {
authorizationCode: context.state.authorizationCode, authorizationCode: context.state.authorizationCode,
codeVerifier, codeVerifier,
redirectUri: context.provider.redirectUrl, redirectUri: context.provider.redirectUrl,
resource: context.state.resource ?? undefined, resource: context.state.resource
? context.state.resource instanceof URL
? context.state.resource
: new URL(context.state.resource)
: undefined,
}); });
context.provider.saveTokens(tokens); context.provider.saveTokens(tokens);

View file

@ -0,0 +1,135 @@
import { validateRedirectUrl } from "../urlValidation";
describe("validateRedirectUrl", () => {
describe("valid URLs", () => {
it("should allow HTTP URLs", () => {
expect(() => validateRedirectUrl("http://example.com")).not.toThrow();
});
it("should allow HTTPS URLs", () => {
expect(() => validateRedirectUrl("https://example.com")).not.toThrow();
});
it("should allow URLs with ports", () => {
expect(() =>
validateRedirectUrl("https://example.com:8080"),
).not.toThrow();
});
it("should allow URLs with paths", () => {
expect(() =>
validateRedirectUrl("https://example.com/path/to/auth"),
).not.toThrow();
});
it("should allow URLs with query parameters", () => {
expect(() =>
validateRedirectUrl("https://example.com?param=value"),
).not.toThrow();
});
});
describe("invalid URLs - XSS vectors", () => {
it("should block javascript: protocol", () => {
expect(() => validateRedirectUrl("javascript:alert('XSS')")).toThrow(
"Authorization URL must be HTTP or HTTPS",
);
});
it("should block javascript: with encoded characters", () => {
expect(() =>
validateRedirectUrl("javascript:alert%28%27XSS%27%29"),
).toThrow("Authorization URL must be HTTP or HTTPS");
});
it("should block data: protocol", () => {
expect(() =>
validateRedirectUrl("data:text/html,<script>alert('XSS')</script>"),
).toThrow("Authorization URL must be HTTP or HTTPS");
});
it("should block vbscript: protocol", () => {
expect(() => validateRedirectUrl("vbscript:msgbox")).toThrow(
"Authorization URL must be HTTP or HTTPS",
);
});
it("should block file: protocol", () => {
expect(() => validateRedirectUrl("file:///etc/passwd")).toThrow(
"Authorization URL must be HTTP or HTTPS",
);
});
it("should block about: protocol", () => {
expect(() => validateRedirectUrl("about:blank")).toThrow(
"Authorization URL must be HTTP or HTTPS",
);
});
it("should block custom protocols", () => {
expect(() => validateRedirectUrl("custom://example")).toThrow(
"Authorization URL must be HTTP or HTTPS",
);
});
});
describe("edge cases", () => {
it("should handle malformed URLs", () => {
expect(() => validateRedirectUrl("not a url")).toThrow(
"Invalid URL: not a url",
);
});
it("should handle empty string", () => {
expect(() => validateRedirectUrl("")).toThrow("Invalid URL: ");
});
it("should handle URLs with unicode characters", () => {
expect(() => validateRedirectUrl("https://例え.jp")).not.toThrow();
});
it("should handle URLs with case variations", () => {
expect(() => validateRedirectUrl("HTTPS://EXAMPLE.COM")).not.toThrow();
expect(() => validateRedirectUrl("HtTpS://example.com")).not.toThrow();
});
it("should handle protocol-relative URLs as invalid", () => {
expect(() => validateRedirectUrl("//example.com")).toThrow(
"Invalid URL: //example.com",
);
});
it("should handle URLs with authentication", () => {
expect(() =>
validateRedirectUrl("https://user:pass@example.com"),
).not.toThrow();
});
});
describe("security considerations", () => {
it("should not be fooled by whitespace", () => {
expect(() => validateRedirectUrl(" javascript:alert('XSS')")).toThrow();
expect(() => validateRedirectUrl("javascript:alert('XSS') ")).toThrow();
});
it("should handle null bytes", () => {
expect(() =>
validateRedirectUrl("java\x00script:alert('XSS')"),
).toThrow();
});
it("should handle tab characters", () => {
expect(() => validateRedirectUrl("java\tscript:alert('XSS')")).toThrow();
});
it("should handle newlines", () => {
expect(() => validateRedirectUrl("java\nscript:alert('XSS')")).toThrow();
});
it("should handle mixed case protocols", () => {
expect(() => validateRedirectUrl("JaVaScRiPt:alert('XSS')")).toThrow(
"Authorization URL must be HTTP or HTTPS",
);
});
});
});

View file

@ -0,0 +1,24 @@
/**
* Validates that a URL is safe for redirection.
* Only allows HTTP and HTTPS protocols to prevent XSS attacks.
*
* @param url - The URL string to validate
* @throws Error if the URL has an unsafe protocol
*/
export function validateRedirectUrl(url: string): void {
try {
const parsedUrl = new URL(url);
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
throw new Error("Authorization URL must be HTTP or HTTPS");
}
} catch (error) {
if (
error instanceof Error &&
error.message === "Authorization URL must be HTTP or HTTPS"
) {
throw error;
}
// If URL parsing fails, it's also invalid
throw new Error(`Invalid URL: ${url}`);
}
}

642
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "@modelcontextprotocol/inspector", "name": "@modelcontextprotocol/inspector",
"version": "0.16.4", "version": "0.16.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@modelcontextprotocol/inspector", "name": "@modelcontextprotocol/inspector",
"version": "0.16.4", "version": "0.16.5",
"license": "MIT", "license": "MIT",
"workspaces": [ "workspaces": [
"client", "client",
@ -14,10 +14,10 @@
"cli" "cli"
], ],
"dependencies": { "dependencies": {
"@modelcontextprotocol/inspector-cli": "^0.16.4", "@modelcontextprotocol/inspector-cli": "^0.16.5",
"@modelcontextprotocol/inspector-client": "^0.16.4", "@modelcontextprotocol/inspector-client": "^0.16.5",
"@modelcontextprotocol/inspector-server": "^0.16.4", "@modelcontextprotocol/inspector-server": "^0.16.5",
"@modelcontextprotocol/sdk": "^1.17.2", "@modelcontextprotocol/sdk": "^1.17.3",
"concurrently": "^9.2.0", "concurrently": "^9.2.0",
"open": "^10.2.0", "open": "^10.2.0",
"shell-quote": "^1.8.3", "shell-quote": "^1.8.3",
@ -33,7 +33,9 @@
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/node": "^22.17.0", "@types/node": "^22.17.0",
"@types/shell-quote": "^1.7.5", "@types/shell-quote": "^1.7.5",
"husky": "^9.1.7",
"jest-fixed-jsdom": "^0.0.9", "jest-fixed-jsdom": "^0.0.9",
"lint-staged": "^16.1.5",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
@ -44,10 +46,10 @@
}, },
"cli": { "cli": {
"name": "@modelcontextprotocol/inspector-cli", "name": "@modelcontextprotocol/inspector-cli",
"version": "0.16.4", "version": "0.16.5",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.17.2", "@modelcontextprotocol/sdk": "^1.17.3",
"commander": "^13.1.0", "commander": "^13.1.0",
"spawn-rx": "^5.1.2" "spawn-rx": "^5.1.2"
}, },
@ -67,10 +69,10 @@
}, },
"client": { "client": {
"name": "@modelcontextprotocol/inspector-client", "name": "@modelcontextprotocol/inspector-client",
"version": "0.16.4", "version": "0.16.5",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.17.2", "@modelcontextprotocol/sdk": "^1.17.3",
"@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-dialog": "^1.1.3", "@radix-ui/react-dialog": "^1.1.3",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
@ -1999,9 +2001,9 @@
"link": true "link": true
}, },
"node_modules/@modelcontextprotocol/sdk": { "node_modules/@modelcontextprotocol/sdk": {
"version": "1.17.2", "version": "1.17.3",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.2.tgz", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.3.tgz",
"integrity": "sha512-EFLRNXR/ixpXQWu6/3Cu30ndDFIFNaqUXcTqsGebujeMan9FzhAaFFswLRiFj61rgygDRr8WO1N+UijjgRxX9g==", "integrity": "sha512-JPwUKWSsbzx+DLFznf/QZ32Qa+ptfbUlHhRLrBQBAFu9iI1iYvizM4p+zhhRDceSsPutXp4z+R/HPVphlIiclg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ajv": "^6.12.6", "ajv": "^6.12.6",
@ -4649,6 +4651,93 @@
"url": "https://polar.sh/cva" "url": "https://polar.sh/cva"
} }
}, },
"node_modules/cli-cursor": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
"integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
"dev": true,
"license": "MIT",
"dependencies": {
"restore-cursor": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-truncate": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz",
"integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==",
"dev": true,
"license": "MIT",
"dependencies": {
"slice-ansi": "^5.0.0",
"string-width": "^7.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-truncate/node_modules/ansi-regex": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz",
"integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/cli-truncate/node_modules/emoji-regex": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
"integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
"dev": true,
"license": "MIT"
},
"node_modules/cli-truncate/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-truncate/node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/cliui": { "node_modules/cliui": {
"version": "8.0.1", "version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@ -4724,6 +4813,13 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/colorette": {
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
"dev": true,
"license": "MIT"
},
"node_modules/combined-stream": { "node_modules/combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -4962,9 +5058,9 @@
} }
}, },
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.0", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "^2.1.3"
@ -5245,6 +5341,19 @@
"url": "https://github.com/fb55/entities?sponsor=1" "url": "https://github.com/fb55/entities?sponsor=1"
} }
}, },
"node_modules/environment": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
"integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/error-ex": { "node_modules/error-ex": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@ -5593,6 +5702,13 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"dev": true,
"license": "MIT"
},
"node_modules/eventsource": { "node_modules/eventsource": {
"version": "3.0.6", "version": "3.0.6",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.6.tgz", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.6.tgz",
@ -6049,6 +6165,19 @@
"node": "6.* || 8.* || >= 10.*" "node": "6.* || 8.* || >= 10.*"
} }
}, },
"node_modules/get-east-asian-width": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz",
"integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-intrinsic": { "node_modules/get-intrinsic": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@ -6328,6 +6457,22 @@
"node": ">=10.17.0" "node": ">=10.17.0"
} }
}, },
"node_modules/husky": {
"version": "9.1.7",
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
"integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
"dev": true,
"license": "MIT",
"bin": {
"husky": "bin.js"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/typicode"
}
},
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@ -7857,6 +8002,160 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lint-staged": {
"version": "16.1.5",
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.1.5.tgz",
"integrity": "sha512-uAeQQwByI6dfV7wpt/gVqg+jAPaSp8WwOA8kKC/dv1qw14oGpnpAisY65ibGHUGDUv0rYaZ8CAJZ/1U8hUvC2A==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "^5.5.0",
"commander": "^14.0.0",
"debug": "^4.4.1",
"lilconfig": "^3.1.3",
"listr2": "^9.0.1",
"micromatch": "^4.0.8",
"nano-spawn": "^1.0.2",
"pidtree": "^0.6.0",
"string-argv": "^0.3.2",
"yaml": "^2.8.1"
},
"bin": {
"lint-staged": "bin/lint-staged.js"
},
"engines": {
"node": ">=20.17"
},
"funding": {
"url": "https://opencollective.com/lint-staged"
}
},
"node_modules/lint-staged/node_modules/chalk": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz",
"integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/lint-staged/node_modules/commander": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz",
"integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
}
},
"node_modules/listr2": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.1.tgz",
"integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"cli-truncate": "^4.0.0",
"colorette": "^2.0.20",
"eventemitter3": "^5.0.1",
"log-update": "^6.1.0",
"rfdc": "^1.4.1",
"wrap-ansi": "^9.0.0"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/listr2/node_modules/ansi-regex": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz",
"integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/listr2/node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/listr2/node_modules/emoji-regex": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
"integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
"dev": true,
"license": "MIT"
},
"node_modules/listr2/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/listr2/node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/listr2/node_modules/wrap-ansi": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
"integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/locate-path": { "node_modules/locate-path": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@ -7893,6 +8192,160 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/log-update": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz",
"integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-escapes": "^7.0.0",
"cli-cursor": "^5.0.0",
"slice-ansi": "^7.1.0",
"strip-ansi": "^7.1.0",
"wrap-ansi": "^9.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-update/node_modules/ansi-escapes": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz",
"integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==",
"dev": true,
"license": "MIT",
"dependencies": {
"environment": "^1.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-update/node_modules/ansi-regex": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz",
"integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/log-update/node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/log-update/node_modules/emoji-regex": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
"integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
"dev": true,
"license": "MIT"
},
"node_modules/log-update/node_modules/is-fullwidth-code-point": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz",
"integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==",
"dev": true,
"license": "MIT",
"dependencies": {
"get-east-asian-width": "^1.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-update/node_modules/slice-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz",
"integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"is-fullwidth-code-point": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/log-update/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-update/node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/log-update/node_modules/wrap-ansi": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
"integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/loose-envify": { "node_modules/loose-envify": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@ -8070,6 +8523,19 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/mimic-function": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
"integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/min-indent": { "node_modules/min-indent": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
@ -8118,6 +8584,19 @@
"thenify-all": "^1.0.0" "thenify-all": "^1.0.0"
} }
}, },
"node_modules/nano-spawn": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-1.0.2.tgz",
"integrity": "sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20.17"
},
"funding": {
"url": "https://github.com/sindresorhus/nano-spawn?sponsor=1"
}
},
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@ -8496,6 +8975,19 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/pidtree": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz",
"integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==",
"dev": true,
"license": "MIT",
"bin": {
"pidtree": "bin/pidtree.js"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/pify": { "node_modules/pify": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
@ -9225,6 +9717,52 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/restore-cursor": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
"integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
"dev": true,
"license": "MIT",
"dependencies": {
"onetime": "^7.0.0",
"signal-exit": "^4.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/restore-cursor/node_modules/onetime": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
"integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"mimic-function": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/restore-cursor/node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/reusify": { "node_modules/reusify": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
@ -9235,6 +9773,13 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"dev": true,
"license": "MIT"
},
"node_modules/rimraf": { "node_modules/rimraf": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz",
@ -9711,6 +10256,49 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/slice-ansi": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz",
"integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.0.0",
"is-fullwidth-code-point": "^4.0.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/slice-ansi/node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/slice-ansi/node_modules/is-fullwidth-code-point": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz",
"integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/source-map": { "node_modules/source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@ -9790,6 +10378,16 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/string-argv": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz",
"integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.6.19"
}
},
"node_modules/string-length": { "node_modules/string-length": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
@ -10979,15 +11577,15 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/yaml": { "node_modules/yaml": {
"version": "2.7.1", "version": "2.8.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
"integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"license": "ISC", "license": "ISC",
"bin": { "bin": {
"yaml": "bin.mjs" "yaml": "bin.mjs"
}, },
"engines": { "engines": {
"node": ">= 14" "node": ">= 14.6"
} }
}, },
"node_modules/yargs": { "node_modules/yargs": {
@ -11059,10 +11657,10 @@
}, },
"server": { "server": {
"name": "@modelcontextprotocol/inspector-server", "name": "@modelcontextprotocol/inspector-server",
"version": "0.16.4", "version": "0.16.5",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.17.2", "@modelcontextprotocol/sdk": "^1.17.3",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^5.1.0", "express": "^5.1.0",
"ws": "^8.18.0", "ws": "^8.18.0",

Some files were not shown because too many files have changed in this diff Show more