fastmcp 4.17.1 → 4.19.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 +662 -0
- package/dist/openapi/index.cjs.map +1 -0
- package/dist/openapi/index.d.cts +181 -0
- package/dist/openapi/index.d.ts +181 -0
- package/dist/openapi/index.js +662 -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,662 @@
|
|
|
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 generateNames(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
|
+
const payload = options.wholeBodyKey ? bodyProps[options.wholeBodyKey] : bodyProps;
|
|
108
|
+
if (options.bodyEncoding === "form") {
|
|
109
|
+
if (!headers.has("content-type")) {
|
|
110
|
+
headers.set("content-type", "application/x-www-form-urlencoded");
|
|
111
|
+
}
|
|
112
|
+
body = encodeFormBody(payload);
|
|
113
|
+
} else {
|
|
114
|
+
if (!headers.has("content-type")) {
|
|
115
|
+
headers.set("content-type", "application/json");
|
|
116
|
+
}
|
|
117
|
+
body = JSON.stringify(payload);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const response = await options.fetchImpl(url.toString(), {
|
|
121
|
+
body,
|
|
122
|
+
headers,
|
|
123
|
+
method: options.route.method.toUpperCase()
|
|
124
|
+
});
|
|
125
|
+
const text = await response.text();
|
|
126
|
+
if (!response.ok) {
|
|
127
|
+
throw new (0, _chunkSYZRGVQXcjs.UserError)(
|
|
128
|
+
`${options.route.method.toUpperCase()} ${path} failed with ${response.status}: ${text.slice(0, 2e3)}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (_optionalChain([response, 'access', _5 => _5.headers, 'access', _6 => _6.get, 'call', _7 => _7("content-type"), 'optionalAccess', _8 => _8.includes, 'call', _9 => _9("json")])) {
|
|
132
|
+
try {
|
|
133
|
+
return JSON.stringify(JSON.parse(text), null, 2);
|
|
134
|
+
} catch (e) {
|
|
135
|
+
return text;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return text;
|
|
139
|
+
}
|
|
140
|
+
function resolveBaseUrl(servers, origin, overrideUrl) {
|
|
141
|
+
if (overrideUrl) {
|
|
142
|
+
return overrideUrl.replace(/\/$/, "");
|
|
143
|
+
}
|
|
144
|
+
const server = _optionalChain([servers, 'optionalAccess', _10 => _10[0]]);
|
|
145
|
+
if (!server) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
"The OpenAPI document has no `servers` entry. Pass `baseUrl` to fromOpenAPI() explicitly."
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
let url = server.url;
|
|
151
|
+
for (const [name, variable] of Object.entries(_nullishCoalesce(server.variables, () => ( {})))) {
|
|
152
|
+
url = url.replaceAll(`{${name}}`, variable.default);
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
return new URL(url).toString().replace(/\/$/, "");
|
|
156
|
+
} catch (e2) {
|
|
157
|
+
if (!origin) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`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.`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return new URL(url, origin).toString().replace(/\/$/, "");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function encodeFormBody(payload) {
|
|
166
|
+
const params = new URLSearchParams();
|
|
167
|
+
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
168
|
+
for (const [key, value] of Object.entries(
|
|
169
|
+
payload
|
|
170
|
+
)) {
|
|
171
|
+
if (value === void 0) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
for (const item of Array.isArray(value) ? value : [value]) {
|
|
175
|
+
params.append(
|
|
176
|
+
key,
|
|
177
|
+
item !== null && typeof item === "object" ? JSON.stringify(item) : String(item)
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return params.toString();
|
|
183
|
+
}
|
|
184
|
+
async function resolveHeaders(headers) {
|
|
185
|
+
if (!headers) {
|
|
186
|
+
return {};
|
|
187
|
+
}
|
|
188
|
+
return typeof headers === "function" ? await headers() : { ...headers };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// src/openapi/resourceMapping.ts
|
|
192
|
+
function buildResourceMapping(route, name, parameterMap, requiredKeys) {
|
|
193
|
+
const entries = Object.entries(parameterMap);
|
|
194
|
+
if (entries.length === 0) {
|
|
195
|
+
return { kind: "resource", uri: `openapi://${name}${route.path}` };
|
|
196
|
+
}
|
|
197
|
+
const required = new Set(_nullishCoalesce(requiredKeys, () => ( [])));
|
|
198
|
+
const args = [];
|
|
199
|
+
const queryKeys = [];
|
|
200
|
+
let path = route.path;
|
|
201
|
+
for (const [flatKey, mapping] of entries) {
|
|
202
|
+
if (mapping.in === "path") {
|
|
203
|
+
if (flatKey !== mapping.name) {
|
|
204
|
+
path = path.replace(`{${mapping.name}}`, `{${flatKey}}`);
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
queryKeys.push(flatKey);
|
|
208
|
+
}
|
|
209
|
+
args.push({ name: flatKey, required: required.has(flatKey) });
|
|
210
|
+
}
|
|
211
|
+
const uriTemplate = `openapi://${name}${path}` + (queryKeys.length > 0 ? `{?${queryKeys.join(",")}}` : "");
|
|
212
|
+
return { args, kind: "template", uriTemplate };
|
|
213
|
+
}
|
|
214
|
+
function isEligibleForResource(route) {
|
|
215
|
+
return route.parameters.every((param) => {
|
|
216
|
+
if (param.in === "header" || param.in === "cookie") {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
return _optionalChain([param, 'access', _11 => _11.schema, 'optionalAccess', _12 => _12.type]) !== "array";
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/openapi/routes.ts
|
|
224
|
+
var HTTP_METHODS = ["get", "put", "post", "delete", "patch"];
|
|
225
|
+
function extractRoutes(document) {
|
|
226
|
+
const routes = [];
|
|
227
|
+
for (const [path, pathItem] of Object.entries(_nullishCoalesce(document.paths, () => ( {})))) {
|
|
228
|
+
const pathLevelParams = (_nullishCoalesce(pathItem.parameters, () => ( []))).map(
|
|
229
|
+
(param) => resolveRef(document, param)
|
|
230
|
+
);
|
|
231
|
+
for (const method of HTTP_METHODS) {
|
|
232
|
+
const operation = pathItem[method];
|
|
233
|
+
if (!operation) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const operationParams = (_nullishCoalesce(operation.parameters, () => ( []))).map(
|
|
237
|
+
(param) => resolveRef(document, param)
|
|
238
|
+
);
|
|
239
|
+
routes.push({
|
|
240
|
+
deprecated: _nullishCoalesce(operation.deprecated, () => ( false)),
|
|
241
|
+
method,
|
|
242
|
+
operationId: operation.operationId,
|
|
243
|
+
parameters: mergeParameters(pathLevelParams, operationParams),
|
|
244
|
+
path,
|
|
245
|
+
requestBody: operation.requestBody ? resolveRef(document, operation.requestBody) : void 0,
|
|
246
|
+
summary: operation.summary,
|
|
247
|
+
tags: _nullishCoalesce(operation.tags, () => ( []))
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return routes;
|
|
252
|
+
}
|
|
253
|
+
function mergeParameters(pathLevel, operationLevel) {
|
|
254
|
+
const overridden = new Set(
|
|
255
|
+
operationLevel.map((param) => `${param.in}:${param.name}`)
|
|
256
|
+
);
|
|
257
|
+
return [
|
|
258
|
+
...pathLevel.filter(
|
|
259
|
+
(param) => !overridden.has(`${param.in}:${param.name}`)
|
|
260
|
+
),
|
|
261
|
+
...operationLevel
|
|
262
|
+
];
|
|
263
|
+
}
|
|
264
|
+
function resolveRef(document, value) {
|
|
265
|
+
if (!value || typeof value !== "object" || !("$ref" in value)) {
|
|
266
|
+
return value;
|
|
267
|
+
}
|
|
268
|
+
const pointer = value.$ref;
|
|
269
|
+
if (!pointer.startsWith("#/")) {
|
|
270
|
+
throw new Error(`Unexpected external $ref after bundling: ${pointer}`);
|
|
271
|
+
}
|
|
272
|
+
const segments = pointer.slice(2).split("/").map(
|
|
273
|
+
(segment) => decodeURIComponent(segment.replaceAll("~1", "/").replaceAll("~0", "~"))
|
|
274
|
+
);
|
|
275
|
+
let node = document;
|
|
276
|
+
for (const segment of segments) {
|
|
277
|
+
node = _optionalChain([node, 'optionalAccess', _13 => _13[segment]]);
|
|
278
|
+
}
|
|
279
|
+
return node;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/openapi/schemas.ts
|
|
283
|
+
var SCHEMA_MAP_KEYS = /* @__PURE__ */ new Set([
|
|
284
|
+
"$defs",
|
|
285
|
+
"definitions",
|
|
286
|
+
"dependentSchemas",
|
|
287
|
+
"patternProperties",
|
|
288
|
+
"properties"
|
|
289
|
+
]);
|
|
290
|
+
var DATA_KEYS = /* @__PURE__ */ new Set(["const", "default", "enum", "example", "examples"]);
|
|
291
|
+
function buildFlatSchema(route, sharedDefs) {
|
|
292
|
+
const byName = /* @__PURE__ */ new Map();
|
|
293
|
+
for (const param of route.parameters) {
|
|
294
|
+
const list = _nullishCoalesce(byName.get(param.name), () => ( []));
|
|
295
|
+
list.push(param);
|
|
296
|
+
byName.set(param.name, list);
|
|
297
|
+
}
|
|
298
|
+
const {
|
|
299
|
+
bodyEncoding,
|
|
300
|
+
properties: bodyProperties,
|
|
301
|
+
unsupportedBodyContentType,
|
|
302
|
+
wholeBodyKey
|
|
303
|
+
} = extractBodyProperties(
|
|
304
|
+
route.method === "get" ? void 0 : route.requestBody,
|
|
305
|
+
sharedDefs
|
|
306
|
+
);
|
|
307
|
+
const properties = {};
|
|
308
|
+
const required = [];
|
|
309
|
+
const parameterMap = {};
|
|
310
|
+
for (const [name, occurrences] of byName) {
|
|
311
|
+
const collides = occurrences.length > 1 || bodyProperties.has(name);
|
|
312
|
+
for (const param of occurrences) {
|
|
313
|
+
const key = collides ? `${name}__${param.in}` : name;
|
|
314
|
+
properties[key] = rewriteComponentRefs(
|
|
315
|
+
_nullishCoalesce(param.schema, () => ( { type: "string" }))
|
|
316
|
+
);
|
|
317
|
+
parameterMap[key] = { in: param.in, name };
|
|
318
|
+
if (param.in === "path" || param.required) {
|
|
319
|
+
required.push(key);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
for (const [name, { required: isRequired, schema }] of bodyProperties) {
|
|
324
|
+
properties[name] = rewriteComponentRefs(schema);
|
|
325
|
+
parameterMap[name] = { in: "body", name };
|
|
326
|
+
if (isRequired) {
|
|
327
|
+
required.push(name);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const flatSchema = {
|
|
331
|
+
additionalProperties: false,
|
|
332
|
+
properties,
|
|
333
|
+
type: "object",
|
|
334
|
+
...required.length > 0 ? { required } : {}
|
|
335
|
+
};
|
|
336
|
+
const usedDefs = sharedDefs && filterReferencedDefs(properties, sharedDefs);
|
|
337
|
+
if (usedDefs) {
|
|
338
|
+
flatSchema.$defs = usedDefs;
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
bodyEncoding,
|
|
342
|
+
flatSchema,
|
|
343
|
+
parameterMap,
|
|
344
|
+
unsupportedBodyContentType,
|
|
345
|
+
wholeBodyKey
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function buildSharedDefs(document) {
|
|
349
|
+
const schemas = _optionalChain([document, 'access', _14 => _14.components, 'optionalAccess', _15 => _15.schemas]);
|
|
350
|
+
if (!schemas || Object.keys(schemas).length === 0) {
|
|
351
|
+
return void 0;
|
|
352
|
+
}
|
|
353
|
+
return rewriteNode(schemas, "schemaMap");
|
|
354
|
+
}
|
|
355
|
+
function rewriteComponentRefs(value) {
|
|
356
|
+
return rewriteNode(value, "schema");
|
|
357
|
+
}
|
|
358
|
+
function childMode(key) {
|
|
359
|
+
if (DATA_KEYS.has(key)) {
|
|
360
|
+
return "data";
|
|
361
|
+
}
|
|
362
|
+
return SCHEMA_MAP_KEYS.has(key) ? "schemaMap" : "schema";
|
|
363
|
+
}
|
|
364
|
+
function componentSchemaName(ref) {
|
|
365
|
+
for (const prefix of ["#/components/schemas/", "#/$defs/"]) {
|
|
366
|
+
if (ref.startsWith(prefix)) {
|
|
367
|
+
return ref.slice(prefix.length);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return void 0;
|
|
371
|
+
}
|
|
372
|
+
function extractBodyProperties(requestBody, sharedDefs) {
|
|
373
|
+
const properties = /* @__PURE__ */ new Map();
|
|
374
|
+
const content = _optionalChain([requestBody, 'optionalAccess', _16 => _16.content]);
|
|
375
|
+
if (!content) {
|
|
376
|
+
return { properties };
|
|
377
|
+
}
|
|
378
|
+
const hasJson = "application/json" in content;
|
|
379
|
+
const hasForm = "application/x-www-form-urlencoded" in content;
|
|
380
|
+
if (!hasJson && !hasForm) {
|
|
381
|
+
const contentTypes = Object.keys(content);
|
|
382
|
+
return contentTypes.length > 0 ? { properties, unsupportedBodyContentType: contentTypes[0] } : { properties };
|
|
383
|
+
}
|
|
384
|
+
const bodyEncoding = hasJson ? "json" : "form";
|
|
385
|
+
const declaredSchema = hasJson ? _optionalChain([content, 'access', _17 => _17["application/json"], 'optionalAccess', _18 => _18.schema]) : _optionalChain([content, 'access', _19 => _19["application/x-www-form-urlencoded"], 'optionalAccess', _20 => _20.schema]);
|
|
386
|
+
if (!declaredSchema) {
|
|
387
|
+
return { bodyEncoding, properties };
|
|
388
|
+
}
|
|
389
|
+
const schema = bodyEncoding === "form" ? resolveComponentRef(declaredSchema, sharedDefs) : declaredSchema;
|
|
390
|
+
const schemaProperties = schema.properties;
|
|
391
|
+
if (schema.type === "object" && schemaProperties) {
|
|
392
|
+
const requiredNames = new Set(
|
|
393
|
+
_nullishCoalesce(schema.required, () => ( []))
|
|
394
|
+
);
|
|
395
|
+
for (const [name, propertySchema] of Object.entries(schemaProperties)) {
|
|
396
|
+
properties.set(name, {
|
|
397
|
+
required: requiredNames.has(name),
|
|
398
|
+
schema: propertySchema
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
return { bodyEncoding, properties };
|
|
402
|
+
}
|
|
403
|
+
if (bodyEncoding === "form") {
|
|
404
|
+
return {
|
|
405
|
+
properties,
|
|
406
|
+
unsupportedBodyContentType: "application/x-www-form-urlencoded"
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
properties.set("body", {
|
|
410
|
+
required: _nullishCoalesce(_optionalChain([requestBody, 'optionalAccess', _21 => _21.required]), () => ( false)),
|
|
411
|
+
schema
|
|
412
|
+
});
|
|
413
|
+
return { bodyEncoding, properties, wholeBodyKey: "body" };
|
|
414
|
+
}
|
|
415
|
+
function filterReferencedDefs(node, allDefs) {
|
|
416
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
417
|
+
const stack = [node];
|
|
418
|
+
while (stack.length > 0) {
|
|
419
|
+
const current = stack.pop();
|
|
420
|
+
if (Array.isArray(current)) {
|
|
421
|
+
stack.push(...current);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if (!current || typeof current !== "object") {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
for (const [key, value] of Object.entries(
|
|
428
|
+
current
|
|
429
|
+
)) {
|
|
430
|
+
if (key === "$ref" && typeof value === "string" && value.startsWith("#/$defs/")) {
|
|
431
|
+
const name = value.slice("#/$defs/".length);
|
|
432
|
+
if (allDefs[name] && !referenced.has(name)) {
|
|
433
|
+
referenced.add(name);
|
|
434
|
+
stack.push(allDefs[name]);
|
|
435
|
+
}
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
stack.push(value);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (referenced.size === 0) {
|
|
442
|
+
return void 0;
|
|
443
|
+
}
|
|
444
|
+
return Object.fromEntries(
|
|
445
|
+
[...referenced].map((name) => [name, allDefs[name]])
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
function normalizeNullable(schema) {
|
|
449
|
+
if (!("nullable" in schema)) {
|
|
450
|
+
return schema;
|
|
451
|
+
}
|
|
452
|
+
const { nullable, type, ...rest } = schema;
|
|
453
|
+
if (nullable !== true) {
|
|
454
|
+
return rest;
|
|
455
|
+
}
|
|
456
|
+
if (typeof type === "string") {
|
|
457
|
+
return { ...rest, type: [type, "null"] };
|
|
458
|
+
}
|
|
459
|
+
if (Array.isArray(type)) {
|
|
460
|
+
return { ...rest, type: [.../* @__PURE__ */ new Set(["null", ...type])] };
|
|
461
|
+
}
|
|
462
|
+
return rest;
|
|
463
|
+
}
|
|
464
|
+
function resolveComponentRef(schema, sharedDefs) {
|
|
465
|
+
const seen = /* @__PURE__ */ new Set();
|
|
466
|
+
let current = schema;
|
|
467
|
+
let ref = current.$ref;
|
|
468
|
+
while (typeof ref === "string") {
|
|
469
|
+
const name = componentSchemaName(ref);
|
|
470
|
+
const target = name === void 0 ? void 0 : _optionalChain([sharedDefs, 'optionalAccess', _22 => _22[name]]);
|
|
471
|
+
if (name === void 0 || target === void 0 || seen.has(name)) {
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
seen.add(name);
|
|
475
|
+
current = target;
|
|
476
|
+
ref = current.$ref;
|
|
477
|
+
}
|
|
478
|
+
return current;
|
|
479
|
+
}
|
|
480
|
+
function rewriteNode(value, mode) {
|
|
481
|
+
if (mode === "data") {
|
|
482
|
+
return value;
|
|
483
|
+
}
|
|
484
|
+
if (Array.isArray(value)) {
|
|
485
|
+
return value.map((item) => rewriteNode(item, "schema"));
|
|
486
|
+
}
|
|
487
|
+
if (!value || typeof value !== "object") {
|
|
488
|
+
return value;
|
|
489
|
+
}
|
|
490
|
+
const entries = Object.entries(value).map(
|
|
491
|
+
([key, entryValue]) => {
|
|
492
|
+
if (mode === "schemaMap") {
|
|
493
|
+
return [key, rewriteNode(entryValue, "schema")];
|
|
494
|
+
}
|
|
495
|
+
if (key === "$ref" && typeof entryValue === "string" && entryValue.startsWith("#/components/schemas/")) {
|
|
496
|
+
return [key, entryValue.replace("#/components/schemas/", "#/$defs/")];
|
|
497
|
+
}
|
|
498
|
+
return [key, rewriteNode(entryValue, childMode(key))];
|
|
499
|
+
}
|
|
500
|
+
);
|
|
501
|
+
const rewritten = Object.fromEntries(entries);
|
|
502
|
+
return mode === "schemaMap" ? rewritten : normalizeNullable(rewritten);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/openapi/selection.ts
|
|
506
|
+
var DEFAULT_MAX_OPERATIONS = 40;
|
|
507
|
+
var METHOD_PRIORITY = {
|
|
508
|
+
delete: 4,
|
|
509
|
+
get: 0,
|
|
510
|
+
patch: 3,
|
|
511
|
+
post: 1,
|
|
512
|
+
put: 2
|
|
513
|
+
};
|
|
514
|
+
function selectRoutes(routes, options) {
|
|
515
|
+
let selected = routes.filter((route) => !route.deprecated);
|
|
516
|
+
if (options.include) {
|
|
517
|
+
const include = options.include;
|
|
518
|
+
selected = selected.filter((route) => include(toSummary(route)));
|
|
519
|
+
}
|
|
520
|
+
if (options.exclude) {
|
|
521
|
+
const exclude = options.exclude;
|
|
522
|
+
selected = selected.filter((route) => !exclude(toSummary(route)));
|
|
523
|
+
}
|
|
524
|
+
selected = [...selected].sort((a, b) => {
|
|
525
|
+
const byMethod = METHOD_PRIORITY[a.method] - METHOD_PRIORITY[b.method];
|
|
526
|
+
return byMethod !== 0 ? byMethod : a.path.localeCompare(b.path);
|
|
527
|
+
});
|
|
528
|
+
const noSelectionGiven = !options.include && !options.exclude;
|
|
529
|
+
if (noSelectionGiven && options.maxTools === void 0 && selected.length > DEFAULT_MAX_OPERATIONS) {
|
|
530
|
+
throw new Error(
|
|
531
|
+
`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.`
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
if (options.maxTools !== void 0 && selected.length > options.maxTools) {
|
|
535
|
+
throw new Error(
|
|
536
|
+
`fromOpenAPI found ${selected.length} operations, which exceeds maxTools (${options.maxTools}). Narrow the spec with \`include\`/\`exclude\`, or raise \`maxTools\`.`
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
return selected;
|
|
540
|
+
}
|
|
541
|
+
function toSummary(route) {
|
|
542
|
+
return {
|
|
543
|
+
deprecated: route.deprecated,
|
|
544
|
+
method: route.method,
|
|
545
|
+
operationId: route.operationId,
|
|
546
|
+
path: route.path,
|
|
547
|
+
tags: route.tags
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// src/openapi/fromOpenAPI.ts
|
|
552
|
+
async function fromOpenAPI(options) {
|
|
553
|
+
const { document, origin } = await loadSpec(options.spec);
|
|
554
|
+
const routes = extractRoutes(document);
|
|
555
|
+
const selected = selectRoutes(routes, options);
|
|
556
|
+
const names = generateNames(selected, options.mcpNames);
|
|
557
|
+
const sharedDefs = buildSharedDefs(document);
|
|
558
|
+
const server = _nullishCoalesce(options.server, () => ( new (0, _chunkSYZRGVQXcjs.FastMCP)({
|
|
559
|
+
name: _nullishCoalesce(_nullishCoalesce(options.name, () => ( _optionalChain([document, 'access', _23 => _23.info, 'optionalAccess', _24 => _24.title]))), () => ( "OpenAPI Server")),
|
|
560
|
+
version: _nullishCoalesce(options.version, () => ( "1.0.0"))
|
|
561
|
+
})));
|
|
562
|
+
const skippedOperations = [];
|
|
563
|
+
for (const route of selected) {
|
|
564
|
+
const name = names.get(route);
|
|
565
|
+
if (!name) {
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
const {
|
|
569
|
+
bodyEncoding,
|
|
570
|
+
flatSchema,
|
|
571
|
+
parameterMap,
|
|
572
|
+
unsupportedBodyContentType,
|
|
573
|
+
wholeBodyKey
|
|
574
|
+
} = buildFlatSchema(route, sharedDefs);
|
|
575
|
+
const execOptions = {
|
|
576
|
+
baseUrlOverride: options.baseUrl,
|
|
577
|
+
fetchImpl: _nullishCoalesce(options.fetch, () => ( fetch)),
|
|
578
|
+
headers: options.headers,
|
|
579
|
+
origin,
|
|
580
|
+
parameterMap,
|
|
581
|
+
route,
|
|
582
|
+
servers: document.servers
|
|
583
|
+
};
|
|
584
|
+
if (options.resources && route.method === "get" && isEligibleForResource(route)) {
|
|
585
|
+
registerResource(
|
|
586
|
+
server,
|
|
587
|
+
route,
|
|
588
|
+
name,
|
|
589
|
+
parameterMap,
|
|
590
|
+
flatSchema.required,
|
|
591
|
+
execOptions
|
|
592
|
+
);
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
if (unsupportedBodyContentType) {
|
|
596
|
+
skippedOperations.push({
|
|
597
|
+
contentType: unsupportedBodyContentType,
|
|
598
|
+
method: route.method,
|
|
599
|
+
path: route.path
|
|
600
|
+
});
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
server.addTool({
|
|
604
|
+
description: _nullishCoalesce(route.summary, () => ( `${route.method.toUpperCase()} ${route.path}`)),
|
|
605
|
+
execute: async (args) => executeRequest({
|
|
606
|
+
...execOptions,
|
|
607
|
+
args,
|
|
608
|
+
bodyEncoding,
|
|
609
|
+
wholeBodyKey
|
|
610
|
+
}),
|
|
611
|
+
name,
|
|
612
|
+
parameters: _chunkSYZRGVQXcjs.jsonSchemaAdapter.call(void 0, flatSchema)
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
if (skippedOperations.length > 0) {
|
|
616
|
+
console.warn(
|
|
617
|
+
`fromOpenAPI: skipped ${skippedOperations.length} operation(s) whose request body can't be turned into tool parameters (supported: application/json, or application/x-www-form-urlencoded with a flat object schema): ` + skippedOperations.map(
|
|
618
|
+
(op) => `${op.method.toUpperCase()} ${op.path} (${op.contentType})`
|
|
619
|
+
).join(", ")
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
return server;
|
|
623
|
+
}
|
|
624
|
+
function registerResource(server, route, name, parameterMap, requiredKeys, execOptions) {
|
|
625
|
+
const mapping = buildResourceMapping(route, name, parameterMap, requiredKeys);
|
|
626
|
+
const description = _nullishCoalesce(route.summary, () => ( `GET ${route.path}`));
|
|
627
|
+
if (mapping.kind === "resource") {
|
|
628
|
+
server.addResource({
|
|
629
|
+
description,
|
|
630
|
+
load: async () => wrapAsResourceResult(
|
|
631
|
+
await executeRequest({ ...execOptions, args: {} })
|
|
632
|
+
),
|
|
633
|
+
name,
|
|
634
|
+
uri: mapping.uri
|
|
635
|
+
});
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
server.addResourceTemplate({
|
|
639
|
+
arguments: mapping.args,
|
|
640
|
+
description,
|
|
641
|
+
load: async (args) => wrapAsResourceResult(
|
|
642
|
+
await executeRequest({
|
|
643
|
+
...execOptions,
|
|
644
|
+
args
|
|
645
|
+
})
|
|
646
|
+
),
|
|
647
|
+
name,
|
|
648
|
+
uriTemplate: mapping.uriTemplate
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
function wrapAsResourceResult(text) {
|
|
652
|
+
try {
|
|
653
|
+
JSON.parse(text);
|
|
654
|
+
return { mimeType: "application/json", text };
|
|
655
|
+
} catch (e3) {
|
|
656
|
+
return { mimeType: "text/plain", text };
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
exports.fromOpenAPI = fromOpenAPI;
|
|
662
|
+
//# sourceMappingURL=index.cjs.map
|