The problem

Giving an AI agent access to an API usually means hand-writing a tool definition per endpoint, then maintaining it as the API changes. Most APIs already describe themselves in an OpenAPI spec. That spec should be the source of truth, not a second document kept in sync by hand.

Approach

Apidiom reads any OpenAPI spec, from a URL, a local file, or a built-in registry entry (Stripe, GitHub, OpenAI, Slack, Discord, Notion, Jira), and generates a single, self-contained MCP server file. No project scaffolding, no dependency tree.

Terminal window
npx apidiom generate mcp stripe --output stripe-mcp.js

Small APIs generate a flat tool list; APIs with 40+ operations switch automatically to a search mode, so an agent isn’t handed hundreds of tool definitions at once.

Why zero dependencies, specifically

This was the decision I went back and forth on most. Building on top of an existing MCP SDK would’ve been faster, less code, someone else already solved the transport layer. Decided against it on purpose.

Building MCP protocol handling from Node’s raw http and stdio primitives meant more code up front. But the output stays genuinely portable, copy one file anywhere Node runs, no node_modules, no supply-chain surface beyond Node itself.

Why two modes instead of one

Flat mode dumps every endpoint into the tool list up front. Fine for 10 endpoints, past 40 it starts eating context budget every turn, and larger tool lists measurably hurt an LLM’s ability to pick the right one.

Search mode exposes a search_tools function the agent calls first, then only loads what it needs. Slower on the first call, cheaper after.

apidiom.yaml
targets:
stripe:
spec: https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json
include:
tags: [Customers, Charges]
stripe-mcp.js (excerpt)
{
"name": "stripe_create_customer",
"description": "Create a new customer",
"inputSchema": { "type": "object", "properties": { "email": { "type": "string" } } }
}

Finding the actual threshold

Didn’t pick 40 out of thin air. Ran the same agent task against Stripe’s full spec in both modes and watched where flat mode started losing. Under 30 tools, flat mode was actually a little faster end to end.

What broke building this

Stripe’s spec alone is enormous, thousands of operations, $ref chains referencing other $ref chains. My first parser choked on circular references and sent a naive recursive resolver into an infinite loop.

Fixed it by tracking visited refs and breaking the cycle, one Set and a few lines. Finding that it was needed took an afternoon staring at a hung process.

Auth was the other messy corner. An API using OAuth2 with a token refresh flow needs the generated tool to know how to refresh a token, not just attach one, I scoped that out explicitly rather than half-implement it.

Stack

Pure TypeScript, compiled to a Node.js CLI. No runtime dependencies by design, the generated MCP server shouldn’t carry a dependency tree of its own.

Known limits

External $ref files aren’t resolved yet (inline only), OAuth2/OpenID Connect auth isn’t implemented, and relative-only server URLs need APIDIOM_BASE_URL set explicitly. Documented gaps, not accidents.