You already have an API, and you want AI agents to use it. The quickest honest route is to turn its OpenAPI spec into an MCP server, but a converter that maps every endpoint to a tool tends to produce something agents struggle with: dozens of look-alike tools, and write access nobody signed off on. This guide uses MCPcast, the OpenAPI-to-MCP generator in Promptise Foundry, to do it properly. You'll generate a server from a real API, read what it decided and why, allow writes behind human approval, connect it to Claude, and measure how well an agent actually uses it. Every command was run against Promptise Foundry 1.3.1 and the public Swagger Petstore, with gpt-5-mini wherever a model is needed, and the output is what it printed.
How do you turn a REST API into an MCP server?
Point MCPcast at your OpenAPI spec:
pip install promptise
promptise mcpcast https://petstore3.swagger.io/api/v3/openapi.jsonThat one command reads the spec, sorts every operation by risk, decides which ones become tools, and writes a complete Python project: the MCP server, a test per tool, a Dockerfile, and copy-paste setup for Claude Desktop, Claude Code and Cursor. By default the server can only read. Anything that writes has to be switched on, and then it waits for a person to approve it.
[02]
Why not one tool per endpoint?
Mapping each endpoint to a tool is easy, and it's where most conversions go wrong. A large API turns into a wall of tools whose names come from the URL structure, and the model has to pick the right one from descriptions written for human developers. Worse, every POST, PUT and DELETE becomes something an agent can call on its own.
MCPcast puts a few deliberate steps between the spec and the server:
Rendering diagram…
The plan file in the middle is the point. Every decision lands there in plain YAML, with a reason, before any code exists. You can read it, change it, and regenerate.
[03]
What you need
Python 3.10 or newer, and pip install "promptise>=1.3.1".
An OpenAPI spec, as a URL or a file. If your API is built with FastAPI, it already serves one at /openapi.json.
Optionally, an API key for a model provider. MCPcast uses a model to design the tools and to test them, but --no-curate runs fully offline.
[04]
Turn the Petstore API into an MCP server, step by step
The Swagger Petstore is a public demo API with 19 operations for pets, orders and users. It's a fair stand-in for a real product API.
Generate a read-only server
Start offline, without a model, so every decision comes from MCPcast's own rules:
promptise mcpcast https://petstore3.swagger.io/api/v3/openapi.json --no-curate -o petstore-readonlyParsed 19 operations from https://petstore3.swagger.io/api/v3/openapi.json;
profile=read-only auth=passthrough
╭────────────────────────────────── mcpcast ───────────────────────────────────╮
│ petstore → petstore-readonly/ │
│ tools: 6 (0 require human approval) │
│ not exposed: 13 operations (with reasons in the plan) │
│ files: petstore_mcp/ (10 modules), tests/ (2), mcpcast.plan.yaml, │
│ server.py, README.md, pyproject.toml, Dockerfile, .env.example, .gitignore │
╰──────────────────────────────────────────────────────────────────────────────╯Six tools out of 19 operations. That's the read-only profile at work: searching pets, reading orders and users stays in, and everything that changes data stays out.
Read the plan
Open mcpcast.plan.yaml. Each tool lists its risk class and the API routes it calls. At the bottom, every operation that didn't become a tool is listed with the reason:
dropped:
- operation_id: addPet
reason: write operation excluded by profile 'read-only'
- operation_id: getPetById
reason: 'unsupported by mcpcast: requires an API key in header ''api_key'', but this server presents
its credential in header ''Authorization'''
- operation_id: deletePet
reason: destructive operation excluded by profile 'read-only'
- operation_id: uploadFile
reason: 'unsupported by mcpcast: unsupported request body media type application/octet-stream'There are two kinds of reasons. Some operations are left out on purpose, because of the safety profile. Others MCPcast can't support yet, and it says so instead of generating a tool that would fail later. The full list is longer; this is an excerpt.
The risk class comes from fixed rules, not guesswork. GET is a read. DELETE, and verbs like cancel or revoke, are destructive. Words like charge or refund make an operation financial. Most other POST, PUT and PATCH calls are writes. The profile decides which classes make it in:
Profile | Reads | Writes | Destructive and financial |
|---|---|---|---|
read-only (default) | Yes | No | No |
standard | Yes | Yes, with approval | No |
full | Yes | Yes, with approval | Yes, with approval |
Allow writes, behind approval
To let agents create pets and place orders, switch to the standard profile. --auth env-token makes the server send one API token, from an environment variable, on every call it makes to your API. It's the simplest setup for a desktop client.
promptise mcpcast https://petstore3.swagger.io/api/v3/openapi.json \
--no-curate --profile standard --auth env-token -o petstore-mcpParsed 19 operations from https://petstore3.swagger.io/api/v3/openapi.json;
profile=standard auth=env-token
╭────────────────────────────────── mcpcast ───────────────────────────────────╮
│ petstore → petstore-mcp/ │
│ tools: 13 (7 require human approval) │
│ not exposed: 6 operations (with reasons in the plan) │
│ files: petstore_mcp/ (10 modules), tests/ (2), mcpcast.plan.yaml, │
│ server.py, README.md, pyproject.toml, Dockerfile, .env.example, .gitignore │
╰──────────────────────────────────────────────────────────────────────────────╯Thirteen tools now, and all seven writes need a person to approve them. The three deletes are still out: standard never generates destructive operations.
Run the generated tests
The project is an ordinary Python package with its own tests. They run against a fake copy of your API, so they never touch real data:
cd petstore-mcp
pip install -e ".[dev]"
pytest…
tests/test_tools.py ...................... [100%]
============================== 22 passed in 0.47s ==============================Each tool is checked three ways: it's listed, it calls the right route, and it's gated when it should be. One more test checks that every parameter advertises the schema from your spec, so the agent sees allowed values and limits, not just a type.
Connect it to Claude
The generated README.md includes setup for Claude Desktop, Claude Code, Cursor and Promptise agents, filled in for your server. For Claude Desktop, add it to claude_desktop_config.json:
{
"mcpServers": {
"petstore": {
"command": "python",
"args": ["/absolute/path/to/server.py"],
"env": { "MCPCAST_UPSTREAM_TOKEN": "Bearer <your API token>" }
}
}
}For Claude Code, it's one command:
claude mcp add petstore -e MCPCAST_UPSTREAM_TOKEN="Bearer <your API token>" -- python /absolute/path/to/server.py[05]
What happens when an agent tries to change something?
Here's a Promptise agent using the generated server. It's the same pattern as in How to Connect MCP Servers to Your AI Agent in Python:
import asyncio
from promptise import build_agent
from promptise.config import StdioServerSpec
async def main():
agent = await build_agent(
model="openai:gpt-5-mini",
servers={
"petstore": StdioServerSpec(
command="python",
args=["petstore-mcp/server.py"],
env={"MCPCAST_UPSTREAM_TOKEN": "Bearer demo"},
),
},
instructions="You help staff run the pet store. Use your tools.",
trace_tools=True,
)
try:
for question in [
"How many pets are available right now?",
"Add a new pet called Rex with the photo https://example.com/rex.jpg.",
]:
result = await agent.ainvoke({"messages": [{"role": "user", "content": question}]})
print(">>>", result["messages"][-1].content)
finally:
await agent.shutdown()
asyncio.run(main())→ Invoking tool: find_pets_by_status with {'status': 'available'}
✔ Tool result from find_pets_by_status: [{"id": 1, "name": "Pet1", "photoUrls": ["test1", "test2"], "tags": [], "status": "available"}]
>>> There is 1 pet available right now: Pet1 (ID 1). Would you like to see more details or the full list?
→ Invoking tool: add_pet with {'name': 'Rex', 'photoUrls': ['https://example.com/rex.jpg'], 'status': 'available'}
Elicitation request failed (McpError: Elicitation not supported) — treated as declined
✖ add_pet error: APPROVAL_DENIED: Approval denied for tool 'add_pet': client declined or returned an invalid elicitation response
→ Invoking tool: add_pet with {'name': 'Rex', 'photoUrls': ['https://example.com/rex.jpg'], 'status': 'available'}
Elicitation request failed (McpError: Elicitation not supported) — treated as declined
✖ add_pet error: APPROVAL_DENIED: Approval denied for tool 'add_pet': client declined or returned an invalid elicitation response
>>> I tried to add Rex but the store API returned an "approval denied" error and the pet wasn’t created.
Before I retry, please confirm or provide any of these details (I can use defaults if you prefer):
- Status (default: available)
- ID (optional — I can generate one)
- Category (optional)
- Tags (optional)
How would you like me to proceed? (e.g., "Retry with defaults", or provide ID/category/tags)The read went straight through and returned live data. The write was stopped, twice: the model tried add_pet again after the first denial, then gave up and asked the user what to do. Look at what it sent: the name and photo the user gave, plus a status nobody asked for. The optional id, category and tags it left out, and since Promptise 1.3.1 an agent's tool call carries only the arguments the model gave, so they never reach the server as null. Small guesses like that status are exactly why a person should see a write before it reaches your API.
By default, approval uses MCP elicitation: the server asks the client to confirm the exact call, and the client asks a person. The agent above has nobody to ask. Give it an approval handler with approval=, and the server's question goes to your handler. This one prints it and declines:
import asyncio
from promptise import build_agent
from promptise.approval import ApprovalDecision, ApprovalRequest
from promptise.config import StdioServerSpec
async def reviewer(request: ApprovalRequest) -> ApprovalDecision:
print(request.context_summary)
return ApprovalDecision(approved=False, reason="Not today.")
async def main():
agent = await build_agent(
model="openai:gpt-5-mini",
servers={
"petstore": StdioServerSpec(
command="python",
args=["petstore-mcp/server.py"],
env={"MCPCAST_UPSTREAM_TOKEN": "Bearer demo"},
),
},
instructions="You help staff run the pet store. Use your tools.",
approval=reviewer,
trace_tools=True,
)
try:
result = await agent.ainvoke({"messages": [{"role": "user", "content": "Add a new pet called Rex with the photo https://example.com/rex.jpg."}]})
print(">>>", result["messages"][-1].content)
finally:
await agent.shutdown()
asyncio.run(main())→ Invoking tool: add_pet with {'name': 'Rex', 'photoUrls': ['https://example.com/rex.jpg'], 'status': 'available'}
Server 'petstore' asks: Approval required: call tool 'add_pet' with arguments {'name': 'Rex', 'photoUrls': ['https://example.com/rex.jpg'], 'id': None, 'category': None, 'tags': None, 'status': 'available'}. Approve?
✖ add_pet error: APPROVAL_DENIED: Approval denied for tool 'add_pet': client declined or returned an invalid elicitation response
>>> Sorry — I wasn’t able to add Rex because the store-add tool call was blocked by an approval error on my side.
…The handler saw the call as the server checked it: the three arguments the model sent, and the optional ones it left out, shown as None. The server leaves those out of the request it sends to the API. When the handler approves, the call goes through and the API's answer comes back. When it declines, or the client can't ask at all, the answer is APPROVAL_DENIED:
Rendering diagram…
That's why the first agent was refused: it was built without an approval handler, so it tells the server it can't answer, and the server fails closed. In production your handler asks a real person, for example through a webhook or a queue. For agents that run without anyone at the client, generate the server with --auth api-key instead. Approvals then use the pending mode: a gated call waits until a different person on the same tenant, with the approver role, approves it through the generated approval tools. Human approval in the docs covers both modes.
[06]
Let a model design the tools
So far every tool mirrors one endpoint. Leave out --no-curate and MCPcast asks a model to design the tool set: clearer names, better descriptions, and related endpoints merged into one tool. By default it uses openai:gpt-5-mini, and --model picks another. This run also adds --eval, so set your model provider's key and MCPCAST_UPSTREAM_TOKEN first; the evaluation reads from the live API.
promptise mcpcast https://petstore3.swagger.io/api/v3/openapi.json \
--profile standard --auth env-token --eval --eval-tasks 8 -o petstore-curatedParsed 19 operations from https://petstore3.swagger.io/api/v3/openapi.json;
profile=standard auth=env-token
Curating with openai:gpt-5-mini (budget 25 tools)…
╭────────────────────────────────── mcpcast ───────────────────────────────────╮
│ petstore → petstore-curated/ │
│ tools: 12 (7 require human approval) │
│ not exposed: 6 operations (with reasons in the plan) │
│ files: petstore_mcp/ (10 modules), tests/ (2), mcpcast.plan.yaml, │
│ server.py, README.md, pyproject.toml, Dockerfile, .env.example, .gitignore │
╰──────────────────────────────────────────────────────────────────────────────╯
Evaluating with openai:gpt-5-mini (8 tasks)…
Agent Readiness: B (6/8 tasks succeeded)
✗ `get_order` (task t6: orderId=3) called the API with identifiers from the
plan's example that it does not recognise — the example teaches both the task
writer and the agent, so replace those example values with ones that exist
• 4 tools not covered by any task: `create_user`, `logout_user`, `get_user`,
`update_user` — raise --eval-tasks to score them
Report: petstore-curated/eval/report.md Tasks: petstore-curated/eval/tasks.yamlThirteen tools became twelve. The two pet searches, by status and by tags, became a single find_pets that calls whichever route fits the arguments. Names got shorter and clearer: get_order_by_id became get_order, get_user_by_name became get_user, and create_users_with_list_input became create_users_bulk. The model proposes, but MCPcast's code decides: it checks every proposal, and it never lets a tool's risk drop below what the rules assigned.
MCPcast also checks what the model writes. A description that sends agents to a tool the profile removed, such as a delete_pet under standard, goes back to the model with the list of tools it may name, and if the last attempt still names one, that sentence is cut. The model can still get other details wrong, which is why the plan stays in your hands. To change a description, edit it in mcpcast.plan.yaml and regenerate from the plan:
promptise mcpcast mcpcast.plan.yamlRegenerating rewrites the server code from the plan, without calling the model again. It also re-checks the plan's safety rules. Try to remove the approval from a write and it refuses:
Error: invalid plan:
1 validation error for MCPcastPlan
(root)
Value error, tool 'add_pet' is write: profile 'standard' requires requires_approval=true[07]
Measure how well agents use it
--eval is the part most converters skip. A model writes realistic tasks for your API, and a real agent tries to complete each one against the generated server. Reads hit your live API. Writes go to mocks built from the spec, so an evaluation never changes real data. The report for the run above:
Measure | Result |
|---|---|
Agent Readiness | B, score 0.85 |
Tasks completed | 6 of 8 |
Right tool chosen first | 100% |
Parameter errors | 0% |
The score is 0.6 × the task success rate plus 0.4 × the rate of choosing the right tool first. The agent chose the right tool first in every task, so neither failure was a wrong pick. One task asked for order 3, the example the model had written into the plan for get_order, and the Petstore has no order 3, so it answered 404. The report blames the plan's example, rightly: an example teaches both the task writer and the agent, so give it an order that exists. The other task logged in, and the Petstore answered with plain text labelled as JSON, which the generated server refuses rather than guess at. The report also flagged four tools no task had covered, and suggested raising --eval-tasks to test them too.
[08]
Honest limits
MCPcast is upfront about what it doesn't do yet, and you'll want to know before you rely on it:
OpenAPI only. Postman collections and GraphQL aren't supported.
No OAuth 2.1 yet. Use a bearer token or pre-shared API keys.
Some operations are skipped. Operations that need header or cookie parameters, or take multipart, plain-text or binary uploads, are left out with a reason in the plan. Petstore's uploadFile is one of them.
Your spec is trusted input. MCPcast fetches spec URLs without checking for private networks and builds code from what the spec says, so only use specs you'd trust as much as code you run.
Claude Desktop needs stdio. Its config can't send headers to an HTTP server, so use the stdio setup shown above.
The full list is in the limits section of the MCPcast reference.
[09]
Frequently asked questions
What is MCPcast?
MCPcast is the part of Promptise Foundry that turns an existing API into an MCP server. It reads an OpenAPI spec and generates a complete, editable Python project. Like the rest of Promptise Foundry, it's open source under the Apache 2.0 license.
Do I need an LLM to use it?
No. With --no-curate, everything runs offline and deterministically. A model is only used to design nicer tools and to run --eval.
Can I edit the generated code?
Edit mcpcast.plan.yaml and regenerate; the server code is rebuilt from it. Your own files and tests in the project are left alone, while generated modules are rewritten. pyproject.toml, the Dockerfile and .env.example are yours after the first run.
Does it work with a FastAPI app?
Yes. FastAPI serves its OpenAPI spec at /openapi.json, so you can point MCPcast at your running app. The guide for Python APIs walks through it.
Is there a guided setup?
Yes. Run promptise mcpcast with no arguments for an interactive setup in your terminal. It walks you through the spec, the model, the safety profile and the auth mode, shows every tool for review, and writes nothing until you confirm. At the end it prints the equivalent one-line command. See Guided setup.
[10]
Where to go next
MCPcast reference: every option, the plan format and the approval modes.
Recipes for Stripe, GitHub and your own app: real APIs, step by step.
Approval gates: how human approval works in any Promptise MCP server.
Deployment: running your server in production.
How to Connect MCP Servers to Your AI Agent in Python: use your new server from your own agent.