agentleFS
pvliesdonkpvliesdonk/markdown-vault-mcp/.agents/skills/tool-registration/SKILL.md

tool-registration

Skill37 starsChanged 6 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
  1. Distinctive:

    Every MCP tool you register must carry the full set of metadata below — not just the behaviour.

    line 13

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md
<!-- ===== 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 only title and 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's annotations={...} dict.
  • Behavioural hints — read_only_hint, and where they apply destructive_hint / idempotent_hint, in the same annotations dict (MCP SDK v2 snake_case; serialized camelCase on the wire). These describe side effects accurately (a destructive tool must set destructive_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 the writing-model-facing-text skill: 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 ToolError and a log level. Decide them with the designing-tool-outcomes skill, which also says how to write each ToolError message so it stands on its own: what was wrong and the next call to make. Put @tool_boundary (from fastmcp_pvl_core) under @mcp.tool so no exception reaches FastMCP unhandled; the template-owned tests/test_tool_outcomes.py fails any registered tool without it.
  • Docs entry — the published reference is generated: run uv run python scripts/gen_reference.py after adding or changing a tool (CI's --check fails otherwise). Add a group:<slug> tag when the registering module is not the right page for it, and put a worked example in the tool's DOMAIN-EXAMPLE-<name> slot on docs/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/markdown_vault_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/markdown_vault_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 markdown_vault_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 markdown_vault_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 a BREAKING 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.

Download now (ZIP)

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.