> ## Documentation Index

> Fetch the complete documentation index at: /llms.txt

> Use this file to discover all available pages before exploring further.

Skip to main content
Working with Tools

# Tool Fingerprinting

Build stable fingerprints for tool identity and schema change detection

New in version `3.0.0` Downstream systems like routers, gateways, and audit loggers often need to detect whether a tool’s schema changed between deployments. Rather than each system inventing its own JSON normalization and hashing logic, you can build stable fingerprints from FastMCP’s existing API surface. FastMCP does not define a single “contract hash” because the inclusion policy is necessarily application-specific: some systems care only about the input schema, others include the description, metadata, tags, or version. Instead, this recipe shows how to assemble a fingerprint payload from the parts you care about, then hash it deterministically.

##
​
The Recipe

The two key building blocks are:

- `tool.key` — FastMCP’s canonical component identity, encoding type, name, and version (e.g. `tool:greet@1.0` or `tool:greet@`)
- `tool.to_mcp_tool()` — the protocol-facing tool object that MCP clients see, including the input schema

Combine them into a payload, serialize deterministically, and hash:

    import hashlib
    import json

    from fastmcp import FastMCP

    mcp = FastMCP("demo")

    @mcp.tool()
    def greet(name: str) -> str:
        """Say hello."""
        return f"Hello {name}"

    async def fingerprint_tool(server: FastMCP, tool_name: str) -> str:
        tool = await server.get_tool(tool_name)
        if tool is None:
            raise ValueError(f"Tool {tool_name!r} not found")

        mcp_tool = tool.to_mcp_tool()
        dumped = mcp_tool.model_dump(mode="json", by_alias=True, exclude_none=True)

        payload = {
            "key": tool.key,
            "inputSchema": dumped["inputSchema"],
        }

        canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"))
        return hashlib.sha256(canonical.encode("utf-8")).hexdigest()

The fingerprint is stable across process restarts as long as the tool’s name, version, and input schema remain the same.

##
​
Why `tool.key`?

`tool.key` is FastMCP’s canonical component identity. It encodes the component type, identifier, and version into a single string:

    tool:greet@1.0    # versioned tool
    tool:greet@       # unversioned tool

Using `key` rather than just the tool name ensures that two versions of the same tool produce distinct fingerprints, and that a tool and a resource with the same name cannot collide.

##
​
Why `to_mcp_tool()`?

`to_mcp_tool()` returns the protocol-facing representation — the shape that MCP clients actually receive. This matters because routers and gateways typically operate on the protocol layer, not FastMCP internals. The `model_dump(mode="json", by_alias=True, exclude_none=True)` call produces a clean, serializable dictionary using the MCP protocol field names.

##
​
Customizing the Payload

You own the inclusion policy. Add or remove fields depending on what constitutes a “contract” in your system:

    async def custom_fingerprint(server: FastMCP, tool_name: str) -> str:
        tool = await server.get_tool(tool_name)
        if tool is None:
            raise ValueError(f"Tool {tool_name!r} not found")

        mcp_tool = tool.to_mcp_tool()
        dumped = mcp_tool.model_dump(mode="json", by_alias=True, exclude_none=True)

        # Include description to detect documentation drift
        payload = {
            "key": tool.key,
            "inputSchema": dumped["inputSchema"],
            "description": dumped.get("description"),
        }

        canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"))
        return hashlib.sha256(canonical.encode("utf-8")).hexdigest()

Common variations:

Field | When to include
--- | ---
`inputSchema` | Always — this is the core contract
`description` | When documentation drift matters (e.g. LLM routing decisions depend on it)
`outputSchema` | When downstream consumers validate response shapes
`annotations` | When behavioral hints (read-only, destructive) affect routing
`_meta` | When custom metadata drives policy decisions

##
​
Detecting Schema Drift in CI

Store fingerprints as artifacts and compare between deployments:

    import json
    import hashlib
    from pathlib import Path

    from fastmcp import FastMCP

    async def generate_manifest(server: FastMCP) -> dict[str, str]:
        """Generate a fingerprint manifest for all tools."""
        manifest = {}

        for tool in await server.list_tools():
            mcp_tool = tool.to_mcp_tool()
            dumped = mcp_tool.model_dump(mode="json", by_alias=True, exclude_none=True)

            payload = {
                "key": tool.key,
                "inputSchema": dumped["inputSchema"],
            }

            canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"))
            manifest[tool.key] = hashlib.sha256(canonical.encode("utf-8")).hexdigest()

        return manifest

    async def check_drift(server: FastMCP, baseline_path: Path) -> list[str]:
        """Compare current fingerprints against a stored baseline."""
        current = await generate_manifest(server)
        baseline = json.loads(baseline_path.read_text())

        changed = []
        for key, fingerprint in current.items():
            if baseline.get(key) != fingerprint:
                changed.append(key)

        for key in baseline:
            if key not in current:
                changed.append(key)

        return changed

Run `generate_manifest` in CI after each build and compare against the previous run. Any differences indicate a schema change that downstream consumers should be aware of.
Prompts as Tools
Previous
Providers
Next
Ctrl+I
Assistant
Responses are generated using AI and may contain mistakes.
\n\n
\n
\n \n
\n
","contacts":" \n
\n\n Prefab\n \n \n \n \n\n
\n
\n \n
\n
","dashboard":" \n
\n\n Prefab\n \n \n \n \n\n
\n
\n \n
\n
","data-table":" \n
\n\n Prefab\n \n \n \n \n\n
\n
\n \n
\n
","hitchhikers":" \n
\n\n Prefab Showcase\n \n \n \n \n\n
\n
\n \n
\n
","pie-chart":" \n
\n\n Prefab\n \n \n \n \n\n
\n
\n \n
\n
","reactive":" \n
\n\n Prefab\n \n \n \n \n\n
\n
\n \n
\n
","team-directory-reactive":" \n
\n\n Prefab\n \n \n \n \n\n
\n
\n \n
\n
","team-directory":" \n
\n\n Prefab\n \n \n \n \n\n
\n
\n \n
\n
"};