Skip to content

Commit 762fb50

Browse files
olaservoclaude
andcommitted
Add skill://index.json discovery and remove SEP-2093 dependencies
Align the reference implementation with PR #69's SEP spec, which defines skill://index.json as the primary enumeration mechanism and requires zero protocol dependencies beyond resources/read. Added: - skill://index.json well-known discovery index (Agent Skills format) - generateSkillIndex() server-side, listSkillsFromIndex() client-side - SkillIndex/SkillIndexEntry types and INDEX_JSON_URI constant - 53 tests across URI utilities, index round-trip, and client parsing Removed (SEP-2093 features not in the SEP): - resources/metadata handler and fetchSkillMetadata() - Scoped resources/list(uri=...) and listSkillsScoped() - Per-resource _meta capabilities on all resource registrations - ResourceCapabilities/ResourceMetadataResult types - Zod schemas and RequestHandlerRegistrar interface Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a0db637 commit 762fb50

File tree

13 files changed

+780
-448
lines changed

13 files changed

+780
-448
lines changed

examples/skills-client/typescript/src/index.ts

Lines changed: 38 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@
44
*
55
* Demonstrates client-side SDK usage for the Skills Extension SEP:
66
*
7-
* 1. READ_RESOURCE_TOOL — Host-provided tool for model-driven loading
8-
* 2. listSkills() — Discover all skills, show prefix + name
9-
* 3. readSkillContent() — Load a multi-segment skill by path
10-
* 4. readSkillManifest() — Get file inventory with SHA256 hashes
11-
* 5. readSkillDocument() — Load a supporting file via resource template
12-
* 6. fetchSkillMetadata()— SEP-2093: metadata without content
13-
* 7. listSkillsScoped() — SEP-2093: URI-scoped listing
7+
* 1. READ_RESOURCE_TOOL — Host-provided tool for model-driven loading
8+
* 2. listSkillsFromIndex() — Discover skills via skill://index.json
9+
* 3. listSkills() — Discover all skills via resources/list
10+
* 4. readSkillContent() — Load a multi-segment skill by path
11+
* 5. readSkillManifest() — Get file inventory with SHA256 hashes
12+
* 6. readSkillDocument() — Load a supporting file via resource template
1413
*
1514
* Connects to the skills-server via stdio (spawns as child process).
1615
*
@@ -24,11 +23,10 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
2423
import {
2524
READ_RESOURCE_TOOL,
2625
listSkills,
26+
listSkillsFromIndex,
2727
readSkillContent,
2828
readSkillManifest,
2929
readSkillDocument,
30-
fetchSkillMetadata,
31-
listSkillsScoped,
3230
buildSkillsSummary,
3331
} from "@modelcontextprotocol/ext-skills/client";
3432
import { buildSkillUri } from "@modelcontextprotocol/ext-skills";
@@ -88,9 +86,31 @@ async function main() {
8886
console.log(JSON.stringify(READ_RESOURCE_TOOL, null, 2));
8987

9088
// -----------------------------------------------------------------------
91-
// 2. List all skills — show multi-segment paths with prefix + name
89+
// 2. Discover skills via skill://index.json (SEP enumeration)
9290
// -----------------------------------------------------------------------
93-
header("2. listSkills() — Discover Skills");
91+
header("2. listSkillsFromIndex() — skill://index.json Discovery");
92+
93+
console.log("Per the SEP, servers whose skill set is enumerable SHOULD expose");
94+
console.log("a skill://index.json resource following the Agent Skills discovery format.\n");
95+
96+
const indexSkills = await listSkillsFromIndex(client);
97+
if (indexSkills) {
98+
console.log(`Found ${indexSkills.length} skill(s) in index:\n`);
99+
for (const skill of indexSkills) {
100+
console.log(` Name: ${skill.name}`);
101+
console.log(` Skill Path: ${skill.skillPath}`);
102+
console.log(` URI: ${skill.uri}`);
103+
console.log(` Description: ${skill.description}`);
104+
console.log();
105+
}
106+
} else {
107+
console.log("Server does not expose skill://index.json (enumeration is optional)");
108+
}
109+
110+
// -----------------------------------------------------------------------
111+
// 3. List all skills via resources/list — show multi-segment paths
112+
// -----------------------------------------------------------------------
113+
header("3. listSkills() — Discover Skills via resources/list");
94114

95115
const skills = await listSkills(client);
96116
console.log(`Found ${skills.length} skill(s):\n`);
@@ -112,9 +132,9 @@ async function main() {
112132
console.log(buildSkillsSummary(skills));
113133

114134
// -----------------------------------------------------------------------
115-
// 3. Read a multi-segment skill
135+
// 4. Read a multi-segment skill
116136
// -----------------------------------------------------------------------
117-
header("3. readSkillContent() — Load Multi-Segment Skill");
137+
header("4. readSkillContent() — Load Multi-Segment Skill");
118138

119139
const refundSkill = skills.find((s) => s.skillPath === "acme/billing/refunds");
120140
if (refundSkill) {
@@ -131,9 +151,9 @@ async function main() {
131151
}
132152

133153
// -----------------------------------------------------------------------
134-
// 4. Read skill manifest
154+
// 5. Read skill manifest
135155
// -----------------------------------------------------------------------
136-
header("4. readSkillManifest() — File Inventory");
156+
header("5. readSkillManifest() — File Inventory");
137157

138158
if (refundSkill) {
139159
const manifest = await readSkillManifest(client, refundSkill.skillPath);
@@ -148,9 +168,9 @@ async function main() {
148168
}
149169

150170
// -----------------------------------------------------------------------
151-
// 5. Read a supporting file via resource template
171+
// 6. Read a supporting file via resource template
152172
// -----------------------------------------------------------------------
153-
header("5. readSkillDocument() — Supporting File");
173+
header("6. readSkillDocument() — Supporting File");
154174

155175
if (refundSkill) {
156176
const docPath = "templates/refund-email-template.md";
@@ -171,40 +191,6 @@ async function main() {
171191
}
172192
}
173193

174-
// -----------------------------------------------------------------------
175-
// 6. SEP-2093: Fetch metadata without content
176-
// -----------------------------------------------------------------------
177-
header("6. fetchSkillMetadata() — SEP-2093 Metadata");
178-
179-
if (refundSkill) {
180-
console.log(`Fetching metadata for: ${refundSkill.uri}\n`);
181-
const metadata = await fetchSkillMetadata(client, refundSkill.uri);
182-
if (metadata) {
183-
console.log("Metadata (no content transferred):");
184-
console.log(JSON.stringify(metadata, null, 2));
185-
} else {
186-
console.log("Server does not support resources/metadata (SEP-2093)");
187-
}
188-
}
189-
190-
// -----------------------------------------------------------------------
191-
// 7. SEP-2093: Scoped listing
192-
// -----------------------------------------------------------------------
193-
header("7. listSkillsScoped() — resources/list with URI Scoping");
194-
195-
console.log('Listing skills under "skill://acme/" (SKILL.md entries only):\n');
196-
const acmeSkills = await listSkillsScoped(client, "skill://acme/");
197-
if (acmeSkills) {
198-
console.log(`Found ${acmeSkills.length} skill(s) under acme/:\n`);
199-
for (const skill of acmeSkills) {
200-
console.log(` ${skill.skillPath} (name: ${skill.name})`);
201-
}
202-
} else {
203-
console.log(
204-
"Server does not support skills/list",
205-
);
206-
}
207-
208194
// -----------------------------------------------------------------------
209195
// Summary
210196
// -----------------------------------------------------------------------
@@ -213,10 +199,8 @@ async function main() {
213199
console.log("Features demonstrated:");
214200
console.log(" [SEP] Skills Extension — skill:// resource convention");
215201
console.log(" [SEP] Multi-segment paths (prefix + name = last segment)");
202+
console.log(" [SEP] skill://index.json — well-known discovery index");
216203
console.log(" [SEP-2133] Extension declaration: io.modelcontextprotocol/skills");
217-
console.log(" [SEP-2093] resources/metadata — metadata without content");
218-
console.log(" [SEP-2093] resources/list(uri=...) — URI-scoped listing");
219-
console.log(" [SEP-2093] Per-resource capabilities via _meta");
220204
console.log();
221205
} finally {
222206
await client.close();

examples/skills-server/typescript/src/index.ts

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
* Demonstrates the Skills Extension SEP with:
66
* - Extension declaration: io.modelcontextprotocol/skills (SEP-2133)
77
* - Multi-segment skill paths: prefix + name (final segment = frontmatter name)
8-
* - SEP-2093 features: resources/metadata, scoped resources/list,
9-
* per-resource capabilities via _meta
8+
* - skill://index.json — well-known discovery index
109
* - skill:// URI scheme with recursive discovery
1110
*
1211
* URI scheme:
1312
* - skill://{skillPath}/SKILL.md — Skill content (listed resource)
1413
* - skill://{skillPath}/_manifest — File manifest with SHA256 hashes
14+
* - skill://index.json — Well-known discovery index (SEP enumeration)
1515
* - skill://{+skillFilePath} — Supporting files (resource template)
1616
* - skill://prompt-xml — XML for system prompt injection
1717
*
@@ -31,8 +31,6 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
3131
import {
3232
discoverSkills,
3333
registerSkillResources,
34-
registerMetadataHandler,
35-
overrideResourcesListWithScoping,
3634
declareSkillsExtension,
3735
} from "@modelcontextprotocol/ext-skills/server";
3836
import type { ServerInternals } from "@modelcontextprotocol/ext-skills/server";
@@ -70,10 +68,9 @@ const server = new McpServer(
7068
{ capabilities: { resources: {} } },
7169
);
7270

73-
// Cast to access low-level Server internals for SEP shims.
74-
// These workarounds can be removed if the SDK adds support for:
71+
// Cast to access low-level Server internals for extension declaration.
72+
// This workaround can be removed if the SDK adds support for:
7573
// - extensions in capabilities (typescript-sdk#1630)
76-
// - uri parameter on resources/list (SEP-2093)
7774
const lowLevelServer = server.server as unknown as ServerInternals;
7875

7976
// --- Declare extension per SEP-2133 ---
@@ -89,20 +86,7 @@ registerSkillResources(server, skillMap, skillsDir, {
8986
promptXml: true,
9087
});
9188

92-
// --- Override resources/list with URI scoping (SEP-2093) ---
93-
94-
// Wraps the McpServer's built-in resources/list handler to add:
95-
// resources/list(uri="skill://") → only SKILL.md entries under that prefix
96-
overrideResourcesListWithScoping(lowLevelServer);
97-
98-
// --- Register resources/metadata (SEP-2093) ---
99-
100-
registerMetadataHandler(lowLevelServer, skillMap);
101-
10289
console.error("[skills-server] Extension: io.modelcontextprotocol/skills");
103-
console.error("[skills-server] SEP-2093 handlers:");
104-
console.error(" - resources/list with uri scoping");
105-
console.error(" - resources/metadata");
10690

10791
// --- Connect via stdio transport ---
10892

typescript/sdk/src/_client.ts

Lines changed: 45 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,17 @@
1010
* Key evolution from previous version:
1111
* - Multi-segment skill paths: skillPath may have a prefix before name
1212
* (per the SEP, the final segment of skillPath equals frontmatter name)
13-
* - SEP-2093: fetchSkillMetadata() for metadata-only access
14-
* - SDK wrappers per the SEP: listSkills(), readSkillUri()
13+
* - SDK wrappers per the SEP: listSkills(), listSkillsFromIndex(), readSkillUri()
1514
*/
1615

17-
import type { SkillManifest, SkillSummary } from "./types.js";
16+
import type { SkillManifest, SkillSummary, SkillIndex } from "./types.js";
1817
import {
1918
buildSkillUri,
2019
MANIFEST_PATH,
20+
INDEX_JSON_URI,
2121
parseSkillUri,
2222
SKILL_FILENAME,
2323
} from "./uri.js";
24-
import { ResourcesMetadataResultSchema, ScopedListResultSchema } from "./resource-extensions.js";
2524

2625
/**
2726
* Minimal structural interface for an MCP Client.
@@ -50,10 +49,6 @@ export interface SkillsClient {
5049
blob?: string;
5150
}>;
5251
}>;
53-
request(
54-
request: { method: string; params?: Record<string, unknown> },
55-
schema: unknown,
56-
): Promise<unknown>;
5752
}
5853

5954
/**
@@ -147,6 +142,48 @@ export async function listSkills(client: SkillsClient): Promise<SkillSummary[]>
147142
return skills;
148143
}
149144

145+
/**
146+
* List skills by reading the well-known skill://index.json resource.
147+
*
148+
* This is the SEP's primary enumeration mechanism, following the Agent Skills
149+
* well-known URI discovery index format. Returns null if the server does not
150+
* expose skill://index.json (enumeration is optional per the SEP).
151+
*
152+
* Hosts MUST NOT treat an absent or empty index as proof that a server has
153+
* no skills — a skill:// URI is always directly readable via resources/read.
154+
*/
155+
export async function listSkillsFromIndex(
156+
client: SkillsClient,
157+
): Promise<SkillSummary[] | null> {
158+
try {
159+
const result = await client.readResource({ uri: INDEX_JSON_URI });
160+
const content = result.contents[0];
161+
if (!content || !("text" in content) || !content.text) return null;
162+
163+
const index = JSON.parse(content.text) as SkillIndex;
164+
if (!index.skills || !Array.isArray(index.skills)) return null;
165+
166+
return index.skills
167+
.filter((entry) => entry.type === "skill-md")
168+
.map((entry) => {
169+
// Extract skillPath from the url field (strip "skill://" prefix and "/SKILL.md" suffix)
170+
const parsed = parseSkillUri(entry.url);
171+
const skillPath = parsed?.skillPath ?? entry.name;
172+
173+
return {
174+
name: entry.name,
175+
skillPath,
176+
uri: entry.url,
177+
description: entry.description,
178+
mimeType: "text/markdown",
179+
};
180+
});
181+
} catch {
182+
// Server doesn't expose skill://index.json — not an error
183+
return null;
184+
}
185+
}
186+
150187
/**
151188
* Read a resource by its full URI from an MCP server.
152189
*
@@ -262,72 +299,3 @@ export async function readSkillDocument(
262299
};
263300
}
264301

265-
/**
266-
* SEP-2093: Fetch resource metadata without content.
267-
*
268-
* Tries the resources/metadata endpoint. Returns null if the server
269-
* doesn't support SEP-2093 (method not found).
270-
*/
271-
export async function fetchSkillMetadata(
272-
client: SkillsClient,
273-
uri: string,
274-
): Promise<{ uri: string; name?: string; description?: string; mimeType?: string; capabilities?: { list?: boolean; subscribe?: boolean } } | null> {
275-
try {
276-
// SEP-2093: response shape is { resource: Resource }
277-
const result = await client.request(
278-
{ method: "resources/metadata", params: { uri } },
279-
ResourcesMetadataResultSchema,
280-
) as { resource: { uri: string; name?: string; description?: string; mimeType?: string; capabilities?: { list?: boolean; subscribe?: boolean }; [key: string]: unknown } };
281-
return result.resource;
282-
} catch {
283-
// Server doesn't support resources/metadata — not an error
284-
return null;
285-
}
286-
}
287-
288-
/**
289-
* List skills via resources/list with URI scoping (SEP-2093).
290-
*
291-
* When a `uriScope` is provided (e.g., "skill://acme/"), the server
292-
* filters to only SKILL.md entries under that prefix. Without a scope,
293-
* this is equivalent to a standard resources/list call.
294-
*
295-
* Returns null if the request fails (e.g., server doesn't support
296-
* the uri parameter on resources/list).
297-
*/
298-
export async function listSkillsScoped(
299-
client: SkillsClient,
300-
uriScope?: string,
301-
): Promise<SkillSummary[] | null> {
302-
try {
303-
const result = await client.request(
304-
{
305-
method: "resources/list",
306-
params: uriScope ? { uri: uriScope } : {},
307-
},
308-
ScopedListResultSchema,
309-
) as { resources: Array<{ uri: string; name?: string; description?: string; mimeType?: string }> };
310-
311-
const skills: SkillSummary[] = [];
312-
for (const resource of result.resources) {
313-
const parsed = parseSkillUri(resource.uri);
314-
if (!parsed) continue;
315-
if (
316-
parsed.filePath !== SKILL_FILENAME &&
317-
parsed.filePath.toLowerCase() !== "skill.md"
318-
)
319-
continue;
320-
321-
skills.push({
322-
name: resource.name ?? parsed.skillPath,
323-
skillPath: parsed.skillPath,
324-
uri: resource.uri,
325-
description: resource.description,
326-
mimeType: resource.mimeType,
327-
});
328-
}
329-
return skills;
330-
} catch {
331-
return null;
332-
}
333-
}

0 commit comments

Comments
 (0)