Skip to main content
Glama
cubicecho

MCP Skills Manager

by cubicecho
README.md
# MCP Skills Manager

An [MCP](https://modelcontextprotocol.io) server that serves **skills** —
reusable markdown documents (instructions, playbooks, references) — to agents.
Write and organize skills in a web UI; agents load them over MCP.

Every skill is exposed two ways at once:

- as an MCP **tool** — calling it returns the skill's markdown body (plus a note
  listing any bundled supporting files), so an agent can pull a skill on demand;
- as an MCP **resource** at `skill://<name>` — for clients that browse and
  attach resources. Bundled supporting files are resources too
  (`skill://<name>/<path>`), the endpoint advertises RFC 6570 **resource
  templates** with argument **completion** (skill names and file paths), and
  `resources/list` is **paginated**. Clients that subscribe get **live updates**
  (`resources/list_changed` + `resources/updated`) when skills change on disk —
  always over stdio, and over HTTP when `httpLiveUpdates` is enabled.

Every endpoint also serves two meta-tools for **discovery**: `list_skills`
returns a text catalogue of the available skills, one line each — name, tags,
supporting files, last change and description — with no bodies, and
`search_skills` filters that catalogue by a free-text query and/or tags, so an
agent can find what's relevant by intent before loading anything.

Skills are advertised as tools in one of two modes (a global default, optionally
overridden per workspace): **per-skill** (the default — one no-arg tool per skill)
or **loader** (a single `load_skill(name)` tool that keeps the tool footprint
fixed no matter how many skills exist).

Agents can also **author skills over MCP**: every endpoint exposes authoring
tools (`create_skill`, `update_skill`, `edit_skill`, `rename_skill`,
`delete_skill`, plus supporting-file tools) so an agent can write and refine its
own skills. `edit_skill` swaps exact passages of a skill's body or of one of its
supporting files, so a small change does not resend the whole text. These are
gated on a setting (on by default), and `disabledAuthoringTools` in
`settings.json` leaves named tools out while the rest stay. To protect an individual skill, mark it
**read-only** (the toggle in the skill editor, or `readonly: true` in its
frontmatter): agents can still load it, but every authoring tool refuses to
edit, rename or delete it — or, for a directory skill, anything in its folder.

The root endpoint `/mcp` serves **all** skills. **Workspaces** are named subsets
served at their own endpoint `/mcp/w/<slug>`, so you can hand a specific agent
just the skills it needs. Everything is also available over **stdio** for local
clients.

## Features

- 📝 **Markdown editor** in the web UI with live split-pane preview
- 🗂️ **Skill CRUD** — create, rename, edit, delete, import (`.md`/dir/`.zip`),
  and export skills; drag-and-drop supporting files for directory-format skills
- 🌿 **Git-linked skills** — point a skill at a folder in a git repo and pull
  the newest version with **Sync now**
- 🧩 **Workspaces** — group skills into filtered endpoints
- 🔍 **Discovery meta-tools** — `list_skills` and `search_skills` on every
  endpoint, plus `tags` on skills for organising and filtering
- 🤖 **MCP authoring** — agents can create and refine their own skills over MCP
- ⌨️ **Skill prompts** — a skill's `prompts/*.md` files are served as MCP
  prompts, so a client can start the skill from a slash command
- 🔧 **Tool modes** — advertise skills as one tool each (`per-skill`) or a
  single `load_skill` loader, globally or per workspace
- 📁 **Two skill formats** — a flat `<name>.md` file, or a
  `<name>/SKILL.md` directory (Claude Code convention) with supporting files
- 🔌 **HTTP and stdio** transports
- 📡 **Live resource updates** — subscribers are notified when skills change on
  disk (always over stdio; over HTTP with `httpLiveUpdates`)
- 🗃️ **Flat-file config** — hand-editable on disk, watched and hot-reloaded
- 🔐 **Bearer-token auth** guarding the API and MCP endpoints

## Quick start

```bash
npm install
npm run dev          # server on :3001, web UI on :3000 (proxies to the server)
```

Open http://localhost:3000. On first run a bearer token is generated into
`data/config/settings.json` and printed to the server logs; paste it into the
web UI when prompted.

### Production

```bash
npm run build        # shared → server → app
npm run start        # HTTP server on :3000, also serving the built web UI
```

Or with Docker:

```bash
docker compose up --build      # mounts ./data at /data, serves on :3000
```

## Connecting an agent

### HTTP

Point an MCP client at the streamable-HTTP endpoint:

- All skills: `http://localhost:3000/mcp`
- A workspace: `http://localhost:3000/mcp/w/<slug>`

Send the bearer token as `Authorization: Bearer <token>`.

### stdio

Run the packaged stdio entry (installed as the `mcp-skills-stdio` bin):

```bash
# all skills
mcp-skills-stdio --data-dir /path/to/data

# only a workspace's skills
mcp-skills-stdio --data-dir /path/to/data --workspace <slug>
```

Example Claude Desktop / MCP client config:

```json
{
  "mcpServers": {
    "skills": {
      "command": "mcp-skills-stdio",
      "args": ["--data-dir", "/path/to/data", "--workspace", "backend"]
    }
  }
}
```

## Skills on disk

Skills live under `DATA_DIR/skills/` in either of two shapes:

**Flat file** — `DATA_DIR/skills/commit-messages.md`:

```markdown
---
name: commit-messages
description: Write clear, conventional git commit messages.
---

# Writing good commit messages

...the skill body...
```

**Directory** (Claude Code convention) — `DATA_DIR/skills/pdf-forms/SKILL.md`
plus any supporting files (`reference.md`, scripts, templates). Supporting files
are listed on the skill and referenced in the rendered tool output so the agent
knows they exist.

The frontmatter `name` must be a slug: lowercase letters, digits, `.`, `_`, `-`
(max 64 chars). `description` is surfaced as the MCP tool/resource description.
An optional `tags` key (a comma-separated string or a YAML list) organises
skills and feeds the `search_skills` filter. `readonly: true` stops agents from
modifying the skill over MCP (the web UI and REST API can still edit it).
Unknown frontmatter keys are preserved across round-trips.

### Skill prompts

A directory skill can carry **prompts**: each Markdown file directly inside its
`prompts/` folder is served as an MCP prompt named after the file, on every
endpoint that serves the skill. Clients that support MCP prompts offer them as
slash commands (Claude Code shows `prompts/refactor.md` as
`/mcp__<server>__refactor`), so a prompt is the way to start a skill by name
instead of asking the agent to go and find it.

`DATA_DIR/skills/coding-standards/prompts/refactor.md`:

```markdown
---
description: Refactor a scope with the coding-standards workflow
arguments:
  - name: scope
    description: Files, directory or concept to refactor
    required: false
---
Refactor: {{scope}}

Load the `coding-standards` skill and follow its refactor workflow.
```

- The frontmatter is optional. `description` is shown in the client's prompt
  list; `arguments` declares what the prompt takes (`name`, and optionally
  `description` and `required`).
- `{{name}}` in the body is replaced by the argument of that name. An optional
  argument that is left out becomes an empty string; a required one that is left
  out fails the request. A placeholder that names no declared argument is left
  as written.
- The body is returned as a single user message.
- The file name (without `.md`) is the prompt name, so it must be letters,
  digits, `-` and `_`. Other files in `prompts/`, and files in its sub-folders,
  are ordinary supporting files.
- When two served skills have a prompt of the same name, the first skill by
  sorted name keeps the bare name and each later one is served as
  `<skill>__<name>`.
- Prompt files are written and read like any other supporting file
  (`write_skill_file`, the web UI, or on disk). They are left out of the
  "bundled supporting files" list an agent sees when it loads the skill.

### Git-linked skills

A skill can be linked to a folder in a git repo — a published skill you want
this server to track rather than copy once. In the web UI use **New skill →
From Git**, or the **Link to Git source** button on an existing skill (which
replaces its content). Give a clone URL, optionally a branch or tag (blank
follows the default branch) and the folder holding the `SKILL.md` (blank is the
repo root); pasting a GitHub/GitLab folder link such as
`https://github.com/owner/repo/tree/main/skills/my-skill` fills all three.

The link is stored in the skill's frontmatter, with the commit the last sync
fetched:

```markdown
---
name: my-skill
description: …
source:
  repo: https://github.com/owner/repo
  ref: main            # optional
  path: skills/my-skill  # optional
  commit: 4f2a9c1…     # written by sync
  syncedAt: 2026-10-04T12:00:00.000Z
---
```

- **Sync is manual.** *Sync now* (or `POST /api/skills/:name/sync`) replaces the
  skill's whole folder with the repo's copy; files removed upstream are removed
  here. Nothing syncs on a timer.
- **Linked skills are locked.** Their content cannot be edited in the web UI,
  over the REST API or by agents over MCP. **Unlink** keeps the current content
  and makes the skill a normal, editable one again.
- **Local settings survive a sync:** the skill's id here (even when the repo's
  `SKILL.md` names it differently), `global`, `readonly` and workspace
  membership. The skill can still be renamed and deleted.
- **Repo URLs** must be `https://…`, `ssh://…` or `user@host:path`. An https URL
  may not embed a username or token — the URL is written into `SKILL.md`, which
  agents read. Symlinks in the repo folder are skipped, and a folder may hold at
  most 500 files.
- **Private repos** use the credentials of the machine (or container) running
  the server: an SSH key the server's user can read, or a git credential helper
  for https. git never prompts — a repo it cannot read fails the sync with git's
  own error. With Docker, mount a key and `known_hosts` (e.g.
  `-v ~/.ssh:/root/.ssh:ro`).
- **`git` must be on the server's `PATH`.** The Docker image ships it.

## Workspaces

A workspace is a JSON file at `DATA_DIR/config/workspaces/<slug>.json`:

```json
{
  "name": "Backend",
  "slug": "backend",
  "enabled": true,
  "description": "Skills for backend work.",
  "skills": ["commit-messages", "pdf-forms"]
}
```

It is served at `/mcp/w/backend` (HTTP) and via `--workspace backend` (stdio).
Disabled workspaces return 404. An optional `skillToolMode` (`per-skill` or
`loader`) overrides the global default for this workspace only. Manage workspaces
from the **Workspaces** page in the web UI, or edit the files directly — changes
are picked up automatically.

## Configuration

Everything lives under `DATA_DIR` (default `./data`):

```
data/
├── config/
│   ├── settings.json          # port, auth token, auth/authoring toggles, tool mode, httpLiveUpdates
│   └── workspaces/
│       └── <slug>.json        # one file per workspace
└── skills/
    ├── <name>.md              # flat-file skill
    └── <name>/                # directory-format skill
        ├── SKILL.md
        └── <supporting files>
```

Edits on disk are watched (debounced) and hot-reloaded; you can also force a
re-read with the **Reload** button on the Settings page or `POST /api/reload`.

### Environment variables

| Variable | Default | Description |
| --- | --- | --- |
| `DATA_DIR` | `./data` | Root directory for skills and config |
| `PORT` | `3000` (or `settings.json`) | HTTP listen port |
| `MCP_SKILLS_TOKEN` | — | Bearer token; overrides the one in `settings.json` |
| `SECURE_LOCAL_NET` | `false` | Set `true` to disable auth entirely (trusted networks only) |
| `HTTP_KEEP_ALIVE_TIMEOUT_MS` | `75000` | How long an idle client connection stays open (Node's own default is 5 s). Keep it above the idle timeout of any reverse proxy in front; `0` never closes one |

If no token is configured and `SECURE_LOCAL_NET` is not set, a random token is
generated into `settings.json` on first run and logged.

## HTTP API

All routes require the bearer token (unless `SECURE_LOCAL_NET=true`).

| Method | Path | Description |
| --- | --- | --- |
| `GET` | `/api/status` | Version, uptime, skill/workspace counts, auth mode, port |
| `GET` | `/api/settings` | Read settings (auth/authoring toggles, tool mode, live updates) |
| `PATCH` | `/api/settings` | Update `authoringEnabled` / `disabledAuthoringTools` / `skillToolMode` / `httpLiveUpdates` |
| `GET` | `/api/skills` | List skills (summaries) |
| `POST` | `/api/skills` | Create a skill |
| `POST` | `/api/skills/import` | Import an uploaded `.md` / directory / `.zip` |
| `POST` | `/api/skills/import-git` | Create a skill linked to a folder in a git repo |
| `GET` | `/api/skills/:name` | Get a skill (with body) |
| `PATCH` | `/api/skills/:name` | Update body/description/tags/global/readOnly, or rename |
| `DELETE` | `/api/skills/:name` | Delete a skill |
| `PUT` | `/api/skills/:name/source` | Link a skill to a git source and replace its content from it |
| `DELETE` | `/api/skills/:name/source` | Unlink it, keeping its content |
| `POST` | `/api/skills/:name/sync` | Re-fetch a linked skill → `{ skill, changed }` |
| `GET` | `/api/skills/:name/export` | Download the skill as a `.zip` |
| `GET` | `/api/skills/:name/files/content?path=` | Read one supporting file |
| `PUT` | `/api/skills/:name/files` | Add/overwrite a supporting file (promotes to a dir) |
| `POST` | `/api/skills/:name/folders` | Create an empty sub-folder |
| `POST` | `/api/skills/:name/files/move` | Rename / move a file or folder |
| `DELETE` | `/api/skills/:name/files?path=` | Delete a file or folder |
| `GET` | `/api/workspaces` | List workspaces |
| `POST` | `/api/workspaces` | Create a workspace |
| `GET` | `/api/workspaces/:slug` | Get a workspace |
| `PATCH` | `/api/workspaces/:slug` | Update a workspace |
| `DELETE` | `/api/workspaces/:slug` | Delete a workspace |
| `POST` | `/api/reload` | Re-read all config from disk |
| `ALL` | `/mcp` | MCP endpoint — all skills |
| `ALL` | `/mcp/w/:slug` | MCP endpoint — a workspace's skills |

## Architecture

Monorepo with npm workspaces:

- **`shared/`** — zod schemas and inferred types; the single source of truth for
  config-file shapes and REST DTOs.
- **`server/`** — Express 5 + the MCP TypeScript SDK. A `ConfigStore` owns the
  flat-file state (atomic writes, chokidar watching); the gateway builds an MCP
  `Server` exposing skills as tools and resources over HTTP
  (`StreamableHTTPServerTransport` — stateless by default, or stateful sessions
  with SSE push when `httpLiveUpdates` is on) or stdio.
- **`app/`** — React 19 + Vite + shadcn/ui + TanStack Router/Query. The
  markdown editor uses `react-markdown` + `remark-gfm`.

See [`AGENTS.md`](AGENTS.md) for development conventions.

## License

MIT