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
1 changed files with 3684 additions and 4 deletions
|
|
@ -49,6 +49,7 @@ import {
|
|||
createModernTaskTools,
|
||||
wireModernTaskHandlers,
|
||||
} from "./modern-tasks.js";
|
||||
import { SKILLS_EXTENSION_KEY, wireSkillsHandlers } from "./skills.js";
|
||||
|
||||
/**
|
||||
* MCP Apps extension id. Hardcoded for the same reason the Inspector's
|
||||
|
|
@ -556,6 +557,12 @@ export interface ServerConfig {
|
|||
* `modern: true`. See `modern-tasks.ts`.
|
||||
*/
|
||||
tasksExtension?: boolean;
|
||||
/**
|
||||
* Advertise the Skills extension (SEP-2640) and serve `skills/list` /
|
||||
* `skills/get` plus the `skill://` files those entries name. The fixture set
|
||||
* deliberately includes non-conforming skills — see `skills.ts`.
|
||||
*/
|
||||
skills?: { directoryRead?: boolean };
|
||||
/**
|
||||
* Advertise the MCP Apps `io.modelcontextprotocol/ui` extension with the
|
||||
* nested `elicitation` setting — the server-side half of the app-rendered
|
||||
|
|
@ -824,6 +831,23 @@ export function createMcpServer(config: ServerConfig): McpServer {
|
|||
};
|
||||
}
|
||||
|
||||
// Skills extension (SEP-2640): a server-declared extension, advertised with
|
||||
// its one sub-option. `directoryRead` is opt-in per config so a client can be
|
||||
// exercised against both a server that offers `resources/directory/read` and
|
||||
// one that does not.
|
||||
if (config.skills) {
|
||||
capabilities.extensions = {
|
||||
...(capabilities.extensions ?? {}),
|
||||
[SKILLS_EXTENSION_KEY]: {
|
||||
...(config.skills.directoryRead ? { directoryRead: true } : {}),
|
||||
},
|
||||
};
|
||||
// Skill files are fetched through ordinary `resources/read`, so the
|
||||
// resources capability has to be advertised even when the config registers
|
||||
// no ordinary resources of its own.
|
||||
capabilities.resources = capabilities.resources ?? {};
|
||||
}
|
||||
|
||||
// MCP Apps app-rendered elicitation (#1854): the server-side half of the
|
||||
// negotiation, on the same extension the Apps work already uses.
|
||||
if (config.appElicitation) {
|
||||
|
|
@ -1503,6 +1527,13 @@ export function createMcpServer(config: ServerConfig): McpServer {
|
|||
wireModernTaskHandlers(mcpServer, modernTaskRuntime);
|
||||
}
|
||||
|
||||
// Skills extension (SEP-2640): raw skills/list + skills/get, and the
|
||||
// `skill://` half of resources/read. Wired after the SDK's own handlers so
|
||||
// the resources/read wrapper can delegate non-skill URIs to them.
|
||||
if (config.skills) {
|
||||
wireSkillsHandlers(mcpServer);
|
||||
}
|
||||
|
||||
// Extension-gated tools (#1739): start each gated tool disabled, then enable
|
||||
// it on `initialized` iff the connected client declared its extension.
|
||||
if (config.extensionGatedTools) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue