@ceski23/openapi-mcp 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +40 -0
  2. package/dist/index.js +323 -0
  3. package/package.json +50 -0
package/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # OpenApi MCP
2
+
3
+ MCP server for loading and exploring OpenAPI/Swagger specifications. Lets AI assistants browse API contracts dynamically — load any OpenAPI spec, search endpoints, inspect schemas, and retrieve operations — without needing the spec in-context.
4
+
5
+ ## Usage
6
+
7
+ ### Manually
8
+
9
+ Add to your MCP client config:
10
+
11
+ ```json
12
+ {
13
+ "mcpServers": {
14
+ "openapi-mcp": {
15
+ "command": "npx",
16
+ "args": ["-y", "@ceski23/openapi-mcp"]
17
+ }
18
+ }
19
+ }
20
+ ```
21
+
22
+ ### Via AI agent
23
+
24
+ Ask your AI assistant to add it:
25
+
26
+ > Add the MCP server `@ceski23/openapi-mcp` to my config. Run it with `npx -y @ceski23/openapi-mcp` via stdio.
27
+
28
+ Most assistants can modify your MCP config file directly.
29
+
30
+ ## Tools
31
+
32
+ | Tool | Description |
33
+ |---|---|
34
+ | `load_spec` | Load an OpenAPI/Swagger spec from a URL or file path. Returns a `specId` used by other tools. |
35
+ | `find_operations` | Search operations by operationId, path, summary, tags, or description. |
36
+ | `get_operation` | Get full details for a specific operation by operationId. |
37
+ | `get_path` | Get all HTTP methods defined on a specific path. |
38
+ | `find_schemas` | Search schema/definition names. |
39
+ | `get_schema` | Get a fully dereferenced schema by name. |
40
+ | `search_contract` | Search both operations and schemas in one call. |
package/dist/index.js ADDED
@@ -0,0 +1,323 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+
7
+ // src/tools/load-spec.ts
8
+ import { z } from "zod";
9
+ import OASNormalize from "oas-normalize";
10
+ import Oas from "oas";
11
+
12
+ // src/cache.ts
13
+ import { LRUCache } from "lru-cache";
14
+ var specCache = new LRUCache({
15
+ max: 10,
16
+ ttl: 1000 * 60 * 60
17
+ });
18
+ function setSpec(id, spec) {
19
+ specCache.set(id, spec);
20
+ }
21
+ function getSpec(id) {
22
+ return specCache.get(id);
23
+ }
24
+
25
+ // src/utils.ts
26
+ var MISSING_SPEC_RESPONSE = {
27
+ content: [
28
+ {
29
+ type: "text",
30
+ text: "Spec not found. The specId may be invalid or expired. Use load_spec to load a spec."
31
+ }
32
+ ],
33
+ isError: true
34
+ };
35
+ function* iteratePathItem(path, pathItem) {
36
+ if (pathItem.get)
37
+ yield { path, method: "GET", operation: pathItem.get };
38
+ if (pathItem.put)
39
+ yield { path, method: "PUT", operation: pathItem.put };
40
+ if (pathItem.post)
41
+ yield { path, method: "POST", operation: pathItem.post };
42
+ if (pathItem.delete)
43
+ yield { path, method: "DELETE", operation: pathItem.delete };
44
+ if (pathItem.options)
45
+ yield { path, method: "OPTIONS", operation: pathItem.options };
46
+ if (pathItem.head)
47
+ yield { path, method: "HEAD", operation: pathItem.head };
48
+ if (pathItem.patch)
49
+ yield { path, method: "PATCH", operation: pathItem.patch };
50
+ if (pathItem.trace)
51
+ yield { path, method: "TRACE", operation: pathItem.trace };
52
+ }
53
+ function* iterateOperations(spec) {
54
+ if (!spec.paths || typeof spec.paths !== "object")
55
+ return;
56
+ for (const path of Object.keys(spec.paths)) {
57
+ const pathItem = spec.paths[path];
58
+ if (!pathItem)
59
+ continue;
60
+ yield* iteratePathItem(path, pathItem);
61
+ }
62
+ }
63
+ var matchesQuery = (operation, path, lowercaseQuery) => (operation.operationId ?? "").toLowerCase().includes(lowercaseQuery) || path.toLowerCase().includes(lowercaseQuery) || (operation.summary ?? "").toLowerCase().includes(lowercaseQuery) || (operation.description ?? "").toLowerCase().includes(lowercaseQuery) || (operation.tags ?? []).join(" ").toLowerCase().includes(lowercaseQuery);
64
+ var defineTool = (tool) => tool;
65
+
66
+ // src/tools/load-spec.ts
67
+ var loadSpec = defineTool({
68
+ name: "load_spec",
69
+ description: "Load and parse an OpenAPI/Swagger specification from a URL or local file path. Parses and validates the spec, returning a specId for use by other tools.",
70
+ inputSchema: z.object({
71
+ source: z.string().describe("URL or local file path to the OpenAPI/Swagger specification (JSON or YAML)")
72
+ }),
73
+ execute: async ({ source }) => {
74
+ try {
75
+ const normalizer = new OASNormalize(source, { enablePaths: true });
76
+ const converted = await normalizer.convert();
77
+ const oas = Oas.init(converted);
78
+ await oas.dereference();
79
+ const specId = crypto.randomUUID();
80
+ setSpec(specId, { oas });
81
+ const spec = oas.getDefinition();
82
+ const endpointCount = Object.keys(spec.paths ?? {}).length;
83
+ const schemaCount = Object.keys(spec.components?.schemas ?? {}).length;
84
+ return {
85
+ content: [
86
+ {
87
+ type: "text",
88
+ text: JSON.stringify({
89
+ success: true,
90
+ specId,
91
+ title: spec.info?.title,
92
+ version: spec.info?.version,
93
+ description: spec.info?.description,
94
+ endpoints: endpointCount,
95
+ schemas: schemaCount,
96
+ source
97
+ }, null, 2)
98
+ }
99
+ ]
100
+ };
101
+ } catch (error) {
102
+ return {
103
+ content: [
104
+ {
105
+ type: "text",
106
+ text: `Failed to load spec: ${error instanceof Error ? error.message : String(error)}`
107
+ }
108
+ ],
109
+ isError: true
110
+ };
111
+ }
112
+ }
113
+ });
114
+
115
+ // src/tools/find-operations.ts
116
+ import { z as z2 } from "zod";
117
+ var findOperations = defineTool({
118
+ name: "find_operations",
119
+ description: "Search for API operations in the loaded spec by matching against operationId, path, summary, tags, and descriptions. Use when you know approximately what you need but not the exact endpoint.",
120
+ inputSchema: z2.object({
121
+ specId: z2.string().describe("The spec ID returned by load_spec"),
122
+ query: z2.string().describe("Search query to match against operation names, paths, summaries, and tags")
123
+ }),
124
+ execute: ({ specId, query }) => {
125
+ const cached = getSpec(specId);
126
+ const spec = cached?.oas?.getDefinition();
127
+ if (!spec)
128
+ return MISSING_SPEC_RESPONSE;
129
+ const lowercaseQuery = query.toLowerCase();
130
+ const results = iterateOperations(spec).filter(({ path, operation }) => matchesQuery(operation, path, lowercaseQuery)).map(({ path, method, operation }) => ({
131
+ operationId: operation.operationId ?? "",
132
+ method,
133
+ path,
134
+ ...operation.summary ? { summary: operation.summary } : {}
135
+ })).toArray();
136
+ return {
137
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
138
+ };
139
+ }
140
+ });
141
+
142
+ // src/tools/get-operation.ts
143
+ import { z as z3 } from "zod";
144
+ var getOperation = defineTool({
145
+ name: "get_operation",
146
+ description: "Retrieve full details for a single API operation by operationId, including method, path, parameters, requestBody, and responses.",
147
+ inputSchema: z3.object({
148
+ specId: z3.string().describe("The spec ID returned by load_spec"),
149
+ operationId: z3.string().describe("The operationId of the endpoint to retrieve")
150
+ }),
151
+ execute: ({ specId, operationId }) => {
152
+ const cached = getSpec(specId);
153
+ const oas = cached?.oas;
154
+ if (!oas)
155
+ return MISSING_SPEC_RESPONSE;
156
+ const operation = oas.getOperationById(operationId);
157
+ if (!operation) {
158
+ return {
159
+ content: [
160
+ {
161
+ type: "text",
162
+ text: `No operation found with operationId "${operationId}".`
163
+ }
164
+ ],
165
+ isError: true
166
+ };
167
+ }
168
+ return {
169
+ content: [
170
+ {
171
+ type: "text",
172
+ text: JSON.stringify({
173
+ method: operation.method.toUpperCase(),
174
+ path: operation.path,
175
+ ...operation.schema
176
+ }, null, 2)
177
+ }
178
+ ]
179
+ };
180
+ }
181
+ });
182
+
183
+ // src/tools/get-path.ts
184
+ import { z as z4 } from "zod";
185
+ var getPath = defineTool({
186
+ name: "get_path",
187
+ description: "Retrieve all operations (GET, POST, PATCH, DELETE, etc.) for a specific path. Use when you know the exact path and want the full contract for all methods on it.",
188
+ inputSchema: z4.object({
189
+ specId: z4.string().describe("The spec ID returned by load_spec"),
190
+ path: z4.string().describe("The exact path from the spec (e.g. /customers/{id})")
191
+ }),
192
+ execute: ({ specId, path }) => {
193
+ const cached = getSpec(specId);
194
+ const spec = cached?.oas?.getDefinition();
195
+ if (!spec)
196
+ return MISSING_SPEC_RESPONSE;
197
+ const pathItem = spec.paths?.[path];
198
+ if (!pathItem) {
199
+ return {
200
+ content: [
201
+ { type: "text", text: `No operations found for path "${path}".` }
202
+ ],
203
+ isError: true
204
+ };
205
+ }
206
+ return {
207
+ content: [{ type: "text", text: JSON.stringify(pathItem, null, 2) }]
208
+ };
209
+ }
210
+ });
211
+
212
+ // src/tools/find-schemas.ts
213
+ import { z as z5 } from "zod";
214
+ var findSchemas = defineTool({
215
+ name: "find_schemas",
216
+ description: "Search component/definition schema names in the loaded spec by matching against schema names. Use when you know approximately what a schema is called but not the exact name.",
217
+ inputSchema: z5.object({
218
+ specId: z5.string().describe("The spec ID returned by load_spec"),
219
+ query: z5.string().describe("Search query to match against schema/definition names")
220
+ }),
221
+ execute: ({ specId, query }) => {
222
+ const cached = getSpec(specId);
223
+ const spec = cached?.oas?.getDefinition();
224
+ if (!spec)
225
+ return MISSING_SPEC_RESPONSE;
226
+ const schemas = spec.components?.schemas;
227
+ const keys = Object.keys(schemas ?? {});
228
+ const lowercaseQuery = query.toLowerCase();
229
+ const results = keys.filter((name) => name.toLowerCase().includes(lowercaseQuery)).toSorted();
230
+ return {
231
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
232
+ };
233
+ }
234
+ });
235
+
236
+ // src/tools/get-schema.ts
237
+ import { z as z6 } from "zod";
238
+ var getSchema = defineTool({
239
+ name: "get_schema",
240
+ description: "Retrieve a fully dereferenced component/definition schema by name. Use when you need the full structure of a specific schema.",
241
+ inputSchema: z6.object({
242
+ specId: z6.string().describe("The spec ID returned by load_spec"),
243
+ name: z6.string().describe("The schema/definition name (case-sensitive, e.g. Invoice)")
244
+ }),
245
+ execute: ({ specId, name }) => {
246
+ const cached = getSpec(specId);
247
+ const spec = cached?.oas?.getDefinition();
248
+ if (!spec)
249
+ return MISSING_SPEC_RESPONSE;
250
+ const schema = spec.components?.schemas?.[name];
251
+ if (!schema) {
252
+ return {
253
+ content: [{ type: "text", text: `No schema found with name "${name}".` }],
254
+ isError: true
255
+ };
256
+ }
257
+ return {
258
+ content: [{ type: "text", text: JSON.stringify(schema, null, 2) }]
259
+ };
260
+ }
261
+ });
262
+
263
+ // src/tools/search-contract.ts
264
+ import { z as z7 } from "zod";
265
+ var searchContract = defineTool({
266
+ name: "search_contract",
267
+ description: "Search across the entire API contract — operations and schemas — by matching against operationIds, summaries, tags, paths, schema names, and schema descriptions. The best first call when exploring an API.",
268
+ inputSchema: z7.object({
269
+ specId: z7.string().describe("The spec ID returned by load_spec"),
270
+ query: z7.string().describe("Search query to match against operationIds, summaries, tags, paths, schema names, and schema descriptions")
271
+ }),
272
+ execute: ({ specId, query }) => {
273
+ const cached = getSpec(specId);
274
+ const spec = cached?.oas?.getDefinition();
275
+ if (!spec)
276
+ return MISSING_SPEC_RESPONSE;
277
+ const lowercaseQuery = query.toLowerCase();
278
+ const operations = iterateOperations(spec).filter(({ path, operation }) => matchesQuery(operation, path, lowercaseQuery)).map(({ path, method, operation }) => ({
279
+ operationId: operation.operationId ?? "",
280
+ method,
281
+ path,
282
+ ...operation.summary ? { summary: operation.summary } : {}
283
+ })).toArray();
284
+ const schemas = Object.keys(spec.components?.schemas ?? {}).filter((name) => {
285
+ const schema = spec.components?.schemas?.[name];
286
+ if (!schema)
287
+ return false;
288
+ if ("$ref" in schema)
289
+ return false;
290
+ return name.toLowerCase().includes(lowercaseQuery) || (schema.description ?? "").toLowerCase().includes(lowercaseQuery);
291
+ }).toSorted();
292
+ return {
293
+ content: [
294
+ {
295
+ type: "text",
296
+ text: JSON.stringify({
297
+ ...operations.length > 0 ? { operations } : {},
298
+ ...schemas.length > 0 ? { schemas } : {}
299
+ }, null, 2)
300
+ }
301
+ ]
302
+ };
303
+ }
304
+ });
305
+
306
+ // src/index.ts
307
+ var server = new McpServer({
308
+ name: "openapi-mcp",
309
+ version: "0.1.0"
310
+ });
311
+ for (const { name, description, inputSchema, execute } of [
312
+ loadSpec,
313
+ findOperations,
314
+ getOperation,
315
+ getPath,
316
+ findSchemas,
317
+ getSchema,
318
+ searchContract
319
+ ]) {
320
+ server.registerTool(name, { description, inputSchema }, execute);
321
+ }
322
+ var transport = new StdioServerTransport;
323
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@ceski23/openapi-mcp",
3
+ "version": "1.0.0-beta.1",
4
+ "license": "MIT",
5
+ "author": "Cezary Bober",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/ceski23/openapi-mcp.git"
9
+ },
10
+ "bin": {
11
+ "openapi-mcp": "./dist/index.js"
12
+ },
13
+ "files": [
14
+ "dist/"
15
+ ],
16
+ "type": "module",
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "scripts": {
21
+ "lint": "oxlint",
22
+ "format": "oxfmt",
23
+ "check": "bun run lint && bun run format --check",
24
+ "typecheck": "bun run tsc --noEmit",
25
+ "build": "bun build src/index.ts --target=node --outdir=dist --packages=external",
26
+ "prepublishOnly": "bun run build",
27
+ "test": "bun test",
28
+ "check:config": "publint"
29
+ },
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "1.29.0",
32
+ "lru-cache": "11.5.1",
33
+ "oas": "36.0.3",
34
+ "oas-normalize": "16.0.5",
35
+ "zod": "4.4.3"
36
+ },
37
+ "devDependencies": {
38
+ "@semantic-release/changelog": "6.0.3",
39
+ "@semantic-release/git": "10.0.1",
40
+ "@types/bun": "latest",
41
+ "oxfmt": "0.56.0",
42
+ "oxlint": "1.71.0",
43
+ "publint": "0.3.21",
44
+ "typescript": "6.0.3"
45
+ },
46
+ "engines": {
47
+ "bun": ">=1.0.0",
48
+ "node": ">=22"
49
+ }
50
+ }