| name | mcporter |
| description | Convert any CLI tool or Python function into an MCP server using fastmcp. Zero-config tool injection into Claude Code. |
| version | 1.0.0 |
| author | hermes-CCC (ported from Hermes Agent by NousResearch) |
| license | MIT |
| metadata | {"hermes":{"tags":["MCP","Tools","Development","fastmcp","Integration","Server"],"related_skills":["native-mcp"]}} |
MCPorter — CLI to MCP Converter
Turn any Python function or CLI tool into an MCP server that Claude Code can call natively.
Setup
pip install fastmcp
Minimal Server
from fastmcp import FastMCP
mcp = FastMCP("my-tool")
@mcp.tool()
def hello(name: str) -> str:
"""Say hello to someone."""
return f"Hello, {name}!"
if __name__ == "__main__":
mcp.run()
Register in .mcp.json:
{
"mcpServers": {
"my-tool": {
"command": "python",
"args": ["my_server.py"]
}
}
}
Restart Claude Code → hello tool appears natively.
Type Annotations Matter
Claude reads your type hints to understand the tool:
@mcp.tool()
def search_files(
query: str,
directory: str = ".",
max_results: int = 10,
case_sensitive: bool = False,
) -> list[str]:
"""Search for files matching query in directory."""
import subprocess
flag = "" if case_sensitive else "-i"
result = subprocess.run(
["grep", "-rl", flag, query, directory],
capture_output=True, text=True
)
return result.stdout.strip().split("\n")[:max_results]
Wrap a CLI Tool
import subprocess
from fastmcp import FastMCP
mcp = FastMCP("git-helper")
@mcp.tool()
def git_log(n: int = 10, format: str = "oneline") -> str:
"""Show git commit history."""
result = subprocess.run(
["git", "log", f"-{n}", f"--format={format}"],
capture_output=True, text=True
)
return result.stdout
@mcp.tool()
def git_diff(staged: bool = False) -> str:
"""Show git diff."""
args = ["git", "diff"]
if staged:
args.append("--staged")
result = subprocess.run(args, capture_output=True, text=True)
return result.stdout
if __name__ == "__main__":
mcp.run()
Async Tools
import asyncio
import httpx
from fastmcp import FastMCP
mcp = FastMCP("web-fetcher")
@mcp.tool()
async def fetch_url(url: str, timeout: int = 10) -> str:
"""Fetch content from a URL."""
async with httpx.AsyncClient() as client:
response = await client.get(url, timeout=timeout)
return response.text[:5000]
if __name__ == "__main__":
mcp.run()
Resources (Read-Only Data)
@mcp.resource("config://settings")
def get_settings() -> str:
"""Return current settings."""
import json, pathlib
settings = pathlib.Path("settings.json").read_text()
return settings
@mcp.resource("file://{path}")
def read_file(path: str) -> str:
"""Read a file by path."""
return pathlib.Path(path).read_text()
Prompts
@mcp.prompt()
def code_review_prompt(code: str, language: str = "python") -> str:
"""Generate a code review prompt."""
return f"Review this {language} code for bugs and improvements:\n\n```{language}\n{code}\n```"
HTTP Transport (for non-CC clients)
if __name__ == "__main__":
mcp.run(transport="http", host="0.0.0.0", port=8080)
Then register as:
{"mcpServers": {"my-tool": {"url": "http://localhost:8080/sse"}}}
Error Handling
@mcp.tool()
def risky_operation(value: str) -> str:
"""Do something that might fail."""
if not value:
raise ValueError("value cannot be empty")
return process(value)
Test Interactively
fastmcp dev my_server.py
With Environment Variables
{
"mcpServers": {
"my-api": {
"command": "python",
"args": ["api_server.py"],
"env": {
"API_KEY": "your-secret-key",
"BASE_URL": "https://api.example.com"
}
}
}
}