One decorator. REST endpoint + MCP tool. Built-in UI, file serving, and LLM client.
Project description
simcpi
Building MCP tools today means maintaining two separate things — an API for your app and an MCP server for your AI client. simcpi collapses that into a single decorator, then gives you a browser tool to test, rate, and improve the result.
Define a function once. Get a REST endpoint, an MCP tool, and a visual tester — all at the same time. Then use MCPark to watch a real LLM call your tools, rate the results, and have simcpi rewrite your tool descriptions from that feedback.
Features:
@create_tool_api— one decorator registers both a FastAPI route and an MCP tool- MCPark — built-in browser UI to inspect, LLM-test, rate, and optimize MCP tools
- Feedback memory — log prompt → tool → 👍/👎 (+ optional reason), retrieve similar rated examples as few-shot hints on future runs
- Docstring optimizer — rewrite tool descriptions and score them against your ratings, then apply live or write straight back into your source
- Remote inspector — point MCPark (or the
mcparkCLI) at any MCP server, not just simcpi ones serve_file()— return DataFrames, charts, images, or any file from an MCP tool in one lineQuickClient/MCPClient— connect to any MCP server and run tools through an LLMconnect_claude()/connect_cursor()— auto-register your server in Claude Desktop or Cursor- Optional bearer-token auth — one
auth_token=argument protects every route - Multi-server support — combine tools from multiple MCP servers in one client
How it works:
simcpi wraps FastAPI and FastMCP behind a single MCPApi class. When you decorate a function with @app.create_tool_api(), it registers the function as a FastAPI route (accessible via REST) and as a FastMCP tool (accessible via the MCP streamable-HTTP transport) simultaneously. The same server handles both protocols. MCPark is mounted automatically and connects to the live MCP transport to test tools interactively.
pip install simcpi
Quick Start
from simcpi import MCPApi
import uvicorn
app = MCPApi(
title="my-api",
provider="openai",
api_key="sk-...",
model="gpt-4o",
)
@app.create_tool_api("/greet")
def greet(name: str) -> str:
"""
Greet the user by name.
Use this when the user wants to say hello.
"""
return f"Hello, {name}!"
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port=8000)
- REST endpoint:
POST /greet - MCP tool:
greet - Visual tester:
http://localhost:8000/mcpark - Swagger UI:
http://localhost:8000/docs
Features
1. @create_tool_api — REST + MCP in one decorator
@app.create_tool_api("/add")
def add(a: int, b: int) -> int:
"""Add two numbers. Use when the user asks to sum or total values."""
return a + b
- Creates
POST /add(FastAPI route) - Registers
addas an MCP tool (FastMCP) - Docstring is the tool description the LLM sees — write it for the model
- Async functions work.
Depends()parameters are automatically stripped from the MCP tool schema (the LLM never sees them; they'reNoneon MCP calls, injected normally on REST calls)
2. MCPark — visual MCP testing UI
Open http://localhost:8000/mcpark in any browser.
- Lists all registered tools in the sidebar
- Run tools with an LLM in the loop — select provider, paste API key
- Assistant toggle — off gives raw tool output, on gives LLM interpretation
- File results render inline: images show as images, Excel as a table, PDFs open in-page
- MCP image content (e.g. a chart returned directly by a tool) renders inline too
- ✨ Generate Test Cases — auto-generates single-tool and multi-tool test prompts for every tool
- Connect to any MCP server — the MCP Server field accepts any URL + optional bearer token, not just this server (see Remote inspector below)
- One-click copy of the Claude Desktop config snippet
No separate install. No external service. Ships inside the package.
3. Feedback memory — learn which tool the LLM should pick
Opt-in, default off. Turn it on and MCPark starts logging every run as prompt → tool(s) + params → rating, then uses that history to steer future runs.
app = MCPApi(
title="my-api",
provider="openai",
api_key="sk-...",
feedback=True, # turn it on
)
In MCPark, each run now shows a 👍 / 👎 block with an optional "why?" note (e.g. "wanted Telugu, got Hindi"). On the next run, simcpi embeds the new prompt, finds similar past prompts you rated, and injects them as few-shot hints — "a similar request got a 👍 with tool X; a 👎 with tool Y."
- 🧠 Recalled from feedback block shows which past examples informed a run, with similarity %
- 🗄 View Feedback DB button — browse the raw SQLite table (time, prompt, tool calls, rating, reason, embedded)
- 🗑 Clear DB — wipe and start fresh (handy after improving docstrings; localhost-only)
- Stored in a local SQLite file (
./simcpi_feedback.dbby default), one vector per row — no external vector DB
Retrieval needs an OpenAI-compatible embeddings key. If none is available it degrades gracefully to log-and-rate (no retrieval). See the embedding parameters in MCPApi parameters.
4. Docstring optimizer — rewrite tool descriptions from your ratings
Once you've rated some runs, click 📝 Improve Docstrings in MCPark. For each tool, simcpi:
- Turns your rated runs into expectations (this prompt should / should not trigger this tool, plus your written reasons as the strongest signal),
- Replays them through a selection-only LLM call to score the current description (tools are never executed),
- Rewrites the description and re-scores it,
- Shows
current% → candidate%and keeps the rewrite only if it scored better.
Each improved result gives you three actions:
- 📋 Copy — copy the new docstring to paste in yourself
- ⚡ Apply (live) — update the running tool's description immediately (lasts until restart)
- 💾 Write to source — rewrite the docstring straight into your
.pyfile (AST-located, body untouched) and update it live. Localhost-only — an exposed MCPark cannot edit your source.
5. Remote inspector — MCPark for any MCP server
MCPark talks the MCP protocol over HTTP, so it works against servers you didn't build with simcpi.
# installed as a console command — point it at any MCP server
mcpark https://mcp.deepwiki.com/mcp
mcpark http://127.0.0.1:9000/mcp/mcp --auth my-token --port 8123
# equivalently
python -m simcpi https://mcp.deepwiki.com/mcp
Flags: --auth (bearer token for the target), --port (local UI port, default 8123), --provider, --api-key, --model, --base-url. With no URL it opens an empty MCPark you can connect from the UI.
Against a remote server you get explore + LLM-test + feedback memory + optimizer scoring + Copy. (Apply-live and Write-to-source need the target to be in-process, so they only apply to your own simcpi server.)
6. serve_file() — return files from MCP tools
MCP tools can only return text. serve_file() bridges that gap — pass your object directly and get back a URL the client can download or MCPark can preview.
@app.create_tool_api("/report")
def sales_report() -> str:
"""Generate a sales report. Use when the user asks for sales data."""
import pandas as pd
df = pd.DataFrame({"Month": ["Jan","Feb","Mar"], "Sales": [1200, 1500, 1800]})
return app.serve_file("report.xlsx", df, port=8000)
@app.create_tool_api("/chart")
def sales_chart() -> str:
"""Generate a sales chart."""
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.bar(["Jan","Feb","Mar"], [1200, 1500, 1800])
return app.serve_file("chart.png", fig, port=8000)
@app.create_tool_api("/image")
def generate_image(prompt: str) -> str:
"""Generate an image from a prompt."""
url = call_image_api(prompt) # returns a remote image URL
return app.serve_file("image.png", url, port=8000)
| Content type | What happens |
|---|---|
pandas.DataFrame |
Serialized to .xlsx, .csv, or .json based on filename |
matplotlib.Figure |
Rendered to .png or .pdf |
bytes |
Written directly |
http://... / https://... |
Downloaded from URL and served |
str / Path |
Treated as a local file path, served as-is |
Served files are tracked with a TTL and a size cap so a long-running server doesn't leak memory or temp files — links expire after file_ttl seconds (default 3600) and the store holds at most file_store_max entries (default 256), evicting the oldest. Temp copies simcpi created are deleted on eviction; file paths you passed in are never touched.
Install extras for DataFrame and Figure support:
pip install simcpi[files]
7. Auth — one argument protects everything
Opt-in, default off. Set auth_token and every route (REST, MCP, MCPark, /files) requires it.
app = MCPApi(title="my-api", auth_token="my-secret-token")
Accepted credentials:
Authorization: Bearer my-secret-token— MCP clients, curl, scripts?key=my-secret-token— first browser visit; plants a cookie so MCPark keeps workingsimcpi_keycookie — subsequent browser requests
The clients and config helpers carry it for you:
QuickClient("http://localhost:8000/mcp/mcp", provider="openai", api_key="sk-...",
auth_token="my-secret-token")
app.connect_claude(port=8000) # writes the bearer header into the Claude config automatically
For custom schemes (JWT, OAuth) you can still add your own BaseHTTPMiddleware to app.api — it runs on all paths including /mcp/mcp.
8. QuickClient — sync MCP client in 5 lines
from simcpi import QuickClient
client = QuickClient(
mcp_server="http://localhost:8000/mcp/mcp",
provider="openai",
api_key="sk-...",
model="gpt-4o",
auth_token="my-secret-token", # only if the server uses auth
)
print(client.run("Greet Mohan in Telugu"))
File results are auto-detected and downloaded:
result = client.run("Give me the sales report")
print(result.save()) # saves to disk, prints the path
Multi-server — tools from all servers combined:
client = QuickClient(
mcp_server=[
"http://localhost:8000/mcp/mcp",
"http://localhost:8080/mcp/mcp",
],
...
)
List available tools:
client.tools()
# {"http://localhost:8000/mcp/mcp": ["greet", "report"], ...}
9. MCPClient — async client with full trace
import asyncio
from simcpi import MCPClient
client = MCPClient(
mcp_server="http://localhost:8000/mcp/mcp",
provider="anthropic",
api_key="sk-ant-...",
model="claude-sonnet-4-20250514",
auth_token="my-secret-token", # only if the server uses auth
)
result = asyncio.run(client.run("Add 42 and 58"))
print(result.answer) # "100"
print(result.tools_called) # [ToolCall(name="add", arguments={...}, result="100")]
print(result.success) # True
| Field | Type | Description |
|---|---|---|
answer |
str |
Final LLM response |
tools_called |
list[ToolCall] |
Every tool invoked, in order |
success |
bool |
False if an exception occurred |
error |
str | None |
Error message if success=False |
10. connect_claude() — Claude Desktop integration
On the server instance:
app.connect_claude(port=8000) # local server
app.connect_claude(url="https://myapi.com/mcp/mcp") # remote server
Standalone — no MCPApi needed (register multiple servers at once):
from simcpi import connect_claude
connect_claude("my-api", port=8000)
connect_claude("reports", port=8080)
connect_claude("prod", url="https://myapi.com/mcp/mcp", auth_token="secret")
Multiple servers append as separate entries — no collision. If the server uses auth, the bearer header is written into the config automatically. Restart Claude Desktop after calling.
11. connect_cursor() — Cursor IDE integration
app.connect_cursor(port=8000) # global (~/.cursor/mcp.json)
app.connect_cursor(port=8000, scope="project") # project (.cursor/mcp.json)
# standalone
from simcpi import connect_cursor
connect_cursor("my-api", port=8000)
connect_cursor("prod", url="https://myapi.com/mcp/mcp", auth_token="secret")
12. get_client() — in-process client
When you're already inside the server process, skip the HTTP round-trip:
client = app.get_client()
result = await client.run("Add 42 and 58")
13. export_mcpb() — one-click Claude Desktop bundle
app.export_mcpb() # writes My API.mcpb — double-click to install in Claude Desktop
MCPApi parameters
app = MCPApi(
title="my-api", # API name — used as key in Claude/Cursor config
provider="openai", # "openai" or "anthropic" — pre-fills MCPark UI
api_key="sk-...", # pre-fills MCPark UI, used by get_client()
base_url="https://...", # custom LLM endpoint (AICredits, OpenRouter, etc.)
model="gpt-4o", # pre-fills model in MCPark UI
description="...", # shown in Swagger + MCP server instructions
mcp_path="/mcp", # MCP transport prefix (default: /mcp)
mcpark_path="/mcpark", # MCPark UI path, set None to disable
version="1.0.0",
# auth (default off)
auth_token=None, # set a string → every route requires Bearer/?key=/cookie
# served-file lifecycle
file_ttl=3600, # seconds a serve_file() link stays valid (None = never)
file_store_max=256, # max live served files; oldest evicted past this
# feedback memory (default off)
feedback=False, # turn on the log → rate → retrieve loop in MCPark
feedback_db=None, # SQLite path (default ./simcpi_feedback.db)
feedback_only_positive=False, # retrieve only 👍 examples
feedback_k=5, # max neighbours retrieved per prompt
feedback_threshold=0.6, # min cosine similarity to count as a neighbour
# embeddings (used by feedback retrieval)
embedding_model="text-embedding-3-small", # fixed per DB — see Embeddings note below
embedding_api_key=None, # separate embeddings key (else reuses api_key if OpenAI)
embedding_base_url=None,
)
Endpoints created automatically
| Path | Description |
|---|---|
GET /docs |
Swagger UI |
GET /mcpark |
MCPark visual testing UI |
POST /mcp/mcp |
MCP streamable-HTTP transport |
GET /files/{token}/{filename} |
File serving for serve_file() |
MCPark also mounts internal routes it drives itself: /mcpark/tools, /mcpark/run, /mcpark/generate-tests, /mcpark/rate, /mcpark/feedback-stats, /mcpark/feedback-rows, /mcpark/feedback-clear (localhost-only), /mcpark/embed-backfill, /mcpark/optimize, /mcpark/apply-docstring, and /mcpark/write-docstring (localhost-only).
Supported providers
Any OpenAI-compatible provider works via base_url:
| Provider | Notes |
|---|---|
| OpenAI | model="gpt-4o" |
| Anthropic | provider="anthropic", model="claude-sonnet-4-20250514" |
| OpenRouter | custom base_url |
| AICredits | custom base_url |
| Any OpenAI-compatible API | custom base_url |
Embeddings note. Feedback-memory retrieval always goes through an OpenAI-style embeddings endpoint —
embedding_modelmust be servable that way (e.g.text-embedding-3-small/-large, not an Anthropic model). With an Anthropic chat key you can still log and rate; for retrieval supply a separateembedding_api_key.Pick one
embedding_modeland keep it. Every stored vector must share one embedding space, so the model is fixed per database — there's no per-run override. If you switch models after collecting data, old vectors stop matching: a different-dimension model leaves them safely ignored (backfill only fills missing embeddings, it never re-embeds), and a same-dimension different model silently mixes spaces and quietly degrades retrieval. Changing models? Click 🗑 Clear DB (or re-embed) first.
Requirements
- Python 3.10+
- fastapi, fastmcp, pydantic, uvicorn, openai, anthropic
Note: simcpi works with
fastmcp>=3.2(verified through 3.4.x). Ifpip install -U fastmcpever leaves you withImportError: cannot import name 'FastMCP', the upgrade got clobbered by a pip ordering quirk — fix it withpip install --force-reinstall "fastmcp-slim[client,server]".
Optional (pip install simcpi[files]):
- pandas + openpyxl — Excel / CSV output from DataFrames
- matplotlib — PNG / PDF output from Figures
License
MIT
Project details
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 simcpi-0.4.0.tar.gz.
File metadata
- Download URL: simcpi-0.4.0.tar.gz
- Upload date:
- Size: 65.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
036d218b98cced80c98a752d09d59592ee9de330a0c82a48dfa95361c0b958aa
|
|
| MD5 |
81133d56bae3e359d6cf56f43490fe2d
|
|
| BLAKE2b-256 |
f0eb71e4668ca739361a854984302a2b97a7c19b7141e59264ed7cd0f1e45903
|
File details
Details for the file simcpi-0.4.0-py3-none-any.whl.
File metadata
- Download URL: simcpi-0.4.0-py3-none-any.whl
- Upload date:
- Size: 62.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
edaed0152707e63344b770e28c5598fa871bded812a3b142455235b93e658b6a
|
|
| MD5 |
fc4e442e90d27b73f27a8bcadd69b974
|
|
| BLAKE2b-256 |
51150201cbc98ddcb6bc0f62f4dfe5ccd7e3315aa051d5c604b5cf0b9991541a
|