any2mcp
Convert plain Python modules into MCP servers without decorators or boilerplate, automatically exposing functions as schematized tools.
README
any2mcp
Turn any Python module into an MCP server — without handing a language model your shell.
You have a file full of useful Python functions and you want an agent to call them. Today that means importing an MCP framework and decorating every function — so your business logic now depends on MCP.
any2mcp skips that. Point it at a module and every public function becomes a fully-schematized MCP tool, with argument types and descriptions derived from the type hints and docstrings you already wrote:
any2mcp ./tools.py
But "expose every function" is a dangerous default, and most MCP tooling stops there. If your module happens to contain a run_shell() or a delete_path(), that naive approach just gave a language model your machine.
So any2mcp reads your code before it exposes it:
$ any2mcp examples/devtools.py --risk-report
any2mcp risk report: examples/devtools.py
policy: guard (allows up to 'moderate')
RISK TOOL CAPABILITIES STATUS
-------- ----------------- ------------ -------
safe word_count - exposed
low read_project_file fs-read exposed
- .read_text (line 36) [heuristic]
low list_directory fs-read exposed
- os.listdir (line 44)
moderate write_note fs-write exposed
- .write_text (line 50) [heuristic]
low parse_json_file fs-read exposed
- open (line 56)
high run_shell subprocess BLOCKED
- subprocess.run (line 66)
! blocked: risk 'high' (subprocess) exceeds policy 'guard' (max 'moderate')
high delete_path fs-delete BLOCKED
- shutil.rmtree (line 85)
- .unlink (line 87) [heuristic]
! blocked: risk 'high' (fs-delete) exceeds policy 'guard' (max 'moderate')
5 exposed, 2 blocked.
The two dangerous functions are not exposed, and no flags were needed to get that.
The difference
Before — one decorator per function, in a file that now depends on MCP:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("calculator")
@mcp.tool()
def add(a: float, b: float) -> float:
"""Add two numbers."""
return a + b
@mcp.tool()
def divide(a: float, b: float) -> float:
"""Divide a by b."""
return a / b
# ...repeat for every function, forever
if __name__ == "__main__":
mcp.run()
After — your module stays plain Python and knows nothing about MCP:
# calculator.py — no imports, no decorators
def add(a: float, b: float) -> float:
"""Add two numbers."""
return a + b
def divide(a: float, b: float) -> float:
"""Divide a by b."""
return a / b
any2mcp calculator.py
Install
pip install any2mcp
# or, with uv:
uv tool install any2mcp
Not yet on PyPI? Install straight from source:
uv tool install "git+https://github.com/SanoberRehman/any2mcp"
Quickstart
any2mcp tools.py --risk-report # audit what each function can reach
any2mcp tools.py --list # show the exposed tools + JSON schemas
any2mcp tools.py # serve over stdio
any2mcp tools.py:add # expose only `add`
any2mcp tools.py --exclude '_*' # filter with globs
any2mcp mypackage.tools # a dotted, importable module works too
The safety model
This is the part worth reading carefully, because the guarantees are uneven and it matters which is which.
What is enforced
A blocked function is never registered as a tool. It is absent from tools/list, so the model cannot see it, cannot call it, and cannot talk its way past it. There is no confirmation token to forge and no runtime check to race — the tool simply does not exist on the server.
You can watch this happen against a real MCP client:
$ python examples/demo_safety.py
=== any2mcp devtools.py (default policy: guard) ===
tools visible to the model: ['word_count', 'read_project_file', 'list_directory', 'write_note', 'parse_json_file']
run_shell: withheld; calling it anyway -> Unknown tool: run_shell
delete_path: withheld; calling it anyway -> Unknown tool: delete_path
word_count still works -> {"words": 3, "lines": 1, "characters": 18}
What is a heuristic
Which functions get classified as risky. any2mcp parses your module and looks for calls that reach the filesystem, the network, subprocesses, or the dynamic-execution builtins. It resolves import aliases, so both of these are caught:
import subprocess as sp
def a(cmd): sp.run(cmd) # -> subprocess (high)
from os import system as sh
def b(cmd): sh(cmd) # -> subprocess (high)
and it follows calls within your module, so a wrapper inherits its helper's capabilities:
def _upload(data): requests.post("https://...", data=data)
def publish(data): _upload(data) # -> network, "via _upload()"
But static analysis is not sound, and indirection defeats it:
def sneaky(cmd):
getattr(os, "system")(cmd) # -> reported SAFE. Not detected.
So a safe classification means "no known-risky call was found", never "this function is harmless". These blind spots are asserted in tests/test_risk.py under TestDocumentedBlindSpots, so this claim cannot quietly drift into implying more than it should.
What it is not
any2mcp is not a sandbox. Tools that are exposed run in-process with your full privileges. The policy decides what gets offered to a model; it does not contain what happens next.
Importing a module also executes its top-level code. That is why --risk-report never imports a file target — it works from source, so you can audit code before running any of it, and even audit a module whose dependencies you don't have installed.
Policies
| Policy | Allows | Use it when |
|---|---|---|
open |
everything | You have read the code and want the 0.1 behaviour. |
guard (default) |
up to moderate — reads, writes, network |
General use. Blocks subprocesses, eval, recursive deletes, and anything unanalyzable. |
readonly |
up to low — reads, env |
The agent should look but not touch. |
strict |
safe only |
Pure computation, no side effects of any kind. |
Overrides, in order of precedence — naming a specific tool beats a category rule, and an explicit deny always wins:
any2mcp tools.py --deny-tool 'admin_*' # never expose these
any2mcp tools.py --allow-tool run_shell # expose this one on purpose
any2mcp tools.py --deny-capability network # block a whole capability
Capabilities are fs-read, fs-write, fs-delete, network, subprocess, dynamic-exec, and env.
Use it as a CI gate
--risk-report exits 3 when anything is blocked and 0 when nothing is, so it can fail a build if someone adds a subprocess call to a module you expose to agents:
- run: any2mcp src/agent_tools.py --risk-report --format json
Audit log
any2mcp tools.py --audit-log calls.jsonl
Every call is appended as one JSON object. Argument names and types are recorded; values are not — tool arguments routinely carry API keys and file contents, and a log that silently copied them to disk would be its own vulnerability. Opt in with --audit-values when you've decided that's appropriate for your data.
{"ts": "2026-07-28T05:45:14.604+00:00", "event": "blocked", "tool": "run_shell",
"risk": "high", "reason": "risk 'high' (subprocess) exceeds policy 'guard' (max 'moderate')"}
{"ts": "2026-07-28T05:45:14.612+00:00", "event": "call", "tool": "write_note",
"risk": "moderate", "duration_ms": 0.431, "ok": true,
"args": {"path": "str", "text": "str"}}
{"ts": "2026-07-28T05:45:14.614+00:00", "event": "call", "tool": "word_count",
"risk": "safe", "duration_ms": 0.003, "ok": true, "args": {"text": "str"}}
Blocked tools are recorded too, so the log shows what was withheld, not just what ran.
Rich schemas, for free
any2mcp doesn't invent its own type system — it hands each function to the MCP SDK's pydantic machinery, the same code path the official @tool decorator uses. So the hard cases just work: Optional[...], unions, Enum, Literal, list/dict/tuple, pydantic models and dataclasses as parameters, defaults, and async def.
// any2mcp calculator.py --list (excerpt for round_to)
{
"name": "round_to",
"input_schema": {
"properties": {
"value": { "type": "number" },
"ndigits": { "type": "integer", "default": 2 },
"mode": { "$ref": "#/$defs/Rounding", "default": "nearest" }
},
"$defs": { "Rounding": { "enum": ["up", "down", "nearest"], "type": "string" } },
"required": ["value"]
}
}
A function whose signature genuinely can't be schematized is skipped with a message on stderr — one unsupported function costs one tool, never the whole server.
Use it with Claude Desktop (or any MCP client)
{
"mcpServers": {
"my-tools": {
"command": "any2mcp",
"args": ["/absolute/path/to/tools.py"]
}
}
}
Add "--policy", "readonly" to the args array to tighten it further.
Which functions get exposed?
Resolved in this order:
- A
:selector(tools.py:add) exposes exactly that one function. - Otherwise, if the module defines
__all__, that list is the allow-list. - Otherwise, every public function (no leading underscore) defined in the module — functions merely imported into it are skipped, so
from os.path import joinwon't leak in as a tool.
--include / --exclude globs apply on top, then the policy decides what survives.
Roadmap
- [ ] Policy file (
any2mcp.toml) so per-tool decisions can be reviewed in git - [ ] Expose class methods and
@staticmethods - [ ] Resources & prompts, not just tools
- [ ] Taint tracking, to tell
read_file("/etc/passwd")fromread_file(user_path)
OpenAPI and FastAPI conversion are intentionally out of scope — fastmcp already does those well. any2mcp focuses on the gap they leave: plain modules, zero decoration, and a defensible answer to "what did I just expose?"
Development
uv sync
uv run pytest # 127 tests, incl. real MCP client sessions
uv run ruff check .
License
MIT — see LICENSE.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
Neon Database
MCP server for interacting with Neon Management API and databases
E2B
Using MCP to run code via e2b.
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.