Zero-dependency tool registry and MCP server for LLM agents, with built-in tool search.
Project description
turbomcp
A zero-dependency tool registry and MCP server for LLM agents — with built-in tool search so agents don't drown in schemas.
Write plain Python functions. Get JSON Schema, OpenAI function-calling definitions, a real MCP server, and ranked tool search — without installing anything else.
from turbomcp import ContextAware
mcp = ContextAware()
@mcp.tool()
def greet(name: str = "World"):
"""Greet a user by name.
Args:
name (str): Who to greet
"""
return f"Hello, {name}!"
That's a working tool. Schemas are generated from the signature and docstring — types from annotations, descriptions from the Args: section, required/optional from defaults.
Why turbomcp?
Most tool servers dump every tool schema into the model's context on every request. That's fine for 5 tools and a disaster for 50. turbomcp is built around progressive disclosure — an agent can work its way down this ladder and only pay for what it needs:
| Call | Returns | Context cost |
|---|---|---|
mcp.list_tools() |
just the names | tiny |
mcp.list_tools(with_summary=True) |
names + one-liners | small |
mcp.search_tools("fetch a webpage") |
ranked matches, full schemas | only what's relevant |
mcp.get_capabilities(name="read_file") |
one full schema on demand | one tool |
Search is keyword-based out of the box (name/description/parameter matching with field weighting), and optionally semantic via static embeddings — no GPU, ~1s startup, instant queries.
Install
pip install turbomcp # core: registry + MCP server, zero dependencies
pip install turbomcp[web] # + built-in web tools (search, scrape)
pip install turbomcp[semantic] # + semantic tool search (model2vec)
pip install turbomcp[http] # + Flask REST API for debugging
pip install turbomcp[all] # everything
Use it as an MCP server
Any MCP client (Claude Code, Claude Desktop, ...) can launch your registry directly:
turbomcp serve my_tools.py # finds the ContextAware instance in the file
turbomcp serve my_tools.py:mcp # or name the variable explicitly
turbomcp demo # try it with the built-in default tools
turbomcp init [my_tools.py] # create a starter template
turbomcp init my_tools.py --with-web --with-semantic # include optional features
turbomcp help # show all commands
Claude Code:
claude mcp add my-tools -- turbomcp serve /path/to/my_tools.py
Claude Desktop (claude_desktop_config.json):
{
"mcpServers": {
"my-tools": {
"command": "turbomcp",
"args": ["serve", "/path/to/my_tools.py"]
}
}
}
The server implements MCP over stdio natively (JSON-RPC 2.0): initialize, ping, tools/list, tools/call, and it emits notifications/tools/list_changed when tools are registered or removed at runtime — register a new tool while the server is live and connected clients find out immediately.
One rule when serving stdio: don't
print()to stdout in your tool file — it corrupts the protocol stream. Usesys.stderr.
Lazy mode: tool search over MCP
turbomcp serve my_tools.py --lazy
In lazy mode the client doesn't see your catalog at all. It sees three meta-tools — search_tools, get_tool, call_tool — and discovers what it needs by searching. A 100-tool registry costs the model three schemas instead of a hundred.
Use it with OpenAI-compatible APIs
The same registry plugs straight into any chat-completions API (OpenAI, Gemini's OpenAI endpoint, Ollama, llama.cpp, vLLM, ...):
from openai import OpenAI
client = OpenAI(base_url="http://localhost:11434/v1", api_key="unused")
response = client.chat.completions.create(
model="your-model",
messages=[{"role": "user", "content": "Greet Ada"}],
tools=mcp.to_openai_tools(), # <- emitted in OpenAI format
)
for call in response.choices[0].message.tool_calls or []:
import json
result = mcp.call(call.function.name, json.loads(call.function.arguments))
Also available: mcp.to_mcp_tools() (MCP format) and mcp.get_tools_prompt() (plain text for models without native tool calling).
Tool search
mcp.search_tools("fetch a webpage", top_n=3)
# {"query": ..., "semantic": False, "tools": [{"name": "webpage_scrape", "score": ..., ...}]}
Enable semantic search to close the synonym gap ("say hello" → greet):
mcp.enable_semantic_search() # pip install turbomcp[semantic]
Keyword and semantic scores are blended (tunable alpha); without the extra installed, search falls back to pure keyword scoring automatically.
Built-in default tools
mcp.register_default(all=True) # everything
mcp.register_default(names=["calculate", "read_file"]) # or pick
| Tool | Needs [web] extra |
|---|---|
internet_search — DuckDuckGo (default) or Google, text or images |
yes |
webpage_scrape — fetch a page, strip boilerplate, return text |
yes |
get_current_time — local/IANA-timezone/UTC time |
no |
read_file — read a local text file |
no |
calculate — math expressions via a safe AST evaluator (no eval) |
no |
get_system_info — OS, arch, hostname, Python version |
no |
Writing good tools
Schemas are only as good as your signatures and docstrings:
from typing import Literal, Optional
@mcp.tool()
def convert(path: str, format: Literal["mp4", "webm", "gif"], quality: Optional[int] = None):
"""Convert a video file to another format.
Args:
path (str): Path to the input file
format (str): Output format
quality (int): Output quality 1-100, default keeps source quality
"""
...
- Annotations → types:
str,int,float,bool,list[str],dict,Optional[X],Literal[...](becomes anenum) all map to proper JSON Schema. - Defaults → optional: parameters without defaults are
required. Args:section → descriptions: Google-style docstrings, one line per parameter.- The first docstring paragraph becomes the tool description — make it say when to use the tool, not just what it does.
Optional HTTP API
For curl-debugging or non-MCP clients (pip install turbomcp[http]):
mcp.start_server(port=5000) # generates an API token, printed to stderr
| Endpoint | Description |
|---|---|
GET /tools?summary=true |
names (+ summaries) |
GET /capabilities |
full catalog |
GET /tool/<name> |
one tool's schema |
GET /search?q=... |
ranked search |
POST /call_tool |
execute — {"name": ..., "params": {...}}, token via X-API-Key header or api_key field |
The server binds 127.0.0.1 and only /call_tool requires the token. It's a Flask dev server: fine on localhost, not meant for the open internet.
Security notes
- Tools run with your user's permissions. A tool like "run shell command" gives the model exactly that power — register it knowingly.
calculateuses an AST walker, noteval— sandbox-escape expressions like(1).__class__...are rejected.- The HTTP token is generated per-process and printed to stderr; MCP stdio needs no token because the client launches the process itself.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file turbomcp-0.2.0.tar.gz.
File metadata
- Download URL: turbomcp-0.2.0.tar.gz
- Upload date:
- Size: 24.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
30ec66ff3a0f717843f44bc8bdbe6e644a3e8439b9630b79d0eae6296b142ee3
|
|
| MD5 |
42b9f80f49e06c43d1b9305a94344573
|
|
| BLAKE2b-256 |
91f866fb95ed9e2a671f3d9598ef92a82c018fc073c91da77fe0a5d94f47abd2
|
File details
Details for the file turbomcp-0.2.0-py3-none-any.whl.
File metadata
- Download URL: turbomcp-0.2.0-py3-none-any.whl
- Upload date:
- Size: 21.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1cd2efaf4b9bb016b7fb5a0b1484b3ea1d5c4cb59f16e1308caf623a3e31ca80
|
|
| MD5 |
e854723e7509ddfd13e3ce41a859ae5f
|
|
| BLAKE2b-256 |
59b9addf35f0c7ac9a25eacca5da6aa0555f0b4413093dc1e1b4f72f11979728
|