Metadata-Version: 2.4
Name: turbomcp
Version: 0.2.0
Summary: Zero-dependency tool registry and MCP server for LLM agents, with built-in tool search.
License: MIT
Keywords: mcp,llm,agents,tools,tool-calling,model-context-protocol
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: http
Requires-Dist: flask; extra == "http"
Provides-Extra: web
Requires-Dist: requests; extra == "web"
Requires-Dist: beautifulsoup4; extra == "web"
Requires-Dist: ddgs; extra == "web"
Requires-Dist: googlesearch-python; extra == "web"
Provides-Extra: semantic
Requires-Dist: model2vec; extra == "semantic"
Requires-Dist: numpy; extra == "semantic"
Provides-Extra: all
Requires-Dist: flask; extra == "all"
Requires-Dist: requests; extra == "all"
Requires-Dist: beautifulsoup4; extra == "all"
Requires-Dist: ddgs; extra == "all"
Requires-Dist: googlesearch-python; extra == "all"
Requires-Dist: model2vec; extra == "all"
Requires-Dist: numpy; extra == "all"
Dynamic: license-file

# 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](https://modelcontextprotocol.io) server, and ranked tool search — without installing anything else.

```python
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

```bash
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:

```bash
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:

```bash
claude mcp add my-tools -- turbomcp serve /path/to/my_tools.py
```

Claude Desktop (`claude_desktop_config.json`):

```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. Use `sys.stderr`.

### Lazy mode: tool search over MCP

```bash
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, ...):

```python
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

```python
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`):

```python
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

```python
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:

```python
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 an `enum`) 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]`):

```python
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.
- `calculate` uses an AST walker, not `eval` — 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
