any2mcp

any2mcp

Convert plain Python modules into MCP servers without decorators or boilerplate, automatically exposing functions as schematized tools.

Category
Visit Server

README

any2mcp

Turn any Python module into an MCP server — without handing a language model your shell.

CI PyPI Python License: MIT

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:

  1. A :selector (tools.py:add) exposes exactly that one function.
  2. Otherwise, if the module defines __all__, that list is the allow-list.
  3. Otherwise, every public function (no leading underscore) defined in the module — functions merely imported into it are skipped, so from os.path import join won'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") from read_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

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.

Official
Featured
TypeScript
Magic Component Platform (MCP)

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.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

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.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

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.

Official
Featured
TypeScript
Kagi MCP Server

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.

Official
Featured
Python
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Exa Search

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.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured