Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
Binary file added docs/docs/assets/social/mcp/skills-over-mcp.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
341 changes: 341 additions & 0 deletions docs/docs/assets/tui/skills-over-mcp.cast

Large diffs are not rendered by default.

70 changes: 70 additions & 0 deletions docs/docs/mcp/skills-over-mcp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
title: Skills over MCP
social:
title: Skills over MCP
tagline: Install Agent Skills from MCP servers that implement SEP-2640.
description: Install Agent Skills from MCP servers that implement SEP-2640.
alt: fast-agent social card - Skills over MCP
---

`fast-agent` supports the registry portion of
[SEP-2640: Skills Extension](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/93d7a9ddb20d4b3594f4a1be7508ee47f0718f17/seps/2640-skills-extension.md).
SEP-2640 is still a draft, so this support is intentionally narrow: an MCP
server can act as a skills registry for installation when it advertises the
`io.modelcontextprotocol/skills` extension capability.

When a connected MCP server advertises this capability, `fast-agent` shows it as
an MCP-backed skills registry. Opening `/skills registry` reads
`skill://index.json` and lists concrete `skill-md` entries; installing a selected
entry reads that skill's `SKILL.md` into the normal managed skills directory.
Installed skills then behave like other local skills and include MCP server
provenance in their sidecar metadata.

## Trying it

Run or connect to a SEP-2640-enabled MCP server. This example uses a local copy
of the Hugging Face MCP Server:

```text
/mcp connect http://localhost:3000/mcp
/mcp
/skills registry
/skills registry <mcp-server>
/skills available
/skills add <number|name>
```

`/mcp` shows when SEP-2640 Skills over MCP is enabled and points you to
`/skills registry` to select the MCP server as the current install source.

<div
class="fa-terminal-demo"
data-fa-asciinema-cast="../../assets/tui/skills-over-mcp.cast"
data-fa-asciinema-cols="96"
data-fa-asciinema-rows="22"
data-fa-asciinema-poster="npt:0:02"
data-fa-asciinema-speed="1"
data-fa-asciinema-idle-time-limit="1.3"
data-fa-asciinema-fit="width"
>
<div class="fa-terminal-theme-switch" aria-label="Terminal theme">
<button type="button" data-fa-terminal-theme="auto">Auto</button>
<button type="button" data-fa-terminal-theme="light">Light</button>
<button type="button" data-fa-terminal-theme="dark">Dark</button>
</div>
<div data-fa-asciinema-target></div>
</div>

<!--
Cast asset:
- Source: docs/docs/assets/tui/skills-over-mcp.cast
- Regenerate: uv run scripts/docs.py cast-build skills-over-mcp
- Replay locally: asciinema play docs/docs/assets/tui/skills-over-mcp.cast
-->

## Current scope

This implementation uses MCP as a registry for installation. It does not expose
MCP-served skill resources directly to the model, and it does not make active
skills read supporting files from the MCP server. That deeper resource-loading
workflow is planned separately.
21 changes: 18 additions & 3 deletions docs/social_cards/contact-sheet.html
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
<h1>fast-agent social cards</h1>
<p>Generated review sheet for committed Open Graph/Twitter images.</p>
</div>
<div class="summary">64 cards · 1200×630px target · 1000 KB max</div>
<div class="summary">65 cards · 1200×630px target · 1000 KB max</div>
</header>

<section>
Expand Down Expand Up @@ -595,6 +595,21 @@ <h3>MCP Resources</h3>
</div>
</article>

<article class="card ok">
<a class="thumb" href="../docs/assets/social/mcp/skills-over-mcp.png"><img src="../docs/assets/social/mcp/skills-over-mcp.png" alt="Skills over MCP"></a>
<div class="meta">
<h3>Skills over MCP</h3>
<dl>
<div><dt>Source</dt><dd>mcp/skills-over-mcp.md</dd></div>
<div><dt>Output</dt><dd>docs/assets/social/mcp/skills-over-mcp.png</dd></div>
<div><dt>Badge</dt><dd>MCP</dd></div>
<div><dt>Theme</dt><dd>doc / protocol-lines</dd></div>
<div><dt>Status</dt><dd><span class="pill">ok</span></dd></div>
<div><dt>Size</dt><dd>1200×630 · 25 KB</dd></div>
</dl>
</div>
</article>

<article class="card ok">
<a class="thumb" href="../docs/assets/social/mcp/state_transfer.png"><img src="../docs/assets/social/mcp/state_transfer.png" alt="MCP State Transfer"></a>
<div class="meta">
Expand Down Expand Up @@ -886,9 +901,9 @@ <h3>OpenRouter</h3>
</article>

<article class="card ok">
<a class="thumb" href="../docs/assets/social/models/providers/xai.png"><img src="../docs/assets/social/models/providers/xai.png" alt="xAI / Grok"></a>
<a class="thumb" href="../docs/assets/social/models/providers/xai.png"><img src="../docs/assets/social/models/providers/xai.png" alt="xAI"></a>
<div class="meta">
<h3>xAI / Grok</h3>
<h3>xAI</h3>
<dl>
<div><dt>Source</dt><dd>models/providers/xai.md</dd></div>
<div><dt>Output</dt><dd>docs/assets/social/models/providers/xai.png</dd></div>
Expand Down
1 change: 1 addition & 0 deletions docs/zensical.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ nav = [
},
{ "MCP" = [
{ "Configuring Servers" = "mcp/index.md" },
{ "Skills over MCP" = "mcp/skills-over-mcp.md" },
{ "Deploying as an MCP Server" = "mcp/mcp-server.md" },
{ "Inspecting Servers" = "mcp/mcp_display.md" },
{ "Quickstart - Elicitations" = "mcp/elicitations.md" },
Expand Down
99 changes: 95 additions & 4 deletions scripts/docs_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,30 @@ def _skills_slash_commands_scenario() -> TerminalCastScenario:
)


def _skills_over_mcp_scenario() -> TerminalCastScenario:
command = os.environ.get(
"FAST_AGENT_SKILLS_MCP_DEMO_COMMAND",
"fast-agent -x --model passthrough",
)
return TerminalCastScenario(
name="skills-over-mcp",
title="fast-agent Skills over MCP",
output=ASSETS / "tui" / "skills-over-mcp.cast",
cols=int(os.environ.get("FAST_AGENT_SKILLS_MCP_DEMO_COLS", "96")),
rows=int(os.environ.get("FAST_AGENT_SKILLS_MCP_DEMO_ROWS", "22")),
idle_time_limit=float(os.environ.get("FAST_AGENT_SKILLS_MCP_DEMO_IDLE_TIME_LIMIT", "1.3")),
prompt="",
shell_command=command,
)


def _scenarios() -> dict[str, TerminalCastScenario]:
scenarios = [
_tui_shell_scenario(),
_model_picker_scenario(),
_skills_direct_install_scenario(),
_skills_slash_commands_scenario(),
_skills_over_mcp_scenario(),
]
return {scenario.name: scenario for scenario in scenarios}

Expand Down Expand Up @@ -135,7 +153,7 @@ def _model_picker_record_script(scenario: TerminalCastScenario) -> str:

tmux kill-session -t "$SESSION" 2>/dev/null || true
tmux new-session -d -s "$SESSION" -x {scenario.cols} -y {scenario.rows} \\
"DEMO_FAST_AGENT_HOME=\\$(mktemp -d) && printf '{{}}\\n' > \\\"\\$DEMO_FAST_AGENT_HOME/fast-agent.yaml\\\" && export FAST_AGENT_HOME=\\\"\\$DEMO_FAST_AGENT_HOME\\\" && DEMO_WORKDIR=\\$(mktemp -d -t fast-agent-model-picker.XXXXXX) && cd \\\"\\$DEMO_WORKDIR\\\" && unset ENVIRONMENT_DIR FAST_AGENT_RUNTIME_ENVIRONMENT VIRTUAL_ENV FAST_AGENT_MODEL && TERM=xterm-256color COLORTERM=truecolor FORCE_COLOR=1 FAST_AGENT_KEYRING_NOTICE=0 bash --noprofile --norc"
"DEMO_FAST_AGENT_HOME=\\$(mktemp -d) && printf '{{}}\\n' > \\\"\\$DEMO_FAST_AGENT_HOME/fast-agent.yaml\\\" && export FAST_AGENT_HOME=\\\"\\$DEMO_FAST_AGENT_HOME\\\" && DEMO_WORKDIR=\\$(mktemp -d -t fast-agent-model-picker.XXXXXX) && cd \\\"\\$DEMO_WORKDIR\\\" && unset ENVIRONMENT_DIR FAST_AGENT_RUNTIME_ENVIRONMENT VIRTUAL_ENV FAST_AGENT_MODEL NO_COLOR && TERM=xterm-256color COLORTERM=truecolor FORCE_COLOR=1 FAST_AGENT_KEYRING_NOTICE=0 bash --noprofile --norc"
tmux set-option -t "$SESSION" status off >/dev/null

(
Expand Down Expand Up @@ -202,7 +220,7 @@ def _skills_direct_install_record_script(scenario: TerminalCastScenario) -> str:
Use this skill to demonstrate direct installation from a local repository.
SKILL
git -C skill-repo init -q && git -C skill-repo config user.email docs-demo@example.com && git -C skill-repo config user.name 'Docs Demo' && git -C skill-repo add . && git -C skill-repo commit -q -m 'Initial demo skill'
unset ENVIRONMENT_DIR FAST_AGENT_RUNTIME_ENVIRONMENT VIRTUAL_ENV && TERM=xterm-256color COLORTERM=truecolor FORCE_COLOR=1 FAST_AGENT_KEYRING_NOTICE=0 bash --noprofile --norc"
unset ENVIRONMENT_DIR FAST_AGENT_RUNTIME_ENVIRONMENT VIRTUAL_ENV NO_COLOR && TERM=xterm-256color COLORTERM=truecolor FORCE_COLOR=1 FAST_AGENT_KEYRING_NOTICE=0 bash --noprofile --norc"
tmux set-option -t "$SESSION" status off >/dev/null

(
Expand Down Expand Up @@ -275,7 +293,7 @@ def _skills_slash_commands_record_script(scenario: TerminalCastScenario) -> str:
Use this skill to demonstrate /skills add and /skills update.
SKILL
git -C skill-repo init -q && git -C skill-repo config user.email docs-demo@example.com && git -C skill-repo config user.name 'Docs Demo' && git -C skill-repo add . && git -C skill-repo commit -q -m 'Initial demo skill'
unset ENVIRONMENT_DIR FAST_AGENT_RUNTIME_ENVIRONMENT VIRTUAL_ENV && TERM=xterm-256color COLORTERM=truecolor FORCE_COLOR=1 FAST_AGENT_KEYRING_NOTICE=0 TUI__COMPLETION_MENU_RESERVED_LINES=${{TUI__COMPLETION_MENU_RESERVED_LINES:-4}} bash --noprofile --norc"
unset ENVIRONMENT_DIR FAST_AGENT_RUNTIME_ENVIRONMENT VIRTUAL_ENV NO_COLOR && TERM=xterm-256color COLORTERM=truecolor FORCE_COLOR=1 FAST_AGENT_KEYRING_NOTICE=0 TUI__COMPLETION_MENU_RESERVED_LINES=${{TUI__COMPLETION_MENU_RESERVED_LINES:-4}} bash --noprofile --norc"
tmux set-option -t "$SESSION" status off >/dev/null

(
Expand Down Expand Up @@ -309,6 +327,75 @@ def _skills_slash_commands_record_script(scenario: TerminalCastScenario) -> str:
"""


def _skills_over_mcp_record_script(scenario: TerminalCastScenario) -> str:
startup_wait = os.environ.get("FAST_AGENT_SKILLS_MCP_DEMO_STARTUP_WAIT", "3.5")
connect_wait = os.environ.get("FAST_AGENT_SKILLS_MCP_DEMO_CONNECT_WAIT", "4.0")
command_wait = os.environ.get("FAST_AGENT_SKILLS_MCP_DEMO_COMMAND_WAIT", "1.4")
final_wait = os.environ.get("FAST_AGENT_SKILLS_MCP_DEMO_FINAL_WAIT", "1.0")
typing_delay = os.environ.get("FAST_AGENT_SKILLS_MCP_DEMO_TYPING_DELAY", "0.035")
server_url = os.environ.get("FAST_AGENT_SKILLS_MCP_DEMO_SERVER", "http://localhost:3000/mcp")
session = f"fast_agent_docs_{scenario.name.replace('-', '_')}"
command = scenario.shell_command.replace("'", "'\"'\"'")
server_url = server_url.replace("'", "'\"'\"'")
return f"""#!/usr/bin/env bash
set -euo pipefail

SESSION='{session}'
ROOT='{ROOT}'

type_slow() {{
local target="$1"
local text="$2"
local delay="$3"
local i char
for (( i=0; i<${{#text}}; i++ )); do
char="${{text:i:1}}"
tmux send-keys -l -t "$target" "$char"
sleep "$delay"
done
}}

tmux kill-session -t "$SESSION" 2>/dev/null || true
tmux new-session -d -s "$SESSION" -x {scenario.cols} -y {scenario.rows} \\
"DEMO_FAST_AGENT_HOME=\\$(mktemp -d) && printf '{{}}\\n' > \\\"\\$DEMO_FAST_AGENT_HOME/fast-agent.yaml\\\" && export FAST_AGENT_HOME=\\\"\\$DEMO_FAST_AGENT_HOME\\\" && DEMO_WORKDIR=\\$(mktemp -d -t fast-agent-skills-mcp.XXXXXX) && cd \\\"\\$DEMO_WORKDIR\\\" && unset ENVIRONMENT_DIR FAST_AGENT_RUNTIME_ENVIRONMENT VIRTUAL_ENV NO_COLOR && TERM=xterm-256color COLORTERM=truecolor FORCE_COLOR=1 FAST_AGENT_KEYRING_NOTICE=0 TUI__COMPLETION_MENU_RESERVED_LINES=${{TUI__COMPLETION_MENU_RESERVED_LINES:-4}} bash --noprofile --norc"
tmux set-option -t "$SESSION" status off >/dev/null

(
sleep 1
type_slow "$SESSION" '{command}' 0.035
tmux send-keys -t "$SESSION" Enter
sleep {startup_wait}
type_slow "$SESSION" '/mcp connect --name hf {server_url}' {typing_delay}
tmux send-keys -t "$SESSION" Enter
sleep {connect_wait}
type_slow "$SESSION" '/mcp' {typing_delay}
tmux send-keys -t "$SESSION" Enter
sleep {command_wait}
type_slow "$SESSION" '/skills registry' {typing_delay}
tmux send-keys -t "$SESSION" Enter
sleep {command_wait}
type_slow "$SESSION" '/skills registry hf' {typing_delay}
tmux send-keys -t "$SESSION" Enter
sleep {command_wait}
type_slow "$SESSION" '/skills add' {typing_delay}
tmux send-keys -t "$SESSION" Enter
sleep {command_wait}
tmux send-keys -t "$SESSION" Enter
sleep 0.4
type_slow "$SESSION" '/skills search dataset viewer' {typing_delay}
tmux send-keys -t "$SESSION" Enter
sleep {command_wait}
type_slow "$SESSION" '/skills add huggingface-datasets' {typing_delay}
tmux send-keys -t "$SESSION" Enter
sleep {command_wait}
sleep {final_wait}
tmux kill-session -t "$SESSION" 2>/dev/null || true
) &

tmux attach-session -t "$SESSION" || true
"""


def _required_assets() -> list[Path]:
return [
VENDOR_ASCIINEMA / "README.md",
Expand Down Expand Up @@ -352,6 +439,8 @@ def _record_script(scenario: TerminalCastScenario) -> str:
return _skills_direct_install_record_script(scenario)
if scenario.name == "skills-slash-commands":
return _skills_slash_commands_record_script(scenario)
if scenario.name == "skills-over-mcp":
return _skills_over_mcp_record_script(scenario)

typing_delay = os.environ.get("FAST_AGENT_TUI_DEMO_TYPING_DELAY", "0.055")
shell_delay = os.environ.get("FAST_AGENT_TUI_DEMO_SHELL_TYPING_DELAY", "0.045")
Expand Down Expand Up @@ -396,7 +485,7 @@ def _record_script(scenario: TerminalCastScenario) -> str:

tmux kill-session -t "$SESSION" 2>/dev/null || true
tmux new-session -d -s "$SESSION" -x {scenario.cols} -y {scenario.rows} \\
"DEMO_FAST_AGENT_HOME=\\$(mktemp -d) && printf '{{}}\\n' > \\\"\\$DEMO_FAST_AGENT_HOME/fast-agent.yaml\\\" && export FAST_AGENT_HOME=\\\"\\$DEMO_FAST_AGENT_HOME\\\" && DEMO_WORKDIR=\\$(mktemp -d -t fast-agent-demo.XXXXXX) && cd \\\"\\$DEMO_WORKDIR\\\" && git init -q && git config user.email docs@example.invalid && git config user.name 'Docs Demo' && printf '# Demo workspace\\n' > README.md && git add README.md && git commit -qm init && printf '\\nLocal edit\\n' >> README.md && unset ENVIRONMENT_DIR FAST_AGENT_RUNTIME_ENVIRONMENT VIRTUAL_ENV && TERM=xterm-256color COLORTERM=truecolor FORCE_COLOR=1 FAST_AGENT_KEYRING_NOTICE=0 TUI__COMPLETION_MENU_RESERVED_LINES=${{TUI__COMPLETION_MENU_RESERVED_LINES:-4}} bash --noprofile --norc"
"DEMO_FAST_AGENT_HOME=\\$(mktemp -d) && printf '{{}}\\n' > \\\"\\$DEMO_FAST_AGENT_HOME/fast-agent.yaml\\\" && export FAST_AGENT_HOME=\\\"\\$DEMO_FAST_AGENT_HOME\\\" && DEMO_WORKDIR=\\$(mktemp -d -t fast-agent-demo.XXXXXX) && cd \\\"\\$DEMO_WORKDIR\\\" && git init -q && git config user.email docs@example.invalid && git config user.name 'Docs Demo' && printf '# Demo workspace\\n' > README.md && git add README.md && git commit -qm init && printf '\\nLocal edit\\n' >> README.md && unset ENVIRONMENT_DIR FAST_AGENT_RUNTIME_ENVIRONMENT VIRTUAL_ENV NO_COLOR && TERM=xterm-256color COLORTERM=truecolor FORCE_COLOR=1 FAST_AGENT_KEYRING_NOTICE=0 TUI__COMPLETION_MENU_RESERVED_LINES=${{TUI__COMPLETION_MENU_RESERVED_LINES:-4}} bash --noprofile --norc"
tmux set-option -t "$SESSION" status off >/dev/null

(
Expand Down Expand Up @@ -479,6 +568,8 @@ def _is_terminal_teardown_event(line: str) -> bool:
or "[detached" in output
or "\x1b[?1049l" in output
or "\u001b[?1049l" in output
or "\x1b[H\x1b[2J" in output
or "\u001b[H\u001b[2J" in output
)


Expand Down
2 changes: 1 addition & 1 deletion src/fast_agent/commands/command_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ class CommandSpec:
action="registry",
aliases=("source",),
help="Set the skills registry",
usage="/skills registry [<number|url|path>]",
usage="/skills registry [<number|url|path|mcp-server>]",
examples=("/skills registry",),
arguments=(
CommandArgumentSpec(
Expand Down
26 changes: 23 additions & 3 deletions src/fast_agent/commands/handlers/mcp_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,9 @@ async def _resolve_configured_server_alias(
return candidate if candidate in configured_names else None


def _format_added_summary(tools_added_count: int, prompts_added_count: int) -> Text:
def _format_added_summary(
tools_added_count: int, prompts_added_count: int, skills_count: int | None = None
) -> Text:
tool_word = "tool" if tools_added_count == 1 else "tools"
prompt_word = "prompt" if prompts_added_count == 1 else "prompts"

Expand All @@ -263,7 +265,13 @@ def _format_added_summary(tools_added_count: int, prompts_added_count: int) -> T
summary.append(str(tools_added_count), style="bold bright_cyan")
summary.append(f" {tool_word} and ", style="dim")
summary.append(str(prompts_added_count), style="bold bright_cyan")
summary.append(f" {prompt_word}.", style="dim")
summary.append(f" {prompt_word}", style="dim")
if skills_count is not None:
skill_word = "skill" if skills_count == 1 else "skills"
summary.append("; ", style="dim")
summary.append(str(skills_count), style="bold bright_cyan")
summary.append(f" {skill_word} available from the server", style="dim")
summary.append(".", style="dim")
return summary


Expand All @@ -273,6 +281,7 @@ def _format_refreshed_summary(
prompts_refreshed_count: int,
tools_added_count: int,
prompts_added_count: int,
skills_count: int | None = None,
) -> Text:
tool_word = "tool" if tools_refreshed_count == 1 else "tools"
prompt_word = "prompt" if prompts_refreshed_count == 1 else "prompts"
Expand All @@ -285,7 +294,13 @@ def _format_refreshed_summary(
summary.append(str(prompts_refreshed_count), style="bold bright_cyan")
summary.append(f" {prompt_word} (", style="dim")
summary.append(str(new_count), style="bold bright_cyan")
summary.append(" new).", style="dim")
summary.append(" new)", style="dim")
if skills_count is not None:
skill_word = "skill" if skills_count == 1 else "skills"
summary.append("; ", style="dim")
summary.append(str(skills_count), style="bold bright_cyan")
summary.append(f" {skill_word} available from the server", style="dim")
summary.append(".", style="dim")
return summary


Expand Down Expand Up @@ -1335,6 +1350,7 @@ async def emit_oauth_event(event: OAuthEvent) -> None:
prompts_added = getattr(result, "prompts_added", [])
tools_total = getattr(result, "tools_total", None)
prompts_total = getattr(result, "prompts_total", None)
skills_total = getattr(result, "skills_total", None)
warnings = getattr(result, "warnings", [])
already_attached = bool(getattr(result, "already_attached", False))

Expand Down Expand Up @@ -1382,6 +1398,7 @@ async def emit_oauth_event(event: OAuthEvent) -> None:
prompts_refreshed_count=prompts_refreshed_count,
tools_added_count=tools_added_count,
prompts_added_count=prompts_added_count,
skills_count=skills_total if isinstance(skills_total, int) else None,
),
right_info="mcp",
agent_name=agent_name,
Expand All @@ -1391,6 +1408,7 @@ async def emit_oauth_event(event: OAuthEvent) -> None:
_format_added_summary(
tools_added_count=tools_added_count,
prompts_added_count=prompts_added_count,
skills_count=skills_total if isinstance(skills_total, int) else None,
),
right_info="mcp",
agent_name=agent_name,
Expand Down Expand Up @@ -1498,6 +1516,7 @@ async def handle_mcp_reconnect(
prompts_added = getattr(result, "prompts_added", [])
tools_total = getattr(result, "tools_total", None)
prompts_total = getattr(result, "prompts_total", None)
skills_total = getattr(result, "skills_total", None)
warnings = getattr(result, "warnings", [])

tools_added_count = len(tools_added)
Expand All @@ -1522,6 +1541,7 @@ async def handle_mcp_reconnect(
prompts_refreshed_count=prompts_refreshed_count,
tools_added_count=tools_added_count,
prompts_added_count=prompts_added_count,
skills_count=skills_total if isinstance(skills_total, int) else None,
),
right_info="mcp",
agent_name=agent_name,
Expand Down
Loading
Loading