-
Notifications
You must be signed in to change notification settings - Fork 22
feat: serve Agent Skills over MCP via SEP-2640 skills/list, skills/get, and resources #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,165 @@ | ||
| import { | ||
| canonicalSkillUri, | ||
| parseSkillResourceUri, | ||
| } from "../skill-references"; | ||
| import type { | ||
| Principal, | ||
| SkillExtensionEntry, | ||
| SkillFile, | ||
| SkillMetadata, | ||
| } from "../shared"; | ||
| import { Problem, search, servedSkillRevision } from "./library"; | ||
|
|
||
| /** SEP-2640 v1 (`skills/list`, `skills/get`); tools remain the stable Skillbox API. */ | ||
| export const SKILLS_EXTENSION_ID = "io.modelcontextprotocol/skills"; | ||
| export const SKILL_META_REVISION = "io.modelcontextprotocol.skills/revision"; | ||
| export const SKILL_META_REFERENCE = "io.modelcontextprotocol.skills/referenceId"; | ||
| export const SKILL_META_CHECKSUM = "io.modelcontextprotocol.skills/checksum"; | ||
| const LIST_PAGE = 50; | ||
| const LIST_CAP = 500; | ||
|
|
||
| export { canonicalSkillUri, parseSkillResourceUri }; | ||
|
|
||
| const MIME_BY_EXTENSION: Record<string, string> = { | ||
| md: "text/markdown", | ||
| markdown: "text/markdown", | ||
| txt: "text/plain", | ||
| json: "application/json", | ||
| js: "text/javascript", | ||
| mjs: "text/javascript", | ||
| cjs: "text/javascript", | ||
| ts: "text/plain", | ||
| sh: "text/x-shellscript", | ||
| bash: "text/x-shellscript", | ||
| html: "text/html", | ||
| css: "text/css", | ||
| yml: "text/yaml", | ||
| yaml: "text/yaml", | ||
| xml: "application/xml", | ||
| svg: "image/svg+xml", | ||
| png: "image/png", | ||
| jpg: "image/jpeg", | ||
| jpeg: "image/jpeg", | ||
| gif: "image/gif", | ||
| webp: "image/webp", | ||
| pdf: "application/pdf", | ||
| bin: "application/octet-stream", | ||
| }; | ||
|
|
||
| export function mimeTypeForPath(path: string) { | ||
| if (path === "SKILL.md") return "text/markdown"; | ||
| const ext = path.split(".").pop()?.toLowerCase(); | ||
| return (ext && MIME_BY_EXTENSION[ext]) || "application/octet-stream"; | ||
| } | ||
|
|
||
| export function isBinaryFile(bytes: Buffer) { | ||
| if (bytes.includes(0)) return true; | ||
| try { | ||
| new TextDecoder("utf-8", { fatal: true }).decode(bytes); | ||
| return false; | ||
| } catch { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| function parseListCursor(cursor?: string) { | ||
| if (!cursor) return 0; | ||
| const match = /^offset:(\d+)$/.exec(cursor); | ||
| if (!match) throw new Problem(400, "Invalid cursor"); | ||
| return Number(match[1]); | ||
| } | ||
|
|
||
| function fileDigest(file: SkillFile) { | ||
| return `sha256:${file.sha256}`; | ||
| } | ||
|
|
||
| export function skillEntry( | ||
| _principal: Principal, | ||
| skill: { id: string; referenceId: string }, | ||
| revision: { | ||
| id: string; | ||
| checksum: string; | ||
| metadata: SkillMetadata; | ||
| files: SkillFile[]; | ||
| }, | ||
| ): SkillExtensionEntry { | ||
| const header = revision.metadata.frontmatter ?? {}; | ||
| return { | ||
| uri: canonicalSkillUri(skill.id, "SKILL.md"), | ||
| frontmatter: { | ||
| ...header, | ||
| name: String(header.name ?? skill.id), | ||
| description: String(header.description ?? revision.metadata.description), | ||
| }, | ||
| resources: revision.files.map((file) => ({ | ||
| uri: canonicalSkillUri(skill.id, file.path), | ||
| digest: fileDigest(file), | ||
| size: file.size, | ||
| })), | ||
| _meta: { | ||
| [SKILL_META_REVISION]: revision.id, | ||
| [SKILL_META_REFERENCE]: skill.referenceId, | ||
| [SKILL_META_CHECKSUM]: revision.checksum, | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| export async function listSkillEntries( | ||
| principal: Principal, | ||
| options: { cursor?: string; limit?: number } = {}, | ||
| ) { | ||
| const offset = parseListCursor(options.cursor); | ||
| const limit = Math.min(Math.max(options.limit ?? LIST_PAGE, 1), LIST_CAP); | ||
| const page = await search(principal, "", limit, offset); | ||
| const skills: SkillExtensionEntry[] = []; | ||
| for (const item of page.items) { | ||
| try { | ||
| const served = await servedSkillRevision(principal, item.id); | ||
| skills.push( | ||
| skillEntry(principal, served.skill, served.revision), | ||
| ); | ||
|
Comment on lines
+115
to
+120
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift 🔎 Supported by static analysis🏁 Script executed: sed -n '100,140p' src/server/skills-mcp.ts
sed -n '360,405p' src/server/library.ts
rg -n 'function canRead|const canRead|async function canRead|servedSkillRevision|function currentRevision|currentRevision' src/serverRepository: kitze/skillbox Length of output: 3798 🏁 Script executed: #!/bin/bash
sed -n '160,235p' src/server/library.ts
sed -n '235,330p' src/server/library.ts
sed -n '1,120p' src/server/skills-mcp.ts
rg -n 'resolveReferenceId|expand|grant|grants|function search|export async function search|LIST_CAP|LIST_PAGE' src/server srcRepository: kitze/skillbox Length of output: 18330 🏁 Script executed: #!/bin/bash
sed -n '1,90p' src/server/bundles.ts
sed -n '835,860p' src/server/library.tsRepository: kitze/skillbox Length of output: 2292 Load the page with bounded database queries.
Fetch eligible skills and current revisions in bulk. Preserve the authorization, skill-kind, archived, and disabled predicates. 🤖 Prompt for AI Agents |
||
| } catch (error) { | ||
| if (error instanceof Problem && error.status === 404) continue; | ||
| throw error; | ||
| } | ||
| } | ||
| return { | ||
| resultType: "complete" as const, | ||
| skills, | ||
| ...(page.nextOffset != null | ||
| ? { nextCursor: `offset:${page.nextOffset}` } | ||
| : {}), | ||
| ttlMs: 0, | ||
| cacheScope: "private" as const, | ||
| }; | ||
| } | ||
|
|
||
| export async function getSkillEntry(principal: Principal, uri: string) { | ||
| const parsed = parseSkillResourceUri(uri); | ||
| if (!parsed || parsed.path !== "SKILL.md") | ||
| throw new Problem(400, "Unknown skill"); | ||
| const served = await servedSkillRevision(principal, parsed.idOrReference); | ||
| return { | ||
| resultType: "complete" as const, | ||
| skill: skillEntry(principal, served.skill, served.revision), | ||
| ttlMs: 0, | ||
| cacheScope: "private" as const, | ||
| }; | ||
| } | ||
|
|
||
| export async function readSkillResource(principal: Principal, uri: string) { | ||
| const parsed = parseSkillResourceUri(uri); | ||
| if (!parsed) throw new Problem(400, "Unknown skill"); | ||
| const served = await servedSkillRevision(principal, parsed.idOrReference); | ||
| const file = served.revision.files.find((entry) => entry.path === parsed.path); | ||
| if (!file) throw new Problem(404, "Unknown skill"); | ||
| const bytes = Buffer.from(file.content, "base64"); | ||
| return { | ||
| uri: canonicalSkillUri(served.skill.id, file.path), | ||
| mimeType: mimeTypeForPath(file.path), | ||
| bytes, | ||
| digest: fileDigest(file), | ||
| size: file.size, | ||
| binary: isBinaryFile(bytes), | ||
| }; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: kitze/skillbox
Length of output: 14951
🏁 Script executed:
Repository: kitze/skillbox
Length of output: 22547
🏁 Script executed:
Repository: kitze/skillbox
Length of output: 16567
🏁 Script executed:
Repository: kitze/skillbox
Length of output: 5602
🌐 Web query:
SEP-2640 Skills Over MCP skills/list skills/get JSON-RPC error InvalidParams InternalError💡 Result:
<search_synthesis>
SEP-2640 is an extensions-track proposal for the Model Context Protocol (MCP) designed to serve "Agent Skills" as standard MCP resources [1][2]. In the finalized "v1" scope of the SEP (as of July 2026), servers implementing the
io.modelcontextprotocol/skillsextension must supportskills/listandskills/getmethods [3][4][5]. Regarding the specific JSON-RPC error codes you mentioned: - InvalidParams (-32602): This is the mandated error code for when askills/getrequest targets a URI that does not identify a valid skill served by the server [6][5]. It is also the standard JSON-RPC code for invalid method parameters or missing entities [7][8]. - InternalError (-32603): This is a generic server-side error code used for unexpected failures [7][8]. If an MCP server returns -32603 in response toskills/listorskills/getinstead of -32602, it typically indicates that a handler exception was not correctly mapped to the specific protocol error or that an internal bug occurred during parameter parsing/execution [9]. Context on SEP-2640 behavior: 1. Retrieval Logic:skills/getis intended to retrieve a single skill entry by its URI, regardless of whether that skill appeared in askills/listresult [10][6][5]. If the URI is unknown to the server, it MUST return -32602 [6][5]. 2. Error Code Mapping: Servers are advised to ensure that parameter validation failures and "not found" states are explicitly mapped to -32602. Failing to do so and letting raw exceptions (like validation errors) escape often results in the SDK wrapping the failure as a generic -32603 InternalError, which is considered a bug in the server implementation [9]. 3. Implementation Status: As of September 2026, these methods are the standard way to interact with Skills, having superseded earlier designs that relied on a reservedskill://index.jsonresource [2][4]. If you are encountering -32603, you should verify if your server-side handler is catching exceptions and explicitly returning a proper MCP protocol error instead of allowing unhandled exceptions to trigger the generic internal error handler [7][9].</search_synthesis>
<source_evidence>
Citations:
🌐 Web query:
site:github.com/modelcontextprotocol/modelcontextprotocol SEP-2640 skills/get resources/read -32602 -32603 error handling💡 Result:
<search_synthesis>
Under the Model Context Protocol (MCP) Skills Extension (SEP-2640), error handling for resources—including those accessed via skills—is standardized to align with general MCP resource patterns [1][2]. Key details regarding error handling and resource access within the context of SEP-2640 include: Resource Read Errors: Skill files are exposed as MCP resources (typically under the skill:// URI scheme) and are read using the standard resources/read method [1]. If a resource (such as a skill file) cannot be found, the server must return a JSON-RPC error [3]. SEP-2164, which is relevant to the broader protocol evolution, specifies that for a non-existent URI, servers should return JSON-RPC error code -32602 (Invalid Params), which is the standard code for invalid parameters [4]. Skills/Get Semantics: The skills/get method is used to retrieve metadata for a specific skill, particularly for refreshing stale metadata or obtaining information for unlisted skills [2]. While skills/get focuses on metadata, it functions alongside the resources-based access model [2]. In the case of a "stale-snapshot" (e.g., a newly listed child entry that does not match expected integrity), hosts are advised to treat the situation as a changed skill requiring re-approval rather than a generic read error [2]. Dynamic Skills and Omissions: The working group has emphasized that skill metadata should be explicit to avoid ambiguity [5]. Omission of a skill in a list should not be conflated with the presence of a "dynamic" skill; rather, dynamic skills should be marked with explicit indicators (such as a digest field marking the content as dynamic) rather than omitting resource information [5]. Implementation Note: Clients and servers should be prepared for potential variations in error handling during the transition to standardized codes, as historical implementations of the MCP protocol have used different error codes (including -32002) for "resource not found" [4]. However, -32602 is the current canonical recommendation for cases where a URI does not resolve to an existing resource [4].
</search_synthesis>
<source_evidence>
Citations:
Preserve internal error classification.
sepErrorreceives failures fromskills/list,skills/get, andresources/read. It rethrows existingMcpErrorvalues, but maps every other value toErrorCode.InvalidParams(-32602). These handlers perform database queries and throwProblem(400)orProblem(404)for invalid or unavailable skill URIs. Database and unexpected exceptions can therefore appear as invalid skill requests instead of internal errors.Map only expected
Problemvalues for invalid or unavailable skills toInvalidParams. Map other failures toInternalError(-32603) as required by the MCP error contract.Proposed fix
function sepError(error: unknown): never { if (error instanceof McpError) throw error; - throw new McpError(ErrorCode.InvalidParams, "Unknown skill"); + if ( + error instanceof library.Problem && + (error.status === 400 || error.status === 404) + ) { + throw new McpError(ErrorCode.InvalidParams, "Unknown skill"); + } + throw new McpError(ErrorCode.InternalError, "Internal error"); }📝 Committable suggestion
🤖 Prompt for AI Agents