Shipping a personal MCP server
Workflow for repos whose entire purpose is producing an MCP server for personal, cross-project use,
rather than a library other code imports. Covers going from a working repo to "loadable in any
project," and how that differs between the dev machine and everywhere else. The worked examples name
the author's own servers — a family of *-polite-mcp repos — because a concrete name reads better
than a placeholder; substitute your own everywhere.
Shipping an Agent Skill is a different job with a different mechanism — see the
skill-authoring skill, which also covers getting an edit to an existing skill deployed.
What this skill reads, runs and writes
This skill ships no scripts; what it touches, it touches through the commands it tells you to run.
- Runs:
uv tool install,claude mcp add,claude mcp list. - Writes:
uv tool installputs a tool environment under uv's tool directory and a shim onPATH(~/.local/bin/by default);claude mcp add --scope userwrites the server registration into Claude Code's user-level config,--scope projectinto the repo's.mcp.json. Both are reversible with the same tools'uninstall/remove. Nothing else, and nothing in the repo the server lives in beyond the[project.scripts]entry you add by hand. - Network:
uv tool install git+…clones the repo you name.
Per-repo dev loop
Two things here are general and one is the author's own stack. The general ones: a repo of this kind wants a one-command setup that syncs the environment from a committed lockfile rather than resolving afresh, and a one-command gate that auto-fixes then runs the same checks CI runs. Whatever your repo already uses for those is the right answer — the point is that both exist and are one command, not which runner provides them.
The rest of this section describes the author's arrangement, and is here as a worked example rather
than as an instruction. On this author's machine every repo in this family takes
repo-tasks as a dev dependency (git-as-artifact-store,
no PyPI) instead of hand-rolled tasks.py logic, so the repo's own tasks.py is just
from repo_tasks import ns; inv dev-env.setup is the setup command and inv quality.precommit
the gate, and the AGENTS.md/CLAUDE.md/skills scaffold is generated by
scaffoldapy at repo-creation time. Substitute your
own equivalents throughout.
The rule worth taking from it, whatever your runner is: a task runner is a per-project
dependency, never assumed to be on PATH. Anything invoking it from outside an already-activated
shell — a CI step, a project-generation hook, any automation — has to go through the project's own
environment (uv run <runner> <task> for a uv-managed project), because nothing guarantees a bare
name resolves to this project's copy rather than to some other one, or to nothing at all. That is
the mistake this section exists to prevent, and it does not depend on which runner you use.
One entry point, uv tool install for a stable PATH binary
Add a [project.scripts] entry point to the MCP repo's pyproject.toml (e.g.
olx-polite-mcp = "olx_polite_mcp.server:main") before writing server.py, not as a retrofit. Then
install it as a real tool via uv tool (validated end-to-end in olx-polite-mcp/README.md) rather
than pointing claude mcp add at a uv run/uvx invocation — uv tool install builds an isolated
env and drops a shim on PATH (~/.local/bin/ by default), so registration itself becomes a bare
binary name with no path/flags to keep in sync:
Install against the local working tree while actively developing (editable — picks up local edits without reinstalling, so the repo has to stay put at that path):
uv tool install -e <path-to-your-checkout>
Install from GitHub once not actively iterating (pin @<tag>/@<sha> for reproducibility, omit
for the default branch):
uv tool install git+https://github.com/TheodoreAD/olx-polite-mcp
Either way, registration is the same one-liner, independent of which source was installed:
claude mcp add --scope user olx-polite-mcp olx-polite-mcp
Switching sources is uv tool install again with the other source (uv replaces the existing tool) —
no claude mcp remove/re-add needed, since the registered command name never changes.
--scope user (not local/project) matches how skills already install globally, so the server is
available in every project on this machine, not just one. Use --scope project instead only for a
consumer repo that wants the server offered automatically to anyone who clones it (see
olx-polite-mcp/README.md's project-scope example) — a different case than personal cross-project
use.
Editable and from-GitHub installs can't be combined (editable needs a real working directory uv can
point at; a git-sourced install doesn't expose one) — for "edit locally, sourced as if from GitHub,"
git clone it yourself, then uv tool install -e that clone, which is just the editable-from-disk
case again.
Project- and user-scope servers need a one-time approval on claude startup before a new session
launches them (claude mcp list shows pending ones) — an already-running session that had it
approved keeps it live.
Distribution: skip PyPI
At this scale (personal, non-commercial), uv tool install resolving straight from a git remote
(see above) makes the GitHub repo itself the artifact store — no version-bump/publish/credentials
ceremony. Pin @<tag>/@<sha> once a repo has a stable point worth freezing; no ref (tracks the
default branch) is fine while iterating.
Full rationale
references/rationale.md — why git+uv beat PyPI (and the
uvx/uv run draft that came first), why --scope user, and why one registered command covers
both install sources.