Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a64d7ea
chore: add SEP-2640 requirement-traceability YAML (Skills Extension)
panyam Jun 4, 2026
2ecad13
style: apply yaml formatter pass
panyam Jun 4, 2026
4fa8b10
chore(sep-2640): record spec source provenance
panyam Jun 4, 2026
fc60fee
Merge branch 'modelcontextprotocol:main' into chore/sep-2640-yaml
panyam Jun 5, 2026
1e50cad
chore(sep-2640): re-extract against SEP HEAD 556154c (drops mcp-resou…
panyam Jun 5, 2026
9a16e54
Merge branch 'main' into chore/sep-2640-yaml
panyam Jun 16, 2026
f1b0695
feat(sep-2640): ResourcesDirectoryReadScenario for resources/director…
panyam Jun 16, 2026
bf08157
Merge pull request #18 from panyam/feat/sep-2640-directory-read
panyam Jun 16, 2026
1711d76
Merge remote-tracking branch 'upstream/main' into chore/sep-2640-yaml
panyam Jul 1, 2026
b8ca9a9
Merge branch 'main' into chore/sep-2640-yaml
panyam Aug 4, 2026
2117d61
feat(sep-2640): expand skills conformance — index + manifest scenario…
panyam Aug 4, 2026
0d077f2
Merge branch 'modelcontextprotocol:main' into chore/sep-2640-yaml
panyam Aug 28, 2026
0707a3e
feat(sep-2640): re-extract against the 2026-08-21 rewrite
panyam Aug 29, 2026
3d75cb0
feat(sep-2640): close the extraction gaps found by a normative-senten…
panyam Aug 29, 2026
22f4ecb
style: apply prettier to the SEP-2640 scenarios and yaml
panyam Aug 29, 2026
e782847
docs(sep-2640): note why resultType is not declared in this yaml
panyam Aug 29, 2026
c119e75
Merge branch 'main' into chore/sep-2640-yaml
panyam Sep 4, 2026
1f21fba
fix(sep-2640): follow nextCursor on resources/directory/read
panyam Sep 4, 2026
fa8aaec
feat(sep-2640): client scenario for the no-prefetch MUST NOT
panyam Sep 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions src/scenarios/client/skills/no-prefetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/**
* SEP-2640 client scenario: hosts MUST NOT retrieve a skill's files ahead of
* need.
*
* This is a `Scenario` rather than a `ClientScenario`: the harness stands up
* the server and the client is the system under test. The rule reduces to
* "did a request arrive", which makes it one of the most wire-observable
* obligations in the SEP despite the first traceability pass filing it as an
* unobservable host obligation.
*
* Contract for the client under test, keyed on MCP_CONFORMANCE_SCENARIO:
* connect, call `skills/list`, then exit. Do not load a skill. A client that
* prefetches will read `SKILL.md` or a supporting file during that window and
* fail the check.
*/

import http from 'http';
import { ConformanceCheck } from '../../../types.js';
import { BaseHttpScenario } from '../http-base.js';

const SPEC_REFERENCE = {
id: 'SEP-2640-Lazy-Retrieval',
url: 'https://modelcontextprotocol.io/seps/2640-skills-extension#integrity-and-verification'
};

const SKILLS_EXTENSION_ID = 'io.modelcontextprotocol/skills';

/** One skill with a supporting file, so a prefetch has something to grab. */
const SKILL_URI = 'skill://pdf-processing/SKILL.md';
const SUPPORTING_URI = 'skill://pdf-processing/references/FORMS.md';

const SKILL_MD = `---
name: pdf-processing
description: Extract, fill, and assemble PDF documents
---

Body the client has no business fetching yet.
`;

const SUPPORTING =
'Supporting content the client has no business fetching yet.\n';

/** sha256 of the two bodies, computed at module load so the entry is honest. */
import { createHash } from 'crypto';
const digestOf = (s: string) =>
'sha256:' + createHash('sha256').update(s, 'utf8').digest('hex');

export class SkillsNoPrefetchScenario extends BaseHttpScenario {
name = 'sep-2640-client-no-prefetch';
description =
'A client MUST NOT retrieve a skill file before the skill is loaded';
readonly source = { extensionId: SKILLS_EXTENSION_ID } as const;

/** Every resources/read URI the client asked for, in order. */
private readsRequested: string[] = [];
private listCalled = false;

protected handlePost(
_req: http.IncomingMessage,
res: http.ServerResponse,
request: any
): void {
switch (request.method) {
case 'initialize':
this.sendInitialize(res, request, {
resources: { listChanged: false },
extensions: { [SKILLS_EXTENSION_ID]: {} }
});
return;

case 'skills/list':
this.listCalled = true;
this.sendJson(res, {
jsonrpc: '2.0',
id: request.id,
result: {
resultType: 'complete',
skills: [
{
uri: SKILL_URI,
frontmatter: {
name: 'pdf-processing',
description: 'Extract, fill, and assemble PDF documents'
},
resources: [
{
uri: SKILL_URI,
digest: digestOf(SKILL_MD),
size: Buffer.byteLength(SKILL_MD)
},
{
uri: SUPPORTING_URI,
digest: digestOf(SUPPORTING),
size: Buffer.byteLength(SUPPORTING)
}
]
}
]
}
});
return;

// Served, but reaching it during this scenario is the failure.
case 'resources/read': {
const uri = request.params?.uri;
if (typeof uri === 'string') this.readsRequested.push(uri);
const body = uri === SUPPORTING_URI ? SUPPORTING : SKILL_MD;
this.sendJson(res, {
jsonrpc: '2.0',
id: request.id,
result: {
resultType: 'complete',
contents: [{ uri, mimeType: 'text/markdown', text: body }]
}
});
return;
}

default:
if (request.id === undefined) {
this.sendNotificationAck(res);
return;
}
this.sendGenericResult(res, request);
}
}

getChecks(): ConformanceCheck[] {
const DESC =
"Hosts MUST NOT retrieve a skill's files ahead of need, not on connection, not on listing, and not at approval.";

// Without a listing there is no window in which prefetching is even
// possible, so the run proves nothing rather than passing.
if (!this.listCalled) {
return [
{
id: 'sep-2640-host-no-prefetch',
name: 'SkillsClientNoPrefetch',
description: DESC,
status: 'SKIPPED',
timestamp: new Date().toISOString(),
errorMessage:
'the client never called skills/list, so no retrieval window was opened',
specReferences: [SPEC_REFERENCE]
}
];
}

const prefetched = this.readsRequested.filter(
(u) => u === SKILL_URI || u === SUPPORTING_URI
);

return [
{
id: 'sep-2640-host-no-prefetch',
name: 'SkillsClientNoPrefetch',
description: DESC,
status: prefetched.length === 0 ? 'SUCCESS' : 'FAILURE',
timestamp: new Date().toISOString(),
errorMessage:
prefetched.length === 0
? undefined
: `client read ${prefetched.length} skill file(s) without loading a skill: ${prefetched.join(', ')}`,
specReferences: [SPEC_REFERENCE],
details: {
skillsListCalled: true,
fileReads: prefetched.length
}
}
];
}
}
28 changes: 26 additions & 2 deletions src/scenarios/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
DRAFT_PROTOCOL_VERSION
} from '../types';
import { InitializeScenario } from './client/initialize';
import { SkillsNoPrefetchScenario } from './client/skills/no-prefetch';
import { ToolsCallScenario } from './client/tools_call';
import { ElicitationClientDefaultsScenario } from './client/elicitation-defaults';
import { SSERetryScenario } from './client/sse-retry';
Expand Down Expand Up @@ -58,6 +59,10 @@ import {
ResourcesNotFoundErrorScenario
} from './server/resources';

import { SkillsDirectoryReadScenario } from './server/skills/directory';
import { SkillsEnumerationScenario } from './server/skills/enumeration';
import { SkillsManifestScenario } from './server/skills/manifest';

import {
PromptsListScenario,
PromptsGetSimpleScenario,
Expand Down Expand Up @@ -151,7 +156,15 @@ const pendingClientScenariosList: ClientScenario[] = [
new TasksDispatchScenario(),
new TasksStatusNotificationsScenario(),
new TasksRequiredTaskErrorScenario(),
new TasksMrtrCompositionScenario()
new TasksMrtrCompositionScenario(),

// SEP-2640 Skills extension. Pending because the everything-server does not
// implement io.modelcontextprotocol/skills; targeted runs point at a
// SEP-2640-conformant fixture via
// `npm start -- server --scenario sep-2640-skills-* --url <fixture>`.
new SkillsDirectoryReadScenario(),
new SkillsEnumerationScenario(),
new SkillsManifestScenario()
];

// All client scenarios
Expand Down Expand Up @@ -203,6 +216,12 @@ const allClientScenariosList: ClientScenario[] = [
// Resources error handling (SEP-2164)
new ResourcesNotFoundErrorScenario(),

// Skills extension (SEP-2640). Fixture-dependent (needs a SEP-2640 server);
// each scenario SKIPs cleanly when the extension is not declared.
new SkillsDirectoryReadScenario(),
new SkillsEnumerationScenario(),
new SkillsManifestScenario(),

// Prompts scenarios
new PromptsListScenario(),
new PromptsGetSimpleScenario(),
Expand Down Expand Up @@ -317,7 +336,12 @@ const scenariosList: Scenario[] = [
new JsonSchemaRefDerefScenario(),

// JSON Schema 2020-12 client-side keyword preservation (SEP-1613, SEP-2106)
new JsonSchema2020_12PreservationScenario()
new JsonSchema2020_12PreservationScenario(),

// SEP-2640 skills, client side. The harness is the server and grades what
// the client requests, which is how the retrieval-policy MUSTs become
// observable at all.
new SkillsNoPrefetchScenario()
];

// Core scenarios (tier 1 requirements)
Expand Down
Loading
Loading