fastmcp 4.17.1 → 4.18.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/README.md +18 -0
- package/dist/openapi/index.cjs +482 -0
- package/dist/openapi/index.cjs.map +1 -0
- package/dist/openapi/index.d.cts +171 -0
- package/dist/openapi/index.d.ts +171 -0
- package/dist/openapi/index.js +482 -0
- package/dist/openapi/index.js.map +1 -0
- package/package.json +10 -1
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ A TypeScript framework for building [MCP](https://glama.ai/mcp) servers capable
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
11
|
- Simple Tool, Resource, Prompt definition
|
|
12
|
+
- [OpenAPI to MCP conversion](#openapi)
|
|
12
13
|
- [Authentication](#authentication)
|
|
13
14
|
- [Passing headers through context](#passing-headers-through-context)
|
|
14
15
|
- [Session ID and Request ID tracking](#session-id-and-request-id-tracking)
|
|
@@ -1873,6 +1874,23 @@ server.addPrompt({
|
|
|
1873
1874
|
});
|
|
1874
1875
|
```
|
|
1875
1876
|
|
|
1877
|
+
### OpenAPI
|
|
1878
|
+
|
|
1879
|
+
`fromOpenAPI()` (from `fastmcp/openapi`) converts an OpenAPI 3.x document into a FastMCP server, one tool per operation — handling external `$ref`s (multi-file specs), relative `servers[0].url` resolution, and parameter-flattening collisions along the way:
|
|
1880
|
+
|
|
1881
|
+
```ts
|
|
1882
|
+
import { fromOpenAPI } from "fastmcp/openapi";
|
|
1883
|
+
|
|
1884
|
+
const server = await fromOpenAPI({
|
|
1885
|
+
spec: "https://petstore3.swagger.io/api/v3/openapi.json",
|
|
1886
|
+
include: (operation) => operation.tags.includes("pet"),
|
|
1887
|
+
});
|
|
1888
|
+
|
|
1889
|
+
await server.start({ transportType: "stdio" });
|
|
1890
|
+
```
|
|
1891
|
+
|
|
1892
|
+
See [OpenAPI to MCP](docs/openapi.md) for the full option reference, authentication, and known limitations.
|
|
1893
|
+
|
|
1876
1894
|
### Authentication
|
|
1877
1895
|
|
|
1878
1896
|
FastMCP supports OAuth 2.1 authentication with pre-configured providers, allowing you to secure your server with minimal setup.
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
var _chunkSYZRGVQXcjs = require('../chunk-SYZRGVQX.cjs');
|
|
6
|
+
require('../chunk-E3HXGE2O.cjs');
|
|
7
|
+
|
|
8
|
+
// src/openapi/loadSpec.ts
|
|
9
|
+
var _swaggerparser = require('@apidevtools/swagger-parser'); var _swaggerparser2 = _interopRequireDefault(_swaggerparser);
|
|
10
|
+
async function loadSpec(spec) {
|
|
11
|
+
const document = await _swaggerparser2.default.bundle(
|
|
12
|
+
spec
|
|
13
|
+
);
|
|
14
|
+
if (!_optionalChain([document, 'access', _ => _.openapi, 'optionalAccess', _2 => _2.startsWith, 'call', _3 => _3("3.")])) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
`fromOpenAPI only supports OpenAPI 3.x documents (found ${_nullishCoalesce(_nullishCoalesce(document.openapi, () => ( document.swagger)), () => ( "an unrecognized version"))}). Swagger 2.0 is not supported.`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
document,
|
|
21
|
+
origin: typeof spec === "string" && isHttpUrl(spec) ? spec : void 0
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function isHttpUrl(value) {
|
|
25
|
+
return value.startsWith("http://") || value.startsWith("https://");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/openapi/naming.ts
|
|
29
|
+
var MAX_NAME_LENGTH = 56;
|
|
30
|
+
var MAX_BASE_LENGTH = MAX_NAME_LENGTH - 5;
|
|
31
|
+
function generateToolNames(routes, mcpNames) {
|
|
32
|
+
const names = /* @__PURE__ */ new Map();
|
|
33
|
+
const used = /* @__PURE__ */ new Set();
|
|
34
|
+
for (const route of routes) {
|
|
35
|
+
const base = slugify(baseNameFor(route, mcpNames));
|
|
36
|
+
let candidate = base;
|
|
37
|
+
let suffix = 1;
|
|
38
|
+
while (used.has(candidate)) {
|
|
39
|
+
suffix += 1;
|
|
40
|
+
candidate = `${base}_${suffix}`;
|
|
41
|
+
}
|
|
42
|
+
used.add(candidate);
|
|
43
|
+
names.set(route, candidate);
|
|
44
|
+
}
|
|
45
|
+
return names;
|
|
46
|
+
}
|
|
47
|
+
function baseNameFor(route, mcpNames) {
|
|
48
|
+
if (route.operationId) {
|
|
49
|
+
return _nullishCoalesce(_optionalChain([mcpNames, 'optionalAccess', _4 => _4[route.operationId]]), () => ( route.operationId.split("__")[0]));
|
|
50
|
+
}
|
|
51
|
+
return route.summary || `${route.method}_${route.path}`;
|
|
52
|
+
}
|
|
53
|
+
function slugify(value) {
|
|
54
|
+
const slug = value.replace(/[^a-zA-Z0-9_]+/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "").slice(0, MAX_BASE_LENGTH);
|
|
55
|
+
return slug || "operation";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/openapi/requestBuilder.ts
|
|
59
|
+
async function executeRequest(options) {
|
|
60
|
+
const baseUrl = resolveBaseUrl(
|
|
61
|
+
options.servers,
|
|
62
|
+
options.origin,
|
|
63
|
+
options.baseUrlOverride
|
|
64
|
+
);
|
|
65
|
+
const pathParams = {};
|
|
66
|
+
const query = new URLSearchParams();
|
|
67
|
+
const headers = new Headers(await resolveHeaders(options.headers));
|
|
68
|
+
const bodyProps = {};
|
|
69
|
+
for (const [key, value] of Object.entries(options.args)) {
|
|
70
|
+
const mapping = options.parameterMap[key];
|
|
71
|
+
if (!mapping || value === void 0) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
switch (mapping.in) {
|
|
75
|
+
case "body":
|
|
76
|
+
bodyProps[mapping.name] = value;
|
|
77
|
+
break;
|
|
78
|
+
case "cookie": {
|
|
79
|
+
const existing = headers.get("cookie");
|
|
80
|
+
headers.set(
|
|
81
|
+
"cookie",
|
|
82
|
+
existing ? `${existing}; ${mapping.name}=${String(value)}` : `${mapping.name}=${String(value)}`
|
|
83
|
+
);
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
case "header":
|
|
87
|
+
headers.set(mapping.name, String(value));
|
|
88
|
+
break;
|
|
89
|
+
case "path":
|
|
90
|
+
pathParams[mapping.name] = String(value);
|
|
91
|
+
break;
|
|
92
|
+
case "query":
|
|
93
|
+
for (const item of Array.isArray(value) ? value : [value]) {
|
|
94
|
+
query.append(mapping.name, String(item));
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
let path = options.route.path;
|
|
100
|
+
for (const [name, value] of Object.entries(pathParams)) {
|
|
101
|
+
path = path.replace(`{${name}}`, encodeURIComponent(value));
|
|
102
|
+
}
|
|
103
|
+
const url = new URL(baseUrl.replace(/\/$/, "") + path);
|
|
104
|
+
url.search = query.toString();
|
|
105
|
+
let body;
|
|
106
|
+
if (Object.keys(bodyProps).length > 0) {
|
|
107
|
+
if (!headers.has("content-type")) {
|
|
108
|
+
headers.set("content-type", "application/json");
|
|
109
|
+
}
|
|
110
|
+
body = JSON.stringify(
|
|
111
|
+
options.wholeBodyKey ? bodyProps[options.wholeBodyKey] : bodyProps
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const response = await options.fetchImpl(url.toString(), {
|
|
115
|
+
body,
|
|
116
|
+
headers,
|
|
117
|
+
method: options.route.method.toUpperCase()
|
|
118
|
+
});
|
|
119
|
+
const text = await response.text();
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
throw new (0, _chunkSYZRGVQXcjs.UserError)(
|
|
122
|
+
`${options.route.method.toUpperCase()} ${path} failed with ${response.status}: ${text.slice(0, 2e3)}`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
if (_optionalChain([response, 'access', _5 => _5.headers, 'access', _6 => _6.get, 'call', _7 => _7("content-type"), 'optionalAccess', _8 => _8.includes, 'call', _9 => _9("json")])) {
|
|
126
|
+
try {
|
|
127
|
+
return JSON.stringify(JSON.parse(text), null, 2);
|
|
128
|
+
} catch (e) {
|
|
129
|
+
return text;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return text;
|
|
133
|
+
}
|
|
134
|
+
function resolveBaseUrl(servers, origin, overrideUrl) {
|
|
135
|
+
if (overrideUrl) {
|
|
136
|
+
return overrideUrl.replace(/\/$/, "");
|
|
137
|
+
}
|
|
138
|
+
const server = _optionalChain([servers, 'optionalAccess', _10 => _10[0]]);
|
|
139
|
+
if (!server) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
"The OpenAPI document has no `servers` entry. Pass `baseUrl` to fromOpenAPI() explicitly."
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
let url = server.url;
|
|
145
|
+
for (const [name, variable] of Object.entries(_nullishCoalesce(server.variables, () => ( {})))) {
|
|
146
|
+
url = url.replaceAll(`{${name}}`, variable.default);
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
return new URL(url).toString().replace(/\/$/, "");
|
|
150
|
+
} catch (e2) {
|
|
151
|
+
if (!origin) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`The OpenAPI document's servers[0].url ("${url}") is relative, and the spec was not loaded from an http(s) URL, so it cannot be resolved to an absolute address. Pass \`baseUrl\` to fromOpenAPI() explicitly.`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return new URL(url, origin).toString().replace(/\/$/, "");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function resolveHeaders(headers) {
|
|
160
|
+
if (!headers) {
|
|
161
|
+
return {};
|
|
162
|
+
}
|
|
163
|
+
return typeof headers === "function" ? await headers() : { ...headers };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/openapi/routes.ts
|
|
167
|
+
var HTTP_METHODS = ["get", "put", "post", "delete", "patch"];
|
|
168
|
+
function extractRoutes(document) {
|
|
169
|
+
const routes = [];
|
|
170
|
+
for (const [path, pathItem] of Object.entries(_nullishCoalesce(document.paths, () => ( {})))) {
|
|
171
|
+
const pathLevelParams = (_nullishCoalesce(pathItem.parameters, () => ( []))).map(
|
|
172
|
+
(param) => resolveRef(document, param)
|
|
173
|
+
);
|
|
174
|
+
for (const method of HTTP_METHODS) {
|
|
175
|
+
const operation = pathItem[method];
|
|
176
|
+
if (!operation) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const operationParams = (_nullishCoalesce(operation.parameters, () => ( []))).map(
|
|
180
|
+
(param) => resolveRef(document, param)
|
|
181
|
+
);
|
|
182
|
+
routes.push({
|
|
183
|
+
deprecated: _nullishCoalesce(operation.deprecated, () => ( false)),
|
|
184
|
+
method,
|
|
185
|
+
operationId: operation.operationId,
|
|
186
|
+
parameters: mergeParameters(pathLevelParams, operationParams),
|
|
187
|
+
path,
|
|
188
|
+
requestBody: operation.requestBody ? resolveRef(document, operation.requestBody) : void 0,
|
|
189
|
+
summary: operation.summary,
|
|
190
|
+
tags: _nullishCoalesce(operation.tags, () => ( []))
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return routes;
|
|
195
|
+
}
|
|
196
|
+
function mergeParameters(pathLevel, operationLevel) {
|
|
197
|
+
const overridden = new Set(
|
|
198
|
+
operationLevel.map((param) => `${param.in}:${param.name}`)
|
|
199
|
+
);
|
|
200
|
+
return [
|
|
201
|
+
...pathLevel.filter(
|
|
202
|
+
(param) => !overridden.has(`${param.in}:${param.name}`)
|
|
203
|
+
),
|
|
204
|
+
...operationLevel
|
|
205
|
+
];
|
|
206
|
+
}
|
|
207
|
+
function resolveRef(document, value) {
|
|
208
|
+
if (!value || typeof value !== "object" || !("$ref" in value)) {
|
|
209
|
+
return value;
|
|
210
|
+
}
|
|
211
|
+
const pointer = value.$ref;
|
|
212
|
+
if (!pointer.startsWith("#/")) {
|
|
213
|
+
throw new Error(`Unexpected external $ref after bundling: ${pointer}`);
|
|
214
|
+
}
|
|
215
|
+
const segments = pointer.slice(2).split("/").map(
|
|
216
|
+
(segment) => decodeURIComponent(segment.replaceAll("~1", "/").replaceAll("~0", "~"))
|
|
217
|
+
);
|
|
218
|
+
let node = document;
|
|
219
|
+
for (const segment of segments) {
|
|
220
|
+
node = _optionalChain([node, 'optionalAccess', _11 => _11[segment]]);
|
|
221
|
+
}
|
|
222
|
+
return node;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/openapi/schemas.ts
|
|
226
|
+
var SCHEMA_MAP_KEYS = /* @__PURE__ */ new Set([
|
|
227
|
+
"$defs",
|
|
228
|
+
"definitions",
|
|
229
|
+
"dependentSchemas",
|
|
230
|
+
"patternProperties",
|
|
231
|
+
"properties"
|
|
232
|
+
]);
|
|
233
|
+
var DATA_KEYS = /* @__PURE__ */ new Set(["const", "default", "enum", "example", "examples"]);
|
|
234
|
+
function buildFlatSchema(route, sharedDefs) {
|
|
235
|
+
const byName = /* @__PURE__ */ new Map();
|
|
236
|
+
for (const param of route.parameters) {
|
|
237
|
+
const list = _nullishCoalesce(byName.get(param.name), () => ( []));
|
|
238
|
+
list.push(param);
|
|
239
|
+
byName.set(param.name, list);
|
|
240
|
+
}
|
|
241
|
+
const { properties: bodyProperties, wholeBodyKey } = extractBodyProperties(
|
|
242
|
+
route.method === "get" ? void 0 : route.requestBody
|
|
243
|
+
);
|
|
244
|
+
const properties = {};
|
|
245
|
+
const required = [];
|
|
246
|
+
const parameterMap = {};
|
|
247
|
+
for (const [name, occurrences] of byName) {
|
|
248
|
+
const collides = occurrences.length > 1 || bodyProperties.has(name);
|
|
249
|
+
for (const param of occurrences) {
|
|
250
|
+
const key = collides ? `${name}__${param.in}` : name;
|
|
251
|
+
properties[key] = rewriteComponentRefs(
|
|
252
|
+
_nullishCoalesce(param.schema, () => ( { type: "string" }))
|
|
253
|
+
);
|
|
254
|
+
parameterMap[key] = { in: param.in, name };
|
|
255
|
+
if (param.in === "path" || param.required) {
|
|
256
|
+
required.push(key);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
for (const [name, { required: isRequired, schema }] of bodyProperties) {
|
|
261
|
+
properties[name] = rewriteComponentRefs(schema);
|
|
262
|
+
parameterMap[name] = { in: "body", name };
|
|
263
|
+
if (isRequired) {
|
|
264
|
+
required.push(name);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const flatSchema = {
|
|
268
|
+
additionalProperties: false,
|
|
269
|
+
properties,
|
|
270
|
+
type: "object",
|
|
271
|
+
...required.length > 0 ? { required } : {}
|
|
272
|
+
};
|
|
273
|
+
const usedDefs = sharedDefs && filterReferencedDefs(properties, sharedDefs);
|
|
274
|
+
if (usedDefs) {
|
|
275
|
+
flatSchema.$defs = usedDefs;
|
|
276
|
+
}
|
|
277
|
+
return { flatSchema, parameterMap, wholeBodyKey };
|
|
278
|
+
}
|
|
279
|
+
function buildSharedDefs(document) {
|
|
280
|
+
const schemas = _optionalChain([document, 'access', _12 => _12.components, 'optionalAccess', _13 => _13.schemas]);
|
|
281
|
+
if (!schemas || Object.keys(schemas).length === 0) {
|
|
282
|
+
return void 0;
|
|
283
|
+
}
|
|
284
|
+
return rewriteNode(schemas, "schemaMap");
|
|
285
|
+
}
|
|
286
|
+
function rewriteComponentRefs(value) {
|
|
287
|
+
return rewriteNode(value, "schema");
|
|
288
|
+
}
|
|
289
|
+
function childMode(key) {
|
|
290
|
+
if (DATA_KEYS.has(key)) {
|
|
291
|
+
return "data";
|
|
292
|
+
}
|
|
293
|
+
return SCHEMA_MAP_KEYS.has(key) ? "schemaMap" : "schema";
|
|
294
|
+
}
|
|
295
|
+
function extractBodyProperties(requestBody) {
|
|
296
|
+
const properties = /* @__PURE__ */ new Map();
|
|
297
|
+
const schema = _optionalChain([requestBody, 'optionalAccess', _14 => _14.content, 'optionalAccess', _15 => _15["application/json"], 'optionalAccess', _16 => _16.schema]);
|
|
298
|
+
if (!schema) {
|
|
299
|
+
return { properties };
|
|
300
|
+
}
|
|
301
|
+
const schemaProperties = schema.properties;
|
|
302
|
+
if (schema.type === "object" && schemaProperties) {
|
|
303
|
+
const requiredNames = new Set(
|
|
304
|
+
_nullishCoalesce(schema.required, () => ( []))
|
|
305
|
+
);
|
|
306
|
+
for (const [name, propertySchema] of Object.entries(schemaProperties)) {
|
|
307
|
+
properties.set(name, {
|
|
308
|
+
required: requiredNames.has(name),
|
|
309
|
+
schema: propertySchema
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
return { properties };
|
|
313
|
+
}
|
|
314
|
+
properties.set("body", {
|
|
315
|
+
required: _nullishCoalesce(_optionalChain([requestBody, 'optionalAccess', _17 => _17.required]), () => ( false)),
|
|
316
|
+
schema
|
|
317
|
+
});
|
|
318
|
+
return { properties, wholeBodyKey: "body" };
|
|
319
|
+
}
|
|
320
|
+
function filterReferencedDefs(node, allDefs) {
|
|
321
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
322
|
+
const stack = [node];
|
|
323
|
+
while (stack.length > 0) {
|
|
324
|
+
const current = stack.pop();
|
|
325
|
+
if (Array.isArray(current)) {
|
|
326
|
+
stack.push(...current);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (!current || typeof current !== "object") {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
for (const [key, value] of Object.entries(
|
|
333
|
+
current
|
|
334
|
+
)) {
|
|
335
|
+
if (key === "$ref" && typeof value === "string" && value.startsWith("#/$defs/")) {
|
|
336
|
+
const name = value.slice("#/$defs/".length);
|
|
337
|
+
if (allDefs[name] && !referenced.has(name)) {
|
|
338
|
+
referenced.add(name);
|
|
339
|
+
stack.push(allDefs[name]);
|
|
340
|
+
}
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
stack.push(value);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (referenced.size === 0) {
|
|
347
|
+
return void 0;
|
|
348
|
+
}
|
|
349
|
+
return Object.fromEntries(
|
|
350
|
+
[...referenced].map((name) => [name, allDefs[name]])
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
function normalizeNullable(schema) {
|
|
354
|
+
if (!("nullable" in schema)) {
|
|
355
|
+
return schema;
|
|
356
|
+
}
|
|
357
|
+
const { nullable, type, ...rest } = schema;
|
|
358
|
+
if (nullable !== true) {
|
|
359
|
+
return rest;
|
|
360
|
+
}
|
|
361
|
+
if (typeof type === "string") {
|
|
362
|
+
return { ...rest, type: [type, "null"] };
|
|
363
|
+
}
|
|
364
|
+
if (Array.isArray(type)) {
|
|
365
|
+
return { ...rest, type: [.../* @__PURE__ */ new Set(["null", ...type])] };
|
|
366
|
+
}
|
|
367
|
+
return rest;
|
|
368
|
+
}
|
|
369
|
+
function rewriteNode(value, mode) {
|
|
370
|
+
if (mode === "data") {
|
|
371
|
+
return value;
|
|
372
|
+
}
|
|
373
|
+
if (Array.isArray(value)) {
|
|
374
|
+
return value.map((item) => rewriteNode(item, "schema"));
|
|
375
|
+
}
|
|
376
|
+
if (!value || typeof value !== "object") {
|
|
377
|
+
return value;
|
|
378
|
+
}
|
|
379
|
+
const entries = Object.entries(value).map(
|
|
380
|
+
([key, entryValue]) => {
|
|
381
|
+
if (mode === "schemaMap") {
|
|
382
|
+
return [key, rewriteNode(entryValue, "schema")];
|
|
383
|
+
}
|
|
384
|
+
if (key === "$ref" && typeof entryValue === "string" && entryValue.startsWith("#/components/schemas/")) {
|
|
385
|
+
return [key, entryValue.replace("#/components/schemas/", "#/$defs/")];
|
|
386
|
+
}
|
|
387
|
+
return [key, rewriteNode(entryValue, childMode(key))];
|
|
388
|
+
}
|
|
389
|
+
);
|
|
390
|
+
const rewritten = Object.fromEntries(entries);
|
|
391
|
+
return mode === "schemaMap" ? rewritten : normalizeNullable(rewritten);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// src/openapi/selection.ts
|
|
395
|
+
var DEFAULT_MAX_OPERATIONS = 40;
|
|
396
|
+
var METHOD_PRIORITY = {
|
|
397
|
+
delete: 4,
|
|
398
|
+
get: 0,
|
|
399
|
+
patch: 3,
|
|
400
|
+
post: 1,
|
|
401
|
+
put: 2
|
|
402
|
+
};
|
|
403
|
+
function selectRoutes(routes, options) {
|
|
404
|
+
let selected = routes.filter((route) => !route.deprecated);
|
|
405
|
+
if (options.include) {
|
|
406
|
+
const include = options.include;
|
|
407
|
+
selected = selected.filter((route) => include(toSummary(route)));
|
|
408
|
+
}
|
|
409
|
+
if (options.exclude) {
|
|
410
|
+
const exclude = options.exclude;
|
|
411
|
+
selected = selected.filter((route) => !exclude(toSummary(route)));
|
|
412
|
+
}
|
|
413
|
+
selected = [...selected].sort((a, b) => {
|
|
414
|
+
const byMethod = METHOD_PRIORITY[a.method] - METHOD_PRIORITY[b.method];
|
|
415
|
+
return byMethod !== 0 ? byMethod : a.path.localeCompare(b.path);
|
|
416
|
+
});
|
|
417
|
+
const noSelectionGiven = !options.include && !options.exclude;
|
|
418
|
+
if (noSelectionGiven && options.maxTools === void 0 && selected.length > DEFAULT_MAX_OPERATIONS) {
|
|
419
|
+
throw new Error(
|
|
420
|
+
`fromOpenAPI found ${selected.length} operations, which exceeds the default limit of ${DEFAULT_MAX_OPERATIONS}. This is a deliberate stop, not a bug: turning every operation in a large spec into a tool produces a tool list most MCP clients can't use well. Pass \`include\`/\`exclude\` to choose the operations you actually want, or \`maxTools\` to raise this limit explicitly.`
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
if (options.maxTools !== void 0 && selected.length > options.maxTools) {
|
|
424
|
+
throw new Error(
|
|
425
|
+
`fromOpenAPI found ${selected.length} operations, which exceeds maxTools (${options.maxTools}). Narrow the spec with \`include\`/\`exclude\`, or raise \`maxTools\`.`
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
return selected;
|
|
429
|
+
}
|
|
430
|
+
function toSummary(route) {
|
|
431
|
+
return {
|
|
432
|
+
deprecated: route.deprecated,
|
|
433
|
+
method: route.method,
|
|
434
|
+
operationId: route.operationId,
|
|
435
|
+
path: route.path,
|
|
436
|
+
tags: route.tags
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// src/openapi/fromOpenAPI.ts
|
|
441
|
+
async function fromOpenAPI(options) {
|
|
442
|
+
const { document, origin } = await loadSpec(options.spec);
|
|
443
|
+
const routes = extractRoutes(document);
|
|
444
|
+
const selected = selectRoutes(routes, options);
|
|
445
|
+
const names = generateToolNames(selected, options.mcpNames);
|
|
446
|
+
const sharedDefs = buildSharedDefs(document);
|
|
447
|
+
const server = _nullishCoalesce(options.server, () => ( new (0, _chunkSYZRGVQXcjs.FastMCP)({
|
|
448
|
+
name: _nullishCoalesce(_nullishCoalesce(options.name, () => ( _optionalChain([document, 'access', _18 => _18.info, 'optionalAccess', _19 => _19.title]))), () => ( "OpenAPI Server")),
|
|
449
|
+
version: _nullishCoalesce(options.version, () => ( "1.0.0"))
|
|
450
|
+
})));
|
|
451
|
+
for (const route of selected) {
|
|
452
|
+
const name = names.get(route);
|
|
453
|
+
if (!name) {
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
const { flatSchema, parameterMap, wholeBodyKey } = buildFlatSchema(
|
|
457
|
+
route,
|
|
458
|
+
sharedDefs
|
|
459
|
+
);
|
|
460
|
+
server.addTool({
|
|
461
|
+
description: _nullishCoalesce(route.summary, () => ( `${route.method.toUpperCase()} ${route.path}`)),
|
|
462
|
+
execute: async (args) => executeRequest({
|
|
463
|
+
args,
|
|
464
|
+
baseUrlOverride: options.baseUrl,
|
|
465
|
+
fetchImpl: _nullishCoalesce(options.fetch, () => ( fetch)),
|
|
466
|
+
headers: options.headers,
|
|
467
|
+
origin,
|
|
468
|
+
parameterMap,
|
|
469
|
+
route,
|
|
470
|
+
servers: document.servers,
|
|
471
|
+
wholeBodyKey
|
|
472
|
+
}),
|
|
473
|
+
name,
|
|
474
|
+
parameters: _chunkSYZRGVQXcjs.jsonSchemaAdapter.call(void 0, flatSchema)
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
return server;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
exports.fromOpenAPI = fromOpenAPI;
|
|
482
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/fastmcp/fastmcp/dist/openapi/index.cjs","../../src/openapi/loadSpec.ts","../../src/openapi/naming.ts","../../src/openapi/requestBuilder.ts","../../src/openapi/routes.ts","../../src/openapi/schemas.ts","../../src/openapi/selection.ts","../../src/openapi/fromOpenAPI.ts"],"names":[],"mappings":"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","file":"/home/runner/work/fastmcp/fastmcp/dist/openapi/index.cjs","sourcesContent":[null,"import SwaggerParser from \"@apidevtools/swagger-parser\";\n\nimport type { BundledOpenApiDocument } from \"./types.js\";\n\nexport interface LoadedSpec {\n document: BundledOpenApiDocument;\n /**\n * The spec's own URL, when it was loaded from one. Used to resolve a\n * relative `servers[0].url` against the document's origin.\n */\n origin?: string;\n}\n\n/**\n * Loads and bundles an OpenAPI document, resolving local *and* external\n * `$ref`s (relative paths, absolute URLs, `other.yaml#/fragment`).\n *\n * `spec` is handed to swagger-parser as-is — a URL, file path, or object —\n * rather than being fetched and re-parsed here first. External refs resolve\n * relative to whatever document they were found in, so pre-fetching the\n * entry document and passing its parsed text as an object would resolve\n * every external ref against the wrong base (or none at all).\n */\nexport async function loadSpec(\n spec: Record<string, unknown> | string,\n): Promise<LoadedSpec> {\n const document = (await SwaggerParser.bundle(\n spec as never,\n )) as unknown as BundledOpenApiDocument;\n\n if (!document.openapi?.startsWith(\"3.\")) {\n throw new Error(\n `fromOpenAPI only supports OpenAPI 3.x documents (found ${\n document.openapi ?? document.swagger ?? \"an unrecognized version\"\n }). Swagger 2.0 is not supported.`,\n );\n }\n\n return {\n document,\n origin: typeof spec === \"string\" && isHttpUrl(spec) ? spec : undefined,\n };\n}\n\nfunction isHttpUrl(value: string): boolean {\n return value.startsWith(\"http://\") || value.startsWith(\"https://\");\n}\n","import type { HttpRoute } from \"./types.js\";\n\nconst MAX_NAME_LENGTH = 56;\n// Reserves room for a \"_<n>\" collision suffix so the final name never\n// exceeds MAX_NAME_LENGTH, however many collisions it takes.\nconst MAX_BASE_LENGTH = MAX_NAME_LENGTH - 5;\n\n/**\n * Generates a unique tool name per route.\n *\n * Ports the Python implementation's naming rule\n * (`server/providers/openapi/provider.py:_generate_default_name`): prefer\n * `mcpNames[operationId]`, then `operationId` (FastAPI-style `__` suffixes\n * stripped), falling back to `summary` or `{method}_{path}`; slugified and\n * capped at 56 characters, with `_2`, `_3`, ... appended on collision.\n *\n * Uniqueness is checked against the final (post-suffix) name, not just the\n * base — otherwise a spec whose own operationIds already look auto-suffixed\n * (e.g. both \"foo\" and \"foo_2\" present) could produce two identically-named\n * tools, one of which `FastMCP.addTool` would silently drop.\n */\nexport function generateToolNames(\n routes: HttpRoute[],\n mcpNames: Record<string, string> | undefined,\n): Map<HttpRoute, string> {\n const names = new Map<HttpRoute, string>();\n const used = new Set<string>();\n\n for (const route of routes) {\n const base = slugify(baseNameFor(route, mcpNames));\n let candidate = base;\n let suffix = 1;\n\n while (used.has(candidate)) {\n suffix += 1;\n candidate = `${base}_${suffix}`;\n }\n\n used.add(candidate);\n names.set(route, candidate);\n }\n\n return names;\n}\n\nfunction baseNameFor(\n route: HttpRoute,\n mcpNames: Record<string, string> | undefined,\n): string {\n if (route.operationId) {\n return mcpNames?.[route.operationId] ?? route.operationId.split(\"__\")[0];\n }\n\n return route.summary || `${route.method}_${route.path}`;\n}\n\nfunction slugify(value: string): string {\n const slug = value\n .replace(/[^a-zA-Z0-9_]+/g, \"_\")\n .replace(/_+/g, \"_\")\n .replace(/^_|_$/g, \"\")\n .slice(0, MAX_BASE_LENGTH);\n\n return slug || \"operation\";\n}\n","import type { ParameterMapping } from \"./schemas.js\";\nimport type { FromOpenAPIOptions, HttpRoute, OpenApiServer } from \"./types.js\";\n\nimport { UserError } from \"../FastMCP.js\";\n\nexport interface ExecuteRequestOptions {\n args: Record<string, unknown>;\n baseUrlOverride?: string;\n fetchImpl: typeof fetch;\n headers?: FromOpenAPIOptions[\"headers\"];\n origin?: string;\n parameterMap: Record<string, ParameterMapping>;\n route: HttpRoute;\n servers: OpenApiServer[] | undefined;\n wholeBodyKey?: string;\n}\n\nexport async function executeRequest(\n options: ExecuteRequestOptions,\n): Promise<string> {\n const baseUrl = resolveBaseUrl(\n options.servers,\n options.origin,\n options.baseUrlOverride,\n );\n\n const pathParams: Record<string, string> = {};\n const query = new URLSearchParams();\n // A plain object keys headers case-sensitively, so a caller-supplied\n // header (e.g. \"Content-Type\") wouldn't be recognized as the same header\n // as one this function sets internally (e.g. \"content-type\") — `Headers`\n // normalizes casing, so `.set()` correctly overrides rather than\n // combining into a comma-joined, malformed value.\n const headers = new Headers(await resolveHeaders(options.headers));\n const bodyProps: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(options.args)) {\n const mapping = options.parameterMap[key];\n\n if (!mapping || value === undefined) {\n continue;\n }\n\n switch (mapping.in) {\n case \"body\":\n bodyProps[mapping.name] = value;\n break;\n case \"cookie\": {\n const existing = headers.get(\"cookie\");\n headers.set(\n \"cookie\",\n existing\n ? `${existing}; ${mapping.name}=${String(value)}`\n : `${mapping.name}=${String(value)}`,\n );\n break;\n }\n case \"header\":\n headers.set(mapping.name, String(value));\n break;\n case \"path\":\n pathParams[mapping.name] = String(value);\n break;\n case \"query\":\n for (const item of Array.isArray(value) ? value : [value]) {\n query.append(mapping.name, String(item));\n }\n break;\n }\n }\n\n let path = options.route.path;\n\n for (const [name, value] of Object.entries(pathParams)) {\n path = path.replace(`{${name}}`, encodeURIComponent(value));\n }\n\n const url = new URL(baseUrl.replace(/\\/$/, \"\") + path);\n url.search = query.toString();\n\n let body: string | undefined;\n\n if (Object.keys(bodyProps).length > 0) {\n if (!headers.has(\"content-type\")) {\n headers.set(\"content-type\", \"application/json\");\n }\n\n body = JSON.stringify(\n options.wholeBodyKey ? bodyProps[options.wholeBodyKey] : bodyProps,\n );\n }\n\n const response = await options.fetchImpl(url.toString(), {\n body,\n headers,\n method: options.route.method.toUpperCase(),\n });\n\n const text = await response.text();\n\n if (!response.ok) {\n throw new UserError(\n `${options.route.method.toUpperCase()} ${path} failed with ${response.status}: ${text.slice(0, 2000)}`,\n );\n }\n\n if (response.headers.get(\"content-type\")?.includes(\"json\")) {\n try {\n return JSON.stringify(JSON.parse(text), null, 2);\n } catch {\n return text;\n }\n }\n\n return text;\n}\n\n/**\n * Resolves `servers[0].url` the way a real HTTP client needs it resolved,\n * not just the way a schema validator would accept it: a relative URL (e.g.\n * Petstore's own `\"/api/v3\"`) is joined against the document's own origin,\n * not passed through verbatim.\n */\nexport function resolveBaseUrl(\n servers: OpenApiServer[] | undefined,\n origin: string | undefined,\n overrideUrl: string | undefined,\n): string {\n if (overrideUrl) {\n return overrideUrl.replace(/\\/$/, \"\");\n }\n\n const server = servers?.[0];\n\n if (!server) {\n throw new Error(\n \"The OpenAPI document has no `servers` entry. Pass `baseUrl` to fromOpenAPI() explicitly.\",\n );\n }\n\n let url = server.url;\n\n for (const [name, variable] of Object.entries(server.variables ?? {})) {\n url = url.replaceAll(`{${name}}`, variable.default);\n }\n\n try {\n return new URL(url).toString().replace(/\\/$/, \"\");\n } catch {\n if (!origin) {\n throw new Error(\n `The OpenAPI document's servers[0].url (\"${url}\") is relative, and the spec was not loaded from an http(s) URL, so it cannot be resolved to an absolute address. Pass \\`baseUrl\\` to fromOpenAPI() explicitly.`,\n );\n }\n\n return new URL(url, origin).toString().replace(/\\/$/, \"\");\n }\n}\n\nasync function resolveHeaders(\n headers: FromOpenAPIOptions[\"headers\"],\n): Promise<Record<string, string>> {\n if (!headers) {\n return {};\n }\n\n return typeof headers === \"function\" ? await headers() : { ...headers };\n}\n","import type {\n BundledOpenApiDocument,\n HttpMethod,\n HttpRoute,\n OpenApiParameter,\n OpenApiParameterRef,\n OpenApiRequestBody,\n} from \"./types.js\";\n\nconst HTTP_METHODS: HttpMethod[] = [\"get\", \"put\", \"post\", \"delete\", \"patch\"];\n\n/**\n * Walks a bundled document's `paths` into a flat list of routes, resolving\n * any structural (non-schema) `$ref`s on parameters and request bodies —\n * e.g. `#/components/parameters/Limit` — against the same document.\n *\n * Bundling (see `loadSpec.ts`) guarantees every remaining `$ref` here is\n * local, so a plain JSON-pointer lookup is enough.\n */\nexport function extractRoutes(document: BundledOpenApiDocument): HttpRoute[] {\n const routes: HttpRoute[] = [];\n\n for (const [path, pathItem] of Object.entries(document.paths ?? {})) {\n const pathLevelParams = (pathItem.parameters ?? []).map((param) =>\n resolveRef<OpenApiParameter>(document, param),\n );\n\n for (const method of HTTP_METHODS) {\n const operation = pathItem[method];\n\n if (!operation) {\n continue;\n }\n\n const operationParams = (operation.parameters ?? []).map((param) =>\n resolveRef<OpenApiParameter>(document, param),\n );\n\n routes.push({\n deprecated: operation.deprecated ?? false,\n method,\n operationId: operation.operationId,\n parameters: mergeParameters(pathLevelParams, operationParams),\n path,\n requestBody: operation.requestBody\n ? resolveRef<OpenApiRequestBody>(document, operation.requestBody)\n : undefined,\n summary: operation.summary,\n tags: operation.tags ?? [],\n });\n }\n }\n\n return routes;\n}\n\nfunction mergeParameters(\n pathLevel: OpenApiParameter[],\n operationLevel: OpenApiParameter[],\n): OpenApiParameter[] {\n const overridden = new Set(\n operationLevel.map((param) => `${param.in}:${param.name}`),\n );\n\n return [\n ...pathLevel.filter(\n (param) => !overridden.has(`${param.in}:${param.name}`),\n ),\n ...operationLevel,\n ];\n}\n\nfunction resolveRef<TValue>(\n document: BundledOpenApiDocument,\n value: OpenApiParameterRef | TValue,\n): TValue {\n if (!value || typeof value !== \"object\" || !(\"$ref\" in value)) {\n return value;\n }\n\n const pointer = value.$ref;\n\n if (!pointer.startsWith(\"#/\")) {\n // Bundling should have already turned every external ref into a local\n // one — if this fires, swagger-parser's output shape has changed.\n throw new Error(`Unexpected external $ref after bundling: ${pointer}`);\n }\n\n // swagger-parser synthesizes these pointers as URI fragments (e.g. a path\n // like \"/pets/{petId}\" becomes \"~1pets~1%7BpetId%7D\"), so each segment\n // needs its \"~1\"/\"~0\" escapes undone *and* percent-decoding, in that order.\n const segments = pointer\n .slice(2)\n .split(\"/\")\n .map((segment) =>\n decodeURIComponent(segment.replaceAll(\"~1\", \"/\").replaceAll(\"~0\", \"~\")),\n );\n\n let node: unknown = document;\n\n for (const segment of segments) {\n node = (node as Record<string, unknown> | undefined)?.[segment];\n }\n\n return node as TValue;\n}\n","import type { JsonSchemaObject } from \"../jsonSchemaAdapter.js\";\nimport type {\n BundledOpenApiDocument,\n HttpRoute,\n OpenApiParameter,\n OpenApiRequestBody,\n OpenApiSchema,\n ParameterLocation,\n} from \"./types.js\";\n\n/**\n * Keys whose values are name-to-schema maps: their child keys are\n * author-chosen names rather than JSON Schema keywords.\n */\nconst SCHEMA_MAP_KEYS = new Set([\n \"$defs\",\n \"definitions\",\n \"dependentSchemas\",\n \"patternProperties\",\n \"properties\",\n]);\n\n/**\n * Keys whose values are arbitrary instance data rather than schemas. A\n * sample payload may well contain a \"$ref\" or \"nullable\" key of its own.\n */\nconst DATA_KEYS = new Set([\"const\", \"default\", \"enum\", \"example\", \"examples\"]);\n\nexport interface FlatSchemaResult {\n flatSchema: JsonSchemaObject;\n parameterMap: Record<string, ParameterMapping>;\n /**\n * Set when the request body's schema is not a flat object (e.g. an array,\n * or a bare non-object `$ref`) — the whole body is exposed as a single\n * property under this key, rather than flattened into individual\n * properties.\n */\n wholeBodyKey?: string;\n}\n\nexport interface ParameterMapping {\n in: \"body\" | ParameterLocation;\n name: string;\n}\n\ntype WalkMode = \"data\" | \"schema\" | \"schemaMap\";\n\n/**\n * Flattens a route's path/query/header/cookie parameters and request body\n * into a single tool input schema.\n *\n * Collision precedence ports the Python implementation's rule\n * (`utilities/openapi/schemas.py:_combine_schemas_and_map_params`): a name\n * that collides across path/query/header/cookie gets suffixed\n * `{name}__{location}`; a request body property with a colliding name always\n * keeps its bare name.\n *\n * `GET` never contributes a request body: `fetch` (and the Fetch spec in\n * general) rejects a body on a GET request, so a tool built from a spec's\n * (legal, if unusual) `GET` + `requestBody` operation would be permanently\n * broken. The request body is simply not flattened into the schema for such\n * a route, rather than surfacing a schema that can never actually be called.\n */\nexport function buildFlatSchema(\n route: HttpRoute,\n sharedDefs: Record<string, OpenApiSchema> | undefined,\n): FlatSchemaResult {\n const byName = new Map<string, OpenApiParameter[]>();\n\n for (const param of route.parameters) {\n const list = byName.get(param.name) ?? [];\n list.push(param);\n byName.set(param.name, list);\n }\n\n const { properties: bodyProperties, wholeBodyKey } = extractBodyProperties(\n route.method === \"get\" ? undefined : route.requestBody,\n );\n\n const properties: Record<string, OpenApiSchema> = {};\n const required: string[] = [];\n const parameterMap: Record<string, ParameterMapping> = {};\n\n for (const [name, occurrences] of byName) {\n const collides = occurrences.length > 1 || bodyProperties.has(name);\n\n for (const param of occurrences) {\n const key = collides ? `${name}__${param.in}` : name;\n\n properties[key] = rewriteComponentRefs(\n param.schema ?? { type: \"string\" },\n );\n parameterMap[key] = { in: param.in, name };\n\n if (param.in === \"path\" || param.required) {\n required.push(key);\n }\n }\n }\n\n for (const [name, { required: isRequired, schema }] of bodyProperties) {\n properties[name] = rewriteComponentRefs(schema);\n parameterMap[name] = { in: \"body\", name };\n\n if (isRequired) {\n required.push(name);\n }\n }\n\n const flatSchema: JsonSchemaObject = {\n additionalProperties: false,\n properties,\n type: \"object\",\n ...(required.length > 0 ? { required } : {}),\n };\n\n // Only the definitions this tool's own schema actually (transitively)\n // references — embedding the whole document's components.schemas into\n // every single tool would multiply the tools/list payload size by the\n // tool count for no benefit.\n const usedDefs = sharedDefs && filterReferencedDefs(properties, sharedDefs);\n\n if (usedDefs) {\n flatSchema.$defs = usedDefs;\n }\n\n return { flatSchema, parameterMap, wholeBodyKey };\n}\n\nexport function buildSharedDefs(\n document: BundledOpenApiDocument,\n): Record<string, OpenApiSchema> | undefined {\n const schemas = document.components?.schemas;\n\n if (!schemas || Object.keys(schemas).length === 0) {\n return undefined;\n }\n\n return rewriteNode(schemas, \"schemaMap\") as Record<string, OpenApiSchema>;\n}\n\n/**\n * Rewrites `$ref`s pointing at `#/components/schemas/...` to `#/$defs/...`,\n * so a per-tool schema that contains one can be handed to AJV standalone,\n * alongside a `$defs` object built from the document's `components.schemas`\n * (see `buildSharedDefs`). Ports the equivalent rewrite from the Python\n * implementation (`utilities/openapi/schemas.py:_replace_ref_with_defs`).\n *\n * Also normalizes OpenAPI 3.0's `nullable` keyword (see `normalizeNullable`),\n * since real specs carry both.\n */\nexport function rewriteComponentRefs<TValue>(value: TValue): TValue {\n return rewriteNode(value, \"schema\") as TValue;\n}\n\nfunction childMode(key: string): WalkMode {\n if (DATA_KEYS.has(key)) {\n return \"data\";\n }\n\n return SCHEMA_MAP_KEYS.has(key) ? \"schemaMap\" : \"schema\";\n}\n\nfunction extractBodyProperties(requestBody: OpenApiRequestBody | undefined): {\n properties: Map<string, { required: boolean; schema: OpenApiSchema }>;\n wholeBodyKey?: string;\n} {\n const properties = new Map<\n string,\n { required: boolean; schema: OpenApiSchema }\n >();\n\n const schema = requestBody?.content?.[\"application/json\"]?.schema;\n\n if (!schema) {\n return { properties };\n }\n\n const schemaProperties = schema.properties as\n | Record<string, OpenApiSchema>\n | undefined;\n\n if (schema.type === \"object\" && schemaProperties) {\n const requiredNames = new Set(\n (schema.required as string[] | undefined) ?? [],\n );\n\n for (const [name, propertySchema] of Object.entries(schemaProperties)) {\n properties.set(name, {\n required: requiredNames.has(name),\n schema: propertySchema,\n });\n }\n\n return { properties };\n }\n\n // Non-object body (array, bare $ref to a scalar/array, etc.) — expose the\n // whole thing as a single \"body\" property rather than flattening it.\n properties.set(\"body\", {\n required: requestBody?.required ?? false,\n schema,\n });\n\n return { properties, wholeBodyKey: \"body\" };\n}\n\n/**\n * Walks a schema fragment for `#/$defs/Name` refs and returns just those\n * definitions (transitively — a referenced def may itself reference\n * others), or `undefined` if none are referenced.\n */\nfunction filterReferencedDefs(\n node: unknown,\n allDefs: Record<string, OpenApiSchema>,\n): Record<string, OpenApiSchema> | undefined {\n const referenced = new Set<string>();\n const stack: unknown[] = [node];\n\n while (stack.length > 0) {\n const current = stack.pop();\n\n if (Array.isArray(current)) {\n stack.push(...current);\n continue;\n }\n\n if (!current || typeof current !== \"object\") {\n continue;\n }\n\n for (const [key, value] of Object.entries(\n current as Record<string, unknown>,\n )) {\n if (\n key === \"$ref\" &&\n typeof value === \"string\" &&\n value.startsWith(\"#/$defs/\")\n ) {\n const name = value.slice(\"#/$defs/\".length);\n\n if (allDefs[name] && !referenced.has(name)) {\n referenced.add(name);\n stack.push(allDefs[name]);\n }\n\n continue;\n }\n\n stack.push(value);\n }\n }\n\n if (referenced.size === 0) {\n return undefined;\n }\n\n return Object.fromEntries(\n [...referenced].map((name) => [name, allDefs[name]]),\n );\n}\n\n/**\n * OpenAPI 3.0's `nullable` keyword only makes sense alongside a sibling\n * `type`, which it widens (`nullable: true` + `type: \"string\"` means\n * \"string or null\") — but it is not itself standard JSON Schema. AJV\n * recognizes the keyword and throws ('\"nullable\" cannot be used without\n * \"type\"') if it finds one with no `type` on the same node, which real\n * specs do produce (e.g. `nullable` sibling to `oneOf`/`allOf`/`$ref`\n * instead of `type`, as in Box's API). Folded into `type` where there is\n * one to widen, dropped otherwise.\n */\nfunction normalizeNullable(\n schema: Record<string, unknown>,\n): Record<string, unknown> {\n if (!(\"nullable\" in schema)) {\n return schema;\n }\n\n const { nullable, type, ...rest } = schema;\n\n if (nullable !== true) {\n return rest;\n }\n\n if (typeof type === \"string\") {\n return { ...rest, type: [type, \"null\"] };\n }\n\n if (Array.isArray(type)) {\n return { ...rest, type: [...new Set([\"null\", ...type])] };\n }\n\n return rest;\n}\n\n/**\n * Walks a schema fragment, distinguishing the three kinds of node it can\n * reach — because only one of them is a schema whose keys are JSON Schema\n * keywords:\n *\n * - `\"schema\"` — a schema object. `$ref`/`nullable` here are keywords.\n * - `\"schemaMap\"` — a name-to-schema map (`properties`, `$defs`, ...). Its\n * keys are author-chosen names, so a property literally named `nullable`\n * or `$ref` is a field, not a keyword, and must survive untouched.\n * - `\"data\"` — arbitrary values (`default`, `enum`, `example`, ...). Not\n * schemas at all; passed through verbatim.\n *\n * Walking every node as a schema (as this originally did) silently deletes\n * a property named `nullable` from the generated tool schema, since\n * `normalizeNullable` cannot tell the keyword from a same-named field.\n */\nfunction rewriteNode(value: unknown, mode: WalkMode): unknown {\n if (mode === \"data\") {\n return value;\n }\n\n if (Array.isArray(value)) {\n return value.map((item) => rewriteNode(item, \"schema\"));\n }\n\n if (!value || typeof value !== \"object\") {\n return value;\n }\n\n const entries = Object.entries(value as Record<string, unknown>).map(\n ([key, entryValue]): [string, unknown] => {\n if (mode === \"schemaMap\") {\n return [key, rewriteNode(entryValue, \"schema\")];\n }\n\n if (\n key === \"$ref\" &&\n typeof entryValue === \"string\" &&\n entryValue.startsWith(\"#/components/schemas/\")\n ) {\n return [key, entryValue.replace(\"#/components/schemas/\", \"#/$defs/\")];\n }\n\n return [key, rewriteNode(entryValue, childMode(key))];\n },\n );\n\n const rewritten = Object.fromEntries(entries) as Record<string, unknown>;\n\n return mode === \"schemaMap\" ? rewritten : normalizeNullable(rewritten);\n}\n","import type {\n FromOpenAPIOptions,\n HttpMethod,\n HttpRoute,\n OperationSummary,\n} from \"./types.js\";\n\n/**\n * If neither `include`/`exclude` nor `maxTools` was given, and a spec still\n * produces more operations than this, `selectRoutes` throws rather than\n * silently generating a wall of tools most clients can't usefully work with.\n */\nexport const DEFAULT_MAX_OPERATIONS = 40;\n\nconst METHOD_PRIORITY: Record<HttpMethod, number> = {\n delete: 4,\n get: 0,\n patch: 3,\n post: 1,\n put: 2,\n};\n\n/**\n * Filters and orders routes into the set that becomes tools.\n *\n * Deprecated operations are excluded by default. Ordering is deterministic\n * (method priority GET→POST→PUT→PATCH→DELETE, then path) so that, combined\n * with `maxTools`, truncation is legible rather than arbitrary.\n */\nexport function selectRoutes(\n routes: HttpRoute[],\n options: Pick<FromOpenAPIOptions, \"exclude\" | \"include\" | \"maxTools\">,\n): HttpRoute[] {\n let selected = routes.filter((route) => !route.deprecated);\n\n if (options.include) {\n const include = options.include;\n selected = selected.filter((route) => include(toSummary(route)));\n }\n\n if (options.exclude) {\n const exclude = options.exclude;\n selected = selected.filter((route) => !exclude(toSummary(route)));\n }\n\n selected = [...selected].sort((a, b) => {\n const byMethod = METHOD_PRIORITY[a.method] - METHOD_PRIORITY[b.method];\n return byMethod !== 0 ? byMethod : a.path.localeCompare(b.path);\n });\n\n const noSelectionGiven = !options.include && !options.exclude;\n\n if (\n noSelectionGiven &&\n options.maxTools === undefined &&\n selected.length > DEFAULT_MAX_OPERATIONS\n ) {\n throw new Error(\n `fromOpenAPI found ${selected.length} operations, which exceeds the default limit of ${DEFAULT_MAX_OPERATIONS}. ` +\n \"This is a deliberate stop, not a bug: turning every operation in a large spec into a tool produces a tool list most MCP clients can't use well. \" +\n \"Pass `include`/`exclude` to choose the operations you actually want, or `maxTools` to raise this limit explicitly.\",\n );\n }\n\n if (options.maxTools !== undefined && selected.length > options.maxTools) {\n throw new Error(\n `fromOpenAPI found ${selected.length} operations, which exceeds maxTools (${options.maxTools}). ` +\n \"Narrow the spec with `include`/`exclude`, or raise `maxTools`.\",\n );\n }\n\n return selected;\n}\n\nfunction toSummary(route: HttpRoute): OperationSummary {\n return {\n deprecated: route.deprecated,\n method: route.method,\n operationId: route.operationId,\n path: route.path,\n tags: route.tags,\n };\n}\n","import type { FromOpenAPIOptions } from \"./types.js\";\n\nimport { FastMCP } from \"../FastMCP.js\";\nimport { jsonSchemaAdapter } from \"../jsonSchemaAdapter.js\";\nimport { loadSpec } from \"./loadSpec.js\";\nimport { generateToolNames } from \"./naming.js\";\nimport { executeRequest } from \"./requestBuilder.js\";\nimport { extractRoutes } from \"./routes.js\";\nimport { buildFlatSchema, buildSharedDefs } from \"./schemas.js\";\nimport { selectRoutes } from \"./selection.js\";\n\n/**\n * Converts an OpenAPI 3.x document into an MCP server, one tool per\n * operation.\n *\n * See docs/openapi.md for the full option reference and known limitations.\n */\nexport async function fromOpenAPI(\n options: FromOpenAPIOptions,\n): Promise<FastMCP> {\n const { document, origin } = await loadSpec(options.spec);\n const routes = extractRoutes(document);\n const selected = selectRoutes(routes, options);\n const names = generateToolNames(selected, options.mcpNames);\n const sharedDefs = buildSharedDefs(document);\n\n const server =\n options.server ??\n new FastMCP({\n name: options.name ?? document.info?.title ?? \"OpenAPI Server\",\n version: options.version ?? \"1.0.0\",\n });\n\n for (const route of selected) {\n const name = names.get(route);\n\n if (!name) {\n continue;\n }\n\n const { flatSchema, parameterMap, wholeBodyKey } = buildFlatSchema(\n route,\n sharedDefs,\n );\n\n server.addTool({\n description:\n route.summary ?? `${route.method.toUpperCase()} ${route.path}`,\n execute: async (args) =>\n executeRequest({\n args: args as Record<string, unknown>,\n baseUrlOverride: options.baseUrl,\n fetchImpl: options.fetch ?? fetch,\n headers: options.headers,\n origin,\n parameterMap,\n route,\n servers: document.servers,\n wholeBodyKey,\n }),\n name,\n parameters: jsonSchemaAdapter(flatSchema),\n });\n }\n\n return server;\n}\n"]}
|