yamcp
by adamgalmor
README.md
# yamcp
[](https://github.com/adamgalmor/yamcp/actions/workflows/ci.yml)
[](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
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues