tool-registration
Skill1 starsChanged 2 days agoMITView on GitHub
Use when adding, renaming, or documenting an MCP tool: the registration checklist, the get_server_info tool, tool icons, and the public import-surface guard.
We split this skill into the rules it gives an agent
1 rule. About 0 are common, 1 is distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 13
Every MCP tool you register must carry the full set of metadata below — not just the behaviour.
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
<!-- ===== TEMPLATE-OWNED — re-rendered on template updates. ===== -->Tool Registration
Tool Registration Checklist
Every MCP tool you register must carry the full set of metadata below — not just the behaviour. A tool that works but lacks a title, hints, or docs is incomplete. When adding or changing a tool, verify each item:
- Title — a human-readable
annotations.title(e.g."Search Vault"). Title-aware clients (notably VS Code, which honours onlytitleand the read-only hint among annotations) render this as the tool's label; without it they fall back to the raw machine name. Set it inline in the tool'sannotations={...}dict. - Behavioural hints —
read_only_hint, and where they applydestructive_hint/idempotent_hint, in the sameannotationsdict (MCP SDK v2 snake_case; serialized camelCase on the wire). These describe side effects accurately (a destructive tool must setdestructive_hint=True). - Icon — an entry wired via
register_tool_icons(...)or@mcp.tool(icons=[...])(see Tool icons). - Docstring — a Google-style docstring; FastMCP surfaces its summary and
Args:entries as the tool description and per-parameter descriptions the model reads. Write it with thewriting-model-facing-textskill: what the tool does, when to call it, what each parameter means; developer notes go in#comments, not the docstring. It states the contract only; how the tool fails is never docstring content. - Outcomes — every way the tool can end (a result, a request the model must change, a stale version, a server fault) maps to a return value, a
ToolErrorand a log level. Decide them with thedesigning-tool-outcomesskill, which also says how to write eachToolErrormessage so it stands on its own: what was wrong and the next call to make. Put@tool_boundary(fromfastmcp_pvl_core) under@mcp.toolso no exception reaches FastMCP unhandled; the template-ownedtests/test_tool_outcomes.pyfails any registered tool without it. - Docs entry — the published reference is generated: run
uv run python scripts/gen_reference.pyafter adding or changing a tool (CI's--checkfails otherwise). Add agroup:<slug>tag when the registering module is not the right page for it, and put a worked example in the tool'sDOMAIN-EXAMPLE-<name>slot ondocs/reference/tools/<group>.md; the slot survives regeneration. - Enforcement test — keep a test that enumerates the registered tools and asserts each carries the metadata above (at minimum a non-empty
annotations.title). Enumerate the full registry, not just the client-facing listing, so app-only / hidden tools cannot slip past. Such a test turns this checklist into a CI gate: a future tool added without a title fails loudly rather than silently shipping its machine name.
Tool icons
Drop SVG / PNG / ICO / JPEG files into src/paperless_mcp/static/icons/ and bulk-attach them to registered tools via fastmcp_pvl_core.register_tool_icons(mcp, {"tool_name": "filename.svg"}, static_dir=...) at the end of register_tools() — or attach at decoration time with @mcp.tool(icons=[make_icon(STATIC / "x.svg")]) (where STATIC = Path(__file__).parent / "static" / "icons" is a shorthand you define at module level). The scaffold ships an empty static/icons/ directory; commented-out wiring lives in tools.py.
Server Info Tool (get_server_info)
make_server() registers get_server_info (via fastmcp_pvl_core.register_server_info_tool) so operators can answer "is the latest fix actually deployed?" with a single MCP call. The default response carries server_name, server_version, and core_version.
For services that talk to a remote upstream (e.g. paperless, an HTTP API), wire the upstream version inside the DOMAIN-UPSTREAM-START / DOMAIN-UPSTREAM-END sentinel in src/paperless_mcp/server.py. Pass upstream_version= (a zero-arg callable returning a dict / str / None) and optionally upstream_label="<service>" (default "upstream"). The simplest pattern is a module-level upstream client (typically constructed from env vars at import time) whose version method is referenced from the callable — CurrentContext() is a FastMCP DI marker that only resolves inside parameter defaults, so it cannot be called directly from a zero-arg provider. The block is preserved across copier update.
Reaching the resolved config from a registrar
make_server() binds the ProjectConfig it resolved (a caller-supplied config= or the environment) to the server before any register_* function runs. Registrars keep their plain register_tools(mcp) signature and read it back with config_for(mcp) from paperless_mcp._server_deps; handlers take it as config: ProjectConfig = Depends(get_config), next to Depends(get_service). Use this for anything built at registration time from configuration — a jobs backend, an upstream client, a parameter default — instead of reading environment variables there: a caller passing an explicit config to make_server() would otherwise get the backend it asked for from make_server()'s own wiring and the one the environment names from yours. config_for() raises on a server not built through make_server(), so a test that calls a registrar on a bare FastMCP() calls bind_config(mcp, config) first. Do not patch the template-owned register_tools(mcp) line in server.py to pass the config: that file is re-rendered on template updates, and the environment fallback takes over silently.
Public import surface guard
tests/test_import_surface.py (template-owned, re-rendered on template updates) asserts the set of public names importable from the paperless_mcp package root against the project-owned snapshot tests/public_import_surface.txt (seeded once, never re-rendered by template updates — yours). The surface is enumerated in a fresh interpreter — every non-underscore name in dir(package) or __all__ that resolves via getattr — so lazy __getattr__ re-exports count, incidental submodule imports from earlier tests do not, and a root holding only a docstring and __version__ has an empty surface. When the test fails:
- A name disappeared — that is a breaking change to the public library interface. Either restore the name, or regenerate the snapshot (
uv run python tests/test_import_surface.py --update) and mark the commit/PR breaking (feat!:/fix!:, or aBREAKING CHANGE:footer) per the versioning policy's public-library-interface tier (pvliesdonk/fastmcp-server-template#342). - A name appeared — not breaking; regenerate the snapshot and commit it alongside the change, so the snapshot diff stays the reviewable record of every surface change.
The guard covers the package root only — submodule paths, env vars, and CLI flags are out of its scope.
Other versions2 projects use a version of this.
Word for word · 0
None found.
Edited · 2
- pvliesdonk/markdown-vault-mcp★ 3798% same
- pvliesdonk/scholar-mcp★ 298% same
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository15 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- applying-template-updatesSkill
- authoring-issues-prsSkill
- config-contractSkill
- designing-tool-outcomesSkill
- logging-standardSkill
- releasingSkill
- repository-protectionSkill
- researching-referencesSkill
- roadmappingSkill
- self-reviewingSkill
- writing-documentationSkill
- writing-model-facing-textSkill
- writing-release-notesSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

