Skip to content

Commit 3c9bd6a

Browse files
committed
Complete OpenAPI-to-MCP feature set with transports, auth, validation, scaffold, and tests
1 parent a000bc5 commit 3c9bd6a

12 files changed

Lines changed: 1158 additions & 661 deletions

File tree

.github/workflows/ci.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
test:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- uses: actions/setup-node@v4
14+
with:
15+
node-version: 20
16+
cache: npm
17+
- run: npm ci
18+
- run: npm run check
19+
- run: npm test
20+
- run: npm run smoke

.github/workflows/release.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*'
7+
8+
jobs:
9+
release:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
contents: write
13+
steps:
14+
- uses: actions/checkout@v4
15+
- uses: actions/setup-node@v4
16+
with:
17+
node-version: 20
18+
cache: npm
19+
- run: npm ci
20+
- run: npm run build
21+
- run: npm test
22+
- run: npm pack
23+
- name: Create GitHub Release
24+
uses: softprops/action-gh-release@v2
25+
with:
26+
generate_release_notes: true
27+
files: |
28+
*.tgz

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ node_modules/
22
dist/
33
.DS_Store
44
.env
5+
.cache/

README.md

Lines changed: 66 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,36 @@
22

33
OpenAPI 3.x to MCP server bridge in TypeScript.
44

5-
This project takes an OpenAPI spec and exposes every operation as an MCP tool, while proxying calls to the original REST API.
6-
7-
## Feature checklist
8-
9-
- OpenAPI 3.0+ support
10-
- Loads YAML/JSON, dereferences `$ref`, compiles operations into MCP tools.
11-
- Proxy behavior
12-
- `tools/call` executes real HTTP requests against your API and returns structured results.
13-
- Authentication support (env-driven)
14-
- API key, Bearer, Basic, OAuth2/OpenID Connect token injection.
15-
- Zod validation
16-
- Generates Zod schemas from OpenAPI-derived JSON Schema for runtime input validation.
17-
- Typed server
18-
- Fully typed TypeScript codebase with strict compile checks.
19-
- Multiple transports
5+
`mcp-openapi` takes an OpenAPI spec and turns it into an MCP server where each OpenAPI operation is an MCP tool. Tool calls are proxied to the original REST API with runtime validation and auth handling.
6+
7+
## Capabilities
8+
9+
- OpenAPI 3.0+ support (YAML/JSON, `$ref` dereference, operation compilation)
10+
- Proxy behavior to upstream REST API
11+
- Authentication via env vars:
12+
- API keys (`in: header|query|cookie`)
13+
- HTTP Bearer
14+
- HTTP Basic
15+
- OAuth2 / OpenID Connect (static token or client credentials token fetch)
16+
- Runtime validation:
17+
- Zod validation generated from OpenAPI-derived JSON Schema
18+
- AJV JSON Schema validation
19+
- Response schema validation by HTTP status
20+
- Typed TypeScript implementation
21+
- Multiple transports:
2022
- `stdio`
2123
- `streamable-http` (Hono)
22-
- deprecated `sse` (Hono + SDK SSE transport)
23-
- Project scaffold
24-
- Includes `tsconfig.json`, `package.json`, scripts, entrypoint, tests.
25-
- Built-in HTML test clients
26-
- `/test/streamable` and `/test/sse` for browser-based interaction testing.
24+
- `sse` (legacy compatibility transport)
25+
- Built-in browser test clients:
26+
- `/test/streamable`
27+
- `/test/sse`
28+
- Project scaffold (`init`) that generates:
29+
- `package.json`
30+
- `tsconfig.json`
31+
- `src/server.ts`
32+
- `.env.example`
33+
- `README.md`
34+
- `Dockerfile`
2735

2836
## Install
2937

@@ -33,13 +41,13 @@ npm install
3341

3442
## Run
3543

36-
### 1) stdio transport (default)
44+
### stdio
3745

3846
```bash
3947
npm run dev -- --spec ./openapi.yaml
4048
```
4149

42-
### 2) StreamableHTTP transport (Hono)
50+
### StreamableHTTP
4351

4452
```bash
4553
npm run dev -- --spec ./openapi.yaml --transport streamable-http --port 3000
@@ -48,10 +56,11 @@ npm run dev -- --spec ./openapi.yaml --transport streamable-http --port 3000
4856
Endpoints:
4957

5058
- `http://localhost:3000/health`
59+
- `http://localhost:3000/metrics`
5160
- `http://localhost:3000/mcp`
5261
- `http://localhost:3000/test/streamable`
5362

54-
### 3) SSE transport (deprecated, for compatibility)
63+
### SSE (legacy)
5564

5665
```bash
5766
npm run dev -- --spec ./openapi.yaml --transport sse --port 3000
@@ -60,99 +69,52 @@ npm run dev -- --spec ./openapi.yaml --transport sse --port 3000
6069
Endpoints:
6170

6271
- `http://localhost:3000/health`
72+
- `http://localhost:3000/metrics`
6373
- `http://localhost:3000/sse`
6474
- `http://localhost:3000/messages?sessionId=...`
6575
- `http://localhost:3000/test/sse`
6676

67-
## Common options
77+
## CLI
6878

6979
```bash
70-
--server-url <url> Override OpenAPI server URL
71-
--timeout-ms <ms> HTTP timeout per attempt (default: 20000)
72-
--retries <n> Retries on 408/429/5xx + transient network errors (default: 2)
73-
--retry-delay-ms <ms> Base retry delay (default: 500)
74-
--watch-spec Reload tools when spec changes
80+
mcp-openapi --spec <openapi-file> [options]
81+
mcp-openapi init [dir]
7582
```
7683

77-
## Authentication env vars
78-
79-
- API key: `MCP_OPENAPI_API_KEY`
80-
- Bearer: `MCP_OPENAPI_BEARER_TOKEN`
81-
- Basic: `MCP_OPENAPI_BASIC_USERNAME`, `MCP_OPENAPI_BASIC_PASSWORD`
82-
- OAuth2/OIDC bearer token: `MCP_OPENAPI_OAUTH2_ACCESS_TOKEN`
83-
- Per-scheme override: `MCP_OPENAPI_<SCHEME_NAME>_TOKEN`
84-
85-
## Validation model
86-
87-
Input validation pipeline for `tools/call`:
88-
89-
1. Zod validation (generated from OpenAPI-derived schema)
90-
2. JSON Schema validation (Ajv)
91-
92-
Success responses can also be validated against OpenAPI response schemas, and tool output is validated against MCP `outputSchema`.
93-
94-
## Generated tool shape
95-
96-
Each OpenAPI operation becomes an MCP tool with:
97-
98-
- `name`
99-
- `description`
100-
- `inputSchema`
101-
- `outputSchema` (when available)
102-
- tool annotations (`readOnlyHint`, `idempotentHint`, etc.)
103-
104-
Input argument groups:
105-
106-
```json
107-
{
108-
"path": {},
109-
"query": {},
110-
"header": {},
111-
"cookie": {},
112-
"body": {},
113-
"pagination": {
114-
"enabled": false,
115-
"mode": "autoCursor",
116-
"maxPages": 5,
117-
"cursorParam": "cursor",
118-
"nextCursorPath": "next_cursor",
119-
"pageParam": "page",
120-
"startPage": 1
121-
}
122-
}
123-
```
124-
125-
## Request/response features
126-
127-
- Query/path/header/cookie style-aware serialization
128-
- Request body content types:
129-
- `application/json`
130-
- `application/x-www-form-urlencoded`
131-
- `multipart/form-data`
132-
- `application/octet-stream`
133-
- Pagination helpers:
134-
- cursor mode
135-
- incrementing page mode
136-
- Retry with `Retry-After` support
137-
- Progress notifications via MCP `_meta.progressToken`
138-
- Cancellation support via MCP cancellation
139-
140-
## Build/test/smoke
84+
Options:
85+
86+
- `--server-url <url>`
87+
- `--cache-path <file>`
88+
- `--print-tools`
89+
- `--validate-spec`
90+
- `--transport stdio|streamable-http|sse`
91+
- `--port <n>`
92+
- `--watch-spec`
93+
- `--timeout-ms <ms>`
94+
- `--retries <n>`
95+
- `--retry-delay-ms <ms>`
96+
- `--max-response-bytes <n>`
97+
- `--max-concurrency <n>`
98+
- `--allow-hosts host1,host2`
99+
100+
## Auth env vars
101+
102+
- `MCP_OPENAPI_API_KEY`
103+
- `MCP_OPENAPI_BEARER_TOKEN`
104+
- `MCP_OPENAPI_BASIC_USERNAME`
105+
- `MCP_OPENAPI_BASIC_PASSWORD`
106+
- `MCP_OPENAPI_OAUTH2_ACCESS_TOKEN`
107+
- `MCP_OPENAPI_OAUTH2_CLIENT_ID`
108+
- `MCP_OPENAPI_OAUTH2_CLIENT_SECRET`
109+
- `MCP_OPENAPI_<SCHEME_NAME>_TOKEN`
110+
- `MCP_OPENAPI_<SCHEME_NAME>_CLIENT_ID`
111+
- `MCP_OPENAPI_<SCHEME_NAME>_CLIENT_SECRET`
112+
113+
## Build and verify
141114

142115
```bash
143116
npm run check
144117
npm run build
145118
npm test
146119
npm run smoke
147120
```
148-
149-
`npm run smoke` starts a full MCP client/server+HTTP flow and verifies that an OpenAPI spec is transformed into callable MCP tools.
150-
151-
## Browser test clients
152-
153-
After launching a web transport:
154-
155-
- StreamableHTTP client: `http://localhost:<port>/test/streamable`
156-
- SSE client: `http://localhost:<port>/test/sse`
157-
158-
These pages let you initialize, list tools, and call tools directly in-browser.

src/compile-cache.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { createHash } from "node:crypto";
2+
import { mkdir, readFile, writeFile } from "node:fs/promises";
3+
import { dirname, resolve } from "node:path";
4+
import { compileOperations } from "./compiler.js";
5+
import { loadOpenApiDocument } from "./openapi.js";
6+
import type { OperationModel } from "./types.js";
7+
8+
interface CacheEntry {
9+
hash: string;
10+
operations: OperationModel[];
11+
}
12+
13+
export async function loadFromCompileCache(specPath: string, serverUrl: string | undefined, cachePath: string): Promise<Map<string, OperationModel> | null> {
14+
try {
15+
const doc = await loadOpenApiDocument(specPath);
16+
const hash = computeHash({ doc, serverUrl });
17+
const raw = await readFile(resolve(cachePath), "utf8");
18+
const parsed = JSON.parse(raw) as CacheEntry;
19+
if (parsed.hash !== hash) {
20+
return null;
21+
}
22+
23+
return new Map(parsed.operations.map((op) => [op.operationId, op]));
24+
} catch {
25+
return null;
26+
}
27+
}
28+
29+
export async function compileWithCache(specPath: string, serverUrl: string | undefined, cachePath: string): Promise<Map<string, OperationModel>> {
30+
const cached = await loadFromCompileCache(specPath, serverUrl, cachePath);
31+
if (cached) {
32+
return cached;
33+
}
34+
35+
const doc = await loadOpenApiDocument(specPath);
36+
const operations = compileOperations(doc, serverUrl);
37+
const entry: CacheEntry = {
38+
hash: computeHash({ doc, serverUrl }),
39+
operations: [...operations.values()]
40+
};
41+
42+
const abs = resolve(cachePath);
43+
await mkdir(dirname(abs), { recursive: true });
44+
await writeFile(abs, JSON.stringify(entry), "utf8");
45+
46+
return operations;
47+
}
48+
49+
function computeHash(value: unknown): string {
50+
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
51+
}

src/compiler.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export function compileOperations(doc: Record<string, unknown>, serverOverride?:
3030
const mergedParameters = mergeParameters(pathParameters, operationParameters);
3131
const requestBody = normalizeRequestBody(operation.requestBody);
3232
const response = normalizeSuccessResponse(operation.responses);
33+
const responseSchemasByStatus = normalizeResponseSchemasByStatus(operation.responses);
3334

3435
const effectiveSecurity = normalizeSecurity(operation.security) ?? pathSecurity ?? globalSecurity;
3536
const authOptions = toAuthRequirements(effectiveSecurity, securitySchemes);
@@ -50,6 +51,7 @@ export function compileOperations(doc: Record<string, unknown>, serverOverride?:
5051
requestBodyContentType: requestBody?.contentType,
5152
responseContentType: response?.contentType,
5253
successResponseSchema: response?.schema,
54+
responseSchemasByStatus,
5355
servers: resolvedServers,
5456
authOptions,
5557
annotations: buildAnnotations(method)
@@ -398,6 +400,22 @@ function normalizeSuccessResponse(value: unknown): { contentType?: string; schem
398400
return undefined;
399401
}
400402

403+
function normalizeResponseSchemasByStatus(value: unknown): Record<string, JsonSchema> {
404+
if (!value || typeof value !== "object") {
405+
return {};
406+
}
407+
408+
const out: Record<string, JsonSchema> = {};
409+
const responses = value as Record<string, unknown>;
410+
for (const [status, rawResponse] of Object.entries(responses)) {
411+
const normalized = normalizeResponseContent(rawResponse);
412+
if (normalized?.schema) {
413+
out[status] = normalized.schema;
414+
}
415+
}
416+
return out;
417+
}
418+
401419
function normalizeResponseContent(value: unknown): { contentType?: string; schema?: JsonSchema } | undefined {
402420
if (!isObject(value)) {
403421
return undefined;
@@ -589,7 +607,9 @@ function getSecuritySchemes(doc: Record<string, unknown>): Record<string, Securi
589607
name,
590608
type: String(scheme.type),
591609
in: isValidIn(scheme.in) ? scheme.in : undefined,
592-
scheme: typeof scheme.scheme === "string" ? scheme.scheme : undefined
610+
scheme: typeof scheme.scheme === "string" ? scheme.scheme : undefined,
611+
tokenUrl: typeof scheme.tokenUrl === "string" ? scheme.tokenUrl : undefined,
612+
scopes: isObject(scheme.scopes) ? (scheme.scopes as Record<string, string>) : undefined
593613
};
594614
}
595615

0 commit comments

Comments
 (0)