Turn any API into a curated MCP server from a single config file.
Point it at an API description, and it exposes that API to any MCP client (Claude Desktop, MCP Inspector, IDEs) as a set of tools. Unlike a 1:1 endpoint dump, simple-mcp curates: it filters to the operations you want, writes structured tool descriptions, and — for large APIs — switches to on-demand tool discovery so it doesn't flood the model's context.
Status: MVP. REST (OpenAPI 3.0/3.1) is fully supported. GraphQL and gRPC are architected for but not yet implemented (the adapter seam exists; the adapters throw "not yet implemented"). LLM-generated descriptions are stubbed — see Describe modes.
Naively turning every API endpoint into an MCP tool breaks down fast: LLM tool-selection degrades once a few dozen tools are in context, and every tool definition costs tokens on every turn. simple-mcp addresses this directly:
- Filter to an intentional surface (default: nothing is exposed unless you include it).
- Describe tools with a structured template (Purpose / Guidelines / Limitations / Parameters).
- Expose them
statically for small APIs, ordynamically (a singlesearch_toolsmeta-tool that loads real tools on demand) for large ones — chosen automatically.
npm install
npm run build
# Run against the bundled petstore example (no auth needed):
node dist/index.js simple-mcp.example.yaml
# or, once published: npx simple-mcp simple-mcp.example.yamlThe server speaks MCP over stdio by default. To try it interactively, point the MCP Inspector at that command.
Add to your claude_desktop_config.json:
{
"mcpServers": {
"petstore": {
"command": "node",
"args": ["/absolute/path/to/simple-mcp/dist/index.js", "/absolute/path/to/simple-mcp.example.yaml"]
}
}
}A single YAML file describes the whole server. Full example (simple-mcp.example.yaml):
server:
name: petstore-mcp
transport: [stdio] # stdio and/or http
source:
protocol: rest # rest (GraphQL/gRPC not yet implemented)
spec: https://petstore3.swagger.io/api/v3/openapi.json # URL or local path
baseUrl: https://petstore3.swagger.io/api/v3 # overrides the spec's server URL
auth:
type: none # none | bearer | apiKey | basic | oauth2
curation:
defaultInclude: false # if false, only `include`d operations are exposed
include:
- "getPetById" # match operationId (glob supported: "get*", "pets.*")
- "findPetsByStatus"
exclude: []
describe:
mode: deterministic # deterministic | llm
exposure: auto # auto | static | dynamic
tools: [] # optional task-oriented consolidation (see below)| Key | Meaning |
|---|---|
name |
Server name advertised to the MCP client. |
transport |
Array of stdio and/or http. http uses Streamable HTTP; set PORT (default 3000). |
| Key | Meaning |
|---|---|
protocol |
rest today. graphql / grpc reserved. |
spec |
OpenAPI document — remote URL or local file path. $refs are bundled/dereferenced automatically. |
baseUrl |
Base URL for live calls. Recommended — overrides the spec's servers entry. If omitted, a relative server URL in the spec (e.g. /api/v3) is resolved against the spec URL. |
Secrets come from environment variables via ${VAR} interpolation — never hardcode them in the file.
type |
Fields | Sent as |
|---|---|---|
none |
— | — |
bearer |
token |
Authorization: Bearer <token> |
apiKey |
token + (headerName or queryParam) |
header or query param |
basic |
username, password |
Authorization: Basic <base64> |
oauth2 |
token |
Authorization: Bearer <token> |
auth:
type: bearer
token: ${ACME_TOKEN} # read from the environment at startupUnset ${VAR} in the auth block is a hard error (so a missing secret fails loudly rather than sending unauthenticated requests). Elsewhere, an unresolved ${...} is left as a literal.
| Key | Meaning |
|---|---|
defaultInclude |
false (default): expose only matched include operations. true: expose everything except exclude. |
include / exclude |
Glob patterns matched against operationId, tags, and path. exclude wins over include. |
describe.mode |
deterministic (default) or llm (see below). |
exposure |
auto (default): dynamic when >30 tools, else static. Or force static / dynamic. |
tools |
Task-oriented consolidation: expose one named tool backed by an ordered sequence of operations. |
- static — every curated tool is registered up front. Best for small, focused APIs.
- dynamic — only a
search_toolstool is registered; the client searches by keyword, and matching tools are registered on demand. Keeps context small for large APIs. - auto — picks
dynamicabove 30 tools,staticotherwise.
- deterministic (default) — builds a structured description (Purpose, Guidelines, Limitations, Parameters, …) from the spec's own metadata. No API key needed.
- llm — *stubbed._ Intended to synthesize richer descriptions via Claude, cached to disk. Currently, if selected without
@anthropic-ai/sdk+ANTHROPIC_API_KEY, it prints a warning and falls back to deterministic.
Instead of exposing findAvailability and createEvent separately, expose one intent-shaped tool:
curation:
tools:
- name: schedule_event
steps: [findAvailability, createEvent]The REST adapter is validated end-to-end against public specs (scripts/validate-realworld.ts):
| API | Operations | Exposure (auto) | Introspect |
|---|---|---|---|
| Swagger Petstore v3 | 19 | static | ~1.5s |
| GitHub REST API | 1,194 | dynamic | ~1.5s |
| Stripe API | 587 | dynamic | ~2.7s |
All three introspect cleanly, and a live findPetsByStatus call against the petstore server round-trips end-to-end (the relative servers: [{url: /api/v3}] in its spec is resolved to an absolute URL automatically). Large APIs like GitHub and Stripe cross the 30-tool threshold and switch to dynamic exposure, so the client sees a single search_tools entry instead of ~1,000 tool definitions.
Re-run it yourself: node --import tsx scripts/validate-realworld.ts.
HTTP transport is local-by-default and optional token auth protects networked MCP access. Outbound calls are restricted to public destinations unless you opt in.
| Env var | Default | Meaning |
|---|---|---|
MCP_HTTP_HOST |
127.0.0.1 |
Bind address for transport: [http]. Set 0.0.0.0 only if you intentionally expose the port. |
MCP_HTTP_TOKEN |
(unset) | When set, require Authorization: Bearer <token> or X-MCP-Token: <token> on every HTTP request. |
MCP_HTTP_MAX_BODY_BYTES |
1048576 (1 MiB) |
Max inbound HTTP request body size (over → 413). |
MCP_FETCH_TIMEOUT_MS |
30000 |
Upstream fetch timeout. |
MCP_MAX_RESPONSE_BYTES |
5242880 (5 MiB) |
Max upstream response body size. |
MCP_OUTBOUND_ALLOW_PRIVATE |
(unset) | Set to 1 to allow loopback/private/metadata hosts (for local tests only). |
Additional hardening: sensitive client-supplied header parameters are dropped; operator auth is not sent to non-approved origins; OpenAPI x-mcp: { enabled: false } disables operations; secrets from auth are redacted from tool error text.
npm run build # tsc → dist/
npm test # node:test suite (unit + integration round-trips)
npx tsc --noEmit # type-check onlyTests use Node's built-in node:test (no framework). The integration suite drives a real MCP client↔server round trip against a mock upstream, covering static and dynamic exposure, path params, and request bodies.
MIT