SLOPSHOPPER

skills-mcp-browser

/mcp-skills opens a pane to browse, preview and use the skills an MCP skills server serves

panecommand
v0.1.0no licenseupdated 2026-10-19sambhav/mcp-demo/mods/skills-mcp-browser
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · skills-mcp-browser
│ ┃ MCP skills ✕ › fix the failing auth test and add an audit log call │ ┃ No connected MCP server serves skill:// │ ┃ resources. ⏺ Read(src/auth.ts) │ ⎿ Read 6 lines │ ⏺ Update(src/auth.ts) │ ⎿ Added 2 lines, removed 1 line │ ⏺ Bash(bun test) │ ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /mcp-skills │ ⎿ skills-mcp-browser: MCP skills pane opened. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · MCP skills
No connected MCP server serves skill:// resources.
README

mcp-demo: Agent Skills over MCP

A basic MCP server that serves Agent Skills, plus Claude Code mods that make those skills feel native, plus configs for other harnesses.

skills/            three demo skills (SKILL.md folders)
server/            skills-mcp: the MCP server (Python, MCP SDK 2.x), and skills-mcp-ui
demo/              live demo: run.sh, SCRIPT.md (talk track), skill packs
mods/              Claude Code mods: skills-mcp, skills-mcp-commands, skills-mcp-browser
scripts/           enable-native-mcp-skills.sh (optional, see below)
.mcp.json          runs the server for Claude Code in this repo

Live demo

Change skills in a web page and watch a running Claude Code pick them up within about two seconds:

demo/run.sh ui       # terminal 1: http://127.0.0.1:8766
demo/run.sh claude   # terminal 2

demo/SCRIPT.md is the talk track: add packs of skills, write one live, edit it, and swap the whole set. Behind it:

  • the server watches its folder (--watch, following a symlink swap);
  • the skills-mcp mod polls the server's live skill://index.json;
  • on a change, Claude Code gets a toast and a note in the conversation.

The server

skills-mcp serves every folder under --skills-dir that holds a SKILL.md, re-read on every request, so editing a skill needs no restart. It does not offer every client everything: each client gets one way to reach the skills, the best it supports.

TierOfferedChosen when
sepThe MCP Skills extension (SEP-2640): skills/list, skills/get (manifests with sha256 digests), resources/directory/read, and the skill:// resources they name. No instructions, no tools.The client declares io.modelcontextprotocol/skills in its capabilities, or calls skills/list. Claude Code with native MCP skills does the latter: it calls skills/list on seeing the server's declaration but doesn't declare anything itself.
resourcesA one-line instructions naming the skills, plus every skill file listed as a skill://<name>/<path> resource.Any other client, by default.
toolsA one-line instructions, plus list_skills, get_skill(name, args?), read_skill_file(name, path).Clients known not to read resources (Zed, Copilot CLI, by clientInfo.name), or --fallback tools. MCP has no capability that says "I read resources", so this is the server's call.

Live changes: the server re-scans its folders every --watch seconds (default 1; a root that is a symlink can be swapped), lists resources from the live catalog, and on a change publishes resources/list_changed and resources/updated to clients subscribed through subscriptions/listen. It also serves skill://index.json, a live index in the pre-v1 SEP-2640 shape, for clients that cache resources/list and poll a read instead. Claude Code 2.1.296 is one: it doesn't subscribe at connect, so the skills-mcp mod polls the index.

The server always declares the extension, so a client that speaks it can find it. Instructions go out in the handshake, before a client can be seen calling skills/list, so they stay one line. Protocol 2026-07-28 keeps no connection between requests, so a skills/list call is remembered against the client's name and version. That's exact over stdio, where one process serves one client. On a shared HTTP server, clients with the same name and version share it; --fallback resources|tools fixes the non-SEP tier, and --trace FILE logs each request with what the client declared and the tier it got.

cd server
uv run skills-mcp                                          # stdio, serves ../skills
uv run skills-mcp --skills-dir ~/my-skills --skills-dir ./team-skills
uv run skills-mcp --transport streamable-http --port 8765  # http://127.0.0.1:8765/mcp
uv run skills-mcp --list                                   # print what it finds
uv run skills-mcp --fallback tools                         # tools tier for every non-SEP client
uv run skills-mcp --watch 0.5 --trace /tmp/trace.jsonl     # check for changes twice a second; log requests
uv run pytest                                              # tests

From anywhere, without a checkout: uvx --from 'git+https://github.com/sambhav/mcp-demo#subdirectory=server' skills-mcp --skills-dir /path/to/skills

Claude Code

Add the server (this repo's .mcp.json already does, for sessions started here):

claude mcp add --scope user skills -- uv run --directory /path/to/mcp-demo/server skills-mcp

Then pick the experience:

1. Plain MCP: every setup

With nothing else, the skills are live over MCP in any Claude Code setup (Anthropic API, Bedrock, Vertex, Foundry, a gateway): Claude Code gets the resources tier, so the model learns the skill names from the server's instructions and reads skill://<name>/SKILL.md with its resource tool.

2. The mods: native-feeling, every setup

/plugin install skills-mcp --marketplace sambhav/mcp-demo
/plugin install skills-mcp-commands --marketplace sambhav/mcp-demo
/plugin install skills-mcp-browser --marketplace sambhav/mcp-demo

(y to add the marketplace, then pick a scope.)

ModWhat it does
skills-mcpFinds every skill://…/SKILL.md resource on every connected server, caches the index (session state, and across sessions so turn one has it), and adds one stable system-prompt section telling the model to read a skill with ReadMcpResourceTool before a matching task. The section changes only when the index does, so the prompt cache holds. Claude Code caches resources/list from connect time, so for servers that publish a live skill://index.json (the pre-v1 SEP-2640 index; this server does) it reads that every 2 seconds instead. When the index changes, it toasts the difference and appends a note to the conversation, which the model reads from its next request. Some managed builds keep user mods out of the system prompt, and the note covers that case. A name two servers share is shown as server:name. Skills Claude Code already loads natively are left to it.
skills-mcp-commandsA /<skill> [args] command per skill. Typing one starts the turn with the skill's SKILL.md (read live as a resource) attached for the model, like a local skill's command. Never takes a name a local command or skill already has.
skills-mcp-browser/mcp-skills opens a pane: the skills, their server and URI, a preview of SKILL.md, and Use to fill the prompt.

Settings (/config): skills-mcp.server limits the index to one server; skills-mcp.refreshSeconds sets the refresh (default 2; 0: start and /skills-mcp only); skills-mcp-commands.prefix prefixes the commands.

To work on the mods from this checkout: claude --plugin-dir mods/skills-mcp --plugin-dir mods/skills-mcp-commands --plugin-dir mods/skills-mcp-browser; claude plugin test mods/<mod> runs a mod's tests.

3. Native MCP skills: behind a flag

Claude Code (seen in 2.1.296) ships its own SEP-2640 client: at connect it calls skills/list on any server that declares the extension, and the skills appear as real skills named <server>:<name> (skills:commit-message), verified by digest and loaded on demand. It is behind the remote feature flag tengu_mcp_skills, off by default. scripts/enable-native-mcp-skills.sh turns it on locally. That is unsupported and relies on undocumented internals; it only works with a first-party provider (Anthropic API or a compatible gateway), and it stops the flag fetch (non-essential traffic off), freezing every other flag at its cached value. Read the script before running it. When the flag reaches you for real, the server needs no change, and the skills-mcp mod steps aside.

Other harnesses

The server offers each harness one tier (see above): today that's resources for most of them, and tools for Zed and Copilot CLI. If a harness on the resources tier turns out not to use resources, start the server with --fallback tools for it. What else a harness picks up decides how skills show up. Checked against each project's docs and source in October 2026; unverified where they did not say.

Harnessreads resourcespasses instructions to the modelTier it getsHow skills show up
Claude Code✓✓resources; sep with native MCP skillsskill:// reads; native skills:<name> skills when on
Codex CLI✓ (read_mcp_resource)✓resourcesthe model reads skill://<name>/SKILL.md
VS Code (Copilot agent)✓✓resourcesresource reads; resources under Add Context
Copilot CLIpartialallowlisted servers, or --allow-all-mcp-server-instructionstoolslist_skills / get_skill
Gemini CLI✓ (read_mcp_resource, @skill://…)✓resourcesresource reads, @skill://name/SKILL.md
Antigravity✓unverifiedresourcesresource reads
OpenCode✓ (list_mcp_resources)✓resourcesresource reads
Goose✓✓resourcesresource reads
Cursor✓unverifiedresourcesresource reads
Zed✗unverifiedtoolslist_skills / get_skill
Cline, Continue✓unverifiedresourcesresource reads

None of these other harnesses implement the SEP-2640 methods yet.

In each snippet replace /path/to/mcp-demo with your checkout (or use the uvx --from … command above).

Codex CLI — ~/.codex/config.toml (or codex mcp add skills -- uv run --directory /path/to/mcp-demo/server skills-mcp)

[mcp_servers.skills]
command = "uv"
args = ["run", "--directory", "/path/to/mcp-demo/server", "skills-mcp"]

VS Code / GitHub Copilot agent — .vscode/mcp.json

{ "servers": { "skills": { "type": "stdio", "command": "uv", "args": ["run", "--directory", "/path/to/mcp-demo/server", "skills-mcp"] } } }

Copilot CLI — ~/.copilot/mcp-config.json (or copilot mcp add skills -- uv run --directory /path/to/mcp-demo/server skills-mcp)

{ "mcpServers": { "skills": { "type": "local", "command": "uv", "args": ["run", "--directory", "/path/to/mcp-demo/server", "skills-mcp"], "tools": ["*"] } } }

Gemini CLI — ~/.gemini/settings.json; Antigravity — ~/.gemini/config/mcp_config.json or .agents/mcp_config.json; Cursor — ~/.cursor/mcp.json; Cline — cline_mcp_settings.json

{ "mcpServers": { "skills": { "command": "uv", "args": ["run", "--directory", "/path/to/mcp-demo/server", "skills-mcp"] } } }

OpenCode — opencode.json

{ "mcp": { "skills": { "type": "local", "command": ["uv", "run", "--directory", "/path/to/mcp-demo/server", "skills-mcp"], "enabled": true } } }

Goose — ~/.config/goose/config.yaml

extensions:
  skills: { type: stdio, name: skills, cmd: uv, args: ["run", "--directory", "/path/to/mcp-demo/server", "skills-mcp"], enabled: true, timeout: 300 }

Zed — settings.json

{ "context_servers": { "skills": { "command": "uv", "args": ["run", "--directory", "/path/to/mcp-demo/server", "skills-mcp"], "env": {} } } }

Continue — .continue/mcpServers/skills.yaml

name: skills
version: 0.0.1
schema: v1
mcpServers:
  - name: skills
    command: uv
    args: ["run", "--directory", "/path/to/mcp-demo/server", "skills-mcp"]

Remote harnesses (or several machines) can share one server over HTTP: uv run skills-mcp --transport streamable-http --host 0.0.0.0 --port 8765, then point the client at http://<host>:8765/mcp (url / serverUrl / httpUrl, per harness).

Source 2 files
hooks/register.tsx 91 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, PluginState, Register } from 'claude-code'
3
4import type { SkillPreview } from '../types'
5
6type RemoteSkill = PluginState['skills-mcp']['skills'][number]
7
8const PANE = 'mcp-skills'
9const skills = atom({ plugin: 'skills-mcp', key: 'skills' } as const, [] as RemoteSkill[])
10const status = atom({ plugin: 'skills-mcp', key: 'status' } as const, {})
11const selected = atom({ plugin: 'skills-mcp-browser', key: 'selected' } as const, null as string | null)
12const preview = atom({ plugin: 'skills-mcp-browser', key: 'preview' } as const, null as SkillPreview | null)
13
14async function loadPreview($: EngineInterface, skill: RemoteSkill) {
15  const ran = await $.tool.call({ tool: 'ReadMcpResourceTool', server: skill.server, uri: skill.uri })
16  const text =
17    ran.deny !== undefined ? `Could not read: ${ran.deny}`
18    : ran.isError ? `Could not read: ${String(ran.result)}`
19    : ran.result.error ?? ran.result.contents.map(c => c.text ?? '').join('\n')
20  await update($, preview, () => ({ name: skill.label, text }))
21}
22
23async function useSkill($: EngineInterface, skill: RemoteSkill) {
24  // With skills-mcp-commands enabled the skill is a slash command; else ask in words.
25  const command = skill.label.replace(/[^A-Za-z0-9_-]/g, '-')
26  const isCommand = (await $.command.list()).some(c => c.name === command && c.plugin === 'skills-mcp-commands')
27  await $.prompt.fill({ text: isCommand ? `/${command} ` : `Use the ${skill.label} skill to ` })
28  await $.ui.close({ id: PANE })
29}
30
31export const register: Register = on => {
32  on('session.start', async ($, e, next) => {
33    await $.command.register({
34      name: 'mcp-skills',
35      description: 'Browse the skills an MCP skills server serves',
36    })
37    return next(e)
38  })
39
40  on('command.run', { command: 'mcp-skills' }, async $ => {
41    await $.ui.open({ id: PANE, title: 'MCP skills', focus: true, closeOnEscape: true })
42    return { text: 'MCP skills pane opened.' }
43  })
44
45  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
46    const { Box, Button, Markdown, Text } = $.ui.resolve(e)
47    const list = await read($, skills)
48    const { error } = await read($, status)
49    const chosen = await read($, selected)
50    const shown = await read($, preview)
51    const skill = list.find(s => s.label === chosen)
52    const width = e.props.bodyColumns ?? 60
53
54    if (list.length === 0) {
55      return <Text dimColor>{error ? `Could not read the MCP skill index: ${error}` : 'No connected MCP server serves skill:// resources.'}</Text>
56    }
57
58    return (
59      <Box flexDirection="column">
60        <Text dimColor>
61          {list.length} MCP skills
62        </Text>
63        {list.map(s => (
64          <Button
65            key={`skill-${s.label}`}
66            plain
67            onPress={() => update($, selected, () => s.label)}
68          >
69            {s.label === chosen ? '▸ ' : '  '}
70            {s.label} <Text dimColor>{s.description.slice(0, Math.max(10, width - s.label.length - 6))}</Text>
71          </Button>
72        ))}
73        {skill && (
74          <Box flexDirection="column" marginTop={1}>
75            <Text bold>{skill.label}</Text>
76            <Text>{skill.description}</Text>
77            <Text dimColor>
78              {skill.server} · {skill.uri}
79            </Text>
80            <Box>
81              <Button key="use" variant="primary" label="Use" hotkey="u" onPress={() => useSkill($, skill)} />
82              <Button key="preview" label="Preview" hotkey="p" onPress={() => loadPreview($, skill)} />
83            </Box>
84            {shown?.name === skill.label && <Markdown text={shown.text} />}
85          </Box>
86        )}
87      </Box>
88    )
89  })
90}
91
types/index.d.ts 11 lines
1export type SkillPreview = { name: string; text: string }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'skills-mcp-browser': {
6      selected: string | null
7      preview: SkillPreview | null
8    }
9  }
10}
11