| name | learn-api |
| description | External API evaluation and MCP server wrapper generation. Load when integrating a REST/GraphQL API as a workspace tool |
| user-invocable | false |
Learn API — Turn Useful APIs into Permanent MCP Tools
Evaluate external APIs encountered during work and, when they provide recurring value, wrap them as MCP servers so they become persistent workspace tools. Covers API assessment, spec discovery, server generation with FastMCP, route filtering, custom tool augmentation, path encoding pitfalls, VS Code stdio configuration, and common errors with solutions.
When to Use This Skill
- An external API is discovered during research or user request that would be useful to have permanently
- The user asks to interact with / connect to an external service (e.g., "search NPM", "check GitHub", "query Jira")
- An API was useful for a task and should be "learned" for future sessions
- Wrapping a REST API as an MCP server for workspace-wide access
- Troubleshooting an existing MCP server wrapping an external API
- Deciding whether an API is worth wrapping vs. using ad-hoc HTTP calls
Methodology
Phase 0: Assess API Value
Before building anything, evaluate whether the API is worth wrapping as a persistent MCP tool.
Assessment criteria:
| Factor | Worth Wrapping | Not Worth Wrapping |
|---|
| Usage frequency | Recurring across tasks/sessions | One-off lookup |
| Scope | Multiple useful endpoints | Single narrow query |
| Spec availability | OpenAPI/Swagger spec exists | No spec, undocumented |
| Auth complexity | Public or simple API key | Complex OAuth flows requiring user interaction |
| Data freshness | Live data matters (registries, status) | Static/archival data |
Decision:
- 3+ factors favor wrapping → proceed — create MCP server
- 1-2 factors favor wrapping → suggest to user, let them decide
- 0 factors → skip — use ad-hoc HTTP calls instead
When proceeding, present to user: "This API ({name}) would be useful as a permanent workspace tool. Want me to create an MCP server for it?"
Phase 1: Choose Your Approach
| Scenario | Approach | Effort |
|---|
| REST API with an OpenAPI spec available | FastMCP.from_openapi() auto-generation | Low — minutes |
| API without spec, or highly custom tool logic | Hand-crafted @mcp.tool() decorators | Medium |
| Hybrid: spec-based + custom overrides | from_openapi() + @server.tool() additions | Low-Medium |
| Existing FastAPI application | FastMCP.from_fastapi(app) conversion | Low |
Decision rule: If the API has an OpenAPI spec → always start with from_openapi(). Add custom tools only where auto-generation falls short (path encoding, response transformation, multi-step orchestration).
Phase 2: Set Up the Server
Install dependencies:
pip install fastmcp httpx
pip install pyyaml
Minimal hand-crafted server:
from fastmcp import FastMCP
mcp = FastMCP("My Server")
@mcp.tool()
async def hello(name: str) -> str:
"""Say hello to someone."""
return f"Hello, {name}!"
if __name__ == "__main__":
mcp.run()
OpenAPI auto-generated server:
from __future__ import annotations
import httpx
import yaml
from fastmcp import FastMCP
from fastmcp.server.openapi import MCPType, RouteMap
def _load_spec() -> dict:
"""Fetch and parse the OpenAPI spec."""
resp = httpx.get("https://api.example.com/openapi.yaml", timeout=30.0)
resp.raise_for_status()
return yaml.safe_load(resp.text)
def _create_client() -> httpx.AsyncClient:
"""Create the shared async HTTP client."""
return httpx.AsyncClient(base_url="https://api.example.com", timeout=30.0)
def create_server() -> FastMCP:
spec = _load_spec()
client = _create_client()
server = FastMCP.from_openapi(
openapi_spec=spec,
client=client,
name="My API Server",
route_maps=[
RouteMap(methods=["GET"], mcp_type=MCPType.TOOL),
],
)
return server
mcp = create_server()
if __name__ == "__main__":
mcp.run()
Phase 3: Filter Routes with RouteMap
RouteMap controls which OpenAPI endpoints become MCP tools, resources, or are excluded. Patterns are evaluated in order — first match wins.
Route filtering patterns:
route_maps=[
RouteMap(pattern=r".*/auth/.*", mcp_type=MCPType.EXCLUDE),
RouteMap(pattern=r".*/admin/.*", mcp_type=MCPType.EXCLUDE),
RouteMap(methods=["PUT", "POST", "DELETE", "PATCH"], mcp_type=MCPType.EXCLUDE),
RouteMap(pattern=r"/v0\.1/.*", mcp_type=MCPType.EXCLUDE),
RouteMap(pattern=r".*/items/\{itemId\}.*", mcp_type=MCPType.EXCLUDE),
RouteMap(methods=["GET"], mcp_type=MCPType.TOOL),
]
MCPType options:
| Type | Effect |
|---|
MCPType.TOOL | Expose as callable tool |
MCPType.RESOURCE | Expose as readable resource |
MCPType.RESOURCE_TEMPLATE | Expose as parameterized resource template |
MCPType.EXCLUDE | Skip entirely |
Phase 4: Add Custom Tools (Hybrid Pattern)
After from_openapi(), you can add custom @server.tool() decorators to the same server. This is essential when:
- Path parameters contain special characters (e.g.,
/ in identifiers)
- You need response transformation or aggregation
- Multi-step orchestration across endpoints
server = FastMCP.from_openapi(openapi_spec=spec, client=client, name="API")
@server.tool()
async def get_item_details(item_name: str) -> dict:
"""Get details for an item whose name may contain slashes.
Args:
item_name: Full item name (e.g. 'com.example/my-item')
"""
from urllib.parse import quote
encoded = quote(item_name, safe="")
resp = await client.get(f"/v1/items/{encoded}")
resp.raise_for_status()
return resp.json()
Phase 5: Configure VS Code
Add a server entry to the workspace MCP configuration:
{
"servers": {
"my-server": {
"type": "stdio",
"command": "python3",
"args": ["${workspaceFolder}/mcp-servers/my-server/server.py"]
}
}
}
Verify the server works:
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.1"}}}' | python3 server.py
Phase 6: Test and Iterate
- Start the server via VS Code MCP panel or terminal
- List tools — verify expected tools appear with correct names and descriptions
- Call each tool — test with representative inputs
- Check edge cases — special characters in parameters, empty results, error responses
Key Patterns
Path Parameter Encoding (Critical)
Problem: REST APIs with path parameters containing / (e.g., reverse-DNS identifiers like io.github.user/my-server) break when FastMCP auto-generates tool calls — the slash is treated as a path separator.
Solution: Exclude these endpoints from auto-generation and create custom tools with explicit URL encoding:
from urllib.parse import quote
RouteMap(pattern=r".*/servers/\{serverName\}.*", mcp_type=MCPType.EXCLUDE),
@server.tool()
async def get_server(server_name: str) -> dict:
"""Get server details. Name uses reverse-DNS format (e.g. 'io.github.user/repo')."""
encoded = quote(server_name, safe="")
resp = await client.get(f"/v0/servers/{encoded}")
resp.raise_for_status()
return resp.json()
Server Lifespans (Resource Management)
For servers that hold connections or caches, use the lifespan pattern:
from contextlib import asynccontextmanager
from dataclasses import dataclass
@dataclass
class ServerContext:
client: httpx.AsyncClient
@asynccontextmanager
async def server_lifespan(server: FastMCP):
client = httpx.AsyncClient(base_url="https://api.example.com", timeout=30.0)
try:
yield ServerContext(client=client)
finally:
await client.aclose()
mcp = FastMCP("Server", lifespan=server_lifespan)
Module-Level Export
The server object must be assigned at module level for stdio transport to work:
def create_server() -> FastMCP:
...
return server
mcp = create_server()
def create_server():
server = FastMCP("Server")
server.run()
Error Catalog
| Category | Error | Fix |
|---|
| Startup | ModuleNotFoundError: fastmcp | pip install fastmcp[openapi] |
| Startup | ImportError: httpx | pip install httpx |
| OpenAPI | Path params not encoded | Custom tool with manual URL construction |
| OpenAPI | Missing routes after filter | Check RouteMap keys match spec paths exactly |
| Runtime | ConnectionError on tool call | Verify base URL + API key env var |
| Runtime | Timeout on large responses | Add timeout param to httpx client |
| VS Code | Server not appearing | Restart VS Code, check settings.json MCP config |
| VS Code | "spawn ENOENT" | Use absolute path to Python in command array |
Anti-Patterns
- ❌ Auto-generate everything — Blindly using
from_openapi() without RouteMap filtering exposes write/auth endpoints as tools. Always exclude dangerous operations.
- ❌ Ignoring path encoding — Assuming FastMCP handles special characters in path parameters. It doesn't —
/ in path params silently breaks routing.
- ❌ Hardcoding base URLs — Embedding API URLs as string literals. Use environment variables or config for deployability.
- ❌ No timeout on HTTP client — Letting
httpx.AsyncClient use default (5s) timeout. Set explicit timeout=30.0 or higher.
- ❌ Creating client per tool call — Instantiating
httpx.AsyncClient inside each tool function. Create once and share via lifespan or module scope.
- ✅ Hybrid approach — Use
from_openapi() for the bulk, add @server.tool() for edge cases requiring custom logic.
- ✅ Read-only by default — Exclude write methods unless explicitly needed. MCP tools are invoked by LLMs — keep the blast radius small.
- ✅ Descriptive docstrings — LLMs read tool docstrings to decide when/how to call them. Invest in clear
Args: sections.
Templates
Complete server templates and configuration examples: see ./TEMPLATES.md
- OpenAPI-to-MCP Server (with RouteMap filtering, custom tools, lifespan)
- VS Code MCP Configuration Entry
Quick Reference
| Task | Command / Code |
|---|
| Install FastMCP | pip install fastmcp |
| Install with YAML support | pip install fastmcp httpx pyyaml |
| Run server standalone | python3 server.py |
| Run with inspector | fastmcp dev server.py |
| Install to Claude Desktop | fastmcp install server.py |
| Debug logging | FASTMCP_LOG_LEVEL=DEBUG python3 server.py |
| FastMCP Version | Key Feature |
|---|
| v2.14.0 | Background tasks (task=True), breaking changes (see docs) |
| v2.14.1 | Sampling with tools, Python 3.13 support |
| v2.14.2 | MCP SDK pinned <2.x, OpenAPI 3.1 support |
| v3.0.0 (beta) | Provider architecture, transforms, component versioning |