diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 0db5467a..31eb7a22 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -34,3 +34,7 @@ If applicable, add screenshots to help explain your problem. **Additional context** Add any other context about the problem here. + +**Version Consideration** + +Inspector V2 is under development to address architectural and UX improvements. See [CONTRIBUTING.md](CONTRIBUTING.md) for information about V2 development. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..50189d36 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,57 @@ +## Summary + + + +> **Note:** Inspector V2 is under development to address architectural and UX improvements. See [CONTRIBUTING.md](CONTRIBUTING.md) for information about V2 development. + +## Type of Change + + + +- [ ] Bug fix (non-breaking change that fixes an issue) +- [ ] New feature (non-breaking change that adds functionality) +- [ ] Documentation update +- [ ] Refactoring (no functional changes) +- [ ] Test updates +- [ ] Build/CI improvements + +## Changes Made + + + +## Related Issues + + + +## Testing + + + +- [ ] Tested in UI mode +- [ ] Tested in CLI mode +- [ ] Tested with STDIO transport +- [ ] Tested with SSE transport +- [ ] Tested with Streamable HTTP transport +- [ ] Added/updated automated tests +- [ ] Manual testing performed + +### Test Results and/or Instructions + + + +Screenshots are encouraged to share your testing results for this change. + +## Checklist + +- [ ] Code follows the style guidelines (ran `npm run prettier-fix`) +- [ ] Self-review completed +- [ ] Code is commented where necessary +- [ ] Documentation updated (README, comments, etc.) + +## Breaking Changes + + + +## Additional Context + + diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index 573672cc..378905b4 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -8,7 +8,7 @@ on: jobs: test: - # Installing Playright dependencies can take quite awhile, and also depends on GitHub CI load. + # Installing Playwright dependencies can take quite awhile, and also depends on GitHub CI load. timeout-minutes: 15 runs-on: ubuntu-latest diff --git a/AGENTS.md b/AGENTS.md index ec826b1f..b0e46712 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,7 @@ # MCP Inspector Development Guide +> **Note:** Inspector V2 is under development to address architectural and UX improvements. These guidelines apply to the current version (V1). See [CONTRIBUTING.md](CONTRIBUTING.md) for information about V2 development. + ## Build Commands - Build all: `npm run build` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0129a9e3..9f98adcf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,6 +9,17 @@ Thanks for your interest in contributing! This guide explains how to get involve 3. Run `npm run dev` to start both client and server in development mode 4. Use the web UI at http://localhost:6274 to interact with the inspector +## Inspector V2 Development + +We're actively developing **Inspector V2** to address architectural and UX improvements. We invite you to follow progress and participate in the Inspector V2 Working Group in [Discord](https://modelcontextprotocol.io/community/communication), [weekly meetings](https://meet.modelcontextprotocol.io/tag/inspector-v2-wg), and [GitHub Discussions](https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/categories/meeting-notes-other) (where notes are posted after meetings). + +**Current version (V1) contribution scope:** + +- Critical issues are actively maintained +- Small UX improvements are welcome +- Documentation updates are always appreciated +- Major changes will be directed to V2 development + ## Development Process & Pull Requests 1. Create a new branch for your changes @@ -30,7 +41,7 @@ If you find a security vulnerability, please refer to our [Security Policy](SECU ## Questions? -Feel free to [open an issue](https://github.com/modelcontextprotocol/mcp-inspector/issues) for questions or create a discussion for general topics. +Feel free to [open an issue](https://github.com/modelcontextprotocol/mcp-inspector/issues) for questions or join the MCP Contributor [Discord server](https://modelcontextprotocol.io/community/communication). Also, please see notes above on Inspector V2 Development. ## License diff --git a/cli/package.json b/cli/package.json index 85431073..b885b775 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-cli", - "version": "0.17.2", + "version": "0.17.3", "description": "CLI for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", diff --git a/client/package.json b/client/package.json index 56317c40..9f97e023 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-client", - "version": "0.17.2", + "version": "0.17.3", "description": "Client-side application for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", diff --git a/client/src/components/DynamicJsonForm.tsx b/client/src/components/DynamicJsonForm.tsx index 96b03b3a..685401ab 100644 --- a/client/src/components/DynamicJsonForm.tsx +++ b/client/src/components/DynamicJsonForm.tsx @@ -113,8 +113,8 @@ const DynamicJsonForm = forwardRef( setJsonError(errorMessage); // Reset to default for clearly invalid JSON (not just incomplete typing) - const trimmed = jsonString.trim(); - if (trimmed.length > 5 && !trimmed.match(/^[\s[{]/)) { + const trimmed = jsonString?.trim(); + if (trimmed && trimmed.length > 5 && !trimmed.match(/^[\s[{]/)) { onChange(generateDefaultValue(schema)); } } @@ -155,7 +155,7 @@ const DynamicJsonForm = forwardRef( const formatJson = () => { try { - const jsonStr = rawJsonValue.trim(); + const jsonStr = rawJsonValue?.trim(); if (!jsonStr) { return; } @@ -171,7 +171,7 @@ const DynamicJsonForm = forwardRef( const validateJson = () => { if (!isJsonMode) return { isValid: true, error: null }; try { - const jsonStr = rawJsonValue.trim(); + const jsonStr = rawJsonValue?.trim(); if (!jsonStr) return { isValid: true, error: null }; const parsed = JSON.parse(jsonStr); // Clear any pending debounced update and immediately update parent diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index f8175f9d..7aab642a 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -225,16 +225,18 @@ const ToolsTab = ({ ...params, [key]: checked ? null - : prop.default !== null - ? prop.default - : prop.type === "boolean" - ? false - : prop.type === "string" - ? "" - : prop.type === "number" || - prop.type === "integer" - ? undefined - : undefined, + : prop.type === "array" + ? undefined + : prop.default !== null + ? prop.default + : prop.type === "boolean" + ? false + : prop.type === "string" + ? "" + : prop.type === "number" || + prop.type === "integer" + ? undefined + : undefined, }) } /> diff --git a/client/src/lib/auth.ts b/client/src/lib/auth.ts index 23e21b6a..79750112 100644 --- a/client/src/lib/auth.ts +++ b/client/src/lib/auth.ts @@ -102,16 +102,40 @@ export const clearClientInformationFromSessionStorage = ({ sessionStorage.removeItem(key); }; +export const getScopeFromSessionStorage = ( + serverUrl: string, +): string | undefined => { + const key = getServerSpecificKey(SESSION_KEYS.SCOPE, serverUrl); + const value = sessionStorage.getItem(key); + return value || undefined; +}; + +export const saveScopeToSessionStorage = ( + serverUrl: string, + scope: string | undefined, +) => { + const key = getServerSpecificKey(SESSION_KEYS.SCOPE, serverUrl); + if (scope) { + sessionStorage.setItem(key, scope); + } else { + sessionStorage.removeItem(key); + } +}; + +export const clearScopeFromSessionStorage = (serverUrl: string) => { + const key = getServerSpecificKey(SESSION_KEYS.SCOPE, serverUrl); + sessionStorage.removeItem(key); +}; + export class InspectorOAuthClientProvider implements OAuthClientProvider { - constructor( - protected serverUrl: string, - scope?: string, - ) { - this.scope = scope; + constructor(protected serverUrl: string) { // Save the server URL to session storage sessionStorage.setItem(SESSION_KEYS.SERVER_URL, serverUrl); } - scope: string | undefined; + + get scope(): string | undefined { + return getScopeFromSessionStorage(this.serverUrl); + } get redirectUrl() { return window.location.origin + "/oauth/callback"; diff --git a/client/src/lib/constants.ts b/client/src/lib/constants.ts index 85cc0b36..e7edc025 100644 --- a/client/src/lib/constants.ts +++ b/client/src/lib/constants.ts @@ -17,6 +17,7 @@ export const SESSION_KEYS = { PREREGISTERED_CLIENT_INFORMATION: "mcp_preregistered_client_information", SERVER_METADATA: "mcp_server_metadata", AUTH_DEBUGGER_STATE: "mcp_auth_debugger_state", + SCOPE: "mcp_scope", } as const; // Generate server-specific session storage keys diff --git a/client/src/lib/hooks/__tests__/useConnection.test.tsx b/client/src/lib/hooks/__tests__/useConnection.test.tsx index a9ba6825..b4d6705b 100644 --- a/client/src/lib/hooks/__tests__/useConnection.test.tsx +++ b/client/src/lib/hooks/__tests__/useConnection.test.tsx @@ -112,6 +112,8 @@ jest.mock("../../auth", () => ({ })), clearClientInformationFromSessionStorage: jest.fn(), saveClientInformationToSessionStorage: jest.fn(), + saveScopeToSessionStorage: jest.fn(), + clearScopeFromSessionStorage: jest.fn(), discoverScopes: jest.fn(), })); diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 4584ff0a..db2064ee 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -45,6 +45,8 @@ import { clearClientInformationFromSessionStorage, InspectorOAuthClientProvider, saveClientInformationToSessionStorage, + saveScopeToSessionStorage, + clearScopeFromSessionStorage, discoverScopes, } from "../auth"; import { @@ -144,6 +146,15 @@ export function useConnection({ }); }, [oauthClientId, oauthClientSecret, sseUrl]); + useEffect(() => { + if (!oauthScope) { + clearScopeFromSessionStorage(sseUrl); + return; + } + + saveScopeToSessionStorage(sseUrl, oauthScope); + }, [oauthScope, sseUrl]); + const pushHistory = (request: object, response?: object) => { setRequestHistory((prev) => [ ...prev, @@ -366,10 +377,9 @@ export function useConnection({ } scope = await discoverScopes(sseUrl, resourceMetadata); } - const serverAuthProvider = new InspectorOAuthClientProvider( - sseUrl, - scope, - ); + + saveScopeToSessionStorage(sseUrl, scope); + const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl); const result = await auth(serverAuthProvider, { serverUrl: sseUrl, diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts index c505f698..8dc9da8f 100644 --- a/client/src/lib/oauth-state-machine.ts +++ b/client/src/lib/oauth-state-machine.ts @@ -80,13 +80,16 @@ export const oauthTransitions: Record = { const metadata = context.state.oauthMetadata!; const clientMetadata = context.provider.clientMetadata; - // Prefer scopes from resource metadata if available - const scopesSupported = - context.state.resourceMetadata?.scopes_supported || - metadata.scopes_supported; - // Add all supported scopes to client registration - if (scopesSupported) { - clientMetadata.scope = scopesSupported.join(" "); + // Priority: user-provided scope > discovered scopes + if (!context.provider.scope || context.provider.scope.trim() === "") { + // Prefer scopes from resource metadata if available + const scopesSupported = + context.state.resourceMetadata?.scopes_supported || + metadata.scopes_supported; + // Add all supported scopes to client registration + if (scopesSupported) { + clientMetadata.scope = scopesSupported.join(" "); + } } // Try Static client first, with DCR as fallback @@ -113,10 +116,14 @@ export const oauthTransitions: Record = { const metadata = context.state.oauthMetadata!; const clientInformation = context.state.oauthClientInfo!; - const scope = await discoverScopes( - context.serverUrl, - context.state.resourceMetadata ?? undefined, - ); + // Priority: user-provided scope > discovered scopes + let scope = context.provider.scope; + if (!scope || scope.trim() === "") { + scope = await discoverScopes( + context.serverUrl, + context.state.resourceMetadata ?? undefined, + ); + } const { authorizationUrl, codeVerifier } = await startAuthorization( context.serverUrl, diff --git a/client/src/utils/jsonUtils.ts b/client/src/utils/jsonUtils.ts index f8378966..334dafc2 100644 --- a/client/src/utils/jsonUtils.ts +++ b/client/src/utils/jsonUtils.ts @@ -85,8 +85,9 @@ export function tryParseJson(str: string): { success: boolean; data: JsonValue; } { - const trimmed = str.trim(); + const trimmed = str?.trim(); if ( + trimmed && !(trimmed.startsWith("{") && trimmed.endsWith("}")) && !(trimmed.startsWith("[") && trimmed.endsWith("]")) ) { diff --git a/client/src/utils/schemaUtils.ts b/client/src/utils/schemaUtils.ts index 5beb25e2..36587021 100644 --- a/client/src/utils/schemaUtils.ts +++ b/client/src/utils/schemaUtils.ts @@ -235,6 +235,16 @@ export function normalizeUnionType(schema: JsonSchemaType): JsonSchemaType { return { ...schema, type: "integer", anyOf: undefined, nullable: true }; } + // Handle anyOf with exactly array and null (FastMCP pattern) + if ( + schema.anyOf && + schema.anyOf.length === 2 && + schema.anyOf.some((t) => (t as JsonSchemaType).type === "array") && + schema.anyOf.some((t) => (t as JsonSchemaType).type === "null") + ) { + return { ...schema, type: "array", anyOf: undefined, nullable: true }; + } + // Handle array type with exactly string and null if ( Array.isArray(schema.type) && diff --git a/package-lock.json b/package-lock.json index e82fa1c6..30966d16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/inspector", - "version": "0.17.2", + "version": "0.17.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/inspector", - "version": "0.17.2", + "version": "0.17.3", "license": "MIT", "workspaces": [ "client", @@ -14,9 +14,9 @@ "cli" ], "dependencies": { - "@modelcontextprotocol/inspector-cli": "^0.17.2", - "@modelcontextprotocol/inspector-client": "^0.17.2", - "@modelcontextprotocol/inspector-server": "^0.17.2", + "@modelcontextprotocol/inspector-cli": "^0.17.3", + "@modelcontextprotocol/inspector-client": "^0.17.3", + "@modelcontextprotocol/inspector-server": "^0.17.3", "@modelcontextprotocol/sdk": "^1.20.1", "concurrently": "^9.2.0", "node-fetch": "^3.3.2", @@ -48,7 +48,7 @@ }, "cli": { "name": "@modelcontextprotocol/inspector-cli", - "version": "0.17.2", + "version": "0.17.3", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.20.1", @@ -71,7 +71,7 @@ }, "client": { "name": "@modelcontextprotocol/inspector-client", - "version": "0.17.2", + "version": "0.17.3", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.20.1", @@ -12028,7 +12028,7 @@ }, "server": { "name": "@modelcontextprotocol/inspector-server", - "version": "0.17.2", + "version": "0.17.3", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.20.1", diff --git a/package.json b/package.json index 35b3f622..d001e665 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector", - "version": "0.17.2", + "version": "0.17.3", "description": "Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", @@ -47,9 +47,9 @@ "check-version": "node scripts/check-version-consistency.js" }, "dependencies": { - "@modelcontextprotocol/inspector-cli": "^0.17.2", - "@modelcontextprotocol/inspector-client": "^0.17.2", - "@modelcontextprotocol/inspector-server": "^0.17.2", + "@modelcontextprotocol/inspector-cli": "^0.17.3", + "@modelcontextprotocol/inspector-client": "^0.17.3", + "@modelcontextprotocol/inspector-server": "^0.17.3", "@modelcontextprotocol/sdk": "^1.20.1", "concurrently": "^9.2.0", "node-fetch": "^3.3.2", diff --git a/server/package.json b/server/package.json index f0d34655..89203f10 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-server", - "version": "0.17.2", + "version": "0.17.3", "description": "Server-side application for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)",