Skip to content

datj9/simple-mcp

Repository files navigation

simple-mcp

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.

Why curation matters

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, or dynamically (a single search_tools meta-tool that loads real tools on demand) for large ones — chosen automatically.

Quick start

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.yaml

The server speaks MCP over stdio by default. To try it interactively, point the MCP Inspector at that command.

Use it from Claude Desktop

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"]
    }
  }
}

Configuration

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)

server

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).

source

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.

auth

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 startup

Unset ${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.

curation

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.

Exposure strategies

  • static — every curated tool is registered up front. Best for small, focused APIs.
  • dynamic — only a search_tools tool is registered; the client searches by keyword, and matching tools are registered on demand. Keeps context small for large APIs.
  • auto — picks dynamic above 30 tools, static otherwise.

Describe modes

  • 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.

Task-oriented tools

Instead of exposing findAvailability and createEvent separately, expose one intent-shaped tool:

curation:
  tools:
    - name: schedule_event
      steps: [findAvailability, createEvent]

Verified against real APIs

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.

Security

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.

Development

npm run build         # tsc → dist/
npm test              # node:test suite (unit + integration round-trips)
npx tsc --noEmit      # type-check only

Tests 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.

License

MIT

About

Turn any API into a curated MCP server from a single config file

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors