feat: support the Skills extension (SEP-2640) with digest verification
Adds detection, enumeration, and conformance checking for the Skills extension (`io.modelcontextprotocol/skills`) — phases 1 and 2 of #2234, which is everything its Acceptance list names. Skills is a *server*-declared extension, read off the connecting server's `capabilities.extensions`, so it is deliberately absent from `ADVERTISABLE_EXTENSIONS` — that registry is what the Inspector advertises and the user toggles, and an entry there would be a meaningless toggle. `skills/list` and `skills/get` are consumer-owned extension methods that neither era codec defines, so the SDK's era gate skips them and they go out as ordinary `client.request` calls with explicit result schemas. The raw-wire channel modern `tasks/*` needs is not used, and the Skills tab is not era-gated: a legacy-era server that declares the extension is serving it. The checks are the point, not the list view. `core/mcp/skills.ts` reports each obligation SEP-2640 states — the name/path invariant, the digest format, the 512-entry and 16 MiB limits, and `resources: "dynamic"`, which means integrity cannot be verified at all. Digest verification hashes the fetched bytes with WebCrypto and returns a mismatch with both digests attached rather than throwing, because showing a mismatch loudly is the whole value proposition. Files are fetched on demand: SEP-2640 is explicit that a `resources/read` of a `SKILL.md` is not a load and confers no standing, so none of the SEP's host machinery is implemented. The `skills-http` fixture serves four skills over two pages, three of them deliberately non-conforming — without those the verification code is untestable. Phase 3 (CLI, TUI, `resources/directory/read`, paged mode) is #2248. Closes #2234 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018kfrH3uRNUM4JAk1reKtyY Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
1c1e70820b
commit
63623b754b
2 changed files with 3684 additions and 4 deletions
|
|
@ -452,6 +452,64 @@ describe("ConnectionInfoContent", () => {
|
|||
expect(screen.getAllByText("—")).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("hides the Skills section when the server declares no skills extension (#2234)", () => {
|
||||
renderWithMantine(
|
||||
<ConnectionInfoContent
|
||||
initializeResult={fullResult}
|
||||
clientCapabilities={fullClientCaps}
|
||||
transport="streamable-http"
|
||||
protocolEra="legacy"
|
||||
/>,
|
||||
);
|
||||
expect(screen.queryByText("Skills Extension")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows the Skills extension and its directoryRead sub-flag (#2234)", () => {
|
||||
// The generic "Server Extensions" row lists the identifier; the sub-flag
|
||||
// that gates `resources/directory/read` is what this section adds, and it
|
||||
// is the fact a server author opens the modal to confirm.
|
||||
renderWithMantine(
|
||||
<ConnectionInfoContent
|
||||
initializeResult={{
|
||||
...fullResult,
|
||||
capabilities: {
|
||||
...fullResult.capabilities,
|
||||
extensions: {
|
||||
"io.modelcontextprotocol/skills": { directoryRead: true },
|
||||
},
|
||||
},
|
||||
}}
|
||||
clientCapabilities={fullClientCaps}
|
||||
transport="streamable-http"
|
||||
protocolEra="legacy"
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("Skills Extension")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("skills-directory-read")).toHaveTextContent(
|
||||
"Supported",
|
||||
);
|
||||
});
|
||||
|
||||
it("reports directory read as unsupported for a bare skills declaration (#2234)", () => {
|
||||
renderWithMantine(
|
||||
<ConnectionInfoContent
|
||||
initializeResult={{
|
||||
...fullResult,
|
||||
capabilities: {
|
||||
...fullResult.capabilities,
|
||||
extensions: { "io.modelcontextprotocol/skills": {} },
|
||||
},
|
||||
}}
|
||||
clientCapabilities={fullClientCaps}
|
||||
transport="streamable-http"
|
||||
protocolEra="legacy"
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId("skills-directory-read")).toHaveTextContent(
|
||||
"Not supported",
|
||||
);
|
||||
});
|
||||
|
||||
it("renders client registration kind when provided", () => {
|
||||
renderWithMantine(
|
||||
<ConnectionInfoContent
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import type {
|
|||
} from "@modelcontextprotocol/client";
|
||||
import type { ServerType } from "@inspector/core/mcp/types.js";
|
||||
import { TASKS_EXTENSION_KEY } from "@inspector/core/mcp/modernTaskSchemas.js";
|
||||
import { SKILLS_EXTENSION_KEY } from "@inspector/core/mcp/skillsSchemas.js";
|
||||
import { getSkillsExtension } from "@inspector/core/mcp/skills.js";
|
||||
import type { OAuthClientRegistrationKind } from "@inspector/core/auth/types.js";
|
||||
import {
|
||||
CapabilityItem,
|
||||
|
|
@ -259,6 +261,9 @@ export function ConnectionInfoContent({
|
|||
}: ConnectionInfoContentProps) {
|
||||
const { serverInfo, protocolVersion, capabilities, instructions } =
|
||||
initializeResult;
|
||||
// `undefined` when the server declared no Skills extension, which is what
|
||||
// hides the section below — an absent extension has no sub-flags to report.
|
||||
const skillsExtension = getSkillsExtension(capabilities);
|
||||
|
||||
// Only trust `serverInfo` when the server actually reported it; otherwise the
|
||||
// name is a catalog fallback. Both rows `?.trim()` before the `||` (not `??`)
|
||||
|
|
@ -362,6 +367,26 @@ export function ConnectionInfoContent({
|
|||
</Stack>
|
||||
</SimpleGrid>
|
||||
|
||||
{/* Skills (SEP-2640). The generic "Server Extensions" row above lists the
|
||||
identifier, but not the one sub-option the extension defines —
|
||||
`directoryRead`, which gates `resources/directory/read`. That flag is
|
||||
exactly what a server author opens this modal to confirm, so it gets a
|
||||
row of its own rather than being flattened into a key list (#2234). */}
|
||||
{skillsExtension && (
|
||||
<SimpleGrid cols={2}>
|
||||
<Stack gap="xs">
|
||||
<SectionHeading>Skills Extension</SectionHeading>
|
||||
<ValueText>{SKILLS_EXTENSION_KEY}</ValueText>
|
||||
</Stack>
|
||||
<Stack gap="xs">
|
||||
<SectionHeading>Directory Read</SectionHeading>
|
||||
<ValueText data-testid="skills-directory-read">
|
||||
{skillsExtension.directoryRead ? "Supported" : "Not supported"}
|
||||
</ValueText>
|
||||
</Stack>
|
||||
</SimpleGrid>
|
||||
)}
|
||||
|
||||
{instructions && (
|
||||
<Stack gap="xs">
|
||||
<SectionHeading>Server Instructions</SectionHeading>
|
||||
|
|
|
|||
Loading…
Reference in a new issue