Skip to main content
Glama
README.md
# simple-mcp

Turn any API into a **curated** [MCP](https://modelcontextprotocol.io) 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](#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 `static`ally for small APIs, or `dynamic`ally (a single `search_tools` meta-tool that loads real tools on demand) for large ones — chosen automatically.

## Quick start

```bash
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](https://github.com/modelcontextprotocol/inspector) at that command.

### Use it from Claude Desktop

Add to your `claude_desktop_config.json`:

```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`):

```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. `$ref`s 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>` |

```yaml
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:

```yaml
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](https://petstore3.swagger.io/api/v3/openapi.json) | 19 | static | ~1.5s |
| [GitHub REST API](https://github.com/github/rest-api-description) | 1,194 | dynamic | ~1.5s |
| [Stripe API](https://github.com/stripe/openapi) | 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

```bash
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

Maintenance

ActivityStale
ResponsivenessNo issues