Skip to main content
Glama

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.

Related MCP server: mcp-api-gateway

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

A
license - permissive license
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/datj9/simple-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server