simple-mcp
by datj9
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
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues