@ttoss/http-server-mcp-openapi 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +143 -0
- package/dist/index.cjs +456 -0
- package/dist/index.d.cts +335 -0
- package/dist/index.d.mts +335 -0
- package/dist/index.mjs +434 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024, Terezinha Tech Operations (ttoss)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# @ttoss/http-server-mcp-openapi
|
|
2
|
+
|
|
3
|
+
Generate [Model Context Protocol (MCP)](https://modelcontextprotocol.io) tools
|
|
4
|
+
from an [OpenAPI](https://www.openapis.org/) specification and register them on
|
|
5
|
+
a [@ttoss/http-server-mcp](https://ttoss.dev/docs/modules/packages/http-server-mcp)
|
|
6
|
+
server.
|
|
7
|
+
|
|
8
|
+
Point it at your existing OpenAPI document and every operation becomes an MCP
|
|
9
|
+
tool whose handler resolves the incoming arguments into an HTTP request against
|
|
10
|
+
your REST API. The OpenAPI spec stays the single source of truth for both your
|
|
11
|
+
REST surface and your MCP tool surface.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add @ttoss/http-server-mcp-openapi @ttoss/http-server-mcp
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { App, bodyParser } from '@ttoss/http-server';
|
|
23
|
+
import { createMcpRouter, McpServer } from '@ttoss/http-server-mcp';
|
|
24
|
+
import { registerOpenApiTools } from '@ttoss/http-server-mcp-openapi';
|
|
25
|
+
|
|
26
|
+
import openApiDocument from './openapi.json' with { type: 'json' };
|
|
27
|
+
|
|
28
|
+
const server = new McpServer({ name: 'my-api', version: '1.0.0' });
|
|
29
|
+
|
|
30
|
+
registerOpenApiTools({
|
|
31
|
+
server,
|
|
32
|
+
spec: openApiDocument,
|
|
33
|
+
// You own how the request is executed — base URL, auth, fetch impl.
|
|
34
|
+
callApi: async ({ method, url, body }) => {
|
|
35
|
+
const res = await fetch(`https://api.example.com${url}`, {
|
|
36
|
+
method,
|
|
37
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
39
|
+
});
|
|
40
|
+
return res.json();
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const app = new App();
|
|
45
|
+
app.use(bodyParser());
|
|
46
|
+
app.use(createMcpRouter(server).routes());
|
|
47
|
+
app.listen(3000);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## How Operations Map to Tools
|
|
51
|
+
|
|
52
|
+
Each OpenAPI operation with an `operationId` and a supported HTTP method
|
|
53
|
+
(`GET`, `POST`, `PUT`, `PATCH`, `DELETE`) becomes one tool:
|
|
54
|
+
|
|
55
|
+
| OpenAPI | MCP tool |
|
|
56
|
+
| ------------------------- | -------------------------------------------- |
|
|
57
|
+
| `operationId: listAgents` | tool name `list-agents` (kebab-case) |
|
|
58
|
+
| path/query/body params | a single camelCase `inputSchema` object |
|
|
59
|
+
| `$ref`, `oneOf`, `anyOf` | dereferenced and merged into a flat schema |
|
|
60
|
+
| snake_case body fields | camelCase tool inputs, mapped back on call |
|
|
61
|
+
| operation `description` | tool description (quotes/newlines sanitised) |
|
|
62
|
+
|
|
63
|
+
Path params are always required strings. Query and body params carry their
|
|
64
|
+
declared type and `required` flag. Array params keep their `items` schema.
|
|
65
|
+
|
|
66
|
+
Tool arguments are **camelCase** (`agentId`, `projectId`); the generated
|
|
67
|
+
request path, query string, and body use the original **snake_case** names.
|
|
68
|
+
|
|
69
|
+
## `registerOpenApiTools`
|
|
70
|
+
|
|
71
|
+
| Field | Description |
|
|
72
|
+
| ---------- | ------------------------------------------------------------------------------------------------------------- |
|
|
73
|
+
| `server` | The `McpServer` to register tools on. |
|
|
74
|
+
| `spec` | One OpenAPI document, or an array of them (tools are flattened). |
|
|
75
|
+
| `callApi` | Runs the resolved `{ method, url, body, tool }` request and returns the raw data. |
|
|
76
|
+
| `toText?` | Serialises the raw data into the tool's text payload. Defaults to pretty JSON; strings pass through verbatim. |
|
|
77
|
+
| `options?` | See [Options](#options). |
|
|
78
|
+
|
|
79
|
+
Returns the list of `ToolDefinition`s that were registered.
|
|
80
|
+
|
|
81
|
+
## `openApiToToolDefinitions`
|
|
82
|
+
|
|
83
|
+
Use the lower-level function when you want the tool definitions without
|
|
84
|
+
registering them — to inspect, filter, or wire handlers yourself:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import { openApiToToolDefinitions } from '@ttoss/http-server-mcp-openapi';
|
|
88
|
+
|
|
89
|
+
const tools = openApiToToolDefinitions({ spec: openApiDocument });
|
|
90
|
+
|
|
91
|
+
for (const tool of tools) {
|
|
92
|
+
// tool.name, tool.method, tool.inputSchema, tool.extensions, ...
|
|
93
|
+
const url = tool.path(args) + (tool.query ? tool.query(args) : '');
|
|
94
|
+
const body = tool.body?.(args);
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Each `ToolDefinition` exposes `name`, `description`, `inputSchema`, `method`,
|
|
99
|
+
`pathTemplate`, `operationId`, the `path`/`query`/`body` builders,
|
|
100
|
+
`acceptedBodyFields`, and `extensions`.
|
|
101
|
+
|
|
102
|
+
## Options
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
registerOpenApiTools({
|
|
106
|
+
server,
|
|
107
|
+
spec,
|
|
108
|
+
callApi,
|
|
109
|
+
options: {
|
|
110
|
+
excludeExtension: 'x-mcp-exclude', // operations flagged truthy are skipped
|
|
111
|
+
serverManagedExtension: 'x-mcp-server-managed', // body fields hidden from the input schema
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
- **`excludeExtension`** (default `x-mcp-exclude`) — an operation with this
|
|
117
|
+
extension set truthy is omitted from the tool surface.
|
|
118
|
+
- **`serverManagedExtension`** (default `x-mcp-server-managed`) — a request-body
|
|
119
|
+
property with this extension set truthy is hidden from the tool's
|
|
120
|
+
`inputSchema` (the caller can't set it) but still appears in
|
|
121
|
+
`acceptedBodyFields`.
|
|
122
|
+
|
|
123
|
+
### Reading custom extensions
|
|
124
|
+
|
|
125
|
+
Every `x-` prefixed extension on an operation is forwarded verbatim on
|
|
126
|
+
`tool.extensions`, so you can attach and read your own metadata without this
|
|
127
|
+
package needing to know about it:
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
const tools = openApiToToolDefinitions({ spec: openApiDocument });
|
|
131
|
+
const iamAction = tools[0].extensions['x-iam-action'];
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Related Packages
|
|
135
|
+
|
|
136
|
+
- [@ttoss/http-server-mcp](https://ttoss.dev/docs/modules/packages/http-server-mcp) - MCP server integration for @ttoss/http-server
|
|
137
|
+
- [@ttoss/http-server](https://ttoss.dev/docs/modules/packages/http-server) - HTTP server foundation
|
|
138
|
+
- [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/sdk) - MCP SDK
|
|
139
|
+
|
|
140
|
+
## Resources
|
|
141
|
+
|
|
142
|
+
- [MCP Documentation](https://modelcontextprotocol.io)
|
|
143
|
+
- [OpenAPI Specification](https://spec.openapis.org/oas/latest.html)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, {
|
|
3
|
+
value: 'Module'
|
|
4
|
+
});
|
|
5
|
+
let _ttoss_http_server_mcp = require("@ttoss/http-server-mcp");
|
|
6
|
+
|
|
7
|
+
//#region src/schema.ts
|
|
8
|
+
var getAlternativeSchemas = schema => {
|
|
9
|
+
if (Array.isArray(schema.oneOf) && schema.oneOf.length > 0) return schema.oneOf;
|
|
10
|
+
if (Array.isArray(schema.anyOf) && schema.anyOf.length > 0) return schema.anyOf;
|
|
11
|
+
};
|
|
12
|
+
var mergeResolvedSchemas = resolvedAlternatives => {
|
|
13
|
+
const mergedProperties = Object.assign({}, ...resolvedAlternatives.map(candidate => {
|
|
14
|
+
return candidate.properties ?? {};
|
|
15
|
+
}));
|
|
16
|
+
const requiredIntersection = resolvedAlternatives.reduce((current, candidate) => {
|
|
17
|
+
const required = candidate.required ?? [];
|
|
18
|
+
if (current === void 0) return [...required];
|
|
19
|
+
return current.filter(field => {
|
|
20
|
+
return required.includes(field);
|
|
21
|
+
});
|
|
22
|
+
}, void 0);
|
|
23
|
+
return {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: Object.keys(mergedProperties).length > 0 ? mergedProperties : void 0,
|
|
26
|
+
required: requiredIntersection && requiredIntersection.length > 0 ? requiredIntersection : void 0
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
var dereferenceValue = args => {
|
|
30
|
+
const {
|
|
31
|
+
value,
|
|
32
|
+
spec,
|
|
33
|
+
seenRefs
|
|
34
|
+
} = args;
|
|
35
|
+
if (Array.isArray(value)) return value.map(item => {
|
|
36
|
+
return dereferenceValue({
|
|
37
|
+
value: item,
|
|
38
|
+
spec,
|
|
39
|
+
seenRefs
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
if (value && typeof value === "object") {
|
|
43
|
+
const obj = value;
|
|
44
|
+
if (typeof obj.$ref === "string") {
|
|
45
|
+
const refName = obj.$ref.replace("#/components/schemas/", "");
|
|
46
|
+
if (seenRefs.has(refName)) return {};
|
|
47
|
+
const resolved = spec.components?.schemas?.[refName];
|
|
48
|
+
return dereferenceValue({
|
|
49
|
+
value: resolved,
|
|
50
|
+
spec,
|
|
51
|
+
seenRefs: new Set(seenRefs).add(refName)
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const result = {};
|
|
55
|
+
for (const [key, entryValue] of Object.entries(obj)) result[key] = dereferenceValue({
|
|
56
|
+
value: entryValue,
|
|
57
|
+
spec,
|
|
58
|
+
seenRefs
|
|
59
|
+
});
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Recursively inlines every `$ref` in a schema (including refs nested inside
|
|
66
|
+
* `properties`, `items`, `oneOf`, `anyOf`, etc.), producing a self-contained
|
|
67
|
+
* schema safe to hand to an MCP client or LLM provider as a tool definition —
|
|
68
|
+
* provider tool schemas have no `components` section to resolve refs against.
|
|
69
|
+
*/
|
|
70
|
+
var dereferenceSchema = (schema, spec) => {
|
|
71
|
+
if (!schema) return schema;
|
|
72
|
+
return dereferenceValue({
|
|
73
|
+
value: schema,
|
|
74
|
+
spec,
|
|
75
|
+
seenRefs: /* @__PURE__ */new Set()
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Resolves a schema down to a single object shape: follows a top-level `$ref`
|
|
80
|
+
* and merges `oneOf` / `anyOf` alternatives (union of properties, intersection
|
|
81
|
+
* of `required`) so the caller sees one flat property set.
|
|
82
|
+
*/
|
|
83
|
+
var resolveSchema = (schema, spec) => {
|
|
84
|
+
if (!schema) return {};
|
|
85
|
+
if (typeof schema.$ref === "string") {
|
|
86
|
+
const refName = schema.$ref.replace("#/components/schemas/", "");
|
|
87
|
+
const resolved = spec.components?.schemas?.[refName];
|
|
88
|
+
return resolveSchema(resolved, spec);
|
|
89
|
+
}
|
|
90
|
+
const alternatives = getAlternativeSchemas(schema);
|
|
91
|
+
if (alternatives) return mergeResolvedSchemas(alternatives.map(candidate => {
|
|
92
|
+
return resolveSchema(candidate, spec);
|
|
93
|
+
}));
|
|
94
|
+
return schema;
|
|
95
|
+
};
|
|
96
|
+
/** Follows a parameter `$ref` into `components.parameters`, if present. */
|
|
97
|
+
var resolveParameter = (param, spec) => {
|
|
98
|
+
if (!param) return {};
|
|
99
|
+
if (typeof param.$ref === "string") {
|
|
100
|
+
const refName = param.$ref.replace("#/components/parameters/", "");
|
|
101
|
+
return spec.components?.parameters?.[refName] || {};
|
|
102
|
+
}
|
|
103
|
+
return param;
|
|
104
|
+
};
|
|
105
|
+
/** Builds a function that substitutes path params into the path template. */
|
|
106
|
+
var buildPathFn = (pathTemplate, pathParams) => {
|
|
107
|
+
return args => {
|
|
108
|
+
let result = pathTemplate;
|
|
109
|
+
for (const {
|
|
110
|
+
name,
|
|
111
|
+
camelName
|
|
112
|
+
} of pathParams) {
|
|
113
|
+
const value = args[camelName];
|
|
114
|
+
if (value !== void 0) result = result.replace(`{${name}}`, encodeURIComponent(String(value)));
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Builds a function that serialises query params into a query string
|
|
121
|
+
* (including the leading `?`). Returns `undefined` when the op has no query
|
|
122
|
+
* params. Array values are appended once per element.
|
|
123
|
+
*/
|
|
124
|
+
var buildQueryFn = queryParams => {
|
|
125
|
+
if (queryParams.length === 0) return void 0;
|
|
126
|
+
return args => {
|
|
127
|
+
const search = new URLSearchParams();
|
|
128
|
+
for (const {
|
|
129
|
+
name,
|
|
130
|
+
camelName
|
|
131
|
+
} of queryParams) {
|
|
132
|
+
const value = args[camelName];
|
|
133
|
+
if (value === void 0 || value === null) continue;
|
|
134
|
+
if (Array.isArray(value)) for (const item of value) search.append(name, String(item));else search.append(name, String(value));
|
|
135
|
+
}
|
|
136
|
+
const qs = search.toString();
|
|
137
|
+
return qs ? `?${qs}` : "";
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Builds a function that maps camelCase args back to a snake_case request
|
|
142
|
+
* body, skipping `undefined` args. Returns `undefined` when the op has no body.
|
|
143
|
+
*/
|
|
144
|
+
var buildBodyFn = bodyProps => {
|
|
145
|
+
if (bodyProps.length === 0) return void 0;
|
|
146
|
+
return args => {
|
|
147
|
+
const body = {};
|
|
148
|
+
for (const {
|
|
149
|
+
snakeName,
|
|
150
|
+
camelName
|
|
151
|
+
} of bodyProps) if (args[camelName] !== void 0) body[snakeName] = args[camelName];
|
|
152
|
+
return body;
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/types.ts
|
|
158
|
+
var DEFAULT_EXCLUDE_EXTENSION = "x-mcp-exclude";
|
|
159
|
+
var DEFAULT_SERVER_MANAGED_EXTENSION = "x-mcp-server-managed";
|
|
160
|
+
|
|
161
|
+
//#endregion
|
|
162
|
+
//#region src/toolDefinitions.ts
|
|
163
|
+
/**
|
|
164
|
+
* Folds `_` and `-` separators into camelCase. OpenAPI operation and parameter
|
|
165
|
+
* names may be snake_case (`agent_id`) or kebab-case (`list-tools`), and MCP
|
|
166
|
+
* tool inputs are camelCase by convention, so both are folded here.
|
|
167
|
+
*/
|
|
168
|
+
var snakeToCamel = str => {
|
|
169
|
+
return str.replace(/[_-]([a-z])/g, (_, letter) => {
|
|
170
|
+
return letter.toUpperCase();
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
/** Converts a camelCase `operationId` to a kebab-case tool name. */
|
|
174
|
+
var operationIdToToolName = operationId => {
|
|
175
|
+
return operationId.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "");
|
|
176
|
+
};
|
|
177
|
+
var getJsonSchemaType = schemaType => {
|
|
178
|
+
if (schemaType === "integer" || schemaType === "number") return "number";
|
|
179
|
+
if (schemaType === "boolean") return "boolean";
|
|
180
|
+
if (schemaType === "array") return "array";
|
|
181
|
+
if (schemaType === "object") return "object";
|
|
182
|
+
return "string";
|
|
183
|
+
};
|
|
184
|
+
var sanitizeDescription = description => {
|
|
185
|
+
return (description || "").replace(/'/g, "\\'").replace(/\n/g, " ").trim();
|
|
186
|
+
};
|
|
187
|
+
var buildInputSchema = (pathParams, queryParams, bodyProps) => {
|
|
188
|
+
const allParams = [...pathParams, ...queryParams, ...bodyProps];
|
|
189
|
+
if (allParams.length === 0) return {
|
|
190
|
+
type: "object"
|
|
191
|
+
};
|
|
192
|
+
const requiredFields = [...pathParams.map(p => {
|
|
193
|
+
return p.camelName;
|
|
194
|
+
}), ...queryParams.filter(p => {
|
|
195
|
+
return p.required;
|
|
196
|
+
}).map(p => {
|
|
197
|
+
return p.camelName;
|
|
198
|
+
}), ...bodyProps.filter(p => {
|
|
199
|
+
return p.required;
|
|
200
|
+
}).map(p => {
|
|
201
|
+
return p.camelName;
|
|
202
|
+
})];
|
|
203
|
+
const properties = {};
|
|
204
|
+
for (const param of allParams) if ("type" in param) {
|
|
205
|
+
const jsonType = getJsonSchemaType(param.type);
|
|
206
|
+
const description = sanitizeDescription(param.description);
|
|
207
|
+
if (param.type === "array") {
|
|
208
|
+
const itemsSchema = "items" in param && param.items ? param.items : {
|
|
209
|
+
type: "string"
|
|
210
|
+
};
|
|
211
|
+
properties[param.camelName] = {
|
|
212
|
+
type: "array",
|
|
213
|
+
items: itemsSchema,
|
|
214
|
+
description
|
|
215
|
+
};
|
|
216
|
+
} else properties[param.camelName] = {
|
|
217
|
+
type: jsonType,
|
|
218
|
+
description
|
|
219
|
+
};
|
|
220
|
+
} else properties[param.camelName] = {
|
|
221
|
+
type: "string",
|
|
222
|
+
description: ""
|
|
223
|
+
};
|
|
224
|
+
return {
|
|
225
|
+
type: "object",
|
|
226
|
+
properties,
|
|
227
|
+
required: requiredFields.length > 0 ? requiredFields : void 0
|
|
228
|
+
};
|
|
229
|
+
};
|
|
230
|
+
var extractPathParams = args => {
|
|
231
|
+
return (args.parameters || []).map(p => {
|
|
232
|
+
return resolveParameter(p, args.spec);
|
|
233
|
+
}).filter(p => {
|
|
234
|
+
return p.in === "path";
|
|
235
|
+
}).map(p => {
|
|
236
|
+
return {
|
|
237
|
+
name: p.name || "",
|
|
238
|
+
camelName: snakeToCamel(p.name || "")
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
};
|
|
242
|
+
var extractQueryParams = args => {
|
|
243
|
+
return (args.parameters || []).map(p => {
|
|
244
|
+
return resolveParameter(p, args.spec);
|
|
245
|
+
}).filter(p => {
|
|
246
|
+
return p.in === "query";
|
|
247
|
+
}).map(p => {
|
|
248
|
+
return {
|
|
249
|
+
name: p.name || "",
|
|
250
|
+
camelName: snakeToCamel(p.name || ""),
|
|
251
|
+
description: p.description || "",
|
|
252
|
+
required: p.required || false,
|
|
253
|
+
type: p.schema?.type || "string"
|
|
254
|
+
};
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
var resolveBodySchema = args => {
|
|
258
|
+
const rawBodySchema = args.requestBody?.content?.["application/json"]?.schema;
|
|
259
|
+
return resolveSchema(dereferenceSchema(rawBodySchema, args.spec), args.spec);
|
|
260
|
+
};
|
|
261
|
+
/**
|
|
262
|
+
* snake_case names of every top-level property an operation's request schema
|
|
263
|
+
* declares, including server-managed ones.
|
|
264
|
+
*/
|
|
265
|
+
var extractAcceptedBodyFields = args => {
|
|
266
|
+
const bodySchema = resolveBodySchema(args);
|
|
267
|
+
return Object.keys(bodySchema?.properties ?? {});
|
|
268
|
+
};
|
|
269
|
+
var extractBodyProps = args => {
|
|
270
|
+
const bodySchema = resolveBodySchema(args);
|
|
271
|
+
if (!bodySchema?.properties) return [];
|
|
272
|
+
return Object.entries(bodySchema.properties).filter(([, value]) => {
|
|
273
|
+
return !value[args.serverManagedExtension];
|
|
274
|
+
}).map(([key, value]) => {
|
|
275
|
+
const val = value;
|
|
276
|
+
return {
|
|
277
|
+
snakeName: key,
|
|
278
|
+
camelName: snakeToCamel(key),
|
|
279
|
+
description: typeof val.description === "string" ? val.description : "",
|
|
280
|
+
required: (bodySchema.required || []).includes(key),
|
|
281
|
+
type: typeof val.type === "string" ? val.type : "string",
|
|
282
|
+
items: val.items
|
|
283
|
+
};
|
|
284
|
+
});
|
|
285
|
+
};
|
|
286
|
+
/** Collects every `x-` prefixed extension declared on the operation. */
|
|
287
|
+
var extractExtensions = operation => {
|
|
288
|
+
const extensions = {};
|
|
289
|
+
for (const [key, value] of Object.entries(operation)) if (key.startsWith("x-")) extensions[key] = value;
|
|
290
|
+
return extensions;
|
|
291
|
+
};
|
|
292
|
+
var processOperation = args => {
|
|
293
|
+
const httpMethod = args.method.toUpperCase();
|
|
294
|
+
if (!["GET", "POST", "PUT", "PATCH", "DELETE"].includes(httpMethod)) return null;
|
|
295
|
+
if (!args.operation.operationId) return null;
|
|
296
|
+
if (args.operation[args.options.excludeExtension]) return null;
|
|
297
|
+
const toolName = operationIdToToolName(args.operation.operationId);
|
|
298
|
+
const pathParams = extractPathParams({
|
|
299
|
+
parameters: args.operation.parameters || [],
|
|
300
|
+
spec: args.spec
|
|
301
|
+
});
|
|
302
|
+
const queryParams = extractQueryParams({
|
|
303
|
+
parameters: args.operation.parameters || [],
|
|
304
|
+
spec: args.spec
|
|
305
|
+
});
|
|
306
|
+
const bodyProps = extractBodyProps({
|
|
307
|
+
requestBody: args.operation.requestBody,
|
|
308
|
+
spec: args.spec,
|
|
309
|
+
serverManagedExtension: args.options.serverManagedExtension
|
|
310
|
+
});
|
|
311
|
+
const inputSchema = buildInputSchema(pathParams, queryParams, bodyProps);
|
|
312
|
+
const acceptedBodyFields = extractAcceptedBodyFields({
|
|
313
|
+
requestBody: args.operation.requestBody,
|
|
314
|
+
spec: args.spec
|
|
315
|
+
});
|
|
316
|
+
return {
|
|
317
|
+
name: toolName,
|
|
318
|
+
description: sanitizeDescription(args.operation.description),
|
|
319
|
+
inputSchema,
|
|
320
|
+
method: httpMethod,
|
|
321
|
+
pathTemplate: args.pathTemplate,
|
|
322
|
+
operationId: args.operation.operationId,
|
|
323
|
+
path: buildPathFn(args.pathTemplate, pathParams),
|
|
324
|
+
query: buildQueryFn(queryParams),
|
|
325
|
+
body: buildBodyFn(bodyProps),
|
|
326
|
+
acceptedBodyFields,
|
|
327
|
+
extensions: extractExtensions(args.operation)
|
|
328
|
+
};
|
|
329
|
+
};
|
|
330
|
+
var processPath = args => {
|
|
331
|
+
const tools = [];
|
|
332
|
+
for (const [method, operation] of Object.entries(args.pathItem)) {
|
|
333
|
+
const tool = processOperation({
|
|
334
|
+
pathTemplate: args.pathTemplate,
|
|
335
|
+
method,
|
|
336
|
+
operation,
|
|
337
|
+
spec: args.spec,
|
|
338
|
+
options: args.options
|
|
339
|
+
});
|
|
340
|
+
if (tool) tools.push(tool);
|
|
341
|
+
}
|
|
342
|
+
return tools;
|
|
343
|
+
};
|
|
344
|
+
/**
|
|
345
|
+
* Translates one or more OpenAPI documents into REST-backed MCP tool
|
|
346
|
+
* definitions. Each translatable operation (has an `operationId`, a supported
|
|
347
|
+
* HTTP method, and is not excluded) becomes one {@link ToolDefinition}.
|
|
348
|
+
*
|
|
349
|
+
* @example
|
|
350
|
+
* ```typescript
|
|
351
|
+
* import { openApiToToolDefinitions } from '@ttoss/http-server-mcp-openapi';
|
|
352
|
+
*
|
|
353
|
+
* const tools = openApiToToolDefinitions({ spec: myOpenApiDocument });
|
|
354
|
+
* ```
|
|
355
|
+
*/
|
|
356
|
+
var openApiToToolDefinitions = args => {
|
|
357
|
+
const options = {
|
|
358
|
+
excludeExtension: args.options?.excludeExtension ?? "x-mcp-exclude",
|
|
359
|
+
serverManagedExtension: args.options?.serverManagedExtension ?? "x-mcp-server-managed"
|
|
360
|
+
};
|
|
361
|
+
const specs = Array.isArray(args.spec) ? args.spec : [args.spec];
|
|
362
|
+
const tools = [];
|
|
363
|
+
for (const spec of specs) {
|
|
364
|
+
const paths = spec.paths || {};
|
|
365
|
+
for (const [pathTemplate, pathItem] of Object.entries(paths)) tools.push(...processPath({
|
|
366
|
+
pathTemplate,
|
|
367
|
+
pathItem,
|
|
368
|
+
spec,
|
|
369
|
+
options
|
|
370
|
+
}));
|
|
371
|
+
}
|
|
372
|
+
return tools;
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
//#endregion
|
|
376
|
+
//#region src/registerOpenApiTools.ts
|
|
377
|
+
var defaultToText = data => {
|
|
378
|
+
return typeof data === "string" ? data : JSON.stringify(data, null, 2);
|
|
379
|
+
};
|
|
380
|
+
/**
|
|
381
|
+
* Derives MCP tools from OpenAPI document(s) and registers each on the given
|
|
382
|
+
* MCP server. Every tool's handler resolves the incoming camelCase args into a
|
|
383
|
+
* concrete HTTP request and delegates execution to `callApi`.
|
|
384
|
+
*
|
|
385
|
+
* @returns The list of {@link ToolDefinition} that were registered.
|
|
386
|
+
*
|
|
387
|
+
* @example
|
|
388
|
+
* ```typescript
|
|
389
|
+
* import { McpServer } from '@ttoss/http-server-mcp';
|
|
390
|
+
* import { registerOpenApiTools } from '@ttoss/http-server-mcp-openapi';
|
|
391
|
+
*
|
|
392
|
+
* const server = new McpServer({ name: 'my-api', version: '1.0.0' });
|
|
393
|
+
*
|
|
394
|
+
* registerOpenApiTools({
|
|
395
|
+
* server,
|
|
396
|
+
* spec: myOpenApiDocument,
|
|
397
|
+
* callApi: async ({ method, url, body }) => {
|
|
398
|
+
* const res = await fetch(`https://api.example.com${url}`, {
|
|
399
|
+
* method,
|
|
400
|
+
* headers: { 'Content-Type': 'application/json' },
|
|
401
|
+
* body: body ? JSON.stringify(body) : undefined,
|
|
402
|
+
* });
|
|
403
|
+
* return res.json();
|
|
404
|
+
* },
|
|
405
|
+
* });
|
|
406
|
+
* ```
|
|
407
|
+
*/
|
|
408
|
+
var registerOpenApiTools = args => {
|
|
409
|
+
const toText = args.toText ?? defaultToText;
|
|
410
|
+
const tools = openApiToToolDefinitions({
|
|
411
|
+
spec: args.spec,
|
|
412
|
+
options: args.options
|
|
413
|
+
});
|
|
414
|
+
for (const tool of tools) (0, _ttoss_http_server_mcp.registerToolFromSchema)(args.server, {
|
|
415
|
+
name: tool.name,
|
|
416
|
+
description: tool.description,
|
|
417
|
+
inputSchema: tool.inputSchema,
|
|
418
|
+
handler: async handlerArgs => {
|
|
419
|
+
const url = tool.path(handlerArgs) + (tool.query ? tool.query(handlerArgs) : "");
|
|
420
|
+
return {
|
|
421
|
+
content: [{
|
|
422
|
+
type: "text",
|
|
423
|
+
text: toText(await args.callApi({
|
|
424
|
+
method: tool.method,
|
|
425
|
+
url,
|
|
426
|
+
body: tool.body ? tool.body(handlerArgs) : void 0,
|
|
427
|
+
tool
|
|
428
|
+
}))
|
|
429
|
+
}]
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
return tools;
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
//#endregion
|
|
437
|
+
exports.DEFAULT_EXCLUDE_EXTENSION = DEFAULT_EXCLUDE_EXTENSION;
|
|
438
|
+
exports.DEFAULT_SERVER_MANAGED_EXTENSION = DEFAULT_SERVER_MANAGED_EXTENSION;
|
|
439
|
+
exports.buildBodyFn = buildBodyFn;
|
|
440
|
+
exports.buildInputSchema = buildInputSchema;
|
|
441
|
+
exports.buildPathFn = buildPathFn;
|
|
442
|
+
exports.buildQueryFn = buildQueryFn;
|
|
443
|
+
exports.dereferenceSchema = dereferenceSchema;
|
|
444
|
+
exports.extractAcceptedBodyFields = extractAcceptedBodyFields;
|
|
445
|
+
exports.extractBodyProps = extractBodyProps;
|
|
446
|
+
exports.extractPathParams = extractPathParams;
|
|
447
|
+
exports.extractQueryParams = extractQueryParams;
|
|
448
|
+
exports.getJsonSchemaType = getJsonSchemaType;
|
|
449
|
+
exports.openApiToToolDefinitions = openApiToToolDefinitions;
|
|
450
|
+
exports.operationIdToToolName = operationIdToToolName;
|
|
451
|
+
exports.processOperation = processOperation;
|
|
452
|
+
exports.processPath = processPath;
|
|
453
|
+
exports.registerOpenApiTools = registerOpenApiTools;
|
|
454
|
+
exports.resolveParameter = resolveParameter;
|
|
455
|
+
exports.resolveSchema = resolveSchema;
|
|
456
|
+
exports.snakeToCamel = snakeToCamel;
|