Skip to main content
Glama
sneha4175

MCP Tool-Retrieval Gateway

by sneha4175

MCP Tool-Retrieval Gateway

A self-hostable MCP proxy that exposes only the top-k semantically relevant tools per query — instead of every tool from every server.

CI tests python license

v0.5 — a retrieval evaluation harness: measure retrieval quality on a labeled set with the standard IR metrics — recall@k, precision@k, and MRR. Ships a small deterministic smoke dataset and a python -m mcp_router.eval CLI. See v0.5: retrieval evaluation.

v0.4hybrid retrieval: blends the semantic (embedding cosine) score with a lexical token-overlap score so a query that names a tool or its keywords surfaces it even when embedding similarity is only moderate. Configurable alpha, default on. See v0.4: hybrid retrieval.

v0.3 — a query→tool-set cache (TTL + LRU) so repeated queries skip re-embedding and vector search, with hit/miss stats on /stats. See v0.3: query caching.

v0.2 — real MCP transport over the official mcp SDK (the gateway is now a real MCP client and a real MCP server), and a real local semantic embedder by default. See what's verified vs roadmap.


The problem

The Model Context Protocol (MCP) lets an LLM-based client connect to many tool servers — filesystem, GitHub, a database, a browser, your internal APIs. But there is a cost that grows with every server you add:

An MCP client loads the full tool definitions from every connected server into the context window on every request — names, descriptions, and complete JSON input schemas — before the user has typed a single word.

A handful of servers can easily contribute 50–100+ tool definitions. Rich JSON schemas are verbose, and in practice this commonly burns ~20–40% of the context window as fixed overhead on every turn. That overhead is:

  • Paid on every request, whether or not any of those tools are relevant.

  • Mostly wasted — a typical query needs 1–3 tools, not 80.

  • Money and latency — more prompt tokens on every call, and a larger prompt the model must attend to.

For a single query like "convert 100 USD to EUR", the model does not need the weather tools, the calendar tools, or the git tools. It needs one.

Related MCP server: context-saver

The solution

This gateway sits between the client and the upstream MCP servers as a proxy. It:

  1. Connects to each upstream MCP server and discovers its real tools.

  2. Embeds every tool definition once, at startup, into a vector store.

  3. Per query, returns only the top-k tools whose embeddings are most similar — not the whole catalogue.

  4. On a call, routes the invocation back to the upstream server that actually owns that tool and returns its real result.

The client sees a small, query-relevant tool list. The context overhead drops from "all tools, always" to "k tools, on demand."

Real end-to-end run (v0.2)

Connected to two real MCP servers over stdio — the bundled example server plus the official @modelcontextprotocol/server-filesystem via npx — using the default sentence-transformers embedder:

$ python examples/real_mcp_demo.py

Upstream MCP servers : 2  (real, live over stdio)
Real tools discovered: 18  (a naive client would load all of these)
Exposed per query    : 3  (top-k relevant)
Reduction            : 18 -> 3  (~83% fewer)

query: 'read the contents of a text file'
   1. read_text_file         [filesystem] score=0.711
   2. read_file              [filesystem] score=0.674
   3. read_multiple_files    [filesystem] score=0.496

query: 'add two numbers together'
   1. add_numbers            [example   ] score=0.784
   2. move_file              [filesystem] score=0.111
   3. reverse_text           [example   ] score=0.092

----------------------------------------------------------------------
tools/call reverse_text('gateway') -> [example] yawetag

Those tools were fetched live from the real servers, and the final line is a real call proxied to the upstream that owns reverse_text — its actual output, yawetag, returned back.

Architecture

                          MCP Tool-Retrieval Gateway
                 ┌────────────────────────────────────────────┐
   MCP host      │                                            │   Upstream MCP servers
 (Claude Desktop)│   MCP server (stdio, mcp SDK)              │   (real, over stdio)
  ┌──────────┐   │   ┌────────────────────────┐               │   ┌────────────────────┐
  │find_tools│──────▶│  find_tools(query,k)    │  top-k        │   │ example  (SDK)     │
  │          │◀──────│                        │◀──────┐        │┌─▶│ filesystem (npx)   │
  │call_tool │──────▶│  call_tool(name,args)   │       │        ││  │ ...your servers... │
  └──────────┘   │   └───────────┬────────────┘       │        ││  └────────────────────┘
                 │               │            ┌────────┴─────┐  ││
   HTTP client   │   FastAPI     ▼            │ ToolRegistry │  ││
  ┌──────────┐   │   ┌────────────────────┐   │  Retriever   │  ││
  │tools/list│──────▶│ POST / (JSON-RPC)  │──▶│  embedder    │  ││
  │  (query) │◀──────│ tools/list+query   │   │  vectorstore │  ││
  │tools/call│──────▶│ tools/call         │───┼─ routes call ─┼──┘│
  └──────────┘   │   └────────────────────┘   │  Upstream ───┼───┘
                 │                            └──────────────┘   (MockUpstream | StdioUpstream)
                 └────────────────────────────────────────────┘

  index (startup):  connect upstream ─▶ discover tools ─▶ embed() ─▶ VectorStore.add()
  query:            text ─▶ embed() ─▶ VectorStore.search(k) ─▶ ToolDefs
  call:             name ─▶ owning Upstream.call() ─▶ real result

Two front doors, one core:

  • MCP server (stdio) — what a standard MCP host (Claude Desktop) connects to. See Run as an MCP server.

  • HTTP JSON-RPC — a convenient HTTP surface whose tools/list takes an extra query param. See Run as an HTTP server.

Module map

Module

Responsibility

mcp_router/config.py

Parse the YAML/JSON config: which upstreams (mock or stdio), which tools.

mcp_router/models.py

ToolDef — an MCP tool + the upstream that owns it.

mcp_router/embedder.py

Pluggable embedders: real sentence-transformers (default) or offline HashingEmbedder.

mcp_router/vectorstore.py

In-memory NumPy cosine-similarity store.

mcp_router/retrieval.py

Retriever — indexes tools, returns top-k for a query.

mcp_router/upstream.py

MockUpstream (offline) and StdioUpstream (real MCP client over stdio).

mcp_router/registry.py

Ties it together; discovers tools from upstreams, owns retrieval + routing.

mcp_router/mcp_server.py

The gateway as a real MCP server (find_tools + call_tool).

mcp_router/server.py

FastAPI app exposing MCP-shaped JSON-RPC over HTTP.

Requirements

  • Python 3.11+

  • fastapi, uvicorn, numpy, pyyaml, mcp (see requirements.txt)

  • Optional: sentence-transformers for the default semantic embedder; Node/npx only if you point at npx-launched upstream servers.

Quick start

# 1. Install (lean runtime + the real MCP SDK)
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt

# 2a. Semantic default: install the embedder (downloads ~80MB on first use)
pip install sentence-transformers

# 2b. ...or run fully offline/deterministic instead:
export MCP_ROUTER_EMBEDDER=hashing

# 3. See the reduction against REAL MCP servers, end to end
python examples/real_mcp_demo.py

examples/demo.py is the original offline demo over inline mock tools; examples/real_mcp_demo.py (above) connects to real MCP servers over stdio.

Run as an MCP server (Claude Desktop)

The gateway serves the real MCP protocol over stdio. Because a vanilla tools/list has nowhere to put a query, it exposes the reduction through progressive disclosure — just two meta-tools, so a host loads 2 tool definitions instead of N:

  • find_tools(query, k) — semantic search across every upstream; returns the top-k matching tool definitions.

  • call_tool(name, arguments) — proxies a call to whichever upstream owns the tool and returns its real result.

Run it standalone:

MCP_ROUTER_CONFIG=config.stdio.example.yaml python -m mcp_router.mcp_server

Add it to Claude Desktop's claude_desktop_config.json:

{
  "mcpServers": {
    "tool-router": {
      "command": "python",
      "args": ["-m", "mcp_router.mcp_server"],
      "env": {
        "MCP_ROUTER_CONFIG": "/absolute/path/to/config.stdio.example.yaml",
        "MCP_ROUTER_EMBEDDER": "sentence-transformers"
      }
    }
  }
}

The host then loads two tools; the model calls find_tools("…") to discover what it needs, then call_tool(...) to run it — keeping context overhead constant no matter how many upstream servers you connect.

Run as an HTTP server

export MCP_ROUTER_CONFIG=config.example.yaml
uvicorn mcp_router.server:app_from_env --factory --port 8000
# tools/list with a query -> only the top-k relevant tools
curl -s localhost:8000/ -H 'content-type: application/json' -d '{
  "jsonrpc": "2.0", "id": 1, "method": "tools/list",
  "params": {"query": "convert dollars to euros", "k": 3}
}'

# tools/call -> routed to the upstream that owns the tool
curl -s localhost:8000/ -H 'content-type: application/json' -d '{
  "jsonrpc": "2.0", "id": 2, "method": "tools/call",
  "params": {"name": "convert_currency", "arguments": {"amount": 100}}
}'

Omit query from tools/list and the gateway returns all tools — behaving as a transparent proxy.

With Docker

The image is lean and defaults to the offline hashing embedder (instant start, no download):

docker build -t mcp-tool-router .
docker run -p 8000:8000 mcp-tool-router
# point it at your own config:
docker run -p 8000:8000 -e MCP_ROUTER_CONFIG=/app/my.yaml -v $PWD/my.yaml:/app/my.yaml mcp-tool-router

Configuration

Each server has a name and a transport.

stdio — a real MCP server launched as a subprocess (config.stdio.example.yaml):

servers:
  - name: filesystem
    transport: stdio
    command: npx
    args: ["-y", "@modelcontextprotocol/server-filesystem", "/data"]
    env: {}
  - name: example
    transport: stdio
    command: python
    args: ["examples/example_upstream_server.py"]

mock — offline; tools listed inline, calls echoed (config.example.yaml):

servers:
  - name: finance
    transport: mock
    tools:
      - name: convert_currency
        description: Convert a monetary amount from one currency to another.
        inputSchema:
          type: object
          properties:
            amount: { type: number }

v0.5: retrieval evaluation

Retrieval quality is the product. Up to v0.4 it was asserted only by crafted unit tests; v0.5 makes it a number you can track across changes to the embedder, the hybrid blend, or alpha. The harness (mcp_router/eval.py) scores the retriever's top-k results against a labeled dataset — each example is a query plus the set of tool names that should come back — using three standard IR metrics:

metric

definition

rewards

recall@k

`

relevant ∩ retrieved_k

precision@k

`

relevant ∩ retrieved_k

MRR

mean of 1 / rank of the first relevant hit (0 if none)

putting a relevant tool near the top

The harness never reimplements retrieval — evaluate(retriever, dataset, k) calls the real retriever.retrieve(query, k) (a Retriever or a ToolRegistry) and scores the names it returns, so you measure the exact code path the gateway serves.

python -m mcp_router.eval          # k=3 by default
python -m mcp_router.eval --k 5
query                               recall@k    prec@k      RR
--------------------------------------------------------------
current temperature and condit...      1.000     0.333   1.000
...
MEAN                                   1.000     0.333   1.000

The bundled dataset (mcp_router/eval_data.py) is a small, deterministic smoke set — a handful of tools and a few queries with known-relevant tools — that runs offline on the hashing embedder. It exists to make the harness runnable out of the box and to guard against regressions; it is not a benchmark or a quality claim. Point evaluate() at your own labeled set to measure a real catalogue.

v0.4: hybrid retrieval

Pure-embedding retrieval scores meaning, which is what you want for "schedule a meeting"create_event. But it has a blind spot: an exact tool-name or keyword hit can score only moderately when the surrounding words differ, so a semantically-fuzzy distractor can edge out the tool the user literally named. v0.4 blends in a lexical signal to fix that.

What it does

  • Computes a lexical score — normalized token overlap between the query and the tool's text (name + description + parameter names): of the meaningful tokens in the query, what fraction appear in the tool? Bounded to [0, 1], stopword-aware, no heavy dependency.

  • Blends it with the semantic cosine score by a weight alpha:

    final = alpha * semantic + (1 - alpha) * lexical
  • Because a strong keyword match may sit outside the semantic top-k, the hybrid path scores the whole catalogue before taking the top-k — affordable since a gateway fronts only tens-to-hundreds of tools.

  • alpha = 1.0 reproduces the v0.3 pure-semantic behaviour; alpha = 0.0 is pure lexical. The default 0.5 is an even blend.

Why it helps — consider the query "delete_user account" against two tools, delete_user ("Remove an account permanently") and user_account. On pure cosine the shorter user_account scores higher (0.82 vs 0.77) and wins — the exact-name match loses. Blending in lexical coverage (the query fully covers delete_user) flips the ranking so the tool the user named comes first. This exact case is pinned in the test suite.

Config — a top-level retrieval: block (all optional; defaults shown):

retrieval:
  hybrid: true         # false -> pure-semantic (v0.3 behaviour)
  alpha: 0.5           # 1.0 = all semantic, 0.0 = all lexical

The active mode is reported on GET /stats (retrieval.hybrid, retrieval.alpha) alongside the cache counters.

v0.3: query caching

Retrieval is the expensive part of every request — embedding the query and searching the vector store. Real traffic is repetitive (the same phrasings recur, clients retry), so redoing that work for an identical query is pure waste. v0.3 adds a small cache in front of the retriever that memoizes the top-k tool set per query.

What it does

  • Keys each entry by a normalized query (lowercased + trimmed) plus k. A repeated query returns the cached tool set without re-embedding or re-searching.

  • TTL expiry — every entry has a lifetime; a hit past its TTL is recomputed, so results can't go stale indefinitely.

  • LRU eviction — the cache holds at most max_entries; inserting beyond that drops the least-recently-used entry, capping memory.

  • Auto-invalidation — when the tool catalogue changes (ToolRegistry.refresh() — upstreams reconnect / tools refresh), the whole cache is cleared so a tool set computed against the old catalogue is never served.

  • Hit/miss stats — exposed on the HTTP GET /stats endpoint (and ToolRegistry.stats()).

Perf rationale: the win is skipping the embed + vector-search on repeated queries. With the semantic embedder that avoids a model forward-pass per repeat; a cache hit is a dict lookup.

Config — a top-level cache: block (all optional; defaults shown):

cache:
  enabled: true        # set false to bypass the cache entirely
  ttl_seconds: 300     # how long a cached tool set stays fresh
  max_entries: 512     # LRU cap on distinct cached (query, k) pairs
# hit/miss counters, hit rate, cached-entry count, evictions, invalidations
curl -s localhost:8000/stats

Embedders

The embedder is selected by MCP_ROUTER_EMBEDDER:

Value

What it is

When

sentence-transformers (default)

Real local model all-MiniLM-L6-v2 (384-dim). Understands meaning, not just shared words — "schedule a meeting" matches "create a calendar event".

Production / real semantic retrieval. Downloads ~80MB once, then cached.

hashing

Dependency-free, deterministic hashing-trick embedder. Lexical overlap only.

Tests, CI, air-gapped runs. No download.

export MCP_ROUTER_EMBEDDER=sentence-transformers   # default
export MCP_ROUTER_ST_MODEL=all-MiniLM-L6-v2         # optional; this is the default
# or, fully offline:
export MCP_ROUTER_EMBEDDER=hashing

To wire in a hosted embedding API, implement the Embedder interface (one method, embed(texts) -> np.ndarray) and return it from get_embedder(). Everything downstream depends only on that interface.

Running the tests

pip install -r requirements.txt
pytest

The suite is offline and deterministic by design: it forces MCP_ROUTER_EMBEDDER=hashing and never downloads a model. It includes real MCP-transport integration tests that launch the bundled example server as a subprocess and speak the actual protocol to it (both the gateway-as-client and gateway-as-server paths).

  • 74 passing, 1 skipped locally. The skipped test connects to the official @modelcontextprotocol/server-filesystem via npx (needs Node + network); enable it with MCP_ROUTER_RUN_NPX_TESTS=1 pytest.

What's verified vs roadmap

Honest status for v0.2.

Verified end-to-end (implemented + tested):

  • Real MCP client transport. StdioUpstream launches a real MCP server and speaks JSON-RPC over stdio via the official mcp SDK; tools are discovered live and calls are proxied to the real server. Exercised against the bundled example server (offline, in CI) and against the official npx filesystem server (opt-in test, and in the demo above).

  • Real MCP server transport. mcp_router.mcp_server runs as a real MCP server over stdio (find_tools + call_tool); a real SDK Client drives the full loop in tests — including a launched-subprocess run that mirrors how Claude Desktop connects.

  • Real semantic embedder by default (sentence-transformers, all-MiniLM-L6-v2), with the offline hashing embedder as the deterministic fallback.

  • Config-driven registry (YAML/JSON), NumPy cosine top-k retrieval, upstream routing, HTTP JSON-RPC surface, Docker image.

Not yet exercised by an external host: connecting the actual Claude Desktop app is documented (config snippet above) and the stdio server it would launch is verified with the SDK's own client, but the end-to-end run inside the Claude Desktop UI has not been performed here.

Deferred (next milestones):

  • Approximate vector index (FAISS/hnswlib) for very large tool catalogues — exact search is the right choice for tens–hundreds of tools.

  • SSE / streamable-HTTP MCP transports (only stdio upstreams today).

  • Resources / prompts passthrough. (Query→tool-set caching shipped in v0.3; hybrid lexical + semantic retrieval shipped in v0.4; a labeled-set evaluation harness with recall@k / precision@k / MRR shipped in v0.5.)

License

MIT — see LICENSE.

Available Tools

2 tools
call_toolA

Invoke a tool by name (as returned by find_tools) on whichever upstream server owns it, forwarding the given arguments, and return that server's result.

ParametersJSON Schema
NameRequiredDescriptionDefault
nameYes
argumentsNo

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden. It discloses the core behavior of forwarding arguments and returning the server's result, but does not mention potential side effects (since invoking arbitrary tools may cause mutations), error handling, or permissions. The generic nature limits deeper transparency, but the forwarding and return behavior is clearly stated.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, front-loaded sentence with no fluff. Every part adds value: invocation, name source, routing, argument forwarding, and return behavior.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple two-parameter proxy with an output schema, the description covers the key aspects: what it invokes, how to get the name, where it routes, and what it returns. It omits edge-case details like errors or timeouts, which is acceptable given the tool's simplicity and the presence of an output schema.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate. It does specify that the 'name' parameter comes from find_tools and that 'arguments' are forwarded, adding some meaning beyond the schema. However, it does not elaborate on argument structure or provide examples, leaving partial compensation.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool invokes a named tool, explicitly references find_tools for name discovery, and distinguishes itself from the sibling by focusing on invocation rather than discovery. The verb 'Invoke' and resource 'tool by name' make the purpose unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description ties usage to find_tools output ('as returned by find_tools'), providing clear context on when to use this tool. It lacks explicit exclusions or 'when not to use' guidance, but the reference to the sibling is strong contextual guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

find_toolsA

Search all connected MCP servers for the tools most relevant to a natural-language query. Returns up to k tool definitions (name, description, input schema) as JSON. Call this first to discover which tool to use, then invoke it with call_tool.

ParametersJSON Schema
NameRequiredDescriptionDefault
kNo
queryYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.7/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden. It discloses that the tool searches across all connected MCP servers and returns a bounded number of tool definitions, which covers the core behavioral traits. However, it does not mention potential latency, network dependency, or failure modes, so it is not fully exhaustive.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two sentences long, front-loaded with the core function, and contains no fluff. Every clause adds value: what it does, what it returns, and how to use it.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's modest complexity (2 parameters, no annotations), the description fully covers the essential context: it explains the output schema content (name, description, input schema), the role in the workflow, and the parameters. It is a complete description for a discovery tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema has 0% description coverage, so the description must compensate. It does so implicitly: 'natural-language query' explains the query parameter, and 'up to k' explains the k parameter. This provides meaningful semantics beyond the raw schema, though it lacks detailed constraints or edge cases.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: 'Search all connected MCP servers for the tools most relevant to a natural-language query.' It also specifies the return format ('Returns up to k tool definitions... as JSON') and distinguishes itself from the sibling tool call_tool by positioning itself as the discovery step.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives explicit usage guidance: 'Call this first to discover which tool to use, then invoke it with call_tool.' This clearly states when to use this tool and how it relates to the alternative, call_tool.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 2 tool updatesv0.2.0
    • First observedcall_tool
    • First observedfind_tools

TDQS

A4.4/5.0

Scored across 2 tools

Disambiguation5/5

The two tools are entirely distinct: find_tools is for discovery/searching across upstream servers, while call_tool is for execution. There is no functional overlap or ambiguity.

Naming Consistency5/5

Both tools follow the verb_noun pattern (find_tools, call_tool) with lowercase and underscores, making the naming predictable and consistent.

Tool Count5/5

As a gateway that dynamically exposes tools from many upstream servers, two meta-tools (discover and invoke) are exactly the right scope. Each tool earns its place, and adding more would be redundant.

Completeness5/5

The tool surface fully covers the domain of a retrieval/calling gateway: discover which tool to use (find_tools) and then invoke it (call_tool). There are no obvious missing operations for this purpose.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    A
    maintenance
    A proxy server that wraps existing MCP servers to significantly reduce token consumption by compressing tool descriptions into a two-step interface. It enables users to integrate extensive toolsets without exceeding context limits or incurring high API costs.
    118
    Apache 2.0
  • A
    license
    Not graded
    quality
    D
    maintenance
    MCP proxy that reduces context usage through semantic tool routing, enabling on-demand discovery and routing of relevant tools.
    MIT
  • -
    license
    Not graded
    quality
    Not graded
    maintenance
    MCP proxy server with semantic tool search for LLM coding agents. It reduces context window usage by activating only relevant tools based on user queries.
    -
  • A
    license
    Not graded
    quality
    C
    maintenance
    Intelligent MCP proxy server that reduces context bloat by serving only the tools your AI actually needs through semantic search and a fixed two-tool surface.
    12
    MIT