Skip to main content
MCPAdapterShipped

mcp-api-adapter — five surfaces, one catalog

Package: mcp-api-adapter (0.5.1+)
Status: Shipped
Essay: Five surfaces, one catalog
Design: docs/design/mcp-api-adapter.md
Example: examples/mcp-api-adapter/

mcp-api-adapter wraps any MCP server — stdio, Streamable HTTP, or gRPC — and exposes five API surfaces from one tool catalog without changing the server. No ClawQL install required.

Any MCP server
  ├─ stdio
  ├─ Streamable HTTP
  └─ gRPC


mcp-api-adapter

         ├── POST /{toolName}     OpenAPI + Swagger at /docs
         ├── POST /graphql        GraphQL mutations + GraphiQL at /graphiql
         ├── /mcp                 Streamable HTTP re-export for IDE clients
         ├── :50051               gRPC (upstream or locally scaffolded)
         └── gen-cli              Generated zero-dependency Node CLI

Point the adapter at one upstream. It calls ListTools at startup, builds the OpenAPI spec and GraphQL schema from each tool's inputSchema, and mounts all five surfaces.

The client fragmentation problem

MCP standardized how agents discover and call tools. It did not standardize how every other consumer reaches those tools.

ConsumerWants
Cloudflare WorkerPOST /memory_recall with a JSON body
OpenWebUI / model config panelsAn OpenAPI URL
Enterprise GraphQL stacksA typed mutation per tool
SREs / service meshgrpcurl on :50051
Cursor / Claude DesktopStreamable HTTP /mcp
Data / ops scriptsA thin CLI

The usual answer is a custom adapter per consumer — or Python mcpo for OpenAPI only. mcp-api-adapter is the TypeScript answer for all five.

Direction: MCP → APIs (inverse of ClawQL Core)

These two directions are complementary and easy to confuse:

PieceDirectionUpstreamConsumer
ClawQL Core search / executeOpenAPI → MCPREST / GraphQL / Discovery APIsAgents
mcp-api-adapterMCP → APIsAny MCP serverWorkers, REST, GraphQL, IDEs, mesh, CLI
Custom sourcesMCP (and APIs) → ClawQL gateway indexOther MCP servers / APIsAgents talking to one ClawQL MCP

In marketing: call this the OpenAPI on-ramp, GraphQL on-ramp, or MCP tools as REST/GraphQL — not “the OpenAPI gateway” (that phrase collides with ClawQL Core’s inverse direction).

Related: Custom sources registers upstream MCP servers into ClawQL. mcp-api-adapter exposes an MCP server outward to non-MCP clients.

Quick start

# Wrap a remote Streamable HTTP server
npx mcp-api-adapter --mcp-url http://127.0.0.1:8080/mcp

# Wrap a stdio package; expose /mcp for IDEs
npx mcp-api-adapter --stdio -- npx -y @modelcontextprotocol/server-everything

# Front an existing gRPC MCP server
npx mcp-api-adapter --grpc-address 127.0.0.1:50051

# Generate a CLI from the tool catalog
npx mcp-api-adapter gen-cli --out ./my-cli --stdio -- \
  npx -y @modelcontextprotocol/server-everything

Defaults: HTTP listen 0.0.0.0:8090. Then open /docs, try POST /\{toolName\}, open /graphiql, point an IDE at /mcp, and grpcurl -plaintext 127.0.0.1:50051 list.

Streamable HTTP with explicit binds

npx mcp-api-adapter \
  --mcp-url http://127.0.0.1:8080/mcp \
  --listen 0.0.0.0:8090 \
  --grpc-listen 127.0.0.1:50051
curl -s -X POST http://127.0.0.1:8090/echo \
  -H 'content-type: application/json' \
  -d '{"message":"hi"}'

curl -s http://127.0.0.1:8090/graphql \
  -H 'content-type: application/json' \
  -d '{"query":"mutation { echo(message: \"hi\") }"}'

grpcurl -plaintext 127.0.0.1:50051 list

Everything after --stdio -- is the child command. The gateway keeps the stdio session open, serves REST/GraphQL//mcp, and (unless --no-grpc) starts a local gRPC bridge that delegates into the same session.

With --grpc-address, no second gRPC server is started; /openapi.json advertises the upstream address in info.x-clawql-grpc.

The five surfaces

OpenAPI — POST /\{toolName\}

Every tool becomes a named REST route. The body is JSON matching the tool's inputSchema. Responses prefer MCP structuredContent, else parse single text content as JSON, else return a \{ content, text, isError \} envelope.

Swagger UI lives at /docs. Every path includes x-clawql-grpc extensions (gRPC endpoint, proto URL, example grpcurl). REST is an on-ramp, not a destination — /docs points developers at gRPC.

GraphQL — mutations per tool

Enterprise tooling is often GraphQL-native. Each tool gets a typed mutation derived from inputSchema. GraphiQL is at /graphiql. Schema includes callTool(name: String!, arguments: JSON): ToolResult for dynamic callers.

/mcp — Streamable HTTP re-export

Re-exports the upstream catalog as standard Streamable HTTP MCP:

  • stdio → remote IDE: wrap a local package; give Cursor/Claude https://your-host/mcp without SSH tunnels.
  • gRPC → MCP SDK: /mcp forwards into CallTool over gRPC, normalizing protobuf content oneofs into \{ type: "text", text \} blocks so MCP SDK validation passes (v0.5.1).

gRPC — the production path

If the upstream is already gRPC, REST and GraphQL forward into it. If the upstream is stdio or Streamable HTTP, the adapter starts a local mcp-grpc-transport server that delegates into the session.

Either way, :50051 is available for grpcurl, mesh routing, and protobuf clients. model_context_protocol.Mcp/CallTool takes a tool name and google.protobuf.Struct arguments. Argument schemas live in OpenAPI and GraphQL — clients do not need generated stubs.

Google proposed gRPC as a first-class MCP transport (February 2026). ClawQL ships the production TypeScript implementation as mcp-grpc-transport; the adapter makes it reachable from clients that cannot speak gRPC natively.

gen-cli — generated CLI

gen-cli reads the catalog and generates a thin Node CLI with one subcommand per tool. Arguments map from inputSchema. The CLI POSTs to the REST surface. PrintingPress will handle signed binary distribution when ready.

npx mcp-api-adapter gen-cli --out ./my-cli --stdio -- \
  npx -y @modelcontextprotocol/server-everything

./my-cli echo --message "hello"

What shipped

VersionWhat landed
0.3.xAny MCP upstream + OpenAPI + GraphQL + gRPC scaffold
0.4.0Renamed from mcp-openapi-gateway to mcp-api-adapter
0.5.0Streamable HTTP /mcp + gen-cli
0.5.1gRPC → /mcp content normalization for MCP SDK clients

When to use it

Use the adapter when you have a working MCP server and need Workers, OpenAPI panels, GraphQL, IDEs, mesh, or CLI access without writing glue per consumer.

Write your own when you need a surface the adapter does not have, significant custom auth, or generality works against you.

CLI reference

Flag / envMeaning
--mcp-urlStreamable HTTP MCP URL
--stdio -- <cmd…>Spawn MCP over stdio
--grpc-address / CLAWQL_MCP_GRPC_ADDRUpstream gRPC host:port
--grpc-host / --grpc-portAlternate gRPC address pieces
--listen / MCP_API_ADAPTER_LISTENHTTP bind (default 0.0.0.0:8090)
--grpc-listen / MCP_API_ADAPTER_GRPC_LISTENScaffolded gRPC bind (default 127.0.0.1:0)
--no-grpcDo not scaffold local gRPC (stdio/HTTP only)
--api-key / MCP_API_ADAPTER_API_KEYRequire X-API-Key or Authorization: Bearer
--refresh-msRe-ListTools poll interval
--titleSwagger / GraphiQL title

Legacy env MCP_OPENAPI_GATEWAY_* is still accepted. Exactly one upstream mode is required (--mcp-url, --stdio, or --grpc-address / env default).

Programmatic API

import { startMcpApiAdapter } from 'mcp-api-adapter'

const adapter = await startMcpApiAdapter({
  upstream: { kind: 'http', url: 'http://127.0.0.1:8080/mcp' },
  host: '0.0.0.0',
  port: 8090,
  grpcListen: '127.0.0.1:50051',
  apiKey: process.env.MCP_API_ADAPTER_API_KEY,
})

// adapter.url — OpenAPI + GraphQL
// adapter.grpcAddress — upstream or scaffolded gRPC
// adapter.getCatalog() — tools + surfaces + upstreamKind
await adapter.close()

Upstream union:

type UpstreamOptions =
  | { kind: 'grpc'; address: string; protocolVersion?: string }
  | { kind: 'http'; url: string }
  | {
      kind: 'stdio'
      command: string
      args?: string[]
      env?: Record<string, string>
    }

Compatibility: startMcpOpenApiGateway(\{ grpcAddress \})startMcpApiAdapter(\{ upstream: \{ kind: "grpc", address \}, grpcListen: false \}).

HTTP surface map

Method / pathRole
GET /healthzLiveness (upstreamKind, surfaces, grpcAddress)
GET /toolsFull catalog JSON
GET /openapi.jsonOpenAPI 3.1 from tool inputSchema
GET /docsSwagger UI
POST /\{toolName\}Invoke tool; JSON body = tool arguments
POST /graphqlGraphQL endpoint
GET /graphiqlGraphiQL IDE
GET /graphql/schema.graphqlSDL
Streamable HTTP /mcpMCP re-export for IDE / SDK clients

GraphQL conventions

  • Query.tools / Query.health — catalog + health
  • Mutation.<toolName>(…) — one field per tool; top-level JSON Schema properties become GraphQL args when GraphQL-safe
  • Mutation.callTool(name, args) — generic escape hatch for awkward schemas

Auth

When auth is set via --api-key (or MCP_API_ADAPTER_API_KEY / legacy MCP_OPENAPI_GATEWAY_API_KEY), all routes except /healthz require:

  • X-API-Key: <key>, or
  • Authorization: Bearer <key>

gRPC auth is not invented here — use mesh/mTLS / interceptors on mcp-grpc-transport for production gRPC.

Relationship to other ClawQL pieces

PieceRole
mcp-api-adapterMCP → OpenAPI + GraphQL + /mcp + gRPC + gen-cli
ClawQL search / executeOpenAPI → MCP tools (inverse)
Custom sourcesRegister other MCP servers into the ClawQL gateway
mcp-grpc-transportProduction TypeScript MCP gRPC transport
Panguard bridgePolicy / JWT ATR in front of MCP

Troubleshooting

SymptomCheck
Provide exactly one upstreamOnly one of --mcp-url / --stdio / --grpc-address
No gRPC surface for HTTP/stdioEnsure --no-grpc is unset; gateway sets ENABLE_GRPC while scaffolding
Empty GraphQL argsUpstream ListTools inputSchema missing/empty — use callTool(name, args: \{…\})
502 upstream CallTool failedUpstream down, wrong URL, or tool threw isError

Further reading