@ttoss/http-server-mcp-openapi 0.5.7 → 0.7.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 +106 -8
- package/dist/index.cjs +293 -33
- package/dist/index.d.cts +156 -4
- package/dist/index.d.mts +156 -4
- package/dist/index.mjs +289 -33
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -89,14 +89,16 @@ repeats array values, `spaceDelimited`/`pipeDelimited` join them, and
|
|
|
89
89
|
|
|
90
90
|
## `registerOpenApiTools`
|
|
91
91
|
|
|
92
|
-
| Field
|
|
93
|
-
|
|
|
94
|
-
| `server`
|
|
95
|
-
| `spec`
|
|
96
|
-
| `callApi`
|
|
97
|
-
| `toText?`
|
|
98
|
-
| `serverParameters?`
|
|
99
|
-
| `
|
|
92
|
+
| Field | Description |
|
|
93
|
+
| ---------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
94
|
+
| `server` | The `McpServer` to register tools on. |
|
|
95
|
+
| `spec` | One OpenAPI document, or an array of them (tools are flattened). |
|
|
96
|
+
| `callApi` | Runs the resolved `{ method, url, body, tool, headers }` request and returns the raw data. |
|
|
97
|
+
| `toText?` | Serialises the raw data into the tool's text payload. Defaults to pretty JSON; strings pass through verbatim. |
|
|
98
|
+
| `serverParameters?` | Supplies server-managed path/query parameter values. See [Server-managed values](#server-managed-values). |
|
|
99
|
+
| `toStructuredContent?` | `({ data, tool })` → the result's `structuredContent`, sent beside the text. `undefined` keeps it text-only. |
|
|
100
|
+
| `toolMeta?` | `({ tool })` → the tool's `_meta` on `tools/list`. See [MCP Apps views](#mcp-apps-views). |
|
|
101
|
+
| `options?` | See [Options](#options). |
|
|
100
102
|
|
|
101
103
|
The default `toText` answers `NO_CONTENT_TEXT` (`Succeeded. The operation
|
|
102
104
|
returned no content.`) when `callApi` resolves `undefined` or `''`, so a `204`
|
|
@@ -104,6 +106,75 @@ reaches the client as a success.
|
|
|
104
106
|
|
|
105
107
|
Returns the list of `ToolDefinition`s that were registered.
|
|
106
108
|
|
|
109
|
+
### MCP Apps views
|
|
110
|
+
|
|
111
|
+
A generated tool links to a view through `toolMeta`, and the view reads the
|
|
112
|
+
result from `structuredContent`:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
import { registerAppResource } from '@ttoss/http-server-mcp';
|
|
116
|
+
|
|
117
|
+
const agentCard = registerAppResource({
|
|
118
|
+
server,
|
|
119
|
+
name: 'agent_card',
|
|
120
|
+
uri: 'ui://agents/card',
|
|
121
|
+
html: agentCardHtml,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
registerOpenApiTools({
|
|
125
|
+
server,
|
|
126
|
+
spec,
|
|
127
|
+
callApi,
|
|
128
|
+
toolMeta: ({ tool }) => {
|
|
129
|
+
return tool.name === 'get-agent' ? agentCard.toolMeta() : undefined;
|
|
130
|
+
},
|
|
131
|
+
toStructuredContent: ({ data, tool }) => {
|
|
132
|
+
return tool.name === 'get-agent'
|
|
133
|
+
? (data as Record<string, unknown>)
|
|
134
|
+
: undefined;
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Keep the text payload: a host without MCP Apps support renders only that.
|
|
140
|
+
|
|
141
|
+
## Calling the API In-Process
|
|
142
|
+
|
|
143
|
+
When the REST API runs in the same process as the MCP server,
|
|
144
|
+
`createInProcessCallApi` dispatches each tool call through the app's own
|
|
145
|
+
middleware chain with no socket (see `dispatchInProcess` in
|
|
146
|
+
[@ttoss/http-server](https://ttoss.dev/docs/modules/packages/http-server)), so
|
|
147
|
+
validation, authorization and error handling exist once, in the routes. The MCP
|
|
148
|
+
request's headers — what `createMcpRouter`'s `getApiHeaders` produced — are
|
|
149
|
+
forwarded onto the dispatched request.
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import {
|
|
153
|
+
createInProcessCallApi,
|
|
154
|
+
registerOpenApiTools,
|
|
155
|
+
} from '@ttoss/http-server-mcp-openapi';
|
|
156
|
+
|
|
157
|
+
registerOpenApiTools({
|
|
158
|
+
server,
|
|
159
|
+
spec,
|
|
160
|
+
callApi: createInProcessCallApi({
|
|
161
|
+
app, // or () => app, when the app is built after the tools
|
|
162
|
+
headers: () => ({ 'x-via': 'mcp' }), // optional, added to every call
|
|
163
|
+
}),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const router = createMcpRouter(server, {
|
|
167
|
+
getApiHeaders: (ctx) => ({ authorization: ctx.headers.authorization ?? '' }),
|
|
168
|
+
});
|
|
169
|
+
app.use(router.routes());
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
A 2xx answers its body. Anything else throws, so the client sees a tool error
|
|
173
|
+
rather than an error body rendered as a result: the message is read from a
|
|
174
|
+
string body, `{ error: '…' }`, `{ error: { code, message } }` (as
|
|
175
|
+
`code: message`) or `{ message: '…' }` (exported as `errorMessageOf`), falling
|
|
176
|
+
back to `HTTP <status>`. Pass `toError` to build the error yourself.
|
|
177
|
+
|
|
107
178
|
## `openApiToToolDefinitions`
|
|
108
179
|
|
|
109
180
|
Use the lower-level function when you want the tool definitions without
|
|
@@ -137,6 +208,8 @@ registerOpenApiTools({
|
|
|
137
208
|
serverManagedExtension: 'x-mcp-server-managed', // or several: ['x-a', 'x-b']
|
|
138
209
|
argumentNames: 'camelCase', // or 'verbatim'
|
|
139
210
|
documents: { './tags.yaml': tagsDocument }, // targets of cross-file $refs
|
|
211
|
+
schemaDetail: 'full', // or 'compact' (default)
|
|
212
|
+
describe: ({ operation, method, pathTemplate }) => operation.summary ?? '',
|
|
140
213
|
},
|
|
141
214
|
});
|
|
142
215
|
```
|
|
@@ -153,6 +226,31 @@ registerOpenApiTools({
|
|
|
153
226
|
a sibling resolve against that sibling. A ref to a file missing from the map
|
|
154
227
|
resolves to an empty schema, which accepts any value.
|
|
155
228
|
|
|
229
|
+
- **`schemaDetail`** (default `compact`) — see [Schema detail](#schema-detail).
|
|
230
|
+
- **`describe`** — builds each tool's description from `{ operation, method, pathTemplate }`
|
|
231
|
+
(method uppercase). The default is the operation's `description` flattened to one line.
|
|
232
|
+
|
|
233
|
+
### Schema detail
|
|
234
|
+
|
|
235
|
+
`compact` gives each top-level argument its `type`, `items` and `description`,
|
|
236
|
+
with descriptions flattened to one line. It is the smallest surface, and the
|
|
237
|
+
model learns nothing about which values are allowed.
|
|
238
|
+
|
|
239
|
+
`full` gives each argument its whole schema: `enum`, `format`, `pattern`,
|
|
240
|
+
`minimum`/`maximum`, `default`, nested `properties` and `required`, `oneOf`,
|
|
241
|
+
`additionalProperties`, and descriptions verbatim. It changes only what JSON
|
|
242
|
+
Schema cannot express:
|
|
243
|
+
|
|
244
|
+
- `allOf` is merged — the properties and `required` of an object composition,
|
|
245
|
+
or a single referenced scalar with the wrapper's own `description` winning;
|
|
246
|
+
- `nullable: true` adds `'null'` to the `type`, and `null` to an `enum`;
|
|
247
|
+
- OpenAPI-only keywords (`example`, `discriminator`, `xml`, `externalDocs`) and
|
|
248
|
+
`x-` extensions are dropped;
|
|
249
|
+
- a path or query parameter's own `description` wins over its schema's.
|
|
250
|
+
|
|
251
|
+
`properties` is always present in `full`, even when empty. The same
|
|
252
|
+
transformation is exported as `toToolSchema`, for a schema you derive yourself.
|
|
253
|
+
|
|
156
254
|
### Server-managed values
|
|
157
255
|
|
|
158
256
|
A value flagged with `serverManagedExtension` is never offered to the model:
|
package/dist/index.cjs
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, {
|
|
3
3
|
value: 'Module'
|
|
4
4
|
});
|
|
5
|
+
let _ttoss_http_server = require("@ttoss/http-server");
|
|
5
6
|
let _ttoss_http_server_mcp = require("@ttoss/http-server-mcp");
|
|
6
7
|
|
|
7
8
|
//#region src/schema.ts
|
|
8
|
-
var isRecord = value => {
|
|
9
|
+
var isRecord$1 = value => {
|
|
9
10
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
11
|
};
|
|
11
12
|
var getAlternativeSchemas = schema => {
|
|
@@ -42,7 +43,7 @@ var followPointer = args => {
|
|
|
42
43
|
const tokens = args.pointer.split("/").slice(1);
|
|
43
44
|
let current = args.root;
|
|
44
45
|
for (const rawToken of tokens) {
|
|
45
|
-
if (!isRecord(current) && !Array.isArray(current)) return void 0;
|
|
46
|
+
if (!isRecord$1(current) && !Array.isArray(current)) return void 0;
|
|
46
47
|
const token = decodeURIComponent(rawToken).replace(/~1/g, "/").replace(/~0/g, "~");
|
|
47
48
|
current = current[token];
|
|
48
49
|
}
|
|
@@ -101,7 +102,7 @@ var dereferenceValue = args => {
|
|
|
101
102
|
seenRefs
|
|
102
103
|
});
|
|
103
104
|
});
|
|
104
|
-
if (isRecord(value)) {
|
|
105
|
+
if (isRecord$1(value)) {
|
|
105
106
|
if (typeof value.$ref === "string") {
|
|
106
107
|
const target = resolveRef({
|
|
107
108
|
ref: value.$ref,
|
|
@@ -181,7 +182,7 @@ var resolveParameter = (param, spec, documents) => {
|
|
|
181
182
|
documents
|
|
182
183
|
}
|
|
183
184
|
});
|
|
184
|
-
return isRecord(target?.value) ? target.value : {};
|
|
185
|
+
return isRecord$1(target?.value) ? target.value : {};
|
|
185
186
|
}
|
|
186
187
|
return param;
|
|
187
188
|
};
|
|
@@ -220,7 +221,7 @@ var appendDeepObject = args => {
|
|
|
220
221
|
});
|
|
221
222
|
return;
|
|
222
223
|
}
|
|
223
|
-
if (isRecord(args.value)) {
|
|
224
|
+
if (isRecord$1(args.value)) {
|
|
224
225
|
for (const [property, propertyValue] of Object.entries(args.value)) appendDeepObject({
|
|
225
226
|
search: args.search,
|
|
226
227
|
key: `${args.key}[${property}]`,
|
|
@@ -272,7 +273,7 @@ var appendQueryValue = args => {
|
|
|
272
273
|
});
|
|
273
274
|
return;
|
|
274
275
|
}
|
|
275
|
-
if (isRecord(args.value)) {
|
|
276
|
+
if (isRecord$1(args.value)) {
|
|
276
277
|
appendObjectValue({
|
|
277
278
|
...serialization,
|
|
278
279
|
value: args.value
|
|
@@ -501,6 +502,8 @@ var extractPathParams = args => {
|
|
|
501
502
|
return {
|
|
502
503
|
name: p.name || "",
|
|
503
504
|
argName: toArgName(p.name || ""),
|
|
505
|
+
description: p.description,
|
|
506
|
+
schema: dereferenceSchema(p.schema, args.spec, args.documents),
|
|
504
507
|
...managedFields(readServerManaged({
|
|
505
508
|
node: p,
|
|
506
509
|
extension: flag
|
|
@@ -522,6 +525,7 @@ var extractQueryParams = args => {
|
|
|
522
525
|
description: p.description || "",
|
|
523
526
|
required: p.required || false,
|
|
524
527
|
type: p.schema?.type || "string",
|
|
528
|
+
schema: dereferenceSchema(p.schema, args.spec, args.documents),
|
|
525
529
|
style: p.style,
|
|
526
530
|
explode: p.explode,
|
|
527
531
|
...managedFields(readServerManaged({
|
|
@@ -614,7 +618,8 @@ var extractBodyProps = args => {
|
|
|
614
618
|
nullable: val.nullable === true,
|
|
615
619
|
oneOf: Array.isArray(val.oneOf) ? val.oneOf : void 0,
|
|
616
620
|
anyOf: Array.isArray(val.anyOf) ? val.anyOf : void 0,
|
|
617
|
-
allOf: Array.isArray(val.allOf) ? val.allOf : void 0
|
|
621
|
+
allOf: Array.isArray(val.allOf) ? val.allOf : void 0,
|
|
622
|
+
schema: value
|
|
618
623
|
};
|
|
619
624
|
});
|
|
620
625
|
};
|
|
@@ -644,6 +649,209 @@ var extractPinnedBody = args => {
|
|
|
644
649
|
return pinned;
|
|
645
650
|
};
|
|
646
651
|
|
|
652
|
+
//#endregion
|
|
653
|
+
//#region src/fullSchema.ts
|
|
654
|
+
var isRecord = value => {
|
|
655
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
656
|
+
};
|
|
657
|
+
var OPENAPI_ONLY_KEYWORDS = new Set(["discriminator", "example", "externalDocs", "xml"]);
|
|
658
|
+
var SCHEMA_MAPS = new Set(["$defs", "definitions", "dependentSchemas", "patternProperties", "properties"]);
|
|
659
|
+
var SCHEMA_VALUES = new Set(["additionalProperties", "contains", "else", "if", "items", "not", "propertyNames", "then", "unevaluatedItems", "unevaluatedProperties"]);
|
|
660
|
+
var SCHEMA_LISTS = new Set(["anyOf", "oneOf", "prefixItems"]);
|
|
661
|
+
/**
|
|
662
|
+
* OpenAPI 3.0's `nullable: true` in JSON Schema: `'null'` joins the `type`,
|
|
663
|
+
* and `null` joins an `enum`, which would otherwise still refuse it.
|
|
664
|
+
*/
|
|
665
|
+
var withNull = schema => {
|
|
666
|
+
const result = {
|
|
667
|
+
...schema
|
|
668
|
+
};
|
|
669
|
+
if (typeof result.type === "string") result.type = [result.type, "null"];else if (Array.isArray(result.type) && !result.type.includes("null")) result.type = [...result.type, "null"];
|
|
670
|
+
if (Array.isArray(result.enum) && !result.enum.includes(null)) result.enum = [...result.enum, null];
|
|
671
|
+
return result;
|
|
672
|
+
};
|
|
673
|
+
/**
|
|
674
|
+
* Merges an `allOf` into one schema, the shape a tool argument needs.
|
|
675
|
+
*
|
|
676
|
+
* Members that declare `properties` are an object composition — a write body
|
|
677
|
+
* that is a patch plus two fields — and merge their properties and `required`.
|
|
678
|
+
* Otherwise the `allOf` only wraps a referenced scalar so it can carry its own
|
|
679
|
+
* `description` (the one way OpenAPI 3.0 allows beside a `$ref`), and the
|
|
680
|
+
* members are merged as they are. Sibling keys win in both, because the
|
|
681
|
+
* wrapper exists to say something the referenced schema does not.
|
|
682
|
+
*/
|
|
683
|
+
var mergeAllOf = args => {
|
|
684
|
+
const {
|
|
685
|
+
members,
|
|
686
|
+
siblings
|
|
687
|
+
} = args;
|
|
688
|
+
if (!members.some(member => {
|
|
689
|
+
return member.properties !== void 0;
|
|
690
|
+
})) return Object.assign({}, ...members, siblings);
|
|
691
|
+
const properties = {};
|
|
692
|
+
const required = [];
|
|
693
|
+
for (const member of members) {
|
|
694
|
+
Object.assign(properties, member.properties);
|
|
695
|
+
if (Array.isArray(member.required)) required.push(...member.required);
|
|
696
|
+
}
|
|
697
|
+
return {
|
|
698
|
+
...siblings,
|
|
699
|
+
type: "object",
|
|
700
|
+
properties,
|
|
701
|
+
...(required.length > 0 ? {
|
|
702
|
+
required: [...new Set(required)]
|
|
703
|
+
} : {})
|
|
704
|
+
};
|
|
705
|
+
};
|
|
706
|
+
var normalizeChildren = args => {
|
|
707
|
+
const {
|
|
708
|
+
schema,
|
|
709
|
+
next
|
|
710
|
+
} = args;
|
|
711
|
+
const result = {};
|
|
712
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
713
|
+
if (OPENAPI_ONLY_KEYWORDS.has(key) || key.startsWith("x-")) continue;
|
|
714
|
+
if (SCHEMA_MAPS.has(key) && isRecord(value)) result[key] = Object.fromEntries(Object.entries(value).map(([name, child]) => {
|
|
715
|
+
return [name, next(child)];
|
|
716
|
+
}));else if (SCHEMA_VALUES.has(key) && isRecord(value)) result[key] = next(value);else if (SCHEMA_LISTS.has(key) && Array.isArray(value)) result[key] = value.map(next);else result[key] = value;
|
|
717
|
+
}
|
|
718
|
+
return result;
|
|
719
|
+
};
|
|
720
|
+
/**
|
|
721
|
+
* Turns an already-dereferenced OpenAPI schema into the JSON Schema a tool
|
|
722
|
+
* argument carries, keeping every constraint it declares — `enum`, `format`,
|
|
723
|
+
* `pattern`, `minimum`, `default`, nested `properties` and `required`,
|
|
724
|
+
* `oneOf` — and changing only what JSON Schema cannot express: `allOf` is
|
|
725
|
+
* merged, `nullable` becomes a `'null'` type, and OpenAPI-only keywords and
|
|
726
|
+
* `x-` extensions are dropped. Descriptions are kept verbatim.
|
|
727
|
+
*/
|
|
728
|
+
var toToolSchema = value => {
|
|
729
|
+
if (!isRecord(value)) return value;
|
|
730
|
+
const {
|
|
731
|
+
allOf,
|
|
732
|
+
nullable,
|
|
733
|
+
...rest
|
|
734
|
+
} = value;
|
|
735
|
+
const own = normalizeChildren({
|
|
736
|
+
schema: rest,
|
|
737
|
+
next: toToolSchema
|
|
738
|
+
});
|
|
739
|
+
const merged = Array.isArray(allOf) ? mergeAllOf({
|
|
740
|
+
members: allOf.filter(isRecord).map(member => {
|
|
741
|
+
return toToolSchema(member);
|
|
742
|
+
}),
|
|
743
|
+
siblings: own
|
|
744
|
+
}) : own;
|
|
745
|
+
return nullable === true ? withNull(merged) : merged;
|
|
746
|
+
};
|
|
747
|
+
/** A path or query parameter's schema, with the parameter's own description. */
|
|
748
|
+
var paramProperty = param => {
|
|
749
|
+
return {
|
|
750
|
+
...(toToolSchema(param.schema) ?? {}),
|
|
751
|
+
...(param.description ? {
|
|
752
|
+
description: param.description
|
|
753
|
+
} : {})
|
|
754
|
+
};
|
|
755
|
+
};
|
|
756
|
+
/**
|
|
757
|
+
* The `inputSchema` of a tool under `schemaDetail: 'full'`: every parameter
|
|
758
|
+
* and body property with its whole schema. `properties` is always present,
|
|
759
|
+
* even when empty, because some clients refuse an object schema without it.
|
|
760
|
+
*/
|
|
761
|
+
var buildFullInputSchema = args => {
|
|
762
|
+
const params = [...args.pathParams, ...args.queryParams].filter(param => {
|
|
763
|
+
return !param.serverManaged;
|
|
764
|
+
});
|
|
765
|
+
const properties = {};
|
|
766
|
+
const required = [];
|
|
767
|
+
for (const param of params) {
|
|
768
|
+
properties[param.argName] = paramProperty(param);
|
|
769
|
+
if (param.required) required.push(param.argName);
|
|
770
|
+
}
|
|
771
|
+
for (const prop of args.bodyProps) {
|
|
772
|
+
properties[prop.argName] = toToolSchema(prop.schema);
|
|
773
|
+
if (prop.required) required.push(prop.argName);
|
|
774
|
+
}
|
|
775
|
+
return {
|
|
776
|
+
type: "object",
|
|
777
|
+
properties,
|
|
778
|
+
...(required.length > 0 ? {
|
|
779
|
+
required: [...new Set(required)]
|
|
780
|
+
} : {})
|
|
781
|
+
};
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
//#endregion
|
|
785
|
+
//#region src/inProcessCallApi.ts
|
|
786
|
+
var nestedMessageOf = error => {
|
|
787
|
+
if (!error || typeof error !== "object") return null;
|
|
788
|
+
const {
|
|
789
|
+
code,
|
|
790
|
+
message
|
|
791
|
+
} = error;
|
|
792
|
+
if (typeof message !== "string") return null;
|
|
793
|
+
return typeof code === "string" ? `${code}: ${message}` : message;
|
|
794
|
+
};
|
|
795
|
+
/**
|
|
796
|
+
* The message an error response carries, read from the envelopes REST APIs
|
|
797
|
+
* commonly answer with: a plain string, `{ error: '…' }`,
|
|
798
|
+
* `{ error: { code, message } }` (as `code: message`) or `{ message: '…' }`.
|
|
799
|
+
* `null` when the body is none of them.
|
|
800
|
+
*/
|
|
801
|
+
var errorMessageOf = body => {
|
|
802
|
+
if (typeof body === "string") return body || null;
|
|
803
|
+
if (!body || typeof body !== "object") return null;
|
|
804
|
+
const {
|
|
805
|
+
error,
|
|
806
|
+
message
|
|
807
|
+
} = body;
|
|
808
|
+
if (typeof error === "string") return error;
|
|
809
|
+
return nestedMessageOf(error) ?? (typeof message === "string" ? message : null);
|
|
810
|
+
};
|
|
811
|
+
var defaultToError = response => {
|
|
812
|
+
return new Error(errorMessageOf(response.body) ?? `HTTP ${response.status}`);
|
|
813
|
+
};
|
|
814
|
+
/**
|
|
815
|
+
* A `callApi` for {@link registerOpenApiTools} that serves each tool call
|
|
816
|
+
* against the REST app in this same process, with no socket: validation,
|
|
817
|
+
* authorization and error handling run once, in the routes, for both
|
|
818
|
+
* surfaces.
|
|
819
|
+
*
|
|
820
|
+
* The MCP request's headers (what `createMcpRouter`'s `getApiHeaders`
|
|
821
|
+
* produced — typically the caller's `Authorization`) are forwarded onto the
|
|
822
|
+
* dispatched request. A 2xx answers its body; anything else throws, so the
|
|
823
|
+
* client sees a tool error rather than an error body rendered as a result.
|
|
824
|
+
*
|
|
825
|
+
* @example
|
|
826
|
+
* ```typescript
|
|
827
|
+
* registerOpenApiTools({
|
|
828
|
+
* server,
|
|
829
|
+
* spec,
|
|
830
|
+
* callApi: createInProcessCallApi({ app }),
|
|
831
|
+
* });
|
|
832
|
+
* ```
|
|
833
|
+
*/
|
|
834
|
+
var createInProcessCallApi = ({
|
|
835
|
+
app,
|
|
836
|
+
headers,
|
|
837
|
+
toError = defaultToError
|
|
838
|
+
}) => {
|
|
839
|
+
return async request => {
|
|
840
|
+
const response = await (0, _ttoss_http_server.dispatchInProcess)({
|
|
841
|
+
app: typeof app === "function" ? await app() : app,
|
|
842
|
+
method: request.method,
|
|
843
|
+
path: request.url,
|
|
844
|
+
headers: {
|
|
845
|
+
...request.headers,
|
|
846
|
+
...headers?.(request)
|
|
847
|
+
},
|
|
848
|
+
body: request.body
|
|
849
|
+
});
|
|
850
|
+
if (response.status < 200 || response.status >= 300) throw toError(response, request);
|
|
851
|
+
return response.body;
|
|
852
|
+
};
|
|
853
|
+
};
|
|
854
|
+
|
|
647
855
|
//#endregion
|
|
648
856
|
//#region src/toolDefinitions.ts
|
|
649
857
|
/** Converts a camelCase `operationId` to a kebab-case tool name. */
|
|
@@ -660,6 +868,11 @@ var getJsonSchemaType = schemaType => {
|
|
|
660
868
|
var sanitizeDescription = description => {
|
|
661
869
|
return (description || "").replace(/'/g, "\\'").replace(/\n/g, " ").trim();
|
|
662
870
|
};
|
|
871
|
+
var defaultDescribe = ({
|
|
872
|
+
operation
|
|
873
|
+
}) => {
|
|
874
|
+
return sanitizeDescription(operation.description);
|
|
875
|
+
};
|
|
663
876
|
/**
|
|
664
877
|
* Forwards a property's `oneOf` / `anyOf` / multi-entry `allOf` verbatim, or
|
|
665
878
|
* returns `undefined` when it declares none.
|
|
@@ -716,8 +929,7 @@ var buildInputSchema = (pathParams, queryParams, bodyProps) => {
|
|
|
716
929
|
const modelQueryParams = queryParams.filter(p => {
|
|
717
930
|
return !p.serverManaged;
|
|
718
931
|
});
|
|
719
|
-
|
|
720
|
-
if (allParams.length === 0) return {
|
|
932
|
+
if ([...modelPathParams, ...modelQueryParams, ...bodyProps].length === 0) return {
|
|
721
933
|
type: "object"
|
|
722
934
|
};
|
|
723
935
|
const requiredFields = [...modelPathParams.map(p => {
|
|
@@ -732,10 +944,11 @@ var buildInputSchema = (pathParams, queryParams, bodyProps) => {
|
|
|
732
944
|
return p.argName;
|
|
733
945
|
})];
|
|
734
946
|
const properties = {};
|
|
735
|
-
for (const param of
|
|
947
|
+
for (const param of modelPathParams) properties[param.argName] = {
|
|
736
948
|
type: "string",
|
|
737
949
|
description: ""
|
|
738
950
|
};
|
|
951
|
+
for (const param of [...modelQueryParams, ...bodyProps]) properties[param.argName] = normalizeNullable(buildTypedProperty(param));
|
|
739
952
|
return {
|
|
740
953
|
type: "object",
|
|
741
954
|
properties,
|
|
@@ -744,6 +957,35 @@ var buildInputSchema = (pathParams, queryParams, bodyProps) => {
|
|
|
744
957
|
} : {})
|
|
745
958
|
};
|
|
746
959
|
};
|
|
960
|
+
var SUPPORTED_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
961
|
+
/** The tool description, from `options.describe` or the default. */
|
|
962
|
+
var describeOperation = args => {
|
|
963
|
+
const {
|
|
964
|
+
options,
|
|
965
|
+
operation,
|
|
966
|
+
method,
|
|
967
|
+
pathTemplate
|
|
968
|
+
} = args;
|
|
969
|
+
return (options.describe ?? defaultDescribe)({
|
|
970
|
+
operation,
|
|
971
|
+
method,
|
|
972
|
+
pathTemplate
|
|
973
|
+
});
|
|
974
|
+
};
|
|
975
|
+
/** The `inputSchema` at the detail {@link OpenApiToToolsOptions.schemaDetail} asks for. */
|
|
976
|
+
var selectInputSchema = args => {
|
|
977
|
+
if (args.schemaDetail !== "full") return buildInputSchema(args.pathParams, args.queryParams, args.bodyProps);
|
|
978
|
+
return buildFullInputSchema({
|
|
979
|
+
pathParams: args.pathParams.map(param => {
|
|
980
|
+
return {
|
|
981
|
+
...param,
|
|
982
|
+
required: true
|
|
983
|
+
};
|
|
984
|
+
}),
|
|
985
|
+
queryParams: args.queryParams,
|
|
986
|
+
bodyProps: args.bodyProps
|
|
987
|
+
});
|
|
988
|
+
};
|
|
747
989
|
/** Collects every `x-` prefixed extension declared on the operation. */
|
|
748
990
|
var extractExtensions = operation => {
|
|
749
991
|
const extensions = {};
|
|
@@ -752,9 +994,7 @@ var extractExtensions = operation => {
|
|
|
752
994
|
};
|
|
753
995
|
var processOperation = args => {
|
|
754
996
|
const httpMethod = args.method.toUpperCase();
|
|
755
|
-
if (!
|
|
756
|
-
if (!args.operation.operationId) return null;
|
|
757
|
-
if (args.operation[args.options.excludeExtension]) return null;
|
|
997
|
+
if (!SUPPORTED_METHODS.includes(httpMethod) || !args.operation.operationId || args.operation[args.options.excludeExtension]) return null;
|
|
758
998
|
const toolName = operationIdToToolName(args.operation.operationId);
|
|
759
999
|
const parameters = [...(args.pathItemParameters ?? []), ...(args.operation.parameters ?? [])];
|
|
760
1000
|
const {
|
|
@@ -762,20 +1002,15 @@ var processOperation = args => {
|
|
|
762
1002
|
serverManagedExtension
|
|
763
1003
|
} = args.options;
|
|
764
1004
|
const toArgName = argNameMapper(args.options.argumentNames);
|
|
765
|
-
const
|
|
1005
|
+
const paramArgs = {
|
|
766
1006
|
parameters,
|
|
767
1007
|
spec: args.spec,
|
|
768
1008
|
documents,
|
|
769
1009
|
toArgName,
|
|
770
1010
|
serverManagedExtension
|
|
771
|
-
}
|
|
772
|
-
const
|
|
773
|
-
|
|
774
|
-
spec: args.spec,
|
|
775
|
-
documents,
|
|
776
|
-
toArgName,
|
|
777
|
-
serverManagedExtension
|
|
778
|
-
});
|
|
1011
|
+
};
|
|
1012
|
+
const pathParams = extractPathParams(paramArgs);
|
|
1013
|
+
const queryParams = extractQueryParams(paramArgs);
|
|
779
1014
|
const bodyArgs = {
|
|
780
1015
|
requestBody: args.operation.requestBody,
|
|
781
1016
|
spec: args.spec,
|
|
@@ -786,7 +1021,12 @@ var processOperation = args => {
|
|
|
786
1021
|
...bodyArgs,
|
|
787
1022
|
toArgName
|
|
788
1023
|
});
|
|
789
|
-
const inputSchema =
|
|
1024
|
+
const inputSchema = selectInputSchema({
|
|
1025
|
+
schemaDetail: args.options.schemaDetail,
|
|
1026
|
+
pathParams,
|
|
1027
|
+
queryParams,
|
|
1028
|
+
bodyProps
|
|
1029
|
+
});
|
|
790
1030
|
const serverManagedParameters = collectServerManagedParameters({
|
|
791
1031
|
pathParams,
|
|
792
1032
|
queryParams
|
|
@@ -794,7 +1034,10 @@ var processOperation = args => {
|
|
|
794
1034
|
const pinned = pinnedArgs(serverManagedParameters);
|
|
795
1035
|
return {
|
|
796
1036
|
name: toolName,
|
|
797
|
-
description:
|
|
1037
|
+
description: describeOperation({
|
|
1038
|
+
...args,
|
|
1039
|
+
method: httpMethod
|
|
1040
|
+
}),
|
|
798
1041
|
inputSchema,
|
|
799
1042
|
method: httpMethod,
|
|
800
1043
|
pathTemplate: args.pathTemplate,
|
|
@@ -843,6 +1086,8 @@ var resolveOptions = (options = {}) => {
|
|
|
843
1086
|
excludeExtension: options.excludeExtension ?? "x-mcp-exclude",
|
|
844
1087
|
serverManagedExtension: options.serverManagedExtension ?? "x-mcp-server-managed",
|
|
845
1088
|
argumentNames: options.argumentNames ?? "camelCase",
|
|
1089
|
+
schemaDetail: options.schemaDetail,
|
|
1090
|
+
describe: options.describe,
|
|
846
1091
|
documents: options.documents
|
|
847
1092
|
};
|
|
848
1093
|
};
|
|
@@ -938,6 +1183,9 @@ var registerOpenApiTools = args => {
|
|
|
938
1183
|
name: tool.name,
|
|
939
1184
|
description: tool.description,
|
|
940
1185
|
inputSchema: tool.inputSchema,
|
|
1186
|
+
_meta: args.toolMeta?.({
|
|
1187
|
+
tool
|
|
1188
|
+
}),
|
|
941
1189
|
handler: async rawArgs => {
|
|
942
1190
|
const headers = (0, _ttoss_http_server_mcp.getApiHeaders)();
|
|
943
1191
|
const handlerArgs = await applyServerParameters({
|
|
@@ -947,17 +1195,25 @@ var registerOpenApiTools = args => {
|
|
|
947
1195
|
serverParameters: args.serverParameters
|
|
948
1196
|
});
|
|
949
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
|
+
});
|
|
950
1209
|
return {
|
|
951
1210
|
content: [{
|
|
952
1211
|
type: "text",
|
|
953
|
-
text: toText(
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
headers
|
|
959
|
-
}))
|
|
960
|
-
}]
|
|
1212
|
+
text: toText(data)
|
|
1213
|
+
}],
|
|
1214
|
+
...(structuredContent === void 0 ? {} : {
|
|
1215
|
+
structuredContent
|
|
1216
|
+
})
|
|
961
1217
|
};
|
|
962
1218
|
}
|
|
963
1219
|
});
|
|
@@ -969,10 +1225,13 @@ exports.DEFAULT_EXCLUDE_EXTENSION = DEFAULT_EXCLUDE_EXTENSION;
|
|
|
969
1225
|
exports.DEFAULT_SERVER_MANAGED_EXTENSION = DEFAULT_SERVER_MANAGED_EXTENSION;
|
|
970
1226
|
exports.NO_CONTENT_TEXT = NO_CONTENT_TEXT;
|
|
971
1227
|
exports.buildBodyFn = buildBodyFn;
|
|
1228
|
+
exports.buildFullInputSchema = buildFullInputSchema;
|
|
972
1229
|
exports.buildInputSchema = buildInputSchema;
|
|
973
1230
|
exports.buildPathFn = buildPathFn;
|
|
974
1231
|
exports.buildQueryFn = buildQueryFn;
|
|
1232
|
+
exports.createInProcessCallApi = createInProcessCallApi;
|
|
975
1233
|
exports.dereferenceSchema = dereferenceSchema;
|
|
1234
|
+
exports.errorMessageOf = errorMessageOf;
|
|
976
1235
|
exports.extractAcceptedBodyFields = extractAcceptedBodyFields;
|
|
977
1236
|
exports.extractBodyProps = extractBodyProps;
|
|
978
1237
|
exports.extractPathParams = extractPathParams;
|
|
@@ -986,4 +1245,5 @@ exports.processPath = processPath;
|
|
|
986
1245
|
exports.registerOpenApiTools = registerOpenApiTools;
|
|
987
1246
|
exports.resolveParameter = resolveParameter;
|
|
988
1247
|
exports.resolveSchema = resolveSchema;
|
|
989
|
-
exports.snakeToCamel = snakeToCamel;
|
|
1248
|
+
exports.snakeToCamel = snakeToCamel;
|
|
1249
|
+
exports.toToolSchema = toToolSchema;
|