Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,12 @@
.claude/settings.local.json

# Example build artifacts
node_modules/
dist/
__pycache__/
*.pyc
.venv/
*.egg-info/

# Reference repos (cloned for development)
%TEMP%/
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ See [problem-statement.md](docs/problem-statement.md) for full details.
| [Open Questions](docs/open-questions.md) | Unresolved questions with community input |
| [Experimental Findings](docs/experimental-findings.md) | Results from implementations and testing |
| [Related Work](docs/related-work.md) | SEPs, implementations, and external resources |
| [Examples](examples/) | Reference implementations for pattern evaluation |
| [Contributing](CONTRIBUTING.md) | How to participate |

## Stakeholder Groups
Expand Down
8 changes: 5 additions & 3 deletions docs/related-work.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@

## Implementations

Original implementations from external repositories (example implementations in this repo will be added to `examples` folder):

| Implementation | Author | URL | Notes |
| :--- | :--- | :--- | :--- |
| skilljack-mcp | Ola Hungerford | [github.com/olaservo/skilljack-mcp](https://github.com/olaservo/skilljack-mcp) | Skills as tools with dynamic updates |
| mcpGraph skill | Bob Dickinson | [github.com/TeamSparkAI/mcpGraph](https://github.com/TeamSparkAI/mcpGraph) | 875+ line skill for graph orchestration |
| skills-over-mcp | Keith Groves | [github.com/keithagroves/skills-over-mcp](https://github.com/keithagroves/skills-over-mcp) | Example using skills with current MCP primitives |
| skilljack-mcp | Ola Hungerford | [github.com/olaservo/skilljack-mcp](https://github.com/olaservo/skilljack-mcp) | Skills as MCP tools, resources, prompts with dynamic updates |
| mcpGraph skill | Bob Dickinson | [github.com/TeamSparkAI/mcpGraph](https://github.com/TeamSparkAI/mcpGraph) | Complex skill example for graph orchestration |
| skills-over-mcp | Keith Groves | [github.com/keithagroves/skills-over-mcp](https://github.com/keithagroves/skills-over-mcp) | Example using skills as MCP resources with current MCP primitives |
| chrome-devtools-mcp | Anthropic | [github.com/anthropics/anthropic-quickstarts/…/chrome-devtools-mcp](https://github.com/anthropics/anthropic-quickstarts/tree/main/mcp-servers/chrome-devtools-mcp) | Real-world example: `skills/` folder requires separate install path |
| NimbleBrain skills repo | NimbleBrain | [github.com/NimbleBrainInc/skills](https://github.com/NimbleBrainInc/skills) | Monorepo with `.skill` artifact format |
| NimbleBrain registry | NimbleBrain | [registry.nimbletools.ai](https://registry.nimbletools.ai/) | Registry with skill metadata support |
Expand Down
119 changes: 119 additions & 0 deletions examples/skills-as-tools/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Skills as Tools — Reference Implementation

> **Experimental** — This is a minimal reference implementation for evaluation by the Skills Over MCP Interest Group. Not intended for production use.

## Pattern Overview

This example demonstrates **Approach 3** from [`docs/approaches.md`](../../docs/approaches.md): exposing agent skills via MCP tools.

An MCP server scans a directory for SKILL.md files and exposes two tools:

| Tool | Purpose |
| :--- | :--- |
| `list_skills` | Returns skill names and descriptions (progressive disclosure — summaries only, not full content) |
| `read_skill` | Accepts a skill `name` and returns the full SKILL.md content on demand |

This is a **model-controlled** approach: the LLM decides when to invoke skills based on tool descriptions. See [Open Question #9](../../docs/open-questions.md) for the control model discussion.

## How It Works

```
┌─────────────┐ ┌──────────────────┐ ┌──────────────┐
│ MCP Client │────▶│ Skills as Tools │────▶│ Skill Files │
│ (e.g. Claude│◀────│ MCP Server │◀────│ (SKILL.md) │
│ Code) │ └──────────────────┘ └──────────────┘
└─────────────┘
```

1. **Startup**: Server scans the configured skills directory for `*/SKILL.md` files
2. **Discovery**: Parses YAML frontmatter to extract `name` and `description`
3. **Registration**: Registers `list_skills` and `read_skill` as MCP tools; tool descriptions include available skill names
4. **Progressive disclosure**: Agent calls `list_skills` to see what's available, then `read_skill(name)` to load full instructions only when needed
5. **Capability declaration**: Server declares `tools.listChanged` capability (dynamic updates could be wired to a file watcher in a full implementation)

## Implementations

Both implementations expose the same tools with the same behavior. They share the `sample-skills/` directory as test data.

### TypeScript

**Prerequisites**: Node.js >= 18, npm

```bash
cd typescript
npm install
npm run build
```

**Run with MCP Inspector**:
```bash
npx @modelcontextprotocol/inspector node dist/index.js ../sample-skills
```

**Development mode** (no build step):
```bash
npm run dev -- ../sample-skills
```

### Python

**Prerequisites**: Python >= 3.10, pip (or uv)

```bash
cd python
pip install -e .
```

**Run with MCP Inspector**:
```bash
npx @modelcontextprotocol/inspector -- python -m skills_as_tools.server ../sample-skills
```

## Security Features

Both implementations include:

- **Path traversal protection** — Resolved paths are checked against the skills directory boundary using `realpathSync` (TS) / `Path.resolve()` (Python). Symlink escapes are detected.
- **Skill name validation** — `read_skill` looks up names by key in the discovered skills map. User input is never used to construct file paths.
- **File size limits** — Files larger than 1MB are skipped during discovery and rejected on read.
- **Allowlisted file types** — Only `.md` files are readable.
- **Safe YAML parsing** — Python uses `yaml.safe_load()` to prevent code execution. TypeScript uses the `yaml` package which is safe by default.

## Sample Skills

Two sample skills are included in `sample-skills/` for testing:

| Skill | Description | Notes |
| :--- | :--- | :--- |
| `git-commit-review` | Review commits for quality and conventional format | Standalone skill (no extra files) |
| `code-review` | Structured code review methodology | Includes `references/REFERENCE.md` (progressive disclosure) |

## Key Design Decisions

- **Tools over resources**: Tools are model-controlled — the LLM decides when to invoke them based on descriptions. Resources are application-controlled, which experimental findings show leads to lower utilization (see [`docs/experimental-findings.md`](../../docs/experimental-findings.md)).
- **Two tools, not one**: Separating `list_skills` from `read_skill` enables progressive disclosure — the model sees summaries first and only loads full content when needed, saving context tokens.
- **Skill manifest in tool description**: The `list_skills` tool description includes available skill names, so the model knows what's available without making a tool call (following the skilljack pattern).

## What This Example Intentionally Omits

- `skill://` URI resources (see: future skills-as-resources example)
- File watching / live dynamic updates (capability is declared but not wired)
- MCP Prompts for explicit skill invocation
- GitHub sync, configuration UI
- `skill-resource` tool for reading files within skill directories

## Relationship to Other Approaches

| Approach | How it differs |
| :--- | :--- |
| **1. Skills as Primitives** (SEP-2076) | Uses dedicated `skills/list` and `skills/get` protocol methods instead of tools |
| **3. Skills as Tools** (this example) | Uses existing MCP tools primitive — no protocol changes needed |
| **5. Server Instructions** | Uses server instructions to point to resources instead of tools |
| **6. Convention** | This example could become part of a documented convention pattern |

## Inspirations and Attribution

This reference implementation is original code inspired by patterns from:

- **[skilljack-mcp](https://github.com/olaservo/skilljack-mcp)** by [Ola Hungerford](https://github.com/olaservo) — Tool description embedding, progressive disclosure, path security, dynamic updates
- **[skills-over-mcp](https://github.com/keithagroves/skills-over-mcp)** by [Keith Groves](https://github.com/keithagroves) — Resource-based skill exposure, skill URI schemes
20 changes: 20 additions & 0 deletions examples/skills-as-tools/python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[project]
name = "skills-as-tools-example"
version = "0.1.0"
description = "Minimal reference implementation: Skills as MCP Tools (Approach 3)"
requires-python = ">=3.10"
dependencies = [
"mcp>=1.0.0",
"pyyaml>=6.0",
]
license = "Apache-2.0"

[project.scripts]
skills-as-tools = "skills_as_tools.server:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/skills_as_tools"]
Empty file.
92 changes: 92 additions & 0 deletions examples/skills-as-tools/python/src/skills_as_tools/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""
Skills as Tools — MCP Server (Python)

A minimal reference implementation demonstrating Approach 3 from the
Skills Over MCP Interest Group: exposing agent skills via MCP tools.

Exposes two tools:
- list_skills: Returns skill names and descriptions (progressive disclosure)
- read_skill: Returns the full SKILL.md content for a named skill
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think read_skill need to return all content in the skill folder given a skill can get pretty complex.

Also we can think about whether it should be read or install here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually when I looked at your PR about adding skills as resources, that solved this problem very well with the file hierarchy!

Thanks for driving all these and they will be good discussion points.


Inspired by:
- skilljack-mcp by Ola Hungerford (https://github.com/olaservo/skilljack-mcp)
- skills-over-mcp by Keith Groves (https://github.com/keithagroves/skills-over-mcp)

License: Apache-2.0
"""

from __future__ import annotations

import json
import sys
from pathlib import Path

from mcp.server.fastmcp import FastMCP

from .skill_discovery import discover_skills, load_skill_content

# Resolve skills directory from CLI arg or default to ../sample-skills
if len(sys.argv) > 1:
skills_dir = str(Path(sys.argv[1]).resolve())
else:
skills_dir = str(Path(__file__).resolve().parent.parent.parent.parent / "sample-skills")

# Discover skills at startup
skill_map = discover_skills(skills_dir)
skill_names = list(skill_map.keys())

print(
f"[skills-as-tools] Discovered {len(skill_map)} skill(s): "
f"{', '.join(skill_names) or 'none'}",
file=sys.stderr,
)

# Create MCP server
mcp = FastMCP(
name="skills-as-tools-example",
)


@mcp.tool(
description=(
"List all available skills with their names and descriptions. "
f"Currently available: {', '.join(skill_names) or 'none'}"
),
)
def list_skills() -> str:
"""List all available skills (progressive disclosure — summaries only)."""
summaries = [
{"name": s.name, "description": s.description}
for s in skill_map.values()
]
return json.dumps(summaries, indent=2)


@mcp.tool(
description=(
"Read the full instructions for a specific skill by name. "
"Returns the complete SKILL.md content with step-by-step guidance."
),
)
def read_skill(name: str) -> str:
"""Read a skill's full SKILL.md content by name."""
# Security: lookup by key only — never construct paths from user input
skill = skill_map.get(name)

if not skill:
available = ", ".join(skill_names) or "none"
return f'Skill "{name}" not found. Available skills: {available}'

try:
return load_skill_content(skill.path, skills_dir)
except (OSError, ValueError) as exc:
return f'Failed to load skill "{name}": {exc}'


def main() -> None:
"""Entry point: run the MCP server via stdio transport."""
mcp.run(transport="stdio")


if __name__ == "__main__":
main()
Loading