Claude Code Plugins + uv: Research Notes
Two systems, often confused
There are two separate extension mechanisms for Claude Code:
Plugins — the newer, unified system (public beta). A plugin is a directory that bundles skills, agents, hooks, MCP servers, LSP servers, and executables. Installed via
/plugin install name@marketplace.MCP servers — the older, standalone mechanism. An MCP server is a single process that exposes tools to Claude over the Model Context Protocol. Configured via
claude mcp addor.mcp.json.
Plugins can contain MCP servers (via .mcp.json at the plugin root), but MCP servers can also be used independently without plugins.
Where uv/uvx fits in
Prerequisite:
uvxdoes not ship with Claude Code. You need to installuvseparately — it gives you bothuvanduvx:curl -LsSf https://astral.sh/uv/install.sh | shWithout this, any
uvx-based MCP server config will fail with “command not found.”
There are 6 distinct integration points between uv and Claude Code:
1. Running published MCP servers via uvx
Many MCP servers are Python packages on PyPI. You run them without installing:
{
"command": "uvx",
"args": ["mcp-server-time", "--local-timezone", "America/New_York"]
}Examples: mcp-server-time, mcp-server-fetch, mcp-server-git, msmcp-azure
CLI registration:
claude mcp add-json server-time --scope user '{
"command": "uvx",
"args": ["mcp-server-time", "--local-timezone", "Australia/Sydney"]
}'2. Running from git repos via uvx --from
The Serena plugin (in the official marketplace) does this:
{
"command": "uvx",
"args": ["--from", "git+https://github.com/oraios/serena", "serena", "start-mcp-server"]
}3. Running local dev servers via uv run
The official MCP tutorial uses this pattern:
{
"command": "uv",
"args": ["--directory", "/path/to/weather", "run", "weather.py"]
}4. FastMCP integration (uses uv under the hood)
fastmcp install claude-code server.py --with pandas --with requestsUnder the hood this registers: uv run --with fastmcp fastmcp run server.py
Manual equivalent:
claude mcp add my-server -- uv run --with fastmcp fastmcp run server.py5. Plugin dependency management via ${CLAUDE_PLUGIN_DATA}
Plugins get a persistent data directory (~/.claude/plugins/data/{id}/) that survives updates. The recommended pattern is a SessionStart hook that diffs the bundled manifest against a stored copy and reinstalls when changed.
The docs show this pattern for Node.js:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "diff -q \"${CLAUDE_PLUGIN_ROOT}/package.json\" \"${CLAUDE_PLUGIN_DATA}/package.json\" >/dev/null 2>&1 || (cd \"${CLAUDE_PLUGIN_DATA}\" && cp \"${CLAUDE_PLUGIN_ROOT}/package.json\" . && npm install) || rm -f \"${CLAUDE_PLUGIN_DATA}/package.json\""
}
]
}
]
}
}The same pattern applies to Python: compare pyproject.toml, run uv sync when changed.
6. Hooks that enforce uv usage
PreToolUse hooks that block pip install commands and suggest uv add instead. Written as Python scripts:
#!/usr/bin/env python3
import json, sys
def main():
data = json.load(sys.stdin)
if data.get("tool") not in ["Bash", "Run"]:
sys.exit(0)
command = data.get("parameters", {}).get("command", "")
patterns = {
"python ": "uv run",
"pip install": "uv add",
"pytest": "uv run pytest",
}
for old, new in patterns.items():
if old in command:
print(f"Detected '{old}'. Use '{new}' instead.", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()Configured in ~/.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|Run",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/pre_tool_use_uv.py",
"timeout": 10
}
]
}
]
}
}Plugin structure
my-plugin/
├── .claude-plugin/
│ └── plugin.json # name, version, description (only manifest goes here)
├── .mcp.json # MCP server configs (optional)
├── .lsp.json # LSP server configs (optional)
├── skills/
│ └── my-skill/
│ └── SKILL.md # model-invoked or user-invoked
├── agents/ # subagent definitions (optional)
│ └── reviewer.md
├── hooks/
│ └── hooks.json # event handlers (optional)
├── bin/ # executables added to PATH (optional)
├── settings.json # default settings (optional)
├── scripts/ # utility scripts for hooks
└── README.md
plugin.json minimal example
{
"name": "my-plugin",
"description": "What this plugin does",
"version": "1.0.0",
"author": { "name": "Your Name" }
}SKILL.md example (user-invoked slash command)
---
name: my-command
description: Short description for /help
argument-hint: <arg1> [optional-arg]
allowed-tools: [Read, Glob, Grep]
---
Instructions for Claude when this skill is invoked.
Use $ARGUMENTS to capture user input..mcp.json example (bundling a Python MCP server)
{
"mcpServers": {
"my-tool": {
"command": "uvx",
"args": ["my-mcp-package"]
}
}
}Or for a bundled server script:
{
"mcpServers": {
"my-tool": {
"command": "uv",
"args": ["--directory", "${CLAUDE_PLUGIN_ROOT}", "run", "server.py"],
"env": {
"MY_CONFIG": "${CLAUDE_PLUGIN_ROOT}/config.json"
}
}
}
}Environment variables available in plugins
${CLAUDE_PLUGIN_ROOT}— absolute path to plugin install dir (changes on update)${CLAUDE_PLUGIN_DATA}— persistent dir for deps/state (survives updates, at~/.claude/plugins/data/{id}/)
Building an MCP server from scratch (the official tutorial pattern)
# 1. Create project
uv init weather
cd weather
uv venv
source .venv/bin/activate
# 2. Install dependencies
uv add "mcp[cli]" httpx
# 3. Write server (weather.py)from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather")
@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
"""Get weather forecast for a location."""
# ... implementation ...
return forecast_text
if __name__ == "__main__":
mcp.run(transport="stdio")# 4. Register with Claude Code
claude mcp add weather -- uv --directory /path/to/weather run weather.py
# Or in .mcp.json / claude_desktop_config.json:{
"mcpServers": {
"weather": {
"command": "uv",
"args": ["--directory", "/absolute/path/to/weather", "run", "weather.py"]
}
}
}FastMCP shortcut
# One command to create + register
fastmcp install claude-code server.py --with pandas --with requests --python 3.11MCP server configuration scopes
| Scope | Stored in | Available in | Shared with team |
|---|---|---|---|
| local | ~/.claude.json |
Current project only | No |
| project | .mcp.json in project root |
Current project only | Yes (via git) |
| user | ~/.claude.json |
All projects | No |
Key getting-started guides (read and verified)
Official documentation
| Guide | URL | What it covers |
|---|---|---|
| Build an MCP Server | https://modelcontextprotocol.io/docs/develop/build-server | uv init + uv add "mcp[cli]" + weather server + Claude Desktop config |
| Create Plugins | https://code.claude.com/docs/en/plugins | plugin.json, skills, --plugin-dir testing, quickstart |
| MCP in Claude Code | https://code.claude.com/docs/en/mcp | claude mcp add, .mcp.json, scopes, plugin MCP servers |
| Plugins Reference | https://code.claude.com/docs/en/plugins-reference | Full schema, CLAUDE_PLUGIN_DATA, SessionStart dep pattern, bin/ |
| Discover Plugins | https://code.claude.com/docs/en/discover-plugins | /plugin install, marketplaces, official marketplace |
| Anthropic Blog | https://claude.com/blog/claude-code-plugins | Announcement, vision, community examples |
Community guides
| Guide | URL | What it covers |
|---|---|---|
| FastMCP + Claude Code | https://gofastmcp.com/integrations/claude-code | fastmcp install claude-code, --with, --python, env vars |
| CloudArtisan: MCP Servers | https://cloudartisan.com/posts/2025-04-12-adding-mcp-servers-claude-code/ | uvx examples for time/fetch/git/filesystem servers |
| pydevtools: Hooks for uv | https://pydevtools.com/blog/claude-code-hooks-for-uv/ | PreToolUse hooks blocking pip, Notification hooks for uv |
| pydevtools: Configure for uv | https://pydevtools.com/handbook/how-to/how-to-configure-claude-code-to-use-uv/ | CLAUDE.md approach to teach Claude to use uv |
| Azure MCP Server | https://devblogs.microsoft.com/azure-sdk/azure-mcp-server-better-python-support/ | uvx --from msmcp-azure azmcp server start pattern |
| Beginner’s MCP with uv | https://mahendranp.medium.com/beginners-guide-to-building-and-testing-your-first-mcp-server-with-uv-and-claude-3bfc6198212a | End-to-end beginner walkthrough |
Source code references
| Resource | URL | What it shows |
|---|---|---|
| Official plugins repo | https://github.com/anthropics/claude-plugins-official | Curated marketplace, plugin structure examples |
| Example plugin | https://github.com/anthropics/claude-plugins-official/tree/main/plugins/example-plugin | Reference implementation with skills + .mcp.json |
| Serena plugin .mcp.json | https://github.com/anthropics/claude-plugins-official/blob/main/external_plugins/serena/.mcp.json | Real uvx --from git+... pattern |
| Demo plugins (claude-code repo) | https://github.com/anthropics/claude-code/tree/main/plugins | PR review, security, commit commands |
| MCP Python SDK | https://github.com/modelcontextprotocol/python-sdk | FastMCP source, SDK reference |
Key takeaways for our article
uvxis the bridge — it lets you run any PyPI-published MCP server without permanent installation. Python equivalent ofnpxfor Node.js servers.uv runis for local dev — when building your own server, execute it in your project’s venv.- FastMCP makes building Python MCP servers trivial —
@mcp.tool()decorator, type hints auto-generate schemas. - Plugins are the distribution mechanism — wrap your MCP server + skills + hooks in a plugin directory, publish to a marketplace for one-command install.
${CLAUDE_PLUGIN_DATA}is how plugins persist venvs/deps across updates.- CLAUDE.md + hooks are the simpler path for teams that just want Claude to use
uvinstead ofpip— no plugin needed, just project configuration. - The official marketplace already has Python-based plugins using
uvx(Serena, Azure MCP, etc.) — good real-world examples to reference.