fix: declare the skills extension in the Inspector's client capabilities (#2373)
SEP-2133 negotiates an extension from both sides, and a strict server refuses skills/* to a client that did not declare io.modelcontextprotocol/skills. The Inspector called those methods without ever declaring it. Add it to ADVERTISABLE_EXTENSIONS, on by default, which also gives it a Server Settings toggle. Give the composable test server skillsRequireClientExtension and two per-era strict showcase configs, so a strict server is reproducible here. Writing the modern-era test exposed two defects the existing suite hid: its "modern" cases set protocolEra on the transport config, which InspectorClient does not read, so they connected on legacy; and once they really ran modern, every skills result failed, because the modern schemas required resultType, which the SDK codec checks and then lifts off before a caller schema runs. Connect with versionNegotiation and assert the era, and drop the resultType requirement (and the two modern schema variants it was the only difference in). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3RsrgCxVoFPokLB4vAkxi Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
fd6e60148b
commit
1d765b8af6
3 changed files with 396 additions and 150 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import type { ClientCapabilities } from "@modelcontextprotocol/client";
|
||||
import { RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/app-bridge";
|
||||
import { TASKS_EXTENSION_KEY } from "./modernTaskSchemas.js";
|
||||
import { SKILLS_EXTENSION_KEY } from "./skillsSchemas.js";
|
||||
|
||||
/**
|
||||
* Extension identifier for SEP-2350 enterprise-managed authorization. Advertised
|
||||
|
|
@ -91,6 +92,18 @@ export const ADVERTISABLE_EXTENSIONS: readonly AdvertisableExtension[] = [
|
|||
defaultAdvertised: true,
|
||||
advertisement: { mimeTypes: [MCP_APP_MIME_TYPE] },
|
||||
},
|
||||
{
|
||||
key: SKILLS_EXTENSION_KEY,
|
||||
label: "Skills (io.modelcontextprotocol/skills)",
|
||||
// The Skills extension (SEP-2640). SEP-2133 negotiates an extension from
|
||||
// both sides, so a server may refuse `skills/list`, `skills/get` and
|
||||
// `resources/directory/read` to a client that did not declare it — and the
|
||||
// Inspector calls all three once the server declares its half. Advertised
|
||||
// by default for that reason; turning it off is how to check a server's
|
||||
// refusal path. Declared with no settings: SEP-2640 defines none for the
|
||||
// client side. (#2373)
|
||||
defaultAdvertised: true,
|
||||
},
|
||||
];
|
||||
|
||||
export interface BuildClientExtensionsInput {
|
||||
|
|
|
|||
|
|
@ -150,8 +150,6 @@ import {
|
|||
DirectoryReadResultSchema,
|
||||
GetSkillEnvelopeSchema,
|
||||
ListSkillsResultSchema,
|
||||
ModernGetSkillEnvelopeSchema,
|
||||
ModernDirectoryReadResultSchema,
|
||||
ModernListSkillsResultSchema,
|
||||
RESOURCES_DIRECTORY_READ_METHOD,
|
||||
SKILLS_EXTENSION_KEY,
|
||||
|
|
@ -5796,21 +5794,16 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
uri,
|
||||
...(effectiveMeta ? { _meta: effectiveMeta } : {}),
|
||||
};
|
||||
// Era-aware for the same reason `skills/list` is: the method is
|
||||
// consumer-owned, so no SDK codec stamps or checks its envelope. The modern
|
||||
// variant requires `resultType` — a base-protocol member SEP-2322 puts on
|
||||
// every modern result — and still not the caching attributes, which
|
||||
// SEP-2640 leaves open. The envelope is returned whole; `getSkill`
|
||||
// unwraps.
|
||||
const resultSchema = this.isModernEra()
|
||||
? ModernGetSkillEnvelopeSchema
|
||||
: GetSkillEnvelopeSchema;
|
||||
// One schema for both eras (#2373): on a modern connection the SDK codec
|
||||
// has already enforced `resultType` and lifted it off, and the caching
|
||||
// attributes are left open by SEP-2640, so there is nothing era-specific
|
||||
// left to require. The envelope is returned whole; `getSkill` unwraps.
|
||||
try {
|
||||
return await this.invokeMcpClient(
|
||||
() =>
|
||||
this.client!.request(
|
||||
{ method: SKILLS_GET_METHOD, params },
|
||||
resultSchema,
|
||||
GetSkillEnvelopeSchema,
|
||||
this.getRequestOptions(this.progressTokenOf(metadata)),
|
||||
),
|
||||
{ method: SKILLS_GET_METHOD },
|
||||
|
|
@ -5872,19 +5865,16 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
// for page one.
|
||||
...(cursor !== undefined ? { cursor } : {}),
|
||||
};
|
||||
// Era-aware for the same reason `skills/list` is — the method is
|
||||
// consumer-owned, so no SDK codec stamps or checks its envelope. The modern
|
||||
// variant requires only `resultType`; see the schema for why it stops
|
||||
// short of the caching attributes that `skills/list` requires.
|
||||
const resultSchema = this.isModernEra()
|
||||
? ModernDirectoryReadResultSchema
|
||||
: DirectoryReadResultSchema;
|
||||
// One schema for both eras (#2373): the SDK codec enforces and lifts
|
||||
// `resultType` on a modern connection, and SEP-2640 requires no caching
|
||||
// attributes of this method — see `skillsSchemas.ts` for why that differs
|
||||
// from `skills/list`.
|
||||
try {
|
||||
return await this.invokeMcpClient(
|
||||
() =>
|
||||
this.client!.request(
|
||||
{ method: RESOURCES_DIRECTORY_READ_METHOD, params },
|
||||
resultSchema,
|
||||
DirectoryReadResultSchema,
|
||||
this.getRequestOptions(this.progressTokenOf(metadata)),
|
||||
),
|
||||
{ method: RESOURCES_DIRECTORY_READ_METHOD },
|
||||
|
|
|
|||
|
|
@ -131,9 +131,18 @@ export type ListSkillsResult = z.infer<typeof ListSkillsResultSchema>;
|
|||
*
|
||||
* ⚠️ Picked by `InspectorClient.listSkills` from the negotiated era — a schema
|
||||
* cannot know it on its own.
|
||||
*
|
||||
* ⚠️ **No `resultType` member, and that is not an omission (#2373).** The
|
||||
* SDK's 2026-07-28 codec checks `resultType` on every result — missing is
|
||||
* `InvalidResult`, anything but `"complete"` is `UnsupportedResultType` — and
|
||||
* then **deletes it** before the result reaches the caller's schema. Requiring
|
||||
* it here therefore rejected every conforming modern page, and did so unseen
|
||||
* only because the integration suite's "modern" cases were connecting on
|
||||
* legacy. The caching attributes are different: the codec neither checks nor
|
||||
* lifts them for a consumer-owned method, so this schema is still the only
|
||||
* thing that does.
|
||||
*/
|
||||
export const ModernListSkillsResultSchema = ListSkillsResultSchema.extend({
|
||||
resultType: z.literal("complete"),
|
||||
// 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.
|
||||
|
|
@ -183,21 +192,6 @@ export const GetSkillEnvelopeSchema = z.looseObject({
|
|||
*/
|
||||
export type GetSkillEnvelope = z.infer<typeof GetSkillEnvelopeSchema>;
|
||||
|
||||
/**
|
||||
* `skills/get` on a **modern** (2026-07-28+) connection: the envelope plus
|
||||
* `resultType`, and deliberately still not the caching attributes.
|
||||
*
|
||||
* The "left open" quote above covers `ttlMs` / `cacheScope` and **only** those.
|
||||
* `resultType` is a different thing: SEP-2322 makes it a member of every modern
|
||||
* result, and SEP-2640's own `skills/get` example carries
|
||||
* `"resultType": "complete"`. Leaving it optional here while requiring it of
|
||||
* `resources/directory/read` was an inconsistency in this module rather than a
|
||||
* distinction the spec draws (Copilot).
|
||||
*/
|
||||
export const ModernGetSkillEnvelopeSchema = GetSkillEnvelopeSchema.extend({
|
||||
resultType: z.literal("complete"),
|
||||
});
|
||||
|
||||
/**
|
||||
* `skills/get` result, unwrapped to the entry it carries.
|
||||
*
|
||||
|
|
@ -252,29 +246,28 @@ export const DirectoryReadResultSchema = z.looseObject({
|
|||
|
||||
export type DirectoryReadResult = z.infer<typeof DirectoryReadResultSchema>;
|
||||
|
||||
/**
|
||||
* `resources/directory/read` result on a **modern** (2026-07-28+) connection:
|
||||
* the page plus `resultType`, and deliberately **not** `ttlMs` / `cacheScope`.
|
||||
/*
|
||||
* There is deliberately **no modern variant** of the `resources/directory/read`
|
||||
* or `skills/get` schemas, though `skills/list` has one (#2373).
|
||||
*
|
||||
* That asymmetry with {@link ModernListSkillsResultSchema} is the one judgement
|
||||
* call in this module, so it is written down rather than left to be re-derived:
|
||||
* On a modern (2026-07-28+) connection the SDK codec already enforces
|
||||
* `resultType` — the base-protocol member SEP-2322 puts on every modern result
|
||||
* — and lifts it off before any caller schema runs. What remains is the
|
||||
* caching attributes, and only `skills/list` is required to carry those:
|
||||
*
|
||||
* - For `skills/list` the SEP states the requirement outright — *"In protocol
|
||||
* versions 2026-07-28 and later, the result also carries … `ttlMs` and
|
||||
* `cacheScope`"* — so requiring them is quoting the spec.
|
||||
* - For `skills/list` the SEP states it outright — *"In protocol versions
|
||||
* 2026-07-28 and later, the result also carries … `ttlMs` and
|
||||
* `cacheScope`"* — so {@link ModernListSkillsResultSchema} requiring them is
|
||||
* quoting the spec.
|
||||
* - For `resources/directory/read` it states **nothing of the kind**, and its
|
||||
* one worked example of the result carries `resultType: "complete"` and no
|
||||
* caching attributes at all. Requiring them here would fail a server that
|
||||
* matched the SEP's own example, which is the failure direction this module
|
||||
* works hardest to avoid.
|
||||
* one worked example carries `resultType: "complete"` and no caching
|
||||
* attributes at all. Requiring them would fail a server that matched the
|
||||
* SEP's own example, the failure direction this module works hardest to
|
||||
* avoid.
|
||||
* - For `skills/get` the SEP leaves the question open in as many words (see
|
||||
* {@link GetSkillEnvelopeSchema}).
|
||||
*
|
||||
* `resultType` is required because it is the base protocol's, not this
|
||||
* extension's: SEP-2322 makes it a member of every modern result, the SEP's
|
||||
* example carries it, and `skills/*` being consumer-owned means the SDK codec
|
||||
* validates none of it — so if this schema does not, nothing does.
|
||||
*
|
||||
* ⚠️ Picked by `InspectorClient.readResourceDirectory` from the negotiated era.
|
||||
* So both eras parse those two results with one schema each. An earlier
|
||||
* revision required `resultType` on "modern" variants of both; since the codec
|
||||
* lifts it, they could never pass on a real modern connection.
|
||||
*/
|
||||
export const ModernDirectoryReadResultSchema = DirectoryReadResultSchema.extend(
|
||||
{ resultType: z.literal("complete") },
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue