Skip to main content
Glama
README.md
# yamcp

[![CI](https://github.com/adamgalmor/yamcp/actions/workflows/ci.yml/badge.svg)](https://github.com/adamgalmor/yamcp/actions/workflows/ci.yml)
[![npm](https://img.shields.io/npm/v/%40adamgalmor%2Fyamcp)](https://www.npmjs.com/package/@adamgalmor/yamcp)

**Serve any OpenAPI spec as a secure MCP server** — scoped auth, per-tool allow/deny policies, rate limiting, and a redacted audit trail. One command.

```bash
npx @adamgalmor/yamcp serve --config yamcp.config.json
```

## The problem

Your customers' AI agents (Claude, ChatGPT, coding assistants) want to call your API — but exposing an API to an autonomous agent is not the same as exposing it to a developer:

- **Every MCP server you find on GitHub skips security.** No caller auth, no audit trail, no way to say "agents may read, never delete."
- **Enterprises block agent access outright** because there is no scoped access, no logging, and no rate control between the model and the API.
- **Building it yourself** means learning the MCP protocol, mapping your OpenAPI operations to tools by hand, and maintaining it as both evolve.

`yamcp` closes that gap: point it at the OpenAPI spec you already have, and it serves a policy-enforced MCP server in front of your API.

## How it works

Every tool call flows through a fixed pipeline — there is no way around it:

```
MCP client ──▶ inbound auth ──▶ allow/deny policy ──▶ rate limit ──▶ schema validation ──▶ upstream API
                                                                                              │
                                              audit log (JSONL, secrets redacted) ◀──────────┘
```

- **Operations → tools, automatically.** Each OpenAPI operation becomes an MCP tool with a JSON Schema derived from its parameters and request body. Arguments are validated before anything touches your API.
- **Credentials never reach the model.** Upstream API keys are referenced by environment-variable name and injected server-side. Config files contain no secrets, and `Authorization` inputs are stripped from tool schemas.
- **Deny means invisible.** Tools removed by policy or scope are not listed to the client at all — an agent cannot ask for what it cannot see.
- **Everything is on the record.** Every call — allowed, denied, rate-limited, or failed — is one JSONL line with caller, decision, latency, and redacted arguments.

## Quickstart (30 seconds)

```bash
# 1. Generate a config from your spec (lists every derived tool in an allowlist)
npx @adamgalmor/yamcp init --spec ./openapi.yaml

# 2. Trim the allowlist, set your upstream base URL, then check the result
npx @adamgalmor/yamcp list --config yamcp.config.json

# 3. Serve
export UPSTREAM_TOKEN=...   # if your API needs auth
npx @adamgalmor/yamcp serve --config yamcp.config.json
```

### Use it from Claude Code / Claude Desktop

```bash
claude mcp add my-api -- npx @adamgalmor/yamcp serve --config /path/to/yamcp.config.json
```

or in `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "my-api": {
      "command": "npx",
      "args": ["@adamgalmor/yamcp", "serve", "--config", "/path/to/yamcp.config.json"],
      "env": { "UPSTREAM_TOKEN": "..." }
    }
  }
}
```

## Configuration

```jsonc
{
  "spec": "./openapi.yaml",
  "upstream": {
    "baseUrl": "https://api.example.com",
    // Secrets are env-var references — never literals in this file.
    "auth": { "type": "bearer", "tokenEnv": "UPSTREAM_TOKEN" },
  },
  "policy": {
    "mode": "allowlist", // or "denylist"
    "allow": ["get*", "list*"], // "*" wildcards
    "deny": ["deleteUser"], // deny always wins
    "readOnly": false, // true = GET/HEAD operations only
  },
  "rateLimit": {
    "global": { "rps": 10 },
    "perTool": { "createOrder": { "rps": 1, "burst": 2 } },
  },
  "auth": {
    // Inbound auth for --http mode (stdio inherits the host process's trust).
    "http": {
      "tokens": [
        { "tokenEnv": "YAMCP_READ_TOKEN", "scopes": ["read"] },
        { "tokenEnv": "YAMCP_ADMIN_TOKEN", "scopes": ["read", "write"] },
      ],
      "scopeMap": {
        "read": ["get*", "list*"],
        "write": ["create*", "update*"],
      },
    },
  },
  "audit": {
    "destination": "./audit.jsonl", // or "stderr"
    "redactFields": ["ssn", "card_number"], // merged with built-in defaults
  },
}
```

## Remote mode (Streamable HTTP)

```bash
npx @adamgalmor/yamcp serve --config yamcp.config.json --http --port 3000
```

- Callers authenticate with bearer tokens; each token's **scopes** control which tools it can see and call. Two callers get two different tool lists.
- `--http` **refuses to start without `auth.http` configured** — secure by default.
- Token comparison is constant-time; tokens themselves live in environment variables.

## CLI

| Command                                           | What it does                                                       |
| ------------------------------------------------- | ------------------------------------------------------------------ |
| `yamcp init --spec <path>`                        | Generate a config scaffold with every derived tool in an allowlist |
| `yamcp validate --config <path>`                  | Validate config + spec without starting a server                   |
| `yamcp list --config <path>`                      | Show every tool and its effective policy decision                  |
| `yamcp serve --config <path> [--http] [--port N]` | Start the MCP server (stdio by default)                            |

## Example

A runnable petstore example lives in [`examples/petstore/`](examples/petstore/) — the audit log below is what one session against it looks like:

```json
{"ts":"2026-07-15T07:25:27.680Z","tool":"getPet","caller":"stdio","decision":"ok","args":{"petId":"42"},"upstreamStatus":200,"latencyMs":99}
{"ts":"2026-07-15T07:25:27.697Z","tool":"deletePet","caller":"stdio","decision":"denied"}
```

## Current limitations

- Request bodies must be JSON (`application/json` or `+json` media types); operations with form/multipart bodies are skipped with a warning at startup.
- Rate limits are in-memory and per-process — the right shape for MCP's process-per-client model, not for a fleet behind a load balancer.
- Upstream auth is static bearer/API-key via env vars; OAuth flows to the upstream are on the roadmap.

## Security model

See [SECURITY.md](SECURITY.md) for the threat model and design decisions.

## Development

```bash
npm install
npm run build
npm test        # 55 tests: unit + end-to-end over in-memory and HTTP transports
```

MIT © yamcp contributors