diff --git a/.claude/skills/project-structure/SKILL.md b/.claude/skills/project-structure/SKILL.md index 5385074f71..4fc7ebf83e 100644 --- a/.claude/skills/project-structure/SKILL.md +++ b/.claude/skills/project-structure/SKILL.md @@ -45,7 +45,7 @@ an MCP server, the request/response lifecycle, and a set of state stores. | `core/mcp/state/` | The stores `core/react/` hooks read | | `core/auth/` | OAuth end to end — providers, discovery, storage, endpoint overrides, scopes, revocation, mid-session recovery — split into isomorphic logic plus `browser/`, `node/` and `remote/` backends | | `core/auth/node/` | Node OAuth storage + loopback callback server, **and** the `SecretStore` backends (keychain / file / memory) and their selection policy | -| `core/extension//` | Host-side adapters for MCP extensions whose protocol an upstream SDK owns. `tasks/` wraps `@modelcontextprotocol/ext-tasks` with what that package leaves to its host: the raw `rawDispatch` channel, progress routing, error identity, and task-view conversions. Imports nothing from `InspectorClient`; host state arrives through narrow interfaces | +| `core/extension//` | Host-side adapters for MCP extensions whose protocol an upstream SDK owns. `tasks/` wraps `@modelcontextprotocol/ext-tasks` with what that package leaves to its host: the raw `rawDispatch` channel, progress routing, error identity, and task-view conversions. `skills/` keeps what `@modelcontextprotocol/client/ext/skills` leaves to an inspector: permissive entry schemas (the SDK's refuse a non-conforming entry), the per-check conformance and verification report, and `SKILL.md` frontmatter parsing; constants, limits and the directory-read schema come from the SDK. Imports nothing from `InspectorClient`; host state arrives through narrow interfaces | | `core/cli/` | Node-only surface the one-shot CLI and `mcpdo` both run on (#2461): `error-handler` (exit codes, `CliExitCodeError`, the error envelope), the method handlers and their option/format types (`handlers/`), the interactive OAuth connect flow (`cliOAuth`, `cli-oauth-navigation`) and output helpers (`style`, `utils/awaitable-log`). One-shot-only output (`emit-result`, `consume-outcome`, `servers-write`, …) stays in `clients/cli/src` | | `core/client/` | Install-level client config (`client.json`): browser-safe parse plus Node load/save, remote backend, secrets, runner | | `core/json/` | JSON + parameter/argument conversion; the schema normalizations all three form builders share (nullable unions, root composition) and the tool-schema portability lint | diff --git a/AGENTS.md b/AGENTS.md index aeefcce692..85ac58b11c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,6 +53,8 @@ inspector/ │ │ exit codes, interactive OAuth flow, output styling); tested + gated by clients/cli │ ├── client/ Install-level client config (`client.json`) │ ├── extension/ Host-side adapters for MCP extensions whose protocol an upstream SDK owns +│ │ ├── skills/ What the SDK's Skills client leaves to an inspector: permissive entry schemas, the +│ │ │ per-check conformance + verification report, SKILL.md frontmatter parsing │ │ └── tasks/ The Inspector's glue around `@modelcontextprotocol/ext-tasks` (raw dispatch, progress routing) │ ├── json/ JSON/schema utilities shared by all three form builders │ ├── logging/ Silent pino logger singleton diff --git a/clients/cli/__tests__/run-method-skills.test.ts b/clients/cli/__tests__/run-method-skills.test.ts index 7b1d239848..bccde8b26e 100644 --- a/clients/cli/__tests__/run-method-skills.test.ts +++ b/clients/cli/__tests__/run-method-skills.test.ts @@ -6,9 +6,9 @@ import { } from "@inspector/core/cli/handlers/skills-verify.js"; import { EXIT_CODES } from "@inspector/core/cli/error-handler.js"; import type { InspectorClient } from "@inspector/core/mcp/index.js"; -import type { SkillEntry } from "@inspector/core/mcp/skillsSchemas.js"; -import { sha256Digest } from "@inspector/core/mcp/skills.js"; -import type { SkillVerifyReport } from "@inspector/core/mcp/skillsVerification.js"; +import type { SkillEntry } from "@inspector/core/extension/skills/schemas.js"; +import { sha256Digest } from "@inspector/core/extension/skills/conformance.js"; +import type { SkillVerifyReport } from "@inspector/core/extension/skills/verification.js"; /** * The three SEP-2640 methods the CLI gained in #2248, plus `--verify`. diff --git a/clients/cli/__tests__/skills-verify-cli.test.ts b/clients/cli/__tests__/skills-verify-cli.test.ts index a3adc63a54..3a8e89c9b6 100644 --- a/clients/cli/__tests__/skills-verify-cli.test.ts +++ b/clients/cli/__tests__/skills-verify-cli.test.ts @@ -3,7 +3,7 @@ import { createTestServerHttp, createTestServerInfo, } from "@modelcontextprotocol/inspector-test-server"; -import type { SkillVerifyReport } from "@inspector/core/mcp/skillsVerification.js"; +import type { SkillVerifyReport } from "@inspector/core/extension/skills/verification.js"; import { runCli } from "../src/cli.js"; import { consumeMethodOutcome } from "../src/handlers/consume-outcome.js"; import { EXIT_CODES } from "@inspector/core/cli/error-handler.js"; diff --git a/clients/mcpdo/__tests__/skills-budget-hint.test.ts b/clients/mcpdo/__tests__/skills-budget-hint.test.ts index 6ff17c7043..ef72c0f0bc 100644 --- a/clients/mcpdo/__tests__/skills-budget-hint.test.ts +++ b/clients/mcpdo/__tests__/skills-budget-hint.test.ts @@ -17,7 +17,7 @@ import { createTestServerHttp, createTestServerInfo, } from "@modelcontextprotocol/inspector-test-server"; -import type { SkillVerifyReport } from "@inspector/core/mcp/skillsVerification.js"; +import type { SkillVerifyReport } from "@inspector/core/extension/skills/verification.js"; import { runMcp, type McpResult } from "./helpers/mcp-runner.js"; import { createTestConfig, diff --git a/clients/tui/__tests__/SkillsTab.test.tsx b/clients/tui/__tests__/SkillsTab.test.tsx index 1efe5faa1c..4672c14336 100644 --- a/clients/tui/__tests__/SkillsTab.test.tsx +++ b/clients/tui/__tests__/SkillsTab.test.tsx @@ -2,9 +2,12 @@ import React from "react"; import { describe, it, expect, vi } from "vitest"; import { render } from "./helpers/renderTui"; import type { InspectorClient } from "@inspector/core/mcp/index.js"; -import type { SkillEntry } from "@inspector/core/mcp/skillsSchemas.js"; +import type { SkillEntry } from "@inspector/core/extension/skills/schemas.js"; import { AuthRecoveryRequiredError } from "@inspector/core/auth/challenge.js"; -import { sha256Digest, textToBytes } from "@inspector/core/mcp/skills.js"; +import { + sha256Digest, + textToBytes, +} from "@inspector/core/extension/skills/conformance.js"; // MUST mock ink-scroll-view: the real ScrollView renders a placeholder minimap // in the non-TTY test env and never mounts its children. diff --git a/clients/tui/src/components/SkillsTab.tsx b/clients/tui/src/components/SkillsTab.tsx index 361140db83..bb10549e67 100644 --- a/clients/tui/src/components/SkillsTab.tsx +++ b/clients/tui/src/components/SkillsTab.tsx @@ -36,17 +36,17 @@ import { skillEntryKey, skillUriIdentity, type SkillIssue, -} from "@inspector/core/mcp/skills.js"; +} from "@inspector/core/extension/skills/conformance.js"; import { DYNAMIC_RESOURCES, type SkillEntry, -} from "@inspector/core/mcp/skillsSchemas.js"; +} from "@inspector/core/extension/skills/schemas.js"; import { verifySkills, type SkillBudgetHint, type SkillFileReport, type SkillVerifyReport, -} from "@inspector/core/mcp/skillsVerification.js"; +} from "@inspector/core/extension/skills/verification.js"; import { useSelectableList } from "../hooks/useSelectableList.js"; import { errorMessage } from "../utils/errorText.js"; import { useListFilter } from "../hooks/useListFilter.js"; diff --git a/clients/web/src/App.tsx b/clients/web/src/App.tsx index ebce84677d..15ea1de32f 100644 --- a/clients/web/src/App.tsx +++ b/clients/web/src/App.tsx @@ -10,7 +10,7 @@ import type { } from "@modelcontextprotocol/client"; import { InspectorClient } from "@inspector/core/mcp/index.js"; import { getServerType } from "@inspector/core/mcp/config.js"; -import { getSkillsExtension } from "@inspector/core/mcp/skills.js"; +import { getSkillsExtension } from "@inspector/core/extension/skills/conformance.js"; import type { JsonValue } from "@inspector/core/mcp/index.js"; import type { TypedEventGeneric } from "@inspector/core/mcp/typedEventTarget.js"; diff --git a/clients/web/src/components/groups/ConnectionInfoContent/ConnectionInfoContent.tsx b/clients/web/src/components/groups/ConnectionInfoContent/ConnectionInfoContent.tsx index 432688a16c..9edc730ce1 100644 --- a/clients/web/src/components/groups/ConnectionInfoContent/ConnectionInfoContent.tsx +++ b/clients/web/src/components/groups/ConnectionInfoContent/ConnectionInfoContent.tsx @@ -28,7 +28,7 @@ import { import { useTickingClock } from "../../../hooks/useTickingClock"; import type { ListCount } from "../../../utils/listCount"; import { TASKS_EXTENSION_KEY } from "@inspector/core/extension/tasks/constants.js"; -import { getSkillsExtension } from "@inspector/core/mcp/skills.js"; +import { getSkillsExtension } from "@inspector/core/extension/skills/conformance.js"; import type { OAuthClientRegistrationKind } from "@inspector/core/auth/types.js"; import { CapabilityItem, diff --git a/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.tsx b/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.tsx index b2551e9460..5694ec3865 100644 --- a/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.tsx +++ b/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.tsx @@ -47,7 +47,7 @@ import { import { isSkillCatalogLimit, resolveSkillCatalogBudget, -} from "@inspector/core/mcp/skills.js"; +} from "@inspector/core/extension/skills/conformance.js"; import type { Root } from "@modelcontextprotocol/client"; /** diff --git a/clients/web/src/components/screens/SkillsScreen/SkillsScreen.stories.tsx b/clients/web/src/components/screens/SkillsScreen/SkillsScreen.stories.tsx index cfb08b60ca..aa15ea0533 100644 --- a/clients/web/src/components/screens/SkillsScreen/SkillsScreen.stories.tsx +++ b/clients/web/src/components/screens/SkillsScreen/SkillsScreen.stories.tsx @@ -2,8 +2,8 @@ import { useState } from "react"; import type { ComponentProps } from "react"; import type { Meta, StoryObj } from "@storybook/react-vite"; import { expect, fn, userEvent, waitFor, within } from "storybook/test"; -import type { SkillEntry } from "@inspector/core/mcp/skillsSchemas"; -import { sha256Bytes } from "@inspector/core/mcp/sha256"; +import type { SkillEntry } from "@inspector/core/extension/skills/schemas"; +import { sha256Bytes } from "@inspector/core/extension/skills/sha256"; import { SkillsScreen } from "./SkillsScreen"; import type { SkillsUiState } from "./SkillsScreen"; import { EMPTY_SKILLS_UI } from "../screenUiState"; diff --git a/clients/web/src/components/screens/SkillsScreen/SkillsScreen.test.tsx b/clients/web/src/components/screens/SkillsScreen/SkillsScreen.test.tsx index 54d1a04573..cfb58ad718 100644 --- a/clients/web/src/components/screens/SkillsScreen/SkillsScreen.test.tsx +++ b/clients/web/src/components/screens/SkillsScreen/SkillsScreen.test.tsx @@ -1,8 +1,11 @@ import { StrictMode, useState } from "react"; import { describe, it, expect, vi } from "vitest"; import userEvent from "@testing-library/user-event"; -import type { SkillEntry } from "@inspector/core/mcp/skillsSchemas"; -import { sha256Digest, textToBytes } from "@inspector/core/mcp/skills"; +import type { SkillEntry } from "@inspector/core/extension/skills/schemas"; +import { + sha256Digest, + textToBytes, +} from "@inspector/core/extension/skills/conformance"; import { act, renderWithMantine, diff --git a/clients/web/src/components/screens/SkillsScreen/SkillsScreen.tsx b/clients/web/src/components/screens/SkillsScreen/SkillsScreen.tsx index ccd800d22e..a4eb99c7d8 100644 --- a/clients/web/src/components/screens/SkillsScreen/SkillsScreen.tsx +++ b/clients/web/src/components/screens/SkillsScreen/SkillsScreen.tsx @@ -22,11 +22,11 @@ import type { DirectoryReadResult, SkillEntry, SkillResource, -} from "@inspector/core/mcp/skillsSchemas.js"; +} from "@inspector/core/extension/skills/schemas.js"; import { DIRECTORY_MIME_TYPE, DYNAMIC_RESOURCES, -} from "@inspector/core/mcp/skillsSchemas.js"; +} from "@inspector/core/extension/skills/schemas.js"; import { checkSkillConformance, checkSkillFrontmatterMatch, @@ -44,12 +44,12 @@ import { type SkillFileContents, type SkillIssue, type SkillVerification, -} from "@inspector/core/mcp/skills.js"; +} from "@inspector/core/extension/skills/conformance.js"; import { CodeHighlight } from "../../elements/CodeHighlight/CodeHighlight"; import { ContentViewer } from "../../elements/ContentViewer/ContentViewer"; import { ListToggle } from "../../elements/ListToggle/ListToggle"; import { useValueChange } from "../../../hooks/useValueChange"; -import { splitSkillFile } from "@inspector/core/mcp/skillFile.js"; +import { splitSkillFile } from "@inspector/core/extension/skills/skillFile.js"; import { inferMimeFromUri, isGenericMime, diff --git a/clients/web/src/components/views/InspectorView/InspectorView.tsx b/clients/web/src/components/views/InspectorView/InspectorView.tsx index 9688ed97db..5be44339b9 100644 --- a/clients/web/src/components/views/InspectorView/InspectorView.tsx +++ b/clients/web/src/components/views/InspectorView/InspectorView.tsx @@ -13,7 +13,7 @@ import type { ServerEntry } from "@inspector/core/mcp/types.js"; import { isTerminalStatus } from "@inspector/core/mcp/types.js"; import { isAppTool } from "@inspector/core/mcp/apps.js"; import { TASKS_EXTENSION_KEY } from "@inspector/core/extension/tasks/constants.js"; -import { isSkillsExtensionSupported } from "@inspector/core/mcp/skills.js"; +import { isSkillsExtensionSupported } from "@inspector/core/extension/skills/conformance.js"; import { ViewHeader } from "../../groups/ViewHeader/ViewHeader"; import { VersionBadge } from "../../elements/VersionBadge/VersionBadge"; import { CopyrightBadge } from "../../elements/CopyrightBadge/CopyrightBadge"; diff --git a/clients/web/src/components/views/InspectorView/types.ts b/clients/web/src/components/views/InspectorView/types.ts index 7aef67b68e..3d1d86c3b5 100644 --- a/clients/web/src/components/views/InspectorView/types.ts +++ b/clients/web/src/components/views/InspectorView/types.ts @@ -60,9 +60,9 @@ import type { } from "../../screens/ResourcesScreen/ResourcesScreen"; import type { LogsUiState } from "../../screens/LoggingScreen/LoggingScreen"; import type { SkillsUiState } from "../../screens/SkillsScreen/SkillsScreen"; -import type { SkillEntry } from "@inspector/core/mcp/skillsSchemas.js"; -import type { SkillFileContents } from "@inspector/core/mcp/skills.js"; -import type { DirectoryReadResult } from "@inspector/core/mcp/skillsSchemas.js"; +import type { SkillEntry } from "@inspector/core/extension/skills/schemas.js"; +import type { SkillFileContents } from "@inspector/core/extension/skills/conformance.js"; +import type { DirectoryReadResult } from "@inspector/core/extension/skills/schemas.js"; import type { TasksUiState } from "../../screens/TasksScreen/TasksScreen"; import type { ProtocolUiState } from "../../screens/ProtocolScreen/ProtocolScreen"; import type { NetworkUiState } from "../../screens/NetworkScreen/NetworkScreen"; diff --git a/clients/web/src/hooks/useInspectorStores.ts b/clients/web/src/hooks/useInspectorStores.ts index 0ae2858825..b731884df0 100644 --- a/clients/web/src/hooks/useInspectorStores.ts +++ b/clients/web/src/hooks/useInspectorStores.ts @@ -13,7 +13,7 @@ import type { ResourceSubscriptionStreamState, StderrLogEntry, } from "@inspector/core/mcp/types.js"; -import type { SkillEntry } from "@inspector/core/mcp/skillsSchemas.js"; +import type { SkillEntry } from "@inspector/core/extension/skills/schemas.js"; import type { InspectorClient } from "@inspector/core/mcp/index.js"; import { ManagedToolsState } from "@inspector/core/mcp/state/managedToolsState.js"; import { ManagedPromptsState } from "@inspector/core/mcp/state/managedPromptsState.js"; diff --git a/clients/web/src/hooks/useServerCommands.tsx b/clients/web/src/hooks/useServerCommands.tsx index 0cf9769dd5..be7a83c6b9 100644 --- a/clients/web/src/hooks/useServerCommands.tsx +++ b/clients/web/src/hooks/useServerCommands.tsx @@ -34,10 +34,10 @@ import type { } from "../components/screens/ToolsScreen/ToolsScreen"; import type { GetPromptState } from "../components/screens/PromptsScreen/PromptsScreen"; import type { ReadResourceState } from "../components/screens/ResourcesScreen/ResourcesScreen"; -import type { SkillFileContents } from "@inspector/core/mcp/skills.js"; -import type { DirectoryReadResult } from "@inspector/core/mcp/skillsSchemas.js"; -import type { SkillEntry } from "@inspector/core/mcp/skillsSchemas.js"; -import { normalizeSkillUri } from "@inspector/core/mcp/skills.js"; +import type { SkillFileContents } from "@inspector/core/extension/skills/conformance.js"; +import type { DirectoryReadResult } from "@inspector/core/extension/skills/schemas.js"; +import type { SkillEntry } from "@inspector/core/extension/skills/schemas.js"; +import { normalizeSkillUri } from "@inspector/core/extension/skills/conformance.js"; import { UrlElicitationErrorToastMessage } from "../components/elements/Toasts/UrlElicitationErrorToastMessage"; import { errorCodeOf, diff --git a/clients/web/src/test/core/mcp/skills.test.ts b/clients/web/src/test/core/extension/skills/conformance.test.ts similarity index 99% rename from clients/web/src/test/core/mcp/skills.test.ts rename to clients/web/src/test/core/extension/skills/conformance.test.ts index 552b02e0a0..b4f93c8004 100644 --- a/clients/web/src/test/core/mcp/skills.test.ts +++ b/clients/web/src/test/core/extension/skills/conformance.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from "vitest"; import type { ServerCapabilities } from "@modelcontextprotocol/client"; -import type { SkillEntry } from "@inspector/core/mcp/skillsSchemas"; -import { SKILLS_EXTENSION_KEY } from "@inspector/core/mcp/skillsSchemas"; +import type { SkillEntry } from "@inspector/core/extension/skills/schemas"; +import { SKILLS_EXTENSION_KEY } from "@inspector/core/extension/skills/schemas"; import { SKILL_MAX_RESOURCE_ENTRIES, SKILL_MAX_TOTAL_BYTES, @@ -21,7 +21,7 @@ import { textToBytes, totalSkillBytes, verifySkillResource, -} from "@inspector/core/mcp/skills"; +} from "@inspector/core/extension/skills/conformance"; /** The digest of the string "hello", precomputed so the assertion is a fact * about SHA-256 rather than a restatement of what the code just did. */ diff --git a/clients/web/src/test/core/mcp/skillsSchemas.test.ts b/clients/web/src/test/core/extension/skills/schemas.test.ts similarity index 95% rename from clients/web/src/test/core/mcp/skillsSchemas.test.ts rename to clients/web/src/test/core/extension/skills/schemas.test.ts index b6940910a7..327d33ab66 100644 --- a/clients/web/src/test/core/mcp/skillsSchemas.test.ts +++ b/clients/web/src/test/core/extension/skills/schemas.test.ts @@ -14,7 +14,11 @@ import { SKILLS_GET_METHOD, SKILLS_LIST_METHOD, SkillEntrySchema, -} from "@inspector/core/mcp/skillsSchemas"; +} from "@inspector/core/extension/skills/schemas"; +import { + ReadResourceDirectoryResultSchema, + SKILLS_EXTENSION_ID, +} from "@modelcontextprotocol/core/ext/skills"; const ENTRY = { uri: "skill://demo/SKILL.md", @@ -25,6 +29,15 @@ const ENTRY = { }; describe("skills wire constants", () => { + // #2804: the SDK owns these, so a spec revision lands with an SDK bump. A + // local restatement would still pass the literal checks below. + it("takes the extension id and the directory schema from the SDK", () => { + expect(SKILLS_EXTENSION_KEY).toBe(SKILLS_EXTENSION_ID); + expect(ModernDirectoryReadResultSchema).toBe( + ReadResourceDirectoryResultSchema, + ); + }); + it("names the extension and its two required methods", () => { expect(SKILLS_EXTENSION_KEY).toBe("io.modelcontextprotocol/skills"); expect(SKILLS_LIST_METHOD).toBe("skills/list"); diff --git a/clients/web/src/test/core/mcp/sha256.test.ts b/clients/web/src/test/core/extension/skills/sha256.test.ts similarity index 95% rename from clients/web/src/test/core/mcp/sha256.test.ts rename to clients/web/src/test/core/extension/skills/sha256.test.ts index a0c66be31e..78d96d0948 100644 --- a/clients/web/src/test/core/mcp/sha256.test.ts +++ b/clients/web/src/test/core/extension/skills/sha256.test.ts @@ -1,6 +1,9 @@ import { describe, it, expect } from "vitest"; -import { sha256Bytes } from "@inspector/core/mcp/sha256"; -import { sha256Digest, textToBytes } from "@inspector/core/mcp/skills"; +import { sha256Bytes } from "@inspector/core/extension/skills/sha256"; +import { + sha256Digest, + textToBytes, +} from "@inspector/core/extension/skills/conformance"; const hex = (bytes: Uint8Array) => Array.from(bytes) diff --git a/clients/web/src/test/core/mcp/skillFile.test.ts b/clients/web/src/test/core/extension/skills/skillFile.test.ts similarity index 99% rename from clients/web/src/test/core/mcp/skillFile.test.ts rename to clients/web/src/test/core/extension/skills/skillFile.test.ts index bb9e5bcf50..b7a6782c5d 100644 --- a/clients/web/src/test/core/mcp/skillFile.test.ts +++ b/clients/web/src/test/core/extension/skills/skillFile.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest"; import { parseSkillFrontmatter, splitSkillFile, -} from "@inspector/core/mcp/skillFile.js"; +} from "@inspector/core/extension/skills/skillFile.js"; describe("splitSkillFile", () => { it("separates a leading frontmatter fence from the body", () => { diff --git a/clients/web/src/test/core/mcp/skillFileBytes.test.ts b/clients/web/src/test/core/extension/skills/skillFileBytes.test.ts similarity index 93% rename from clients/web/src/test/core/mcp/skillFileBytes.test.ts rename to clients/web/src/test/core/extension/skills/skillFileBytes.test.ts index 0aab3c9dab..60d1b7ccf2 100644 --- a/clients/web/src/test/core/mcp/skillFileBytes.test.ts +++ b/clients/web/src/test/core/extension/skills/skillFileBytes.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { skillFileBytes } from "@inspector/core/mcp/skills.js"; +import { skillFileBytes } from "@inspector/core/extension/skills/conformance.js"; describe("skillFileBytes", () => { it("encodes a text content block as UTF-8", () => { diff --git a/clients/web/src/test/core/mcp/skillsVerification.test.ts b/clients/web/src/test/core/extension/skills/verification.test.ts similarity index 99% rename from clients/web/src/test/core/mcp/skillsVerification.test.ts rename to clients/web/src/test/core/extension/skills/verification.test.ts index 5ac858676a..0d23ec245e 100644 --- a/clients/web/src/test/core/mcp/skillsVerification.test.ts +++ b/clients/web/src/test/core/extension/skills/verification.test.ts @@ -3,11 +3,11 @@ import type { InspectorClientProtocol } from "@inspector/core/mcp/inspectorClien import { DYNAMIC_RESOURCES, type SkillEntry, -} from "@inspector/core/mcp/skillsSchemas.js"; +} from "@inspector/core/extension/skills/schemas.js"; import { SKILL_MAX_CATALOG_SKILLS, sha256Digest, -} from "@inspector/core/mcp/skills.js"; +} from "@inspector/core/extension/skills/conformance.js"; import { AuthRecoveryRequiredError } from "@inspector/core/auth/challenge.js"; import { allSkillsVerified, @@ -16,7 +16,7 @@ import { DEFAULT_SKILL_BUDGET_HINT, utf8Length, verifySkills, -} from "@inspector/core/mcp/skillsVerification.js"; +} from "@inspector/core/extension/skills/verification.js"; /** * `verifySkills` is the fetch-and-verify half of the SEP-2640 checks (#2248) — diff --git a/clients/web/src/test/core/mcp/extensions.test.ts b/clients/web/src/test/core/mcp/extensions.test.ts index 8ec3ff6540..dfafb99ba1 100644 --- a/clients/web/src/test/core/mcp/extensions.test.ts +++ b/clients/web/src/test/core/mcp/extensions.test.ts @@ -8,7 +8,7 @@ import { isAdvertisedByDefault, } from "@inspector/core/mcp/extensions.js"; import { TASKS_EXTENSION_KEY } from "@inspector/core/extension/tasks/constants.js"; -import { SKILLS_EXTENSION_KEY } from "@inspector/core/mcp/skillsSchemas.js"; +import { SKILLS_EXTENSION_KEY } from "@inspector/core/extension/skills/schemas.js"; // The `ui` extension carries a non-empty advertisement value; the others are // declared with `{}`. Spelled out here so the map assertions stay readable. diff --git a/clients/web/src/test/core/mcp/inspectorClient-skills.test.ts b/clients/web/src/test/core/mcp/inspectorClient-skills.test.ts index 77eb4f0cae..aa6afa9547 100644 --- a/clients/web/src/test/core/mcp/inspectorClient-skills.test.ts +++ b/clients/web/src/test/core/mcp/inspectorClient-skills.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi } from "vitest"; import { InspectorClient } from "@inspector/core/mcp/inspectorClient.js"; import type { ServerCapabilities } from "@modelcontextprotocol/client"; -import { SKILLS_EXTENSION_KEY } from "@inspector/core/mcp/skillsSchemas.js"; +import { SKILLS_EXTENSION_KEY } from "@inspector/core/extension/skills/schemas.js"; import { SdkError, SdkErrorCode } from "@modelcontextprotocol/client"; /** diff --git a/clients/web/src/test/core/mcp/state/managedSkillsState.test.ts b/clients/web/src/test/core/mcp/state/managedSkillsState.test.ts index df22fae75d..39f87017de 100644 --- a/clients/web/src/test/core/mcp/state/managedSkillsState.test.ts +++ b/clients/web/src/test/core/mcp/state/managedSkillsState.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach } from "vitest"; -import type { SkillEntry } from "@inspector/core/mcp/skillsSchemas"; +import type { SkillEntry } from "@inspector/core/extension/skills/schemas"; import { ManagedSkillsState, REPEATED_CURSOR_MESSAGE, diff --git a/clients/web/src/test/core/react/useManagedSkills.test.tsx b/clients/web/src/test/core/react/useManagedSkills.test.tsx index a9c744e48c..ffda98e73d 100644 --- a/clients/web/src/test/core/react/useManagedSkills.test.tsx +++ b/clients/web/src/test/core/react/useManagedSkills.test.tsx @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach } from "vitest"; import { act, renderHook } from "@testing-library/react"; -import type { SkillEntry } from "@inspector/core/mcp/skillsSchemas"; +import type { SkillEntry } from "@inspector/core/extension/skills/schemas"; import { FakeInspectorClient } from "@inspector/core/mcp/__tests__/fakeInspectorClient"; import { ManagedSkillsState } from "@inspector/core/mcp/state/managedSkillsState"; import { useManagedSkills } from "@inspector/core/react/useManagedSkills"; diff --git a/clients/web/src/test/integration/mcp/inspectorClient-skills.test.ts b/clients/web/src/test/integration/mcp/inspectorClient-skills.test.ts index aeab38bd5e..cdff0c6481 100644 --- a/clients/web/src/test/integration/mcp/inspectorClient-skills.test.ts +++ b/clients/web/src/test/integration/mcp/inspectorClient-skills.test.ts @@ -2,12 +2,12 @@ import { describe, it, expect, afterEach } from "vitest"; import { InspectorClient } from "@inspector/core/mcp/inspectorClient.js"; import { createTransportNode } from "@inspector/core/mcp/node/transport.js"; import { eraToVersionNegotiation } from "@inspector/core/mcp/types.js"; -import { getSkillsExtension } from "@inspector/core/mcp/skills.js"; +import { getSkillsExtension } from "@inspector/core/extension/skills/conformance.js"; import { ManagedSkillsState } from "@inspector/core/mcp/state/managedSkillsState.js"; import { allSkillsVerified, verifySkills, -} from "@inspector/core/mcp/skillsVerification.js"; +} from "@inspector/core/extension/skills/verification.js"; import { createTestServerHttp, type TestServerHttp, diff --git a/core/cli/handlers/output-file.ts b/core/cli/handlers/output-file.ts index ee46c1719b..4ca1468756 100644 --- a/core/cli/handlers/output-file.ts +++ b/core/cli/handlers/output-file.ts @@ -21,7 +21,7 @@ * with a file encoding would make `--format json --output x` ambiguous. */ import { writeFile } from "node:fs/promises"; -import { base64ToBytes } from "../../mcp/skills.js"; +import { base64ToBytes } from "../../extension/skills/conformance.js"; import { CliExitCodeError, EXIT_CODES } from "../error-handler.js"; import type { McpResponse } from "./method-types.js"; diff --git a/core/cli/handlers/run-method.ts b/core/cli/handlers/run-method.ts index f2891e1f5d..3dd13900a2 100644 --- a/core/cli/handlers/run-method.ts +++ b/core/cli/handlers/run-method.ts @@ -9,7 +9,7 @@ import { ManagedSkillsState, MessageLogState, } from "../../mcp/state/index.js"; -import { SKILLS_EXTENSION_KEY } from "../../mcp/skillsSchemas.js"; +import { SKILLS_EXTENSION_KEY } from "../../extension/skills/schemas.js"; import { CliExitCodeError, EXIT_CODES } from "../error-handler.js"; import { collectAppInfo } from "./collect-app-info.js"; import { @@ -20,7 +20,7 @@ import { import { verifySkills, type SkillBudgetHint, -} from "../../mcp/skillsVerification.js"; +} from "../../extension/skills/verification.js"; import type { CliAppInfo, McpResponse, diff --git a/core/cli/handlers/skills-verify.ts b/core/cli/handlers/skills-verify.ts index be49f8b9a6..c33a29258f 100644 --- a/core/cli/handlers/skills-verify.ts +++ b/core/cli/handlers/skills-verify.ts @@ -18,7 +18,7 @@ import { anySkillUnverifiable, type SkillBudgetHint, type SkillVerifyReport, -} from "../../mcp/skillsVerification.js"; +} from "../../extension/skills/verification.js"; import { EXIT_CODES } from "../error-handler.js"; /** diff --git a/core/mcp/skills.ts b/core/extension/skills/conformance.ts similarity index 96% rename from core/mcp/skills.ts rename to core/extension/skills/conformance.ts index cacb8f9f45..db465e9a00 100644 --- a/core/mcp/skills.ts +++ b/core/extension/skills/conformance.ts @@ -33,17 +33,34 @@ * in transit and says nothing about whether the *listing* described that file * honestly. A server could advertise one description, serve another, and pass * every other check in this module. {@link checkSkillFrontmatterMatch} is the - * check; it needs a real YAML parser, and `core/mcp/skillFile.ts` explains why + * check; it needs a real YAML parser, and `skillFile.ts` explains why * that dependency is imported rather than approximated. + * + * **Why this stays ours rather than the SDK's `SkillsClient` (#2804).** The SDK + * implements the same rules as a *host* needs them: `skills/list` refuses an + * entry that breaks one (or, with `skipInvalid`, replaces it with a one-line + * `reason`), and `read()` throws a `SkillVerificationError` on the first check + * that fails. Both are right for a host deciding whether to load a skill and + * wrong for a tool whose output *is* the list of what failed. This module + * reports every rule separately, with a code and a severity, keeps + * `"dynamic"` resources a warning rather than a silent `verified: false` + * (#2405), and runs against entries the SDK would never hand back. The + * interoperability limits and the digest grammar are the SDK's, though — + * {@link SKILL_MAX_RESOURCE_ENTRIES}, {@link SKILL_MAX_TOTAL_BYTES} and the + * digest pattern are read from `@modelcontextprotocol/core/ext/skills`. */ import type { ServerCapabilities } from "@modelcontextprotocol/client"; +import { + SKILL_DIGEST_PATTERN, + SKILL_LIMITS, +} from "@modelcontextprotocol/core/ext/skills"; import { DYNAMIC_RESOURCES, SKILLS_EXTENSION_KEY, type SkillEntry, type SkillResource, -} from "./skillsSchemas.js"; +} from "./schemas.js"; import { sha256Bytes } from "./sha256.js"; import { jsonGraphError, @@ -51,11 +68,17 @@ import { splitSkillFile, } from "./skillFile.js"; -/** Maximum resource entries a single skill may declare (SEP-2640). */ -export const SKILL_MAX_RESOURCE_ENTRIES = 512; +/** + * Maximum resource entries a single skill may declare (SEP-2640) — the SDK's + * `SKILL_LIMITS.files`. + */ +export const SKILL_MAX_RESOURCE_ENTRIES = SKILL_LIMITS.files; -/** Maximum total size, in bytes, of a single skill's resources (16 MiB). */ -export const SKILL_MAX_TOTAL_BYTES = 16 * 1024 * 1024; +/** + * Maximum total size, in bytes, of a single skill's resources (16 MiB) — the + * SDK's `SKILL_LIMITS.bytes`. + */ +export const SKILL_MAX_TOTAL_BYTES = SKILL_LIMITS.bytes; /** * Maximum skills one verification run will actually read from, and the byte @@ -123,8 +146,8 @@ export function resolveSkillCatalogBudget(settings?: { /** The suffix every skill URI ends with; the segment before it is the name. */ export const SKILL_FILE_SUFFIX = "/SKILL.md"; -/** `sha256:` followed by exactly 64 lowercase hex characters. */ -const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/; +/** `sha256:` followed by exactly 64 lowercase hex characters (the SDK's). */ +const DIGEST_PATTERN = SKILL_DIGEST_PATTERN; /** * The Agent Skills name format SEP-2640 requires of `frontmatter.name`: 1–64 @@ -175,6 +198,12 @@ export interface SkillsExtensionSupport { * method names in either codec, so nothing about the negotiated era changes * whether the extension can be served or called. A legacy-era server that * declares it is serving it. + * + * Not the SDK's `skillsCapabilityOf` either, which runs the declaration + * through `SkillsCapabilitySchema` and returns `undefined` when it does not + * parse. A server declaring `{ directoryRead: "yes" }` would then lose the + * Skills tab outright, hiding the very server an inspector is for; here any + * object is a declaration and only a literal `true` enables `directoryRead`. */ export function getSkillsExtension( capabilities: ServerCapabilities | undefined, diff --git a/core/mcp/skillsSchemas.ts b/core/extension/skills/schemas.ts similarity index 55% rename from core/mcp/skillsSchemas.ts rename to core/extension/skills/schemas.ts index e915f91007..bfb76cb3b1 100644 --- a/core/mcp/skillsSchemas.ts +++ b/core/extension/skills/schemas.ts @@ -1,21 +1,28 @@ /** * Skills extension wire schemas — SEP-2640 (`io.modelcontextprotocol/skills`). * - * There is no `@modelcontextprotocol/ext-skills` package (404 on npm) and the - * SDK's era codecs define none of these methods, so the Inspector drives them - * as ordinary `client.request(…, ResultSchema)` calls with the explicit result - * schemas below. That is exactly what the SDK prescribes for a consumer-owned - * extension method: `Protocol._assertOutboundRequestInEra` only fires for names - * one of the era codecs knows, so `skills/list` and `skills/get` are era-blind - * and go out unchanged on both the 2025- and 2026-era legs. The raw-wire escape - * hatch that modern `tasks/*` needs is deliberately NOT used here — `tasks/*` - * are spec names the 2026 codec deleted, which is a different problem. + * **The SDK owns the wire constants, and owns the schemas wherever the + * Inspector can take them unchanged (#2804).** MCP TypeScript SDK 2.4.0 ships + * the client side of this extension (`@modelcontextprotocol/client/ext/skills`) + * and its schemas (`@modelcontextprotocol/core/ext/skills`). From the second + * this module re-exports the method names, the extension id and the directory + * mime type, and builds every caching attribute and the whole + * `resources/directory/read` result out of the SDK's schemas rather than + * restating them, so a spec revision lands with an SDK bump instead of a + * hand edit here. * - * **This module is the wire surface for the two methods the Inspector calls.** - * SEP-2640 is Accepted, so the method names and the entry shape are settled; - * the skill *format* is delegated to the independently-versioned Agent Skills - * specification. Keeping every wire type here makes a spec revision a - * single-file edit (#2234). + * **The entry schemas stay the Inspector's, and that is the one deliberate + * divergence.** The SDK's `SkillSchema` is a *host's* schema: `digest` must + * match the `sha256:` pattern, `size` must be a non-negative integer, and + * `name` and `description` are required, so a non-conforming entry fails the + * parse. `SkillsClient` then either throws on it or, with `skipInvalid`, + * returns `{ index, uri, reason }` in place of the entry. An inspector has to + * show that entry — its frontmatter, its manifest and one finding per broken + * rule — so the schemas below parse it permissively and leave the verdict to + * `checkSkillConformance`. Taking the SDK's schema here would turn every + * finding the conformance report exists to show into a page that fails to + * load. The same reason keeps `SkillsClient.list()` / `get()` out of the call + * path: both run that schema over every entry. * * **Permissive where a defect should be reported; strict where a shape is * settled.** `looseObject`, and a `digest` typed as a plain string rather than @@ -27,30 +34,48 @@ * `skills/list`, `skills/get` or `resources/directory/read` result requires * the caching attributes (see `ModernListSkillsResultSchema`, * `ModernGetSkillEnvelopeSchema` and `ModernDirectoryReadResultSchema`). + * + * The method names are consumer-owned as far as the SDK's era codecs are + * concerned: `Protocol._assertOutboundRequestInEra` only fires for names one + * of the codecs knows, so `skills/list` and `skills/get` go out unchanged on + * both the 2025- and 2026-era legs as ordinary `client.request(…, Schema)` + * calls. The raw-wire escape hatch modern `tasks/*` needs is deliberately NOT + * used here — `tasks/*` are spec names the 2026 codec deleted, which is a + * different problem. */ import { z } from "zod/v4"; -import { ResourceSchema } from "@modelcontextprotocol/core"; - -/** SEP-2133 extension identifier for the Skills extension (SEP-2640). */ -export const SKILLS_EXTENSION_KEY = "io.modelcontextprotocol/skills"; - -/** The `skills/list` JSON-RPC method name. */ -export const SKILLS_LIST_METHOD = "skills/list"; - -/** The `skills/get` JSON-RPC method name. */ -export const SKILLS_GET_METHOD = "skills/get"; +import { + ReadResourceDirectoryResultSchema, + SkillCacheScopeSchema, +} from "@modelcontextprotocol/core/ext/skills"; /** - * The `resources/directory/read` method name, gated on the server declaring - * `directoryRead: true` in its extension advertisement. Declared here so the - * one wire-surface module names every method the extension defines, even though - * the Inspector does not call it yet (phase 3 of #2234). + * SEP-2133 extension identifier, the three method names and the directory + * mime type, re-exported from the SDK rather than restated so each string has + * one owner — the same shape `core/extension/tasks/constants.ts` uses. + * `RESOURCES_DIRECTORY_READ_METHOD` is gated on the server declaring + * `directoryRead: true`. */ -export const RESOURCES_DIRECTORY_READ_METHOD = "resources/directory/read"; +export { + SKILLS_EXTENSION_ID as SKILLS_EXTENSION_KEY, + SKILLS_LIST_METHOD, + SKILLS_GET_METHOD, + RESOURCES_DIRECTORY_READ_METHOD, + DIRECTORY_MIME_TYPE, +} from "@modelcontextprotocol/core/ext/skills"; -/** `mimeType` marking a resource as a directory rather than a file. */ -export const DIRECTORY_MIME_TYPE = "inode/directory"; +/** + * `CacheableResult`'s two attributes, as the SDK states them: `ttlMs` a + * non-negative integer, `cacheScope` `public` or `private`. Taken from the + * SDK's own directory-read schema, the one result schema it exports that the + * Inspector uses whole, so the caching fields on every era-aware schema here + * are the SDK's and cannot drift from it. + */ +const CACHE_FIELDS = { + ttlMs: ReadResourceDirectoryResultSchema.shape.ttlMs, + cacheScope: SkillCacheScopeSchema, +}; /** * The literal `resources` value meaning "this skill's file set is generated and @@ -119,13 +144,13 @@ export type ListSkillsResult = z.infer; * * SEP-2640's `skills/list` section states: *"In protocol versions 2026-07-28 * and later, the result also carries … `ttlMs` and `cacheScope`."* The field - * shapes mirror `ModernResultEnvelopeSchema` in `listSalvage.ts`, which is this - * repo's existing statement of a modern result envelope and is already applied - * to modern list results on the salvage path — so the two cannot drift. + * shapes are the SDK's own ({@link CACHE_FIELDS}), the same two `SkillsClient` + * requires of a modern page. * - * The era split is load-bearing rather than defensive. `skills/*` is a - * consumer-owned method, so it is absent from the SDK's cacheable-method - * registry and **nothing else validates this**: without an era-aware schema a + * The era split is load-bearing rather than defensive. `skills/*` is a method + * the SDK's era codecs do not define, so it is absent from their + * cacheable-method registry and **nothing else validates this**: without an + * era-aware schema a * modern server could answer `{ skills: [] }` and the conformance UI would * present it as a clean list. The legacy shape stays permissive because these * are 2026-era attributes a legacy server has no business sending. @@ -143,13 +168,11 @@ export type ListSkillsResult = z.infer; * lifts them for a consumer-owned method, so this schema is still the only * thing that does. */ -export const ModernListSkillsResultSchema = ListSkillsResultSchema.extend({ - // A non-negative INTEGER, matching the codec: `ttlMs: -1` or `0.5` is an - // envelope violation, and accepting it here would hide exactly the kind of - // defect this schema exists to surface. - ttlMs: z.int().min(0), - cacheScope: z.enum(["public", "private"]), -}); +export const ModernListSkillsResultSchema = + // A non-negative INTEGER `ttlMs`, as the SDK states it: `ttlMs: -1` or `0.5` + // is an envelope violation, and accepting it here would hide exactly the + // kind of defect this schema exists to surface. + ListSkillsResultSchema.extend(CACHE_FIELDS); /** * The `skills/get` result envelope on a **legacy** connection: the entry @@ -183,10 +206,8 @@ export const GetSkillEnvelopeSchema = z.looseObject({ * * ⚠️ Picked by `InspectorClient.getSkillResult` from the negotiated era. */ -export const ModernGetSkillEnvelopeSchema = GetSkillEnvelopeSchema.extend({ - ttlMs: z.int().min(0), - cacheScope: z.enum(["public", "private"]), -}); +export const ModernGetSkillEnvelopeSchema = + GetSkillEnvelopeSchema.extend(CACHE_FIELDS); /** * The `skills/get` result envelope as the SDK decoded it: the `skill` wrapper @@ -218,46 +239,50 @@ export const GetSkillResultSchema = GetSkillEnvelopeSchema.transform( export type GetSkillResult = SkillEntry; /** - * One `resources/directory/read` child: **the SDK's own `Resource`**, not a - * shape restated here. + * `resources/directory/read` result on a **modern** (2026-07-28+) connection: + * **the SDK's own `ReadResourceDirectoryResultSchema`**, used whole (#2804). + * + * Unlike the skill entry schemas above, nothing here needed to stay ours. A + * directory child is a base-protocol `Resource` — SEP-2640 defines the result + * as carrying "the same `Resource` objects that `resources/list` returns, with + * the same `nextCursor` pagination contract" — and the SDK's + * `DirectoryResourceSchema` is exactly `ResourceSchema`'s shape. The stable + * ext-skills specification makes the result a `CacheableResult` (ext-skills#157, + * #2740), so `ttlMs` and `cacheScope` are required, and the SDK's schema says + * so. * - * SEP-2640 defines the result as carrying "the same `Resource` objects that - * `resources/list` returns, with the same `nextCursor` pagination contract", so - * the schema that already decodes `resources/list` in this app is the literal - * statement of that sentence — and one the SDK, not this module, keeps current. - * Restating it would let the two drift, at which point a directory child and a - * listed resource could disagree about what a `Resource` is while both claimed - * to be one. + * ⚠️ It is **stricter than the entry schemas**, and that is the deliberate + * exception rather than an oversight. `Resource` requires `name`, so one child + * missing `name` rejects the whole page instead of being reported as a + * per-child finding — the opposite of the posture the entry schemas take. The + * reason the trade goes the other way here is ownership: the skills entry + * types are consumer-owned in the sense that nothing else reports on them, so + * this module has to; `Resource` is base-protocol and already validated + * exactly this strictly on the `resources/list` path, where `listSalvage` is + * the answer to a single bad entry. Being *more* permissive here would mean a + * URI that fails as a listed resource succeeds as a directory child, which is + * a worse inconsistency than an all-or-nothing page. * - * ⚠️ It is **stricter than everything else in this module**, and that is the - * deliberate exception rather than an oversight. `ResourceSchema` requires - * `name` and strips unknown members, so one child missing `name` rejects the - * whole page instead of being reported as a per-child finding — the opposite of - * the posture the entry schemas above take. The reason the trade goes the other - * way here is ownership: the skills entry types are consumer-owned and nothing - * else validates them, so this module has to be the reporter; `Resource` is - * base-protocol and already validated exactly this strictly on the - * `resources/list` path, where {@link listSalvage} is the answer to a single bad - * entry. Being *more* permissive here would mean a URI that fails as a listed - * resource succeeds as a directory child, which is a worse inconsistency than - * an all-or-nothing page. + * The SDK codec checks and lifts `resultType` on a modern result (#2373) but, + * for a method it does not define, neither caching attribute — so this schema + * is the only thing that does. + * + * ⚠️ Picked by `InspectorClient.readResourceDirectory` from the negotiated + * era — a schema cannot know it on its own. */ -export const DirectoryChildSchema = ResourceSchema; +export const ModernDirectoryReadResultSchema = + ReadResourceDirectoryResultSchema; /** - * `resources/directory/read` result on a **legacy** connection: the directory's - * direct children plus the opaque cursor, and nothing required beyond that. - * - * Its modern counterpart is {@link ModernDirectoryReadResultSchema}. The - * caching attributes are 2026-era, so the legacy shape stays permissive, - * exactly as {@link ListSkillsResultSchema} and {@link GetSkillEnvelopeSchema} - * do: `looseObject`, so a legacy server that also sends them is not failed for - * doing so. + * `resources/directory/read` result on a **legacy** connection: the SDK's + * schema with the caching attributes made optional, which is exactly how + * `SkillsClient` decodes a 2025-11-25 result (the backport has no caching + * fields). Still `looseObject`, so a legacy server that also sends them is not + * failed for doing so — the same posture {@link ListSkillsResultSchema} and + * {@link GetSkillEnvelopeSchema} take. */ -export const DirectoryReadResultSchema = z.looseObject({ - resources: z.array(DirectoryChildSchema), - nextCursor: z.string().optional(), -}); +export const DirectoryReadResultSchema = + ReadResourceDirectoryResultSchema.partial({ ttlMs: true, cacheScope: true }); /** * The result as the SDK decoded it, on either era. Typed from the legacy @@ -266,34 +291,3 @@ export const DirectoryReadResultSchema = z.looseObject({ * {@link GetSkillEnvelope} is typed from the legacy envelope. */ export type DirectoryReadResult = z.infer; - -/** - * `resources/directory/read` result on a **modern** (2026-07-28+) connection: - * the page plus `CacheableResult`'s required `ttlMs` / `cacheScope` (#2740). - * - * SEP-2640 as accepted said nothing about caching attributes on this method, - * and its one worked example carried `resultType` alone — so an earlier - * revision of this module deliberately had no modern variant here, on the - * reading that requiring more than the SEP's own example would fail a - * conforming server. The stable ext-skills specification settles it the way - * it settled `skills/get` (#2404): *"`ReadResourceDirectoryResult` extends - * `CacheableResult`, so `ttlMs` and `cacheScope` are **REQUIRED**, as they - * are on `resources/list`, and carry the same semantics. They describe the - * freshness of the directory listing, not of the held entry"* (ext-skills#157, - * recorded in its `docs/decisions.md`), and its directory-read examples now - * carry `"ttlMs": 300000, "cacheScope": "public"`. - * - * Same field shapes as {@link ModernListSkillsResultSchema}, and the same - * reason it exists: this is a consumer-owned method, so the SDK codec checks - * and lifts `resultType` (#2373) but checks neither caching attribute, and - * without this a modern server omitting them would be reported as clean. - * - * ⚠️ Picked by `InspectorClient.readResourceDirectory` from the negotiated - * era — a schema cannot know it on its own. - */ -export const ModernDirectoryReadResultSchema = DirectoryReadResultSchema.extend( - { - ttlMs: z.int().min(0), - cacheScope: z.enum(["public", "private"]), - }, -); diff --git a/core/mcp/sha256.ts b/core/extension/skills/sha256.ts similarity index 100% rename from core/mcp/sha256.ts rename to core/extension/skills/sha256.ts diff --git a/core/mcp/skillFile.ts b/core/extension/skills/skillFile.ts similarity index 100% rename from core/mcp/skillFile.ts rename to core/extension/skills/skillFile.ts diff --git a/core/mcp/skillsVerification.ts b/core/extension/skills/verification.ts similarity index 95% rename from core/mcp/skillsVerification.ts rename to core/extension/skills/verification.ts index dc570a9713..3d0887b973 100644 --- a/core/mcp/skillsVerification.ts +++ b/core/extension/skills/verification.ts @@ -2,7 +2,7 @@ * Fetch-and-verify: run every SEP-2640 check that needs the *bytes* of a skill's * files, over a whole set of entries (#2248). * - * Separate from `skills.ts`, which is deliberately I/O-free — + * Separate from `conformance.ts`, which is deliberately I/O-free — * `checkSkillConformance` reads a manifest, `verifySkillResource` compares bytes * it is handed, `checkSkillFrontmatterMatch` compares text it is handed, and * none of them knows how to obtain a file. This module is the part that does, @@ -24,9 +24,11 @@ * CI diff of two reports shows what changed rather than what raced. */ -import { AuthRecoveryRequiredError } from "../auth/challenge.js"; -import type { InspectorClientProtocol } from "./inspectorClientProtocol.js"; -import type { RequestMetadata } from "./types.js"; +import { AuthRecoveryRequiredError } from "../../auth/challenge.js"; +import type { + RequestMetadata, + ResourceReadInvocation, +} from "../../mcp/types.js"; import { bytesToText, checkSkillConformance, @@ -41,12 +43,15 @@ import { verifySkillResource, type SkillIssue, type SkillVerification, -} from "./skills.js"; +} from "./conformance.js"; + +/** The per-server settings {@link resolveSkillCatalogBudget} reads. */ +type SkillCatalogSettings = Parameters[0]; import { DYNAMIC_RESOURCES, type SkillEntry, type SkillResource, -} from "./skillsSchemas.js"; +} from "./schemas.js"; /** * What a client tells its user to do about a skill the run's catalog budget @@ -81,6 +86,30 @@ export const DEFAULT_SKILL_BUDGET_HINT: SkillBudgetHint = { verifyOne: "by verifying this skill alone", }; +/** + * What the walk needs from a connection: `resources/read`, and the server's + * catalog-budget settings. + * + * Declared here rather than taking `InspectorClientProtocol`, so this module + * imports nothing from `InspectorClient` — the rule every + * `core/extension//` module keeps. `InspectorClient` satisfies it as is. + * + * ⚠️ **Not the SDK's `SkillsClient.read()`.** That method verifies a file and + * throws a `SkillVerificationError` on the first check that fails, and it + * refuses an entry that is not valid before any request. This walk reads every + * file of every entry it is given, valid or not, and records each check's + * outcome — including `unverifiable` for a missing or malformed digest and the + * read-budget cut-off — which is a report a thrown error cannot carry (#2804). + */ +export interface SkillResourceReader { + readResource( + uri: string, + metadata?: RequestMetadata, + ): Promise; + /** Optional, so a test double need not implement it — see `verifySkills`. */ + getServerSettings?(): SkillCatalogSettings | undefined; +} + /** One manifest entry's outcome. `read-error` means the fetch itself failed. */ export type SkillFileStatus = SkillVerification["status"] | "read-error"; @@ -346,7 +375,7 @@ function responseBytes(result: unknown): number { * could not be read, with no way offered to fix it. */ export async function verifySkills( - client: InspectorClientProtocol, + client: SkillResourceReader, entries: readonly SkillEntry[], metadata?: RequestMetadata, budgetHint: SkillBudgetHint = DEFAULT_SKILL_BUDGET_HINT, diff --git a/core/mcp/__tests__/fakeInspectorClient.ts b/core/mcp/__tests__/fakeInspectorClient.ts index 2c230df3be..83706a5518 100644 --- a/core/mcp/__tests__/fakeInspectorClient.ts +++ b/core/mcp/__tests__/fakeInspectorClient.ts @@ -42,8 +42,11 @@ import type { TaskCapabilities } from "@modelcontextprotocol/ext-tasks/client"; import { INACTIVE_SUBSCRIPTION_STREAM_STATE } from "../types.js"; import type { MalformedListItem } from "../listSalvage.js"; import type { ConnectionDiagnostics } from "../connectionDiagnostics.js"; -import type { SkillEntry, SkillResource } from "../skillsSchemas.js"; -import type { SkillsExtensionSupport } from "../skills.js"; +import type { + SkillEntry, + SkillResource, +} from "../../extension/skills/schemas.js"; +import type { SkillsExtensionSupport } from "../../extension/skills/conformance.js"; import type { JsonValue } from "../../json/jsonUtils.js"; type ListResult = { diff --git a/core/mcp/extensions.ts b/core/mcp/extensions.ts index 538c4655d6..ef7ca7abe8 100644 --- a/core/mcp/extensions.ts +++ b/core/mcp/extensions.ts @@ -1,7 +1,7 @@ import type { ClientCapabilities } from "@modelcontextprotocol/client"; import { RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/app-bridge"; import { TASKS_EXTENSION_KEY } from "../extension/tasks/constants.js"; -import { SKILLS_EXTENSION_KEY } from "./skillsSchemas.js"; +import { SKILLS_EXTENSION_KEY } from "../extension/skills/schemas.js"; /** * Extension identifier for SEP-2350 enterprise-managed authorization. Advertised diff --git a/core/mcp/inspectorClient.ts b/core/mcp/inspectorClient.ts index c0fe604f33..4a60dfd323 100644 --- a/core/mcp/inspectorClient.ts +++ b/core/mcp/inspectorClient.ts @@ -194,8 +194,11 @@ import { SKILLS_GET_METHOD, SKILLS_LIST_METHOD, type SkillEntry, -} from "./skillsSchemas.js"; -import { getSkillsExtension, type SkillsExtensionSupport } from "./skills.js"; +} from "../extension/skills/schemas.js"; +import { + getSkillsExtension, + type SkillsExtensionSupport, +} from "../extension/skills/conformance.js"; import { getElicitationUiResourceUri, isFormElicitation, diff --git a/core/mcp/inspectorClientProtocol.ts b/core/mcp/inspectorClientProtocol.ts index 2aeb860631..c290be0059 100644 --- a/core/mcp/inspectorClientProtocol.ts +++ b/core/mcp/inspectorClientProtocol.ts @@ -42,9 +42,9 @@ import type { JsonValue } from "../json/jsonUtils.js"; import type { MalformedListItem } from "./listSalvage.js"; import type { ConnectionDiagnostics } from "./connectionDiagnostics.js"; import type { InspectorClientEventTarget } from "./inspectorClientEventTarget.js"; -import type { SkillEntry } from "./skillsSchemas.js"; -import type { SkillsExtensionSupport } from "./skills.js"; -import type { DirectoryReadResult } from "./skillsSchemas.js"; +import type { SkillEntry } from "../extension/skills/schemas.js"; +import type { SkillsExtensionSupport } from "../extension/skills/conformance.js"; +import type { DirectoryReadResult } from "../extension/skills/schemas.js"; import type { TaskCapabilities } from "@modelcontextprotocol/ext-tasks/client"; import type { SamplingCreateMessage } from "./samplingCreateMessage.js"; import type { ElicitationCreateMessage } from "./elicitationCreateMessage.js"; diff --git a/core/mcp/node/config.ts b/core/mcp/node/config.ts index 00b7b45572..e4ba610198 100644 --- a/core/mcp/node/config.ts +++ b/core/mcp/node/config.ts @@ -9,7 +9,7 @@ import type { StreamableHttpServerConfig, } from "../types.js"; import { isProtocolEra, normalizeServerType } from "../serverList.js"; -import { isSkillCatalogLimit } from "../skills.js"; +import { isSkillCatalogLimit } from "../../extension/skills/conformance.js"; import type { ServerProtocolEra } from "../types.js"; import { toRecord } from "../../json/jsonUtils.js"; import { getOwnEntry } from "../../storage/own-entry.js"; diff --git a/core/mcp/remote/node/server.ts b/core/mcp/remote/node/server.ts index db64eb74c9..37176aa403 100644 --- a/core/mcp/remote/node/server.ts +++ b/core/mcp/remote/node/server.ts @@ -81,7 +81,7 @@ import { stripInspectorFields, } from "../../serverList.js"; import { toRecord } from "../../../json/jsonUtils.js"; -import { isSkillCatalogLimit } from "../../skills.js"; +import { isSkillCatalogLimit } from "../../../extension/skills/conformance.js"; import { resolveImportSource } from "../../import/resolveSource.js"; import { RemoteSession } from "./remote-session.js"; import { createRemoteAuthProvider } from "./tokenAuthProvider.js"; diff --git a/core/mcp/serverList.ts b/core/mcp/serverList.ts index 1849c2ff4e..9707bab885 100644 --- a/core/mcp/serverList.ts +++ b/core/mcp/serverList.ts @@ -18,7 +18,7 @@ import { isSkillCatalogLimit, SKILL_MAX_CATALOG_BYTES, SKILL_MAX_CATALOG_SKILLS, -} from "./skills.js"; +} from "../extension/skills/conformance.js"; import type { Root } from "@modelcontextprotocol/client"; import type { ElicitCapabilityMode, diff --git a/core/mcp/state/managedSkillsState.ts b/core/mcp/state/managedSkillsState.ts index c0d75bf8a2..07767c23ad 100644 --- a/core/mcp/state/managedSkillsState.ts +++ b/core/mcp/state/managedSkillsState.ts @@ -46,7 +46,10 @@ */ import type { InspectorClientProtocol } from "../inspectorClientProtocol.js"; -import { SKILLS_LIST_METHOD, type SkillEntry } from "../skillsSchemas.js"; +import { + SKILLS_LIST_METHOD, + type SkillEntry, +} from "../../extension/skills/schemas.js"; import { LIST_MAX_PAGES, isClientDecodeRejection } from "../listSalvage.js"; import { isTerminalStatus } from "../types.js"; import type { RequestMetadata } from "../types.js"; diff --git a/core/react/useManagedSkills.ts b/core/react/useManagedSkills.ts index e02e121940..27381a8023 100644 --- a/core/react/useManagedSkills.ts +++ b/core/react/useManagedSkills.ts @@ -4,7 +4,7 @@ import type { ManagedSkillsState, SkillsPaginationState, } from "../mcp/state/managedSkillsState.js"; -import type { SkillEntry } from "../mcp/skillsSchemas.js"; +import type { SkillEntry } from "../extension/skills/schemas.js"; import { useListError } from "./useListError.js"; import { useStoreSnapshot } from "./useStoreSnapshot.js"; diff --git a/docs/inspector-roadmap-2026-h2.md b/docs/inspector-roadmap-2026-h2.md index 3e4998c541..66effcb557 100644 --- a/docs/inspector-roadmap-2026-h2.md +++ b/docs/inspector-roadmap-2026-h2.md @@ -328,7 +328,7 @@ official status through the Extensions Track of | -------------------------------- | ---------------------------------------------------------- | --- | --- | --- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | MCP Apps | `io.modelcontextprotocol/ui` | ✅ | — | — | Inspector row, cell blank | Apps tab. Columns are rendering support: rendering needs a browser, so neither CLI nor TUI renders Apps (the CLI does offer an `--app-info` metadata probe). The shared client still advertises the extension from CLI and TUI, which is a compatibility bug tracked in [#2403](https://github.com/modelcontextprotocol/inspector/issues/2403). | | Tasks | `io.modelcontextprotocol/tasks` | 🟡 | 🟡 | 🟡 | No column in the matrix | Raw-wire channel; stays for the horizon (§3.1). All three clients are currently partial, for two separate reasons. (1) Over Streamable HTTP, the shared `InspectorClient` sends modern `tasks/*` requests without the `Mcp-Name` header SEP-2663 requires, so strict servers reject them; stdio is unaffected. It is fixed once the upstream SDK change tracked by #1917 is released. (2) CLI and TUI have no user-facing task surface: `mcp-inspector --cli` rejects `tasks/*` (they are not in `ONE_SHOT_METHODS`), and the TUI has no Tasks pane. #1917 does not change that. | -| Skills over MCP | `io.modelcontextprotocol/skills` | ✅ | ✅ | ✅ | "Partial" (CLI README) | [#2234](https://github.com/modelcontextprotocol/inspector/issues/2234), [#2248](https://github.com/modelcontextprotocol/inspector/issues/2248). Checks mean the inspection surface is complete (list, get, digest and frontmatter verification), including the cache fields the stable spec requires on modern `skills/list` and `skills/get` results ([#2404](https://github.com/modelcontextprotocol/inspector/issues/2404)). Host behaviors (activation, per-skill consent, content-bound approval) are out of scope by design, since the Inspector is not a host (`core/mcp/skills.ts`); that is also why the upstream matrix says "Partial". | +| Skills over MCP | `io.modelcontextprotocol/skills` | ✅ | ✅ | ✅ | "Partial" (CLI README) | [#2234](https://github.com/modelcontextprotocol/inspector/issues/2234), [#2248](https://github.com/modelcontextprotocol/inspector/issues/2248). Checks mean the inspection surface is complete (list, get, digest and frontmatter verification), including the cache fields the stable spec requires on modern `skills/list` and `skills/get` results ([#2404](https://github.com/modelcontextprotocol/inspector/issues/2404)). Host behaviors (activation, per-skill consent, content-bound approval) are out of scope by design, since the Inspector is not a host (`core/extension/skills/conformance.ts`); that is also why the upstream matrix says "Partial". | | Enterprise-Managed Authorization | `io.modelcontextprotocol/enterprise-managed-authorization` | ✅ | 🟡 | 🟡 | Inspector row, cell blank | [#1509](https://github.com/modelcontextprotocol/inspector/issues/1509). CLI and TUI have no in-client Client Settings surface; they consume the `client.json` / `mcp.json` and keychain state the web settings flows write (or hand-edited files), and terminal EMA follow-ups remain (`specification/v2_auth_ema.md`). | | OAuth Client Credentials | `io.modelcontextprotocol/oauth-client-credentials` | ❌ | ❌ | ❌ | Inspector row, cell blank | **Gap** (§3.3). [#1225](https://github.com/modelcontextprotocol/inspector/issues/1225) was closed only because v1 is frozen. |