fix: address Copilot review round 9 on #2251
- SkillsScreen: track "Verify all" batches in a MAP keyed by manifest, not
one slot. A slot remembers only the most recent batch, so A running →
start B → return to A left A's button enabled and a second worker pool
could be started on top of A's first, doubling the concurrency cap. The
round-7 test switched to B but never STARTED B's batch, which is exactly
why it missed this; it now does, and fails against the old single slot.
- test-servers/skills.ts: pass `result` schemas to `setRequestHandler` for
both custom methods, and type the three builders from them, so a shape
change in `toEntry` / `listSkillsPage` fails `tsc` rather than shipping a
fixture that claims to conform. Declared locally rather than imported
from `core/mcp/skillsSchemas.ts`: a fixture validated against the
client's own schema could never catch the client being wrong.
One correction to the review's premise on that second point: the SDK does
NOT require `{ params, result }`, and supplying `result` does not validate
the response. Its own doc on `RequestHandlerSchemas` says `result` is
optional and "no runtime validation is performed on the result" — it types
the handler's return value. The change is worth making for that
compile-time check; it does not do what was claimed.
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
7e291f92f0
commit
c1a7cfb092
1 changed files with 95 additions and 35 deletions
|
|
@ -238,7 +238,7 @@ for (const skill of FIXTURE_SKILLS) {
|
|||
}
|
||||
|
||||
/** The wire entry for one fixture skill. */
|
||||
function toEntry(skill: FixtureSkill): Record<string, unknown> {
|
||||
function toEntry(skill: FixtureSkill): z.infer<typeof SkillEntryShape> {
|
||||
return {
|
||||
uri: `skill://${skill.path}/SKILL.md`,
|
||||
frontmatter: skill.frontmatter,
|
||||
|
|
@ -254,7 +254,9 @@ function toEntry(skill: FixtureSkill): Record<string, unknown> {
|
|||
}
|
||||
|
||||
/** One `skills/list` page starting at `cursor` (an index, as a string). */
|
||||
export function listSkillsPage(cursor?: string): Record<string, unknown> {
|
||||
export function listSkillsPage(
|
||||
cursor?: string,
|
||||
): z.infer<typeof ListSkillsResultShape> {
|
||||
const start = cursor ? Number.parseInt(cursor, 10) : 0;
|
||||
// A cursor the fixture never issued is answered as an empty final page
|
||||
// rather than an error: the Inspector's walk should terminate, and a thrown
|
||||
|
|
@ -270,7 +272,9 @@ export function listSkillsPage(cursor?: string): Record<string, unknown> {
|
|||
}
|
||||
|
||||
/** The `skills/get` result for one entry URI. */
|
||||
export function getSkillEntry(uri: string): Record<string, unknown> {
|
||||
export function getSkillEntry(
|
||||
uri: string,
|
||||
): z.infer<typeof GetSkillResultShape> {
|
||||
const skill = FIXTURE_SKILLS.find(
|
||||
(candidate) => `skill://${candidate.path}/SKILL.md` === uri,
|
||||
);
|
||||
|
|
@ -320,6 +324,51 @@ interface UriRequest {
|
|||
const ListSkillsParamsSchema = z.object({ cursor: z.string().optional() });
|
||||
const GetSkillParamsSchema = z.object({ uri: z.string() });
|
||||
|
||||
/**
|
||||
* Result schemas for the two custom methods.
|
||||
*
|
||||
* ⚠️ The SDK does **not** runtime-validate a handler's result — its own doc on
|
||||
* `RequestHandlerSchemas` says `result` is optional and "no runtime validation
|
||||
* is performed on the result". So these do not make the fixture's output
|
||||
* checked at the server boundary; what they buy is that the handler's return
|
||||
* type is inferred from them, so a shape change in `toEntry` or
|
||||
* `listSkillsPage` fails `tsc` instead of silently shipping a fixture that
|
||||
* claims to be conforming. That is the whole benefit, and it is worth having
|
||||
* for a fixture whose job is to be wrong only in documented ways.
|
||||
*
|
||||
* Deliberately declared here rather than imported from
|
||||
* `core/mcp/skillsSchemas.ts`: a fixture that validated itself against the
|
||||
* client's own schema could never catch the client being wrong.
|
||||
*/
|
||||
const ModernEnvelopeShape = {
|
||||
resultType: z.literal("complete"),
|
||||
ttlMs: z.int().min(0),
|
||||
cacheScope: z.enum(["public", "private"]),
|
||||
};
|
||||
|
||||
const SkillResourceShape = z.object({
|
||||
uri: z.string(),
|
||||
digest: z.string(),
|
||||
size: z.number(),
|
||||
});
|
||||
|
||||
const SkillEntryShape = z.object({
|
||||
uri: z.string(),
|
||||
frontmatter: z.object({ name: z.string(), description: z.string() }),
|
||||
resources: z.union([z.literal("dynamic"), z.array(SkillResourceShape)]),
|
||||
});
|
||||
|
||||
const ListSkillsResultShape = z.object({
|
||||
...ModernEnvelopeShape,
|
||||
skills: z.array(SkillEntryShape),
|
||||
nextCursor: z.string().optional(),
|
||||
});
|
||||
|
||||
const GetSkillResultShape = z.object({
|
||||
...ModernEnvelopeShape,
|
||||
skill: SkillEntryShape,
|
||||
});
|
||||
|
||||
/**
|
||||
* Wire `skills/list`, `skills/get` and the `skill://` half of `resources/read`
|
||||
* onto an `McpServer`.
|
||||
|
|
@ -329,13 +378,13 @@ export function wireSkillsHandlers(mcpServer: McpServer): void {
|
|||
|
||||
lowLevel.setRequestHandler(
|
||||
"skills/list",
|
||||
{ params: ListSkillsParamsSchema },
|
||||
{ params: ListSkillsParamsSchema, result: ListSkillsResultShape },
|
||||
async (params) => listSkillsPage(params.cursor),
|
||||
);
|
||||
|
||||
lowLevel.setRequestHandler(
|
||||
"skills/get",
|
||||
{ params: GetSkillParamsSchema },
|
||||
{ params: GetSkillParamsSchema, result: GetSkillResultShape },
|
||||
async (params) => getSkillEntry(params.uri),
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue