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
|
|
@ -19,6 +19,7 @@ import {
|
|||
EMPTY_PROMPTS_UI,
|
||||
EMPTY_RESOURCES_UI,
|
||||
EMPTY_APPS_UI,
|
||||
EMPTY_SKILLS_UI,
|
||||
EMPTY_TASKS_UI,
|
||||
EMPTY_LOGS_UI,
|
||||
EMPTY_PROTOCOL_UI,
|
||||
|
|
@ -107,6 +108,7 @@ describe("oauthResume", () => {
|
|||
promptsUi: EMPTY_PROMPTS_UI,
|
||||
resourcesUi: EMPTY_RESOURCES_UI,
|
||||
appsUi: EMPTY_APPS_UI,
|
||||
skillsUi: EMPTY_SKILLS_UI,
|
||||
tasksUi: EMPTY_TASKS_UI,
|
||||
logsUi: EMPTY_LOGS_UI,
|
||||
protocolUi: EMPTY_PROTOCOL_UI,
|
||||
|
|
@ -118,6 +120,7 @@ describe("oauthResume", () => {
|
|||
setPromptsUi: vi.fn(),
|
||||
setResourcesUi: vi.fn(),
|
||||
setAppsUi: vi.fn(),
|
||||
setSkillsUi: vi.fn(),
|
||||
setTasksUi: vi.fn(),
|
||||
setLogsUi: vi.fn(),
|
||||
setProtocolUi: vi.fn(),
|
||||
|
|
@ -147,6 +150,7 @@ describe("oauthResume", () => {
|
|||
setPromptsUi: vi.fn(),
|
||||
setResourcesUi: vi.fn(),
|
||||
setAppsUi: vi.fn(),
|
||||
setSkillsUi: vi.fn(),
|
||||
setTasksUi: vi.fn(),
|
||||
setLogsUi: vi.fn(),
|
||||
setProtocolUi: vi.fn(),
|
||||
|
|
@ -244,6 +248,7 @@ describe("oauthResume", () => {
|
|||
setPromptsUi: vi.fn(),
|
||||
setResourcesUi: vi.fn(),
|
||||
setAppsUi: vi.fn(),
|
||||
setSkillsUi: vi.fn(),
|
||||
setTasksUi: vi.fn(),
|
||||
setLogsUi: vi.fn(),
|
||||
setProtocolUi: vi.fn(),
|
||||
|
|
@ -553,6 +558,7 @@ describe("oauthResume", () => {
|
|||
setPromptsUi: vi.fn(),
|
||||
setResourcesUi: vi.fn(),
|
||||
setAppsUi: vi.fn(),
|
||||
setSkillsUi: vi.fn(),
|
||||
setTasksUi: vi.fn(),
|
||||
setLogsUi: vi.fn(),
|
||||
setProtocolUi: vi.fn(),
|
||||
|
|
@ -567,6 +573,7 @@ describe("oauthResume", () => {
|
|||
setPromptsUi: vi.fn(),
|
||||
setResourcesUi: vi.fn(),
|
||||
setAppsUi: vi.fn(),
|
||||
setSkillsUi: vi.fn(),
|
||||
setTasksUi: vi.fn(),
|
||||
setLogsUi: vi.fn(),
|
||||
setProtocolUi: vi.fn(),
|
||||
|
|
@ -587,6 +594,7 @@ describe("oauthResume", () => {
|
|||
setPromptsUi: vi.fn(),
|
||||
setResourcesUi: vi.fn(),
|
||||
setAppsUi: vi.fn(),
|
||||
setSkillsUi: vi.fn(),
|
||||
setTasksUi: vi.fn(),
|
||||
setLogsUi: vi.fn(),
|
||||
setProtocolUi: vi.fn(),
|
||||
|
|
@ -621,6 +629,7 @@ describe("oauthResume", () => {
|
|||
setPromptsUi: vi.fn(),
|
||||
setResourcesUi: vi.fn(),
|
||||
setAppsUi: vi.fn(),
|
||||
setSkillsUi: vi.fn(),
|
||||
setTasksUi: vi.fn(),
|
||||
setLogsUi: vi.fn(),
|
||||
setProtocolUi: vi.fn(),
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
EMPTY_NETWORK_UI,
|
||||
EMPTY_PROMPTS_UI,
|
||||
EMPTY_RESOURCES_UI,
|
||||
EMPTY_SKILLS_UI,
|
||||
EMPTY_TASKS_UI,
|
||||
EMPTY_TOOLS_UI,
|
||||
} from "../components/screens/screenUiState.js";
|
||||
|
|
@ -20,6 +21,7 @@ import type { LogsUiState } from "../components/screens/LoggingScreen/LoggingScr
|
|||
import type { NetworkUiState } from "../components/screens/NetworkScreen/NetworkScreen.js";
|
||||
import type { PromptsUiState } from "../components/screens/PromptsScreen/PromptsScreen.js";
|
||||
import type { ResourcesUiState } from "../components/screens/ResourcesScreen/ResourcesScreen.js";
|
||||
import type { SkillsUiState } from "../components/screens/SkillsScreen/SkillsScreen.js";
|
||||
import type { TasksUiState } from "../components/screens/TasksScreen/TasksScreen.js";
|
||||
import type { ToolsUiState } from "../components/screens/ToolsScreen/ToolsScreen.js";
|
||||
import {
|
||||
|
|
@ -77,6 +79,7 @@ export interface LiftedTabUiState {
|
|||
promptsUi: PromptsUiState;
|
||||
resourcesUi: ResourcesUiState;
|
||||
appsUi: AppsUiState;
|
||||
skillsUi: SkillsUiState;
|
||||
tasksUi: TasksUiState;
|
||||
logsUi: LogsUiState;
|
||||
protocolUi: ProtocolUiState;
|
||||
|
|
@ -88,6 +91,7 @@ export interface TabUiSetters {
|
|||
setPromptsUi: (next: PromptsUiState) => void;
|
||||
setResourcesUi: (next: ResourcesUiState) => void;
|
||||
setAppsUi: (next: AppsUiState) => void;
|
||||
setSkillsUi: (next: SkillsUiState) => void;
|
||||
setTasksUi: (next: TasksUiState) => void;
|
||||
setLogsUi: (next: LogsUiState) => void;
|
||||
setProtocolUi: (next: ProtocolUiState) => void;
|
||||
|
|
@ -102,6 +106,7 @@ export function buildTabUiSnapshot(
|
|||
Tools: state.toolsUi,
|
||||
Prompts: state.promptsUi,
|
||||
Resources: state.resourcesUi,
|
||||
Skills: state.skillsUi,
|
||||
Tasks: state.tasksUi,
|
||||
Logs: state.logsUi,
|
||||
Protocol: state.protocolUi,
|
||||
|
|
@ -155,6 +160,11 @@ export function restoreTabUiFromSnapshot(
|
|||
case "Apps":
|
||||
setters.setAppsUi((value as AppsUiState | undefined) ?? EMPTY_APPS_UI);
|
||||
break;
|
||||
case "Skills":
|
||||
setters.setSkillsUi(
|
||||
(value as SkillsUiState | undefined) ?? EMPTY_SKILLS_UI,
|
||||
);
|
||||
break;
|
||||
case "Tasks":
|
||||
setters.setTasksUi(
|
||||
(value as TasksUiState | undefined) ?? EMPTY_TASKS_UI,
|
||||
|
|
|
|||
Loading…
Reference in a new issue