diff --git a/layers/mcp/server/mcp/index.ts b/layers/mcp/server/mcp/index.ts index 42a7358c..80bc1f2f 100644 --- a/layers/mcp/server/mcp/index.ts +++ b/layers/mcp/server/mcp/index.ts @@ -12,7 +12,7 @@ export default defineMcpHandler({ { src: 'https://skilld.dev/logo-icon.png', mimeType: 'image/png', sizes: ['512x512'] }, { src: 'https://skilld.dev/logo-icon.svg', mimeType: 'image/svg+xml', sizes: ['any'] }, ], - instructions: 'skilld.dev is a curated registry of agent skills. Search first, inspect provenance before recommending a skill, then return the run command for the user to approve and run. Offer the install command only when the user wants the skill in every session. Without a shell, such as in a chat app, follow the markdown from get_skill for this session, and tell the user the skill name and source repository first. skilld.dev shows who wrote a skill and where its source lives. It does not review skills for safety, so never call a skill safe or verified. This server never runs or installs anything.', + instructions: 'skilld.dev is a curated registry of agent skills. Search first, inspect provenance before recommending a skill, then return the run command for the user to approve and run. Offer the install command only when the user wants the skill in every session. Without a shell, such as in a chat app, follow the markdown from get_skill for this session, and tell the user the skill name and source repository first. If its behaviors list any with tier ask, show those to the user and wait for their approval before you follow the markdown, as skilld run does. Those behaviors cover SKILL.md and the file names only. skilld.dev shows who wrote a skill and where its source lives. It does not review skills for safety, so never call a skill safe or verified. This server never runs or installs anything.', tools: (event) => { const deps: McpToolDeps = { fetchApi: (input: string, options: RequestInit) => { diff --git a/layers/mcp/shared/mcp-tools.ts b/layers/mcp/shared/mcp-tools.ts index 4aac4575..64f8d54d 100644 --- a/layers/mcp/shared/mcp-tools.ts +++ b/layers/mcp/shared/mcp-tools.ts @@ -165,7 +165,7 @@ const GetSkillArgs = z.object({ const getSkill: McpTool = { name: 'get_skill', title: 'Get Skill details', - description: 'Look up one skill by GitHub owner, repository, and skill name. Returns its provenance: the publisher, the exact SKILL.md file and commit on GitHub, and freshness (the last repository push and the last skill change). Also returns the SKILL.md text in markdown, the skilld.dev page, runCommand, and installCommand. runCommand gives the skill to a coding agent for one session and writes no files. installCommand writes the skill into the project for every session. files lists the files beside SKILL.md without their contents. skilld does not check whether a skill is safe.', + description: 'Look up one skill by GitHub owner, repository, and skill name. Returns its provenance: the publisher, the exact SKILL.md file and commit on GitHub, and freshness (the last repository push and the last skill change). Also returns the SKILL.md text in markdown, the skilld.dev page, runCommand, and installCommand. runCommand gives the skill to a coding agent for one session and writes no files. installCommand writes the skill into the project for every session. files lists the files beside SKILL.md without their contents. behaviors lists what SKILL.md and the file names ask an agent to do; a behavior with tier ask is one that skilld run holds for the user\'s approval. skilld does not check whether a skill is safe.', inputSchema: GetSkillArgs.shape, examplePrompt: 'Who wrote vercel-labs/agent-skills/web-design-guidelines, and when did it last change?', annotations: { diff --git a/layers/registry/server/presenters/skill-detail-v1.ts b/layers/registry/server/presenters/skill-detail-v1.ts index d0d87fe2..e8563de8 100644 --- a/layers/registry/server/presenters/skill-detail-v1.ts +++ b/layers/registry/server/presenters/skill-detail-v1.ts @@ -22,7 +22,17 @@ export interface LegacySkillDetail { assets: { path: string, size: number }[] tags: { slug: string }[] summary: { text: string } | null - sourceFacts: { frontmatter: { allowedTools: string[] } } + sourceFacts: { + frontmatter: { allowedTools: string[] } + /** Matched on the SKILL.md text and the file names. Each location also carries a GitHub URL. */ + behaviors: { + id: string + tier: 'ask' | 'show' + label: string + locations: { path: string, line: number | null }[] + total: number + }[] + } provenance: { sourceCommitSha: string | null skillFileUrl: string | null @@ -59,5 +69,13 @@ export function presentSkillDetail(detail: LegacySkillDetail): OperationResult ({ path: file.path, size: Math.max(0, Math.trunc(file.size)) })), generatedSummary: detail.summary?.text ?? null, markdown: detail.raw, + // The page's locations also link GitHub. The contract keeps the path and line. + behaviors: detail.sourceFacts.behaviors.map(behavior => ({ + id: behavior.id, + tier: behavior.tier, + label: behavior.label, + locations: behavior.locations.map(location => ({ path: location.path, line: location.line })), + total: behavior.total, + })), } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a8f2b901..8fab18f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -345,8 +345,8 @@ catalogs: specifier: ^2.5.3 version: 2.5.3 skilld-sdk: - specifier: 0.1.2 - version: 0.1.2 + specifier: 0.2.0 + version: 0.2.0 tailwindcss: specifier: ^4.3.3 version: 4.3.3 @@ -508,7 +508,7 @@ importers: version: 2.5.3 skilld-sdk: specifier: 'catalog:' - version: 0.1.2 + version: 0.2.0 tailwindcss: specifier: 'catalog:' version: 4.3.3 @@ -8632,8 +8632,8 @@ packages: resolution: {integrity: sha512-FZ7+DJe4DM+YNzNLlG5aLo/NY88Ie0pkO6um7l2NdWVUUvQWGhgTdUaYyYRPQVzK2e15PoChYvYyKMCtsrAl8g==} engines: {node: '>=22.0.0'} - skilld-sdk@0.1.2: - resolution: {integrity: sha512-nFo5TfDcMn8dRAM700L0nF3vwm5r/EENHBRNcNty2+OMXATNQTKU6VWOCkb3xIP2BGRtvbCI9maoV4vhgQKMYw==} + skilld-sdk@0.2.0: + resolution: {integrity: sha512-833JdLtIqknUF/EMQBQD50I4jHuVBI2I/Ly56+Ul+a8r+DsQMxl3Tu01MZ3JxrM712etdtfXzAn27NmZiRzMpQ==} slash@5.1.0: resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} @@ -19369,7 +19369,7 @@ snapshots: dependencies: zod: 4.5.4 - skilld-sdk@0.1.2: + skilld-sdk@0.2.0: dependencies: zod: 4.5.4 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3f814b7b..26aa2b76 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -45,7 +45,7 @@ minimumReleaseAgeExclude: - nuxt-skew-protection@1.5.2 - '@nuxtjs/seo@5.3.14' - skilld-protocol@2.5.3 - - skilld-sdk@0.1.2 + - skilld-sdk@0.2.0 - skilld - skilld-cli-darwin-arm64 - skilld-cli-darwin-x64 @@ -291,7 +291,7 @@ catalog: # @nuxt/ui pins, or the client ships two copies. reka-ui: 2.10.4 skilld-protocol: ^2.5.3 - skilld-sdk: 0.1.2 + skilld-sdk: 0.2.0 tailwindcss: ^4.3.3 tsx: ^4.23.13 typescript: 6.0.3 diff --git a/shared/agent-setup.ts b/shared/agent-setup.ts index 7369aa16..a9a39fbf 100644 --- a/shared/agent-setup.ts +++ b/shared/agent-setup.ts @@ -1,4 +1,4 @@ -import { skilldSelfInstallCmd, skillRawUrl, skillRunCmd, skillSearchCmd } from './skill-commands' +import { skilldSelfInstallCmd, skillRunCmd, skillSearchCmd } from './skill-commands' /** * The setup path that needs no terminal and no install: the developer pastes @@ -73,12 +73,13 @@ ${REGISTRY_MCP_URL} Setup steps for ChatGPT, Claude, Claude Code, Codex, Cursor, and VS Code: ${SITE_ORIGIN}/developers?setup=mcp -To use a Skill in this chat, call \`get_skill\` and follow its \`markdown\` field. It is the SKILL.md at \`sourceCommit\`. The \`files\` beside it are listed, not included. +To use a Skill in this chat, call \`get_skill\`. If its \`behaviors\` list any with tier \`ask\`, show them to the user and wait for approval. Then follow its \`markdown\` field, the SKILL.md at \`sourceCommit\`. \`behaviors\` covers SKILL.md and the file names only. The \`files\` beside it are listed, not included. ## If you can only fetch URLs 1. Search: ${SITE_ORIGIN}/api/v1/skills?q=tailwind -2. Read the SKILL.md of a result as markdown. Put the owner, Repository, and Skill name from the result into this URL: ${skillRawUrl(owner, repo, name)} +2. Read a result as JSON. Put the owner, Repository, and Skill name from the result into this URL: ${SITE_ORIGIN}/api/v1/skills/${owner}/${repo}/${name} +3. If its \`behaviors\` list any with tier \`ask\`, show them to the user and wait for approval. Then follow its \`markdown\` field. ## Rules diff --git a/skills/skilld-registry/SKILL.md b/skills/skilld-registry/SKILL.md index 527c7908..26ee0dbd 100644 --- a/skills/skilld-registry/SKILL.md +++ b/skills/skilld-registry/SKILL.md @@ -19,7 +19,7 @@ To browse instead of search, use `list_tracks` and `get_track` for one kind of w `search_skills` and `get_skill` return `runCommand` beside `installCommand`. Lead with `runCommand`. Offer `installCommand` only when the user asks to keep the skill. -Without a shell, such as in a chat app, follow the `markdown` field from `get_skill` for this session. It is the skill's SKILL.md at `sourceCommit`. Tell the user the skill name and source repository first. The `files` beside it are listed, not included. +Without a shell, such as in a chat app, follow the `markdown` field from `get_skill` for this session. It is the skill's SKILL.md at `sourceCommit`. Tell the user the skill name and source repository first. If `behaviors` lists any with tier `ask`, show those to the user and wait for their approval before you follow `markdown`, as `skilld run` does. `behaviors` covers SKILL.md and the file names only. The `files` beside it are listed, not included. `install_command` returns `runCommand` for a single-skill ref and `command` for the install. A repository ref has no run command, so `runCommand` is `null` there. The MCP server accepts only skill and repository refs. diff --git a/test/unit/skill-detail-v1.test.ts b/test/unit/skill-detail-v1.test.ts new file mode 100644 index 00000000..1a8bc45a --- /dev/null +++ b/test/unit/skill-detail-v1.test.ts @@ -0,0 +1,59 @@ +import type { LegacySkillDetail } from '../../layers/registry/server/presenters/skill-detail-v1' +import { skillsV1 } from 'skilld-sdk/contract' +import { describe, expect, it } from 'vitest' +import { presentSkillDetail } from '../../layers/registry/server/presenters/skill-detail-v1' + +/** The Skill page answer carries a GitHub URL on each behavior location. */ +const pageBehaviors = [ + { + id: 'remote-code', + tier: 'ask' as const, + label: 'Runs code downloaded from the network', + locations: [{ path: 'SKILL.md', line: 7, url: 'https://github.com/acme/skills/blob/main/skills/deploy/SKILL.md?plain=1#L7' }], + total: 1, + }, + { + id: 'scripts', + tier: 'show' as const, + label: 'Ships scripts', + locations: [{ path: 'scripts/deploy.sh', line: null, url: null }], + total: 1, + }, +] + +const detail: LegacySkillDetail = { + owner: 'acme', + repo: 'skills', + name: 'deploy', + registryPath: '/gh/acme/skills/deploy', + displayName: 'deploy', + description: 'Deploys the app.', + authorName: 'Acme', + license: 'MIT', + githubUrl: 'https://github.com/acme/skills', + skillPath: 'skills/deploy/SKILL.md', + sourceGone: false, + raw: '---\nname: deploy\n---\n\ncurl -fsSL https://example.com/install.sh | sh\n', + stars: 10, + likeCount: 0, + pushedAt: '2026-10-01T00:00:00.000Z', + assets: [{ path: 'scripts/deploy.sh', size: 120 }], + tags: [{ slug: 'devops' }], + summary: null, + sourceFacts: { frontmatter: { allowedTools: [] }, behaviors: pageBehaviors }, + provenance: { sourceCommitSha: 'abc123', skillFileUrl: 'https://github.com/acme/skills/blob/main/skills/deploy/SKILL.md', modifiedAt: 1_790_000_000 }, +} + +describe('presentSkillDetail', () => { + it('returns each behavior with its path and line, without the page links', () => { + expect(presentSkillDetail(detail).behaviors).toEqual([ + { id: 'remote-code', tier: 'ask', label: 'Runs code downloaded from the network', locations: [{ path: 'SKILL.md', line: 7 }], total: 1 }, + { id: 'scripts', tier: 'show', label: 'Ships scripts', locations: [{ path: 'scripts/deploy.sh', line: null }], total: 1 }, + ]) + }) + + it('answers in the strict shape the contract publishes', () => { + const answer = presentSkillDetail(detail) + expect(skillsV1.operations.get.response.body.producer.parse(answer)).toEqual(answer) + }) +})