@ttoss/http-server-mcp-openapi 0.7.3 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATIONS.md +35 -0
- package/README.md +85 -32
- package/dist/index.cjs +95 -60
- package/dist/index.d.cts +44 -22
- package/dist/index.d.mts +44 -22
- package/dist/index.mjs +96 -61
- package/package.json +4 -3
package/MIGRATIONS.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Migrations
|
|
2
|
+
|
|
3
|
+
## `openApiToTools` replaces `registerOpenApiTools`
|
|
4
|
+
|
|
5
|
+
The package now derives tools and leaves registering them to `registerTools`
|
|
6
|
+
from `@ttoss/http-server-mcp`, which can also
|
|
7
|
+
[defer](./README.md#deferring-the-tools-of-a-large-api) them behind
|
|
8
|
+
`search` / `describe` / `call`.
|
|
9
|
+
|
|
10
|
+
```diff
|
|
11
|
+
-import { registerOpenApiTools } from '@ttoss/http-server-mcp-openapi';
|
|
12
|
+
+import { registerTools } from '@ttoss/http-server-mcp';
|
|
13
|
+
+import { openApiToTools } from '@ttoss/http-server-mcp-openapi';
|
|
14
|
+
|
|
15
|
+
-registerOpenApiTools({ server, spec, callApi, serverParameters });
|
|
16
|
+
+registerTools({
|
|
17
|
+
+ server,
|
|
18
|
+
+ tools: openApiToTools({ spec, callApi, serverParameters }),
|
|
19
|
+
+});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Every other argument keeps its name and meaning; only `server` moves to
|
|
23
|
+
`registerTools`. `RegisterOpenApiToolsArgs` is now `OpenApiToToolsArgs`,
|
|
24
|
+
without `server`.
|
|
25
|
+
|
|
26
|
+
The return value changed: `registerOpenApiTools` answered the
|
|
27
|
+
`ToolDefinition`s, `openApiToTools` answers `OpenApiTool`s, each carrying its
|
|
28
|
+
`ToolDefinition` as `definition`. Replace `tool.method` with
|
|
29
|
+
`tool.definition.method`, and so on, wherever you read the result.
|
|
30
|
+
|
|
31
|
+
`ToolDefinition` gained `tags`, copied from the operation. A consumer that
|
|
32
|
+
builds `ToolDefinition` objects by hand must add it.
|
|
33
|
+
|
|
34
|
+
**What you will observe if you miss this.** The build fails on the missing
|
|
35
|
+
`registerOpenApiTools` import. Generated tools answer exactly as before.
|
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# @ttoss/http-server-mcp-openapi
|
|
2
2
|
|
|
3
3
|
Generate [Model Context Protocol (MCP)](https://modelcontextprotocol.io) tools
|
|
4
|
-
from an [OpenAPI](https://www.openapis.org/) specification
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
from an [OpenAPI](https://www.openapis.org/) specification, ready for
|
|
5
|
+
[@ttoss/http-server-mcp](https://ttoss.dev/docs/modules/packages/http-server-mcp)'s
|
|
6
|
+
`registerTools`.
|
|
7
7
|
|
|
8
8
|
Point it at your existing OpenAPI document and every operation becomes an MCP
|
|
9
9
|
tool whose handler resolves the incoming arguments into an HTTP request against
|
|
@@ -20,15 +20,18 @@ pnpm add @ttoss/http-server-mcp-openapi @ttoss/http-server-mcp
|
|
|
20
20
|
|
|
21
21
|
```typescript
|
|
22
22
|
import { App, bodyParser } from '@ttoss/http-server';
|
|
23
|
-
import {
|
|
24
|
-
|
|
23
|
+
import {
|
|
24
|
+
createMcpRouter,
|
|
25
|
+
McpServer,
|
|
26
|
+
registerTools,
|
|
27
|
+
} from '@ttoss/http-server-mcp';
|
|
28
|
+
import { openApiToTools } from '@ttoss/http-server-mcp-openapi';
|
|
25
29
|
|
|
26
30
|
import openApiDocument from './openapi.json' with { type: 'json' };
|
|
27
31
|
|
|
28
32
|
const server = new McpServer({ name: 'my-api', version: '1.0.0' });
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
server,
|
|
34
|
+
const tools = openApiToTools({
|
|
32
35
|
spec: openApiDocument,
|
|
33
36
|
// You own how the request is executed — base URL, auth, fetch impl.
|
|
34
37
|
// `headers` is what createMcpRouter's `getApiHeaders` produced for this
|
|
@@ -44,6 +47,8 @@ registerOpenApiTools({
|
|
|
44
47
|
},
|
|
45
48
|
});
|
|
46
49
|
|
|
50
|
+
registerTools({ server, tools });
|
|
51
|
+
|
|
47
52
|
const app = new App();
|
|
48
53
|
app.use(bodyParser());
|
|
49
54
|
app.use(createMcpRouter(server).routes());
|
|
@@ -55,15 +60,18 @@ app.listen(3000);
|
|
|
55
60
|
Each OpenAPI operation with an `operationId` and a supported HTTP method
|
|
56
61
|
(`GET`, `POST`, `PUT`, `PATCH`, `DELETE`) becomes one tool:
|
|
57
62
|
|
|
58
|
-
| OpenAPI | MCP tool
|
|
59
|
-
| ------------------------- |
|
|
60
|
-
| `operationId: listAgents` | tool name `list-agents` (kebab-case)
|
|
61
|
-
| path/query/body params | a single `inputSchema` object
|
|
62
|
-
| `$ref`, `oneOf`, `anyOf` | dereferenced and merged into a flat schema
|
|
63
|
-
| snake_case names | camelCase tool inputs, mapped back on call
|
|
64
|
-
| operation `description` | tool description
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
| OpenAPI | MCP tool |
|
|
64
|
+
| ------------------------- | --------------------------------------------- |
|
|
65
|
+
| `operationId: listAgents` | tool name `list-agents` (kebab-case) |
|
|
66
|
+
| path/query/body params | a single `inputSchema` object |
|
|
67
|
+
| `$ref`, `oneOf`, `anyOf` | dereferenced and merged into a flat schema |
|
|
68
|
+
| snake_case names | camelCase tool inputs, mapped back on call |
|
|
69
|
+
| operation `description` | tool description, newlines flattened |
|
|
70
|
+
| operation `tags` | tool `tags`, for the deferred `search` filter |
|
|
71
|
+
|
|
72
|
+
Path params are always required strings, carrying their own `description`; a
|
|
73
|
+
call missing one is refused before `callApi` runs, whatever
|
|
74
|
+
`validateArguments` says. Query and body params carry their
|
|
67
75
|
declared type and `required` flag. Array params keep their `items` schema. A
|
|
68
76
|
body property declared as a single-entry `allOf` (usually `allOf: [{ $ref }]`
|
|
69
77
|
beside its own `description`) takes `type`, `nullable` and `items` from the
|
|
@@ -87,24 +95,63 @@ repeats array values, `spaceDelimited`/`pipeDelimited` join them, and
|
|
|
87
95
|
`deepObject` emits bracketed keys — including nested objects and arrays, so
|
|
88
96
|
`{ documentId: { $eq: 'doc_1' } }` becomes `filters[documentId][$eq]=doc_1`.
|
|
89
97
|
|
|
90
|
-
## `
|
|
98
|
+
## `openApiToTools`
|
|
91
99
|
|
|
92
100
|
| Field | Description |
|
|
93
101
|
| ---------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
94
|
-
| `server` | The `McpServer` to register tools on. |
|
|
95
102
|
| `spec` | One OpenAPI document, or an array of them (tools are flattened). |
|
|
96
103
|
| `callApi` | Runs the resolved `{ method, url, body, tool, headers }` request and returns the raw data. |
|
|
97
104
|
| `toText?` | Serialises the raw data into the tool's text payload. Defaults to pretty JSON; strings pass through verbatim. |
|
|
98
105
|
| `serverParameters?` | Supplies server-managed path/query parameter values. See [Server-managed values](#server-managed-values). |
|
|
99
106
|
| `toStructuredContent?` | `({ data, tool })` → the result's `structuredContent`, sent beside the text. `undefined` keeps it text-only. |
|
|
100
107
|
| `toolMeta?` | `({ tool })` → the tool's `_meta` on `tools/list`. See [MCP Apps views](#mcp-apps-views). |
|
|
108
|
+
| `validateArguments?` | Enforce each generated `inputSchema` before `callApi` runs. Default `false`; see the caveat below. |
|
|
101
109
|
| `options?` | See [Options](#options). |
|
|
102
110
|
|
|
103
111
|
The default `toText` answers `NO_CONTENT_TEXT` (`Succeeded. The operation
|
|
104
112
|
returned no content.`) when `callApi` resolves `undefined` or `''`, so a `204`
|
|
105
113
|
reaches the client as a success.
|
|
106
114
|
|
|
107
|
-
Returns
|
|
115
|
+
Returns one `OpenApiTool` per operation: a `Tool` with the operation's `tags`,
|
|
116
|
+
a `summary` of `METHOD /path — description`, and the `definition` it was
|
|
117
|
+
derived from. Enable `validateArguments` only when the spec describes every
|
|
118
|
+
value the API accepts: a generated schema is easily narrower than the API, and
|
|
119
|
+
validating against it rejects calls the API would have taken.
|
|
120
|
+
|
|
121
|
+
### Deferring the tools of a large API
|
|
122
|
+
|
|
123
|
+
Every tool definition ships to the model on every turn, so a large API
|
|
124
|
+
overflows the client's context. [Defer](https://github.com/ttoss/ttoss/tree/main/packages/http-server-mcp#deferring-tools-for-large-tool-sets)
|
|
125
|
+
the same tools behind `search` / `describe` / `call`, for example on a second
|
|
126
|
+
endpoint next to the full one:
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
registerTools({ server: fullServer, tools });
|
|
130
|
+
registerTools({ server: deferredServer, tools, defer: true });
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`search` filters by the operations' tags and shows each tool's route; tools
|
|
134
|
+
linked to an MCP Apps view stay registered directly.
|
|
135
|
+
|
|
136
|
+
### Gating generated tools
|
|
137
|
+
|
|
138
|
+
A generated tool is an ordinary `Tool`, so a gate wraps it like any other.
|
|
139
|
+
`definition` carries what the operation declares, such as a custom extension
|
|
140
|
+
naming the scope:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
const gate = createToolGate({ gates: [subscriptionGate] });
|
|
144
|
+
|
|
145
|
+
registerTools({
|
|
146
|
+
server,
|
|
147
|
+
tools: tools.map((tool) => {
|
|
148
|
+
return gate({
|
|
149
|
+
tool,
|
|
150
|
+
requiredScope: tool.definition.extensions['x-required-scope'] as string,
|
|
151
|
+
});
|
|
152
|
+
}),
|
|
153
|
+
});
|
|
154
|
+
```
|
|
108
155
|
|
|
109
156
|
### MCP Apps views
|
|
110
157
|
|
|
@@ -112,7 +159,7 @@ A generated tool links to a view through `toolMeta`, and the view reads the
|
|
|
112
159
|
result from `structuredContent`:
|
|
113
160
|
|
|
114
161
|
```typescript
|
|
115
|
-
import { registerAppResource } from '@ttoss/http-server-mcp';
|
|
162
|
+
import { registerAppResource, registerTools } from '@ttoss/http-server-mcp';
|
|
116
163
|
|
|
117
164
|
const agentCard = registerAppResource({
|
|
118
165
|
server,
|
|
@@ -121,8 +168,7 @@ const agentCard = registerAppResource({
|
|
|
121
168
|
html: agentCardHtml,
|
|
122
169
|
});
|
|
123
170
|
|
|
124
|
-
|
|
125
|
-
server,
|
|
171
|
+
const tools = openApiToTools({
|
|
126
172
|
spec,
|
|
127
173
|
callApi,
|
|
128
174
|
toolMeta: ({ tool }) => {
|
|
@@ -134,6 +180,8 @@ registerOpenApiTools({
|
|
|
134
180
|
: undefined;
|
|
135
181
|
},
|
|
136
182
|
});
|
|
183
|
+
|
|
184
|
+
registerTools({ server, tools });
|
|
137
185
|
```
|
|
138
186
|
|
|
139
187
|
Keep the text payload: a host without MCP Apps support renders only that.
|
|
@@ -151,11 +199,10 @@ forwarded onto the dispatched request.
|
|
|
151
199
|
```typescript
|
|
152
200
|
import {
|
|
153
201
|
createInProcessCallApi,
|
|
154
|
-
|
|
202
|
+
openApiToTools,
|
|
155
203
|
} from '@ttoss/http-server-mcp-openapi';
|
|
156
204
|
|
|
157
|
-
|
|
158
|
-
server,
|
|
205
|
+
const tools = openApiToTools({
|
|
159
206
|
spec,
|
|
160
207
|
callApi: createInProcessCallApi({
|
|
161
208
|
app, // or () => app, when the app is built after the tools
|
|
@@ -163,6 +210,8 @@ registerOpenApiTools({
|
|
|
163
210
|
}),
|
|
164
211
|
});
|
|
165
212
|
|
|
213
|
+
registerTools({ server, tools });
|
|
214
|
+
|
|
166
215
|
const router = createMcpRouter(server, {
|
|
167
216
|
getApiHeaders: (ctx) => ({ authorization: ctx.headers.authorization ?? '' }),
|
|
168
217
|
});
|
|
@@ -178,7 +227,7 @@ back to `HTTP <status>`. Pass `toError` to build the error yourself.
|
|
|
178
227
|
## `openApiToToolDefinitions`
|
|
179
228
|
|
|
180
229
|
Use the lower-level function when you want the tool definitions without
|
|
181
|
-
|
|
230
|
+
handlers — to inspect, filter, or wire handlers yourself:
|
|
182
231
|
|
|
183
232
|
```typescript
|
|
184
233
|
import { openApiToToolDefinitions } from '@ttoss/http-server-mcp-openapi';
|
|
@@ -193,14 +242,13 @@ for (const tool of tools) {
|
|
|
193
242
|
```
|
|
194
243
|
|
|
195
244
|
Each `ToolDefinition` exposes `name`, `description`, `inputSchema`, `method`,
|
|
196
|
-
`pathTemplate`, `operationId`, the `path`/`query`/`body` builders,
|
|
245
|
+
`pathTemplate`, `operationId`, `tags`, the `path`/`query`/`body` builders,
|
|
197
246
|
`acceptedBodyFields`, `extensions`, and `serverManagedParameters`.
|
|
198
247
|
|
|
199
248
|
## Options
|
|
200
249
|
|
|
201
250
|
```typescript
|
|
202
|
-
|
|
203
|
-
server,
|
|
251
|
+
openApiToTools({
|
|
204
252
|
spec,
|
|
205
253
|
callApi,
|
|
206
254
|
options: {
|
|
@@ -259,12 +307,11 @@ A value flagged with `serverManagedExtension` is never offered to the model:
|
|
|
259
307
|
never sent (the API sets it itself). It still appears in
|
|
260
308
|
`acceptedBodyFields`.
|
|
261
309
|
- A **path or query parameter** is hidden from `inputSchema` and listed in
|
|
262
|
-
`tool.serverManagedParameters`. `
|
|
310
|
+
`tool.serverManagedParameters`. `openApiToTools` discards anything the
|
|
263
311
|
model sent for it and fills it from `serverParameters`, keyed by spec name:
|
|
264
312
|
|
|
265
313
|
```typescript
|
|
266
|
-
|
|
267
|
-
server,
|
|
314
|
+
openApiToTools({
|
|
268
315
|
spec,
|
|
269
316
|
callApi,
|
|
270
317
|
serverParameters: ({ tool, headers }) => ({
|
|
@@ -299,6 +346,12 @@ const tools = openApiToToolDefinitions({ spec: openApiDocument });
|
|
|
299
346
|
const iamAction = tools[0].extensions['x-iam-action'];
|
|
300
347
|
```
|
|
301
348
|
|
|
349
|
+
## Migrations
|
|
350
|
+
|
|
351
|
+
Breaking changes and what they require of consumers are listed in
|
|
352
|
+
[MIGRATIONS.md](https://github.com/ttoss/ttoss/blob/main/packages/http-server-mcp-openapi/MIGRATIONS.md),
|
|
353
|
+
newest first.
|
|
354
|
+
|
|
302
355
|
## Related Packages
|
|
303
356
|
|
|
304
357
|
- [@ttoss/http-server-mcp](https://ttoss.dev/docs/modules/packages/http-server-mcp) - MCP server integration for @ttoss/http-server
|
package/dist/index.cjs
CHANGED
|
@@ -354,7 +354,7 @@ var pinnedArgs = parameters => {
|
|
|
354
354
|
/**
|
|
355
355
|
* Wraps a `path` / `query` builder so pinned values replace whatever the args
|
|
356
356
|
* carry. Applied in the builder itself, so every consumer of the tool
|
|
357
|
-
* definition sends them, not only `
|
|
357
|
+
* definition sends them, not only `openApiToTools`.
|
|
358
358
|
*/
|
|
359
359
|
var withPinned = args => {
|
|
360
360
|
const {
|
|
@@ -812,7 +812,7 @@ var defaultToError = response => {
|
|
|
812
812
|
return new Error(errorMessageOf(response.body) ?? `HTTP ${response.status}`);
|
|
813
813
|
};
|
|
814
814
|
/**
|
|
815
|
-
* A `callApi` for {@link
|
|
815
|
+
* A `callApi` for {@link openApiToTools} that serves each tool call
|
|
816
816
|
* against the REST app in this same process, with no socket: validation,
|
|
817
817
|
* authorization and error handling run once, in the routes, for both
|
|
818
818
|
* surfaces.
|
|
@@ -824,10 +824,9 @@ var defaultToError = response => {
|
|
|
824
824
|
*
|
|
825
825
|
* @example
|
|
826
826
|
* ```typescript
|
|
827
|
-
*
|
|
827
|
+
* registerTools({
|
|
828
828
|
* server,
|
|
829
|
-
* spec,
|
|
830
|
-
* callApi: createInProcessCallApi({ app }),
|
|
829
|
+
* tools: openApiToTools({ spec, callApi: createInProcessCallApi({ app }) }),
|
|
831
830
|
* });
|
|
832
831
|
* ```
|
|
833
832
|
*/
|
|
@@ -866,7 +865,7 @@ var getJsonSchemaType = schemaType => {
|
|
|
866
865
|
return "string";
|
|
867
866
|
};
|
|
868
867
|
var sanitizeDescription = description => {
|
|
869
|
-
return (description || "").replace(
|
|
868
|
+
return (description || "").replace(/\n/g, " ").trim();
|
|
870
869
|
};
|
|
871
870
|
var defaultDescribe = ({
|
|
872
871
|
operation
|
|
@@ -946,7 +945,7 @@ var buildInputSchema = (pathParams, queryParams, bodyProps) => {
|
|
|
946
945
|
const properties = {};
|
|
947
946
|
for (const param of modelPathParams) properties[param.argName] = {
|
|
948
947
|
type: "string",
|
|
949
|
-
description:
|
|
948
|
+
description: sanitizeDescription(param.description)
|
|
950
949
|
};
|
|
951
950
|
for (const param of [...modelQueryParams, ...bodyProps]) properties[param.argName] = normalizeNullable(buildTypedProperty(param));
|
|
952
951
|
return {
|
|
@@ -957,6 +956,25 @@ var buildInputSchema = (pathParams, queryParams, bodyProps) => {
|
|
|
957
956
|
} : {})
|
|
958
957
|
};
|
|
959
958
|
};
|
|
959
|
+
/**
|
|
960
|
+
* Builds the request path. A path without one of its params is another route —
|
|
961
|
+
* `/projects/{id}/members` reaches the API as a `404` naming the resource, not
|
|
962
|
+
* the missing argument — so the call is refused instead.
|
|
963
|
+
*/
|
|
964
|
+
var pathFn = args => {
|
|
965
|
+
const build = buildPathFn(args.pathTemplate, args.pathParams);
|
|
966
|
+
const required = values => {
|
|
967
|
+
const missing = args.pathParams.find(param => {
|
|
968
|
+
return !param.serverManaged && values[param.argName] === void 0;
|
|
969
|
+
});
|
|
970
|
+
if (missing) throw new Error(`Missing required argument "${missing.argName}".`);
|
|
971
|
+
return build(values);
|
|
972
|
+
};
|
|
973
|
+
return withPinned({
|
|
974
|
+
build: required,
|
|
975
|
+
pinned: args.pinned
|
|
976
|
+
});
|
|
977
|
+
};
|
|
960
978
|
var SUPPORTED_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
961
979
|
/** The tool description, from `options.describe` or the default. */
|
|
962
980
|
var describeOperation = args => {
|
|
@@ -992,6 +1010,12 @@ var extractExtensions = operation => {
|
|
|
992
1010
|
for (const [key, value] of Object.entries(operation)) if (key.startsWith("x-")) extensions[key] = value;
|
|
993
1011
|
return extensions;
|
|
994
1012
|
};
|
|
1013
|
+
/** The operation's tags, ignoring entries a malformed spec typed otherwise. */
|
|
1014
|
+
var operationTags = operation => {
|
|
1015
|
+
return (operation.tags ?? []).filter(tag => {
|
|
1016
|
+
return typeof tag === "string";
|
|
1017
|
+
});
|
|
1018
|
+
};
|
|
995
1019
|
var processOperation = args => {
|
|
996
1020
|
const httpMethod = args.method.toUpperCase();
|
|
997
1021
|
if (!SUPPORTED_METHODS.includes(httpMethod) || !args.operation.operationId || args.operation[args.options.excludeExtension]) return null;
|
|
@@ -1042,8 +1066,10 @@ var processOperation = args => {
|
|
|
1042
1066
|
method: httpMethod,
|
|
1043
1067
|
pathTemplate: args.pathTemplate,
|
|
1044
1068
|
operationId: args.operation.operationId,
|
|
1045
|
-
|
|
1046
|
-
|
|
1069
|
+
tags: operationTags(args.operation),
|
|
1070
|
+
path: pathFn({
|
|
1071
|
+
pathTemplate: args.pathTemplate,
|
|
1072
|
+
pathParams,
|
|
1047
1073
|
pinned
|
|
1048
1074
|
}),
|
|
1049
1075
|
query: withPinned({
|
|
@@ -1120,7 +1146,7 @@ var openApiToToolDefinitions = args => {
|
|
|
1120
1146
|
};
|
|
1121
1147
|
|
|
1122
1148
|
//#endregion
|
|
1123
|
-
//#region src/
|
|
1149
|
+
//#region src/openApiToTools.ts
|
|
1124
1150
|
/** The text the default `toText` answers when the API returned no body. */
|
|
1125
1151
|
var NO_CONTENT_TEXT = "Succeeded. The operation returned no content.";
|
|
1126
1152
|
var defaultToText = data => {
|
|
@@ -1146,21 +1172,21 @@ var applyServerParameters = async args => {
|
|
|
1146
1172
|
return result;
|
|
1147
1173
|
};
|
|
1148
1174
|
/**
|
|
1149
|
-
* Derives MCP tools from OpenAPI document(s)
|
|
1150
|
-
*
|
|
1151
|
-
*
|
|
1175
|
+
* Derives MCP tools from OpenAPI document(s). Each tool's handler resolves
|
|
1176
|
+
* the incoming args into a concrete HTTP request and delegates execution to
|
|
1177
|
+
* `callApi`. Register the result with `registerTools` from
|
|
1178
|
+
* `@ttoss/http-server-mcp` — one MCP tool each, or deferred.
|
|
1152
1179
|
*
|
|
1153
|
-
* @returns
|
|
1180
|
+
* @returns One {@link OpenApiTool} per translatable operation.
|
|
1154
1181
|
*
|
|
1155
1182
|
* @example
|
|
1156
1183
|
* ```typescript
|
|
1157
|
-
* import { McpServer } from '@ttoss/http-server-mcp';
|
|
1158
|
-
* import {
|
|
1184
|
+
* import { McpServer, registerTools } from '@ttoss/http-server-mcp';
|
|
1185
|
+
* import { openApiToTools } from '@ttoss/http-server-mcp-openapi';
|
|
1159
1186
|
*
|
|
1160
1187
|
* const server = new McpServer({ name: 'my-api', version: '1.0.0' });
|
|
1161
1188
|
*
|
|
1162
|
-
*
|
|
1163
|
-
* server,
|
|
1189
|
+
* const tools = openApiToTools({
|
|
1164
1190
|
* spec: myOpenApiDocument,
|
|
1165
1191
|
* callApi: async ({ method, url, body, headers }) => {
|
|
1166
1192
|
* const res = await fetch(`https://api.example.com${url}`, {
|
|
@@ -1171,53 +1197,62 @@ var applyServerParameters = async args => {
|
|
|
1171
1197
|
* return res.json();
|
|
1172
1198
|
* },
|
|
1173
1199
|
* });
|
|
1200
|
+
*
|
|
1201
|
+
* registerTools({ server, tools });
|
|
1174
1202
|
* ```
|
|
1175
1203
|
*/
|
|
1176
|
-
var
|
|
1204
|
+
var openApiToTools = args => {
|
|
1177
1205
|
const toText = args.toText ?? defaultToText;
|
|
1178
|
-
|
|
1206
|
+
return openApiToToolDefinitions({
|
|
1179
1207
|
spec: args.spec,
|
|
1180
1208
|
options: args.options
|
|
1209
|
+
}).map(definition => {
|
|
1210
|
+
const route = `${definition.method} ${definition.pathTemplate}`;
|
|
1211
|
+
return {
|
|
1212
|
+
name: definition.name,
|
|
1213
|
+
description: definition.description,
|
|
1214
|
+
inputSchema: definition.inputSchema,
|
|
1215
|
+
validateArguments: args.validateArguments ?? false,
|
|
1216
|
+
...(definition.tags.length > 0 ? {
|
|
1217
|
+
tags: definition.tags
|
|
1218
|
+
} : {}),
|
|
1219
|
+
summary: definition.description ? `${route} — ${definition.description}` : route,
|
|
1220
|
+
_meta: args.toolMeta?.({
|
|
1221
|
+
tool: definition
|
|
1222
|
+
}),
|
|
1223
|
+
definition,
|
|
1224
|
+
handler: async rawArgs => {
|
|
1225
|
+
const headers = (0, _ttoss_http_server_mcp.getApiHeaders)();
|
|
1226
|
+
const handlerArgs = await applyServerParameters({
|
|
1227
|
+
tool: definition,
|
|
1228
|
+
handlerArgs: rawArgs,
|
|
1229
|
+
headers,
|
|
1230
|
+
serverParameters: args.serverParameters
|
|
1231
|
+
});
|
|
1232
|
+
const url = definition.path(handlerArgs) + (definition.query ? definition.query(handlerArgs) : "");
|
|
1233
|
+
const data = await args.callApi({
|
|
1234
|
+
method: definition.method,
|
|
1235
|
+
url,
|
|
1236
|
+
body: definition.body ? definition.body(handlerArgs) : void 0,
|
|
1237
|
+
tool: definition,
|
|
1238
|
+
headers
|
|
1239
|
+
});
|
|
1240
|
+
const structuredContent = args.toStructuredContent?.({
|
|
1241
|
+
data,
|
|
1242
|
+
tool: definition
|
|
1243
|
+
});
|
|
1244
|
+
return {
|
|
1245
|
+
content: [{
|
|
1246
|
+
type: "text",
|
|
1247
|
+
text: toText(data)
|
|
1248
|
+
}],
|
|
1249
|
+
...(structuredContent === void 0 ? {} : {
|
|
1250
|
+
structuredContent
|
|
1251
|
+
})
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
};
|
|
1181
1255
|
});
|
|
1182
|
-
for (const tool of tools) (0, _ttoss_http_server_mcp.registerToolFromSchema)(args.server, {
|
|
1183
|
-
name: tool.name,
|
|
1184
|
-
description: tool.description,
|
|
1185
|
-
inputSchema: tool.inputSchema,
|
|
1186
|
-
_meta: args.toolMeta?.({
|
|
1187
|
-
tool
|
|
1188
|
-
}),
|
|
1189
|
-
handler: async rawArgs => {
|
|
1190
|
-
const headers = (0, _ttoss_http_server_mcp.getApiHeaders)();
|
|
1191
|
-
const handlerArgs = await applyServerParameters({
|
|
1192
|
-
tool,
|
|
1193
|
-
handlerArgs: rawArgs,
|
|
1194
|
-
headers,
|
|
1195
|
-
serverParameters: args.serverParameters
|
|
1196
|
-
});
|
|
1197
|
-
const url = tool.path(handlerArgs) + (tool.query ? tool.query(handlerArgs) : "");
|
|
1198
|
-
const data = await args.callApi({
|
|
1199
|
-
method: tool.method,
|
|
1200
|
-
url,
|
|
1201
|
-
body: tool.body ? tool.body(handlerArgs) : void 0,
|
|
1202
|
-
tool,
|
|
1203
|
-
headers
|
|
1204
|
-
});
|
|
1205
|
-
const structuredContent = args.toStructuredContent?.({
|
|
1206
|
-
data,
|
|
1207
|
-
tool
|
|
1208
|
-
});
|
|
1209
|
-
return {
|
|
1210
|
-
content: [{
|
|
1211
|
-
type: "text",
|
|
1212
|
-
text: toText(data)
|
|
1213
|
-
}],
|
|
1214
|
-
...(structuredContent === void 0 ? {} : {
|
|
1215
|
-
structuredContent
|
|
1216
|
-
})
|
|
1217
|
-
};
|
|
1218
|
-
}
|
|
1219
|
-
});
|
|
1220
|
-
return tools;
|
|
1221
1256
|
};
|
|
1222
1257
|
|
|
1223
1258
|
//#endregion
|
|
@@ -1239,10 +1274,10 @@ exports.extractPinnedBody = extractPinnedBody;
|
|
|
1239
1274
|
exports.extractQueryParams = extractQueryParams;
|
|
1240
1275
|
exports.getJsonSchemaType = getJsonSchemaType;
|
|
1241
1276
|
exports.openApiToToolDefinitions = openApiToToolDefinitions;
|
|
1277
|
+
exports.openApiToTools = openApiToTools;
|
|
1242
1278
|
exports.operationIdToToolName = operationIdToToolName;
|
|
1243
1279
|
exports.processOperation = processOperation;
|
|
1244
1280
|
exports.processPath = processPath;
|
|
1245
|
-
exports.registerOpenApiTools = registerOpenApiTools;
|
|
1246
1281
|
exports.resolveParameter = resolveParameter;
|
|
1247
1282
|
exports.resolveSchema = resolveSchema;
|
|
1248
1283
|
exports.snakeToCamel = snakeToCamel;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import { JsonObjectSchema,
|
|
2
|
+
import { JsonObjectSchema, Tool } from "@ttoss/http-server-mcp";
|
|
3
3
|
import { App, InProcessResponse } from "@ttoss/http-server";
|
|
4
4
|
|
|
5
5
|
//#region src/types.d.ts
|
|
@@ -49,7 +49,12 @@ interface ToolDefinition {
|
|
|
49
49
|
pathTemplate: string;
|
|
50
50
|
/** The operation's `operationId`. */
|
|
51
51
|
operationId: string;
|
|
52
|
-
/**
|
|
52
|
+
/** The operation's `tags`, e.g. to filter the deferred `search` by module. */
|
|
53
|
+
tags: string[];
|
|
54
|
+
/**
|
|
55
|
+
* Builds the request path, substituting path params from the args. Throws
|
|
56
|
+
* when an argument a path param needs is missing.
|
|
57
|
+
*/
|
|
53
58
|
path: (args: Record<string, unknown>) => string;
|
|
54
59
|
/** Builds the query string (including leading `?`), or `undefined` if none. */
|
|
55
60
|
query?: (args: Record<string, unknown>) => string;
|
|
@@ -73,7 +78,7 @@ interface ToolDefinition {
|
|
|
73
78
|
* Path and query parameters flagged with the server-managed extension. They
|
|
74
79
|
* are absent from `inputSchema`, but `path` / `query` still read them from
|
|
75
80
|
* the args under `argName`, so the consumer fills them before building the
|
|
76
|
-
* request — `
|
|
81
|
+
* request — `openApiToTools` does it through `serverParameters`.
|
|
77
82
|
*/
|
|
78
83
|
serverManagedParameters: ServerManagedParameter[];
|
|
79
84
|
}
|
|
@@ -122,6 +127,7 @@ type RequestBodySpec = {
|
|
|
122
127
|
};
|
|
123
128
|
interface OperationSpec {
|
|
124
129
|
operationId?: string;
|
|
130
|
+
tags?: string[];
|
|
125
131
|
summary?: string;
|
|
126
132
|
description?: string;
|
|
127
133
|
parameters?: Array<{
|
|
@@ -342,7 +348,7 @@ declare const buildFullInputSchema: (args: {
|
|
|
342
348
|
bodyProps: FullBodyProp[];
|
|
343
349
|
}) => JsonObjectSchema;
|
|
344
350
|
//#endregion
|
|
345
|
-
//#region src/
|
|
351
|
+
//#region src/openApiToTools.d.ts
|
|
346
352
|
/** The resolved HTTP request a tool call maps to, before transport concerns. */
|
|
347
353
|
interface ResolvedRequest {
|
|
348
354
|
/** Uppercase HTTP method. */
|
|
@@ -360,9 +366,7 @@ interface ResolvedRequest {
|
|
|
360
366
|
*/
|
|
361
367
|
headers: Record<string, string>;
|
|
362
368
|
}
|
|
363
|
-
interface
|
|
364
|
-
/** The MCP server the generated tools are registered on. */
|
|
365
|
-
server: McpServer;
|
|
369
|
+
interface OpenApiToToolsArgs {
|
|
366
370
|
/** One or more OpenAPI documents to derive tools from. */
|
|
367
371
|
spec: OpenApiSpec | OpenApiSpec[];
|
|
368
372
|
/** Tuning options forwarded to {@link openApiToToolDefinitions}. */
|
|
@@ -399,7 +403,7 @@ interface RegisterOpenApiToolsArgs {
|
|
|
399
403
|
/**
|
|
400
404
|
* Builds each tool's `_meta`, advertised on `tools/list`. This is how a
|
|
401
405
|
* generated tool links to an MCP Apps view: return the bag from
|
|
402
|
-
* `registerAppResource(...).toolMeta()`. `undefined`
|
|
406
|
+
* `registerAppResource(...).toolMeta()`. `undefined` sets no `_meta`.
|
|
403
407
|
*
|
|
404
408
|
* @example
|
|
405
409
|
* ```typescript
|
|
@@ -427,25 +431,41 @@ interface RegisterOpenApiToolsArgs {
|
|
|
427
431
|
tool: ToolDefinition;
|
|
428
432
|
headers: Record<string, string>;
|
|
429
433
|
}) => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
434
|
+
/**
|
|
435
|
+
* Sets every tool's `validateArguments`: enforce the generated
|
|
436
|
+
* `inputSchema` before `callApi` runs. Leave it off unless the spec
|
|
437
|
+
* describes every value the API accepts — see `Tool.validateArguments`.
|
|
438
|
+
* @default false
|
|
439
|
+
*/
|
|
440
|
+
validateArguments?: boolean;
|
|
441
|
+
}
|
|
442
|
+
/** A {@link Tool} derived from one OpenAPI operation. */
|
|
443
|
+
interface OpenApiTool extends Tool {
|
|
444
|
+
/**
|
|
445
|
+
* The operation it was derived from — method, path, extensions — for
|
|
446
|
+
* decisions that key on the API rather than the tool name, such as which
|
|
447
|
+
* scope a gate requires.
|
|
448
|
+
*/
|
|
449
|
+
definition: ToolDefinition;
|
|
430
450
|
}
|
|
431
451
|
/** The text the default `toText` answers when the API returned no body. */
|
|
432
452
|
declare const NO_CONTENT_TEXT = "Succeeded. The operation returned no content.";
|
|
433
453
|
/**
|
|
434
|
-
* Derives MCP tools from OpenAPI document(s)
|
|
435
|
-
*
|
|
436
|
-
*
|
|
454
|
+
* Derives MCP tools from OpenAPI document(s). Each tool's handler resolves
|
|
455
|
+
* the incoming args into a concrete HTTP request and delegates execution to
|
|
456
|
+
* `callApi`. Register the result with `registerTools` from
|
|
457
|
+
* `@ttoss/http-server-mcp` — one MCP tool each, or deferred.
|
|
437
458
|
*
|
|
438
|
-
* @returns
|
|
459
|
+
* @returns One {@link OpenApiTool} per translatable operation.
|
|
439
460
|
*
|
|
440
461
|
* @example
|
|
441
462
|
* ```typescript
|
|
442
|
-
* import { McpServer } from '@ttoss/http-server-mcp';
|
|
443
|
-
* import {
|
|
463
|
+
* import { McpServer, registerTools } from '@ttoss/http-server-mcp';
|
|
464
|
+
* import { openApiToTools } from '@ttoss/http-server-mcp-openapi';
|
|
444
465
|
*
|
|
445
466
|
* const server = new McpServer({ name: 'my-api', version: '1.0.0' });
|
|
446
467
|
*
|
|
447
|
-
*
|
|
448
|
-
* server,
|
|
468
|
+
* const tools = openApiToTools({
|
|
449
469
|
* spec: myOpenApiDocument,
|
|
450
470
|
* callApi: async ({ method, url, body, headers }) => {
|
|
451
471
|
* const res = await fetch(`https://api.example.com${url}`, {
|
|
@@ -456,9 +476,11 @@ declare const NO_CONTENT_TEXT = "Succeeded. The operation returned no content.";
|
|
|
456
476
|
* return res.json();
|
|
457
477
|
* },
|
|
458
478
|
* });
|
|
479
|
+
*
|
|
480
|
+
* registerTools({ server, tools });
|
|
459
481
|
* ```
|
|
460
482
|
*/
|
|
461
|
-
declare const
|
|
483
|
+
declare const openApiToTools: (args: OpenApiToToolsArgs) => OpenApiTool[];
|
|
462
484
|
//#endregion
|
|
463
485
|
//#region src/inProcessCallApi.d.ts
|
|
464
486
|
interface CreateInProcessCallApiArgs {
|
|
@@ -487,7 +509,7 @@ interface CreateInProcessCallApiArgs {
|
|
|
487
509
|
*/
|
|
488
510
|
declare const errorMessageOf: (body: unknown) => string | null;
|
|
489
511
|
/**
|
|
490
|
-
* A `callApi` for {@link
|
|
512
|
+
* A `callApi` for {@link openApiToTools} that serves each tool call
|
|
491
513
|
* against the REST app in this same process, with no socket: validation,
|
|
492
514
|
* authorization and error handling run once, in the routes, for both
|
|
493
515
|
* surfaces.
|
|
@@ -499,10 +521,9 @@ declare const errorMessageOf: (body: unknown) => string | null;
|
|
|
499
521
|
*
|
|
500
522
|
* @example
|
|
501
523
|
* ```typescript
|
|
502
|
-
*
|
|
524
|
+
* registerTools({
|
|
503
525
|
* server,
|
|
504
|
-
* spec,
|
|
505
|
-
* callApi: createInProcessCallApi({ app }),
|
|
526
|
+
* tools: openApiToTools({ spec, callApi: createInProcessCallApi({ app }) }),
|
|
506
527
|
* });
|
|
507
528
|
* ```
|
|
508
529
|
*/
|
|
@@ -590,6 +611,7 @@ declare const getJsonSchemaType: (schemaType: string | undefined) => JsonSchemaP
|
|
|
590
611
|
declare const buildInputSchema: (pathParams: Array<{
|
|
591
612
|
name: string;
|
|
592
613
|
argName: string;
|
|
614
|
+
description?: string;
|
|
593
615
|
serverManaged?: boolean;
|
|
594
616
|
}>, queryParams: Array<{
|
|
595
617
|
name: string;
|
|
@@ -650,4 +672,4 @@ declare const openApiToToolDefinitions: (args: {
|
|
|
650
672
|
options?: OpenApiToToolsOptions;
|
|
651
673
|
}) => ToolDefinition[];
|
|
652
674
|
//#endregion
|
|
653
|
-
export { type CreateInProcessCallApiArgs, DEFAULT_EXCLUDE_EXTENSION, DEFAULT_SERVER_MANAGED_EXTENSION, type JsonSchemaProperty, NO_CONTENT_TEXT, type OpenApiDocuments, type OpenApiSpec, type OpenApiToToolsOptions, type
|
|
675
|
+
export { type CreateInProcessCallApiArgs, DEFAULT_EXCLUDE_EXTENSION, DEFAULT_SERVER_MANAGED_EXTENSION, type JsonSchemaProperty, NO_CONTENT_TEXT, type OpenApiDocuments, type OpenApiSpec, type OpenApiToToolsArgs, type OpenApiToToolsOptions, type OpenApiTool, type OperationSpec, type QueryParamSerialization, type RequestBodySpec, type ResolvedParameter, type ResolvedRequest, type ResolvedToolOptions, type ServerManagedParameter, type ToolDefinition, buildBodyFn, buildFullInputSchema, buildInputSchema, buildPathFn, buildQueryFn, createInProcessCallApi, dereferenceSchema, errorMessageOf, extractAcceptedBodyFields, extractBodyProps, extractPathParams, extractPinnedBody, extractQueryParams, getJsonSchemaType, openApiToToolDefinitions, openApiToTools, operationIdToToolName, processOperation, processPath, resolveParameter, resolveSchema, snakeToCamel, toToolSchema };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
import { App, InProcessResponse } from "@ttoss/http-server";
|
|
3
|
-
import { JsonObjectSchema,
|
|
3
|
+
import { JsonObjectSchema, Tool } from "@ttoss/http-server-mcp";
|
|
4
4
|
|
|
5
5
|
//#region src/types.d.ts
|
|
6
6
|
/** The JSON Schema primitive types this generator can derive from an OpenAPI `type`. */
|
|
@@ -49,7 +49,12 @@ interface ToolDefinition {
|
|
|
49
49
|
pathTemplate: string;
|
|
50
50
|
/** The operation's `operationId`. */
|
|
51
51
|
operationId: string;
|
|
52
|
-
/**
|
|
52
|
+
/** The operation's `tags`, e.g. to filter the deferred `search` by module. */
|
|
53
|
+
tags: string[];
|
|
54
|
+
/**
|
|
55
|
+
* Builds the request path, substituting path params from the args. Throws
|
|
56
|
+
* when an argument a path param needs is missing.
|
|
57
|
+
*/
|
|
53
58
|
path: (args: Record<string, unknown>) => string;
|
|
54
59
|
/** Builds the query string (including leading `?`), or `undefined` if none. */
|
|
55
60
|
query?: (args: Record<string, unknown>) => string;
|
|
@@ -73,7 +78,7 @@ interface ToolDefinition {
|
|
|
73
78
|
* Path and query parameters flagged with the server-managed extension. They
|
|
74
79
|
* are absent from `inputSchema`, but `path` / `query` still read them from
|
|
75
80
|
* the args under `argName`, so the consumer fills them before building the
|
|
76
|
-
* request — `
|
|
81
|
+
* request — `openApiToTools` does it through `serverParameters`.
|
|
77
82
|
*/
|
|
78
83
|
serverManagedParameters: ServerManagedParameter[];
|
|
79
84
|
}
|
|
@@ -122,6 +127,7 @@ type RequestBodySpec = {
|
|
|
122
127
|
};
|
|
123
128
|
interface OperationSpec {
|
|
124
129
|
operationId?: string;
|
|
130
|
+
tags?: string[];
|
|
125
131
|
summary?: string;
|
|
126
132
|
description?: string;
|
|
127
133
|
parameters?: Array<{
|
|
@@ -342,7 +348,7 @@ declare const buildFullInputSchema: (args: {
|
|
|
342
348
|
bodyProps: FullBodyProp[];
|
|
343
349
|
}) => JsonObjectSchema;
|
|
344
350
|
//#endregion
|
|
345
|
-
//#region src/
|
|
351
|
+
//#region src/openApiToTools.d.ts
|
|
346
352
|
/** The resolved HTTP request a tool call maps to, before transport concerns. */
|
|
347
353
|
interface ResolvedRequest {
|
|
348
354
|
/** Uppercase HTTP method. */
|
|
@@ -360,9 +366,7 @@ interface ResolvedRequest {
|
|
|
360
366
|
*/
|
|
361
367
|
headers: Record<string, string>;
|
|
362
368
|
}
|
|
363
|
-
interface
|
|
364
|
-
/** The MCP server the generated tools are registered on. */
|
|
365
|
-
server: McpServer;
|
|
369
|
+
interface OpenApiToToolsArgs {
|
|
366
370
|
/** One or more OpenAPI documents to derive tools from. */
|
|
367
371
|
spec: OpenApiSpec | OpenApiSpec[];
|
|
368
372
|
/** Tuning options forwarded to {@link openApiToToolDefinitions}. */
|
|
@@ -399,7 +403,7 @@ interface RegisterOpenApiToolsArgs {
|
|
|
399
403
|
/**
|
|
400
404
|
* Builds each tool's `_meta`, advertised on `tools/list`. This is how a
|
|
401
405
|
* generated tool links to an MCP Apps view: return the bag from
|
|
402
|
-
* `registerAppResource(...).toolMeta()`. `undefined`
|
|
406
|
+
* `registerAppResource(...).toolMeta()`. `undefined` sets no `_meta`.
|
|
403
407
|
*
|
|
404
408
|
* @example
|
|
405
409
|
* ```typescript
|
|
@@ -427,25 +431,41 @@ interface RegisterOpenApiToolsArgs {
|
|
|
427
431
|
tool: ToolDefinition;
|
|
428
432
|
headers: Record<string, string>;
|
|
429
433
|
}) => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
434
|
+
/**
|
|
435
|
+
* Sets every tool's `validateArguments`: enforce the generated
|
|
436
|
+
* `inputSchema` before `callApi` runs. Leave it off unless the spec
|
|
437
|
+
* describes every value the API accepts — see `Tool.validateArguments`.
|
|
438
|
+
* @default false
|
|
439
|
+
*/
|
|
440
|
+
validateArguments?: boolean;
|
|
441
|
+
}
|
|
442
|
+
/** A {@link Tool} derived from one OpenAPI operation. */
|
|
443
|
+
interface OpenApiTool extends Tool {
|
|
444
|
+
/**
|
|
445
|
+
* The operation it was derived from — method, path, extensions — for
|
|
446
|
+
* decisions that key on the API rather than the tool name, such as which
|
|
447
|
+
* scope a gate requires.
|
|
448
|
+
*/
|
|
449
|
+
definition: ToolDefinition;
|
|
430
450
|
}
|
|
431
451
|
/** The text the default `toText` answers when the API returned no body. */
|
|
432
452
|
declare const NO_CONTENT_TEXT = "Succeeded. The operation returned no content.";
|
|
433
453
|
/**
|
|
434
|
-
* Derives MCP tools from OpenAPI document(s)
|
|
435
|
-
*
|
|
436
|
-
*
|
|
454
|
+
* Derives MCP tools from OpenAPI document(s). Each tool's handler resolves
|
|
455
|
+
* the incoming args into a concrete HTTP request and delegates execution to
|
|
456
|
+
* `callApi`. Register the result with `registerTools` from
|
|
457
|
+
* `@ttoss/http-server-mcp` — one MCP tool each, or deferred.
|
|
437
458
|
*
|
|
438
|
-
* @returns
|
|
459
|
+
* @returns One {@link OpenApiTool} per translatable operation.
|
|
439
460
|
*
|
|
440
461
|
* @example
|
|
441
462
|
* ```typescript
|
|
442
|
-
* import { McpServer } from '@ttoss/http-server-mcp';
|
|
443
|
-
* import {
|
|
463
|
+
* import { McpServer, registerTools } from '@ttoss/http-server-mcp';
|
|
464
|
+
* import { openApiToTools } from '@ttoss/http-server-mcp-openapi';
|
|
444
465
|
*
|
|
445
466
|
* const server = new McpServer({ name: 'my-api', version: '1.0.0' });
|
|
446
467
|
*
|
|
447
|
-
*
|
|
448
|
-
* server,
|
|
468
|
+
* const tools = openApiToTools({
|
|
449
469
|
* spec: myOpenApiDocument,
|
|
450
470
|
* callApi: async ({ method, url, body, headers }) => {
|
|
451
471
|
* const res = await fetch(`https://api.example.com${url}`, {
|
|
@@ -456,9 +476,11 @@ declare const NO_CONTENT_TEXT = "Succeeded. The operation returned no content.";
|
|
|
456
476
|
* return res.json();
|
|
457
477
|
* },
|
|
458
478
|
* });
|
|
479
|
+
*
|
|
480
|
+
* registerTools({ server, tools });
|
|
459
481
|
* ```
|
|
460
482
|
*/
|
|
461
|
-
declare const
|
|
483
|
+
declare const openApiToTools: (args: OpenApiToToolsArgs) => OpenApiTool[];
|
|
462
484
|
//#endregion
|
|
463
485
|
//#region src/inProcessCallApi.d.ts
|
|
464
486
|
interface CreateInProcessCallApiArgs {
|
|
@@ -487,7 +509,7 @@ interface CreateInProcessCallApiArgs {
|
|
|
487
509
|
*/
|
|
488
510
|
declare const errorMessageOf: (body: unknown) => string | null;
|
|
489
511
|
/**
|
|
490
|
-
* A `callApi` for {@link
|
|
512
|
+
* A `callApi` for {@link openApiToTools} that serves each tool call
|
|
491
513
|
* against the REST app in this same process, with no socket: validation,
|
|
492
514
|
* authorization and error handling run once, in the routes, for both
|
|
493
515
|
* surfaces.
|
|
@@ -499,10 +521,9 @@ declare const errorMessageOf: (body: unknown) => string | null;
|
|
|
499
521
|
*
|
|
500
522
|
* @example
|
|
501
523
|
* ```typescript
|
|
502
|
-
*
|
|
524
|
+
* registerTools({
|
|
503
525
|
* server,
|
|
504
|
-
* spec,
|
|
505
|
-
* callApi: createInProcessCallApi({ app }),
|
|
526
|
+
* tools: openApiToTools({ spec, callApi: createInProcessCallApi({ app }) }),
|
|
506
527
|
* });
|
|
507
528
|
* ```
|
|
508
529
|
*/
|
|
@@ -590,6 +611,7 @@ declare const getJsonSchemaType: (schemaType: string | undefined) => JsonSchemaP
|
|
|
590
611
|
declare const buildInputSchema: (pathParams: Array<{
|
|
591
612
|
name: string;
|
|
592
613
|
argName: string;
|
|
614
|
+
description?: string;
|
|
593
615
|
serverManaged?: boolean;
|
|
594
616
|
}>, queryParams: Array<{
|
|
595
617
|
name: string;
|
|
@@ -650,4 +672,4 @@ declare const openApiToToolDefinitions: (args: {
|
|
|
650
672
|
options?: OpenApiToToolsOptions;
|
|
651
673
|
}) => ToolDefinition[];
|
|
652
674
|
//#endregion
|
|
653
|
-
export { type CreateInProcessCallApiArgs, DEFAULT_EXCLUDE_EXTENSION, DEFAULT_SERVER_MANAGED_EXTENSION, type JsonSchemaProperty, NO_CONTENT_TEXT, type OpenApiDocuments, type OpenApiSpec, type OpenApiToToolsOptions, type
|
|
675
|
+
export { type CreateInProcessCallApiArgs, DEFAULT_EXCLUDE_EXTENSION, DEFAULT_SERVER_MANAGED_EXTENSION, type JsonSchemaProperty, NO_CONTENT_TEXT, type OpenApiDocuments, type OpenApiSpec, type OpenApiToToolsArgs, type OpenApiToToolsOptions, type OpenApiTool, type OperationSpec, type QueryParamSerialization, type RequestBodySpec, type ResolvedParameter, type ResolvedRequest, type ResolvedToolOptions, type ServerManagedParameter, type ToolDefinition, buildBodyFn, buildFullInputSchema, buildInputSchema, buildPathFn, buildQueryFn, createInProcessCallApi, dereferenceSchema, errorMessageOf, extractAcceptedBodyFields, extractBodyProps, extractPathParams, extractPinnedBody, extractQueryParams, getJsonSchemaType, openApiToToolDefinitions, openApiToTools, operationIdToToolName, processOperation, processPath, resolveParameter, resolveSchema, snakeToCamel, toToolSchema };
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
|
|
2
2
|
import { dispatchInProcess } from "@ttoss/http-server";
|
|
3
|
-
import { getApiHeaders
|
|
3
|
+
import { getApiHeaders } from "@ttoss/http-server-mcp";
|
|
4
4
|
|
|
5
5
|
//#region src/schema.ts
|
|
6
6
|
var isRecord$1 = value => {
|
|
@@ -351,7 +351,7 @@ var pinnedArgs = parameters => {
|
|
|
351
351
|
/**
|
|
352
352
|
* Wraps a `path` / `query` builder so pinned values replace whatever the args
|
|
353
353
|
* carry. Applied in the builder itself, so every consumer of the tool
|
|
354
|
-
* definition sends them, not only `
|
|
354
|
+
* definition sends them, not only `openApiToTools`.
|
|
355
355
|
*/
|
|
356
356
|
var withPinned = args => {
|
|
357
357
|
const {
|
|
@@ -809,7 +809,7 @@ var defaultToError = response => {
|
|
|
809
809
|
return new Error(errorMessageOf(response.body) ?? `HTTP ${response.status}`);
|
|
810
810
|
};
|
|
811
811
|
/**
|
|
812
|
-
* A `callApi` for {@link
|
|
812
|
+
* A `callApi` for {@link openApiToTools} that serves each tool call
|
|
813
813
|
* against the REST app in this same process, with no socket: validation,
|
|
814
814
|
* authorization and error handling run once, in the routes, for both
|
|
815
815
|
* surfaces.
|
|
@@ -821,10 +821,9 @@ var defaultToError = response => {
|
|
|
821
821
|
*
|
|
822
822
|
* @example
|
|
823
823
|
* ```typescript
|
|
824
|
-
*
|
|
824
|
+
* registerTools({
|
|
825
825
|
* server,
|
|
826
|
-
* spec,
|
|
827
|
-
* callApi: createInProcessCallApi({ app }),
|
|
826
|
+
* tools: openApiToTools({ spec, callApi: createInProcessCallApi({ app }) }),
|
|
828
827
|
* });
|
|
829
828
|
* ```
|
|
830
829
|
*/
|
|
@@ -863,7 +862,7 @@ var getJsonSchemaType = schemaType => {
|
|
|
863
862
|
return "string";
|
|
864
863
|
};
|
|
865
864
|
var sanitizeDescription = description => {
|
|
866
|
-
return (description || "").replace(
|
|
865
|
+
return (description || "").replace(/\n/g, " ").trim();
|
|
867
866
|
};
|
|
868
867
|
var defaultDescribe = ({
|
|
869
868
|
operation
|
|
@@ -943,7 +942,7 @@ var buildInputSchema = (pathParams, queryParams, bodyProps) => {
|
|
|
943
942
|
const properties = {};
|
|
944
943
|
for (const param of modelPathParams) properties[param.argName] = {
|
|
945
944
|
type: "string",
|
|
946
|
-
description:
|
|
945
|
+
description: sanitizeDescription(param.description)
|
|
947
946
|
};
|
|
948
947
|
for (const param of [...modelQueryParams, ...bodyProps]) properties[param.argName] = normalizeNullable(buildTypedProperty(param));
|
|
949
948
|
return {
|
|
@@ -954,6 +953,25 @@ var buildInputSchema = (pathParams, queryParams, bodyProps) => {
|
|
|
954
953
|
} : {})
|
|
955
954
|
};
|
|
956
955
|
};
|
|
956
|
+
/**
|
|
957
|
+
* Builds the request path. A path without one of its params is another route —
|
|
958
|
+
* `/projects/{id}/members` reaches the API as a `404` naming the resource, not
|
|
959
|
+
* the missing argument — so the call is refused instead.
|
|
960
|
+
*/
|
|
961
|
+
var pathFn = args => {
|
|
962
|
+
const build = buildPathFn(args.pathTemplate, args.pathParams);
|
|
963
|
+
const required = values => {
|
|
964
|
+
const missing = args.pathParams.find(param => {
|
|
965
|
+
return !param.serverManaged && values[param.argName] === void 0;
|
|
966
|
+
});
|
|
967
|
+
if (missing) throw new Error(`Missing required argument "${missing.argName}".`);
|
|
968
|
+
return build(values);
|
|
969
|
+
};
|
|
970
|
+
return withPinned({
|
|
971
|
+
build: required,
|
|
972
|
+
pinned: args.pinned
|
|
973
|
+
});
|
|
974
|
+
};
|
|
957
975
|
var SUPPORTED_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
958
976
|
/** The tool description, from `options.describe` or the default. */
|
|
959
977
|
var describeOperation = args => {
|
|
@@ -989,6 +1007,12 @@ var extractExtensions = operation => {
|
|
|
989
1007
|
for (const [key, value] of Object.entries(operation)) if (key.startsWith("x-")) extensions[key] = value;
|
|
990
1008
|
return extensions;
|
|
991
1009
|
};
|
|
1010
|
+
/** The operation's tags, ignoring entries a malformed spec typed otherwise. */
|
|
1011
|
+
var operationTags = operation => {
|
|
1012
|
+
return (operation.tags ?? []).filter(tag => {
|
|
1013
|
+
return typeof tag === "string";
|
|
1014
|
+
});
|
|
1015
|
+
};
|
|
992
1016
|
var processOperation = args => {
|
|
993
1017
|
const httpMethod = args.method.toUpperCase();
|
|
994
1018
|
if (!SUPPORTED_METHODS.includes(httpMethod) || !args.operation.operationId || args.operation[args.options.excludeExtension]) return null;
|
|
@@ -1039,8 +1063,10 @@ var processOperation = args => {
|
|
|
1039
1063
|
method: httpMethod,
|
|
1040
1064
|
pathTemplate: args.pathTemplate,
|
|
1041
1065
|
operationId: args.operation.operationId,
|
|
1042
|
-
|
|
1043
|
-
|
|
1066
|
+
tags: operationTags(args.operation),
|
|
1067
|
+
path: pathFn({
|
|
1068
|
+
pathTemplate: args.pathTemplate,
|
|
1069
|
+
pathParams,
|
|
1044
1070
|
pinned
|
|
1045
1071
|
}),
|
|
1046
1072
|
query: withPinned({
|
|
@@ -1117,7 +1143,7 @@ var openApiToToolDefinitions = args => {
|
|
|
1117
1143
|
};
|
|
1118
1144
|
|
|
1119
1145
|
//#endregion
|
|
1120
|
-
//#region src/
|
|
1146
|
+
//#region src/openApiToTools.ts
|
|
1121
1147
|
/** The text the default `toText` answers when the API returned no body. */
|
|
1122
1148
|
var NO_CONTENT_TEXT = "Succeeded. The operation returned no content.";
|
|
1123
1149
|
var defaultToText = data => {
|
|
@@ -1143,21 +1169,21 @@ var applyServerParameters = async args => {
|
|
|
1143
1169
|
return result;
|
|
1144
1170
|
};
|
|
1145
1171
|
/**
|
|
1146
|
-
* Derives MCP tools from OpenAPI document(s)
|
|
1147
|
-
*
|
|
1148
|
-
*
|
|
1172
|
+
* Derives MCP tools from OpenAPI document(s). Each tool's handler resolves
|
|
1173
|
+
* the incoming args into a concrete HTTP request and delegates execution to
|
|
1174
|
+
* `callApi`. Register the result with `registerTools` from
|
|
1175
|
+
* `@ttoss/http-server-mcp` — one MCP tool each, or deferred.
|
|
1149
1176
|
*
|
|
1150
|
-
* @returns
|
|
1177
|
+
* @returns One {@link OpenApiTool} per translatable operation.
|
|
1151
1178
|
*
|
|
1152
1179
|
* @example
|
|
1153
1180
|
* ```typescript
|
|
1154
|
-
* import { McpServer } from '@ttoss/http-server-mcp';
|
|
1155
|
-
* import {
|
|
1181
|
+
* import { McpServer, registerTools } from '@ttoss/http-server-mcp';
|
|
1182
|
+
* import { openApiToTools } from '@ttoss/http-server-mcp-openapi';
|
|
1156
1183
|
*
|
|
1157
1184
|
* const server = new McpServer({ name: 'my-api', version: '1.0.0' });
|
|
1158
1185
|
*
|
|
1159
|
-
*
|
|
1160
|
-
* server,
|
|
1186
|
+
* const tools = openApiToTools({
|
|
1161
1187
|
* spec: myOpenApiDocument,
|
|
1162
1188
|
* callApi: async ({ method, url, body, headers }) => {
|
|
1163
1189
|
* const res = await fetch(`https://api.example.com${url}`, {
|
|
@@ -1168,54 +1194,63 @@ var applyServerParameters = async args => {
|
|
|
1168
1194
|
* return res.json();
|
|
1169
1195
|
* },
|
|
1170
1196
|
* });
|
|
1197
|
+
*
|
|
1198
|
+
* registerTools({ server, tools });
|
|
1171
1199
|
* ```
|
|
1172
1200
|
*/
|
|
1173
|
-
var
|
|
1201
|
+
var openApiToTools = args => {
|
|
1174
1202
|
const toText = args.toText ?? defaultToText;
|
|
1175
|
-
|
|
1203
|
+
return openApiToToolDefinitions({
|
|
1176
1204
|
spec: args.spec,
|
|
1177
1205
|
options: args.options
|
|
1206
|
+
}).map(definition => {
|
|
1207
|
+
const route = `${definition.method} ${definition.pathTemplate}`;
|
|
1208
|
+
return {
|
|
1209
|
+
name: definition.name,
|
|
1210
|
+
description: definition.description,
|
|
1211
|
+
inputSchema: definition.inputSchema,
|
|
1212
|
+
validateArguments: args.validateArguments ?? false,
|
|
1213
|
+
...(definition.tags.length > 0 ? {
|
|
1214
|
+
tags: definition.tags
|
|
1215
|
+
} : {}),
|
|
1216
|
+
summary: definition.description ? `${route} — ${definition.description}` : route,
|
|
1217
|
+
_meta: args.toolMeta?.({
|
|
1218
|
+
tool: definition
|
|
1219
|
+
}),
|
|
1220
|
+
definition,
|
|
1221
|
+
handler: async rawArgs => {
|
|
1222
|
+
const headers = getApiHeaders();
|
|
1223
|
+
const handlerArgs = await applyServerParameters({
|
|
1224
|
+
tool: definition,
|
|
1225
|
+
handlerArgs: rawArgs,
|
|
1226
|
+
headers,
|
|
1227
|
+
serverParameters: args.serverParameters
|
|
1228
|
+
});
|
|
1229
|
+
const url = definition.path(handlerArgs) + (definition.query ? definition.query(handlerArgs) : "");
|
|
1230
|
+
const data = await args.callApi({
|
|
1231
|
+
method: definition.method,
|
|
1232
|
+
url,
|
|
1233
|
+
body: definition.body ? definition.body(handlerArgs) : void 0,
|
|
1234
|
+
tool: definition,
|
|
1235
|
+
headers
|
|
1236
|
+
});
|
|
1237
|
+
const structuredContent = args.toStructuredContent?.({
|
|
1238
|
+
data,
|
|
1239
|
+
tool: definition
|
|
1240
|
+
});
|
|
1241
|
+
return {
|
|
1242
|
+
content: [{
|
|
1243
|
+
type: "text",
|
|
1244
|
+
text: toText(data)
|
|
1245
|
+
}],
|
|
1246
|
+
...(structuredContent === void 0 ? {} : {
|
|
1247
|
+
structuredContent
|
|
1248
|
+
})
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1178
1252
|
});
|
|
1179
|
-
for (const tool of tools) registerToolFromSchema(args.server, {
|
|
1180
|
-
name: tool.name,
|
|
1181
|
-
description: tool.description,
|
|
1182
|
-
inputSchema: tool.inputSchema,
|
|
1183
|
-
_meta: args.toolMeta?.({
|
|
1184
|
-
tool
|
|
1185
|
-
}),
|
|
1186
|
-
handler: async rawArgs => {
|
|
1187
|
-
const headers = getApiHeaders();
|
|
1188
|
-
const handlerArgs = await applyServerParameters({
|
|
1189
|
-
tool,
|
|
1190
|
-
handlerArgs: rawArgs,
|
|
1191
|
-
headers,
|
|
1192
|
-
serverParameters: args.serverParameters
|
|
1193
|
-
});
|
|
1194
|
-
const url = tool.path(handlerArgs) + (tool.query ? tool.query(handlerArgs) : "");
|
|
1195
|
-
const data = await args.callApi({
|
|
1196
|
-
method: tool.method,
|
|
1197
|
-
url,
|
|
1198
|
-
body: tool.body ? tool.body(handlerArgs) : void 0,
|
|
1199
|
-
tool,
|
|
1200
|
-
headers
|
|
1201
|
-
});
|
|
1202
|
-
const structuredContent = args.toStructuredContent?.({
|
|
1203
|
-
data,
|
|
1204
|
-
tool
|
|
1205
|
-
});
|
|
1206
|
-
return {
|
|
1207
|
-
content: [{
|
|
1208
|
-
type: "text",
|
|
1209
|
-
text: toText(data)
|
|
1210
|
-
}],
|
|
1211
|
-
...(structuredContent === void 0 ? {} : {
|
|
1212
|
-
structuredContent
|
|
1213
|
-
})
|
|
1214
|
-
};
|
|
1215
|
-
}
|
|
1216
|
-
});
|
|
1217
|
-
return tools;
|
|
1218
1253
|
};
|
|
1219
1254
|
|
|
1220
1255
|
//#endregion
|
|
1221
|
-
export { DEFAULT_EXCLUDE_EXTENSION, DEFAULT_SERVER_MANAGED_EXTENSION, NO_CONTENT_TEXT, buildBodyFn, buildFullInputSchema, buildInputSchema, buildPathFn, buildQueryFn, createInProcessCallApi, dereferenceSchema, errorMessageOf, extractAcceptedBodyFields, extractBodyProps, extractPathParams, extractPinnedBody, extractQueryParams, getJsonSchemaType, openApiToToolDefinitions, operationIdToToolName, processOperation, processPath,
|
|
1256
|
+
export { DEFAULT_EXCLUDE_EXTENSION, DEFAULT_SERVER_MANAGED_EXTENSION, NO_CONTENT_TEXT, buildBodyFn, buildFullInputSchema, buildInputSchema, buildPathFn, buildQueryFn, createInProcessCallApi, dereferenceSchema, errorMessageOf, extractAcceptedBodyFields, extractBodyProps, extractPathParams, extractPinnedBody, extractQueryParams, getJsonSchemaType, openApiToToolDefinitions, openApiToTools, operationIdToToolName, processOperation, processPath, resolveParameter, resolveSchema, snakeToCamel, toToolSchema };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttoss/http-server-mcp-openapi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Generate Model Context Protocol (MCP) tools from an OpenAPI specification for @ttoss/http-server-mcp",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -32,11 +32,12 @@
|
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
34
|
"files": [
|
|
35
|
+
"MIGRATIONS.md",
|
|
35
36
|
"dist"
|
|
36
37
|
],
|
|
37
38
|
"dependencies": {
|
|
38
|
-
"@ttoss/http-server": "^0.
|
|
39
|
-
"@ttoss/http-server
|
|
39
|
+
"@ttoss/http-server-mcp": "^0.34.1",
|
|
40
|
+
"@ttoss/http-server": "^0.11.1"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
43
|
"@modelcontextprotocol/server": "^2.0.0",
|