@ttoss/http-server-mcp-openapi 0.5.6 → 0.6.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 +64 -0
- package/dist/index.cjs +274 -25
- package/dist/index.d.cts +125 -4
- package/dist/index.d.mts +125 -4
- package/dist/index.mjs +270 -25
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -104,6 +104,43 @@ reaches the client as a success.
|
|
|
104
104
|
|
|
105
105
|
Returns the list of `ToolDefinition`s that were registered.
|
|
106
106
|
|
|
107
|
+
## Calling the API In-Process
|
|
108
|
+
|
|
109
|
+
When the REST API runs in the same process as the MCP server,
|
|
110
|
+
`createInProcessCallApi` dispatches each tool call through the app's own
|
|
111
|
+
middleware chain with no socket (see `dispatchInProcess` in
|
|
112
|
+
[@ttoss/http-server](https://ttoss.dev/docs/modules/packages/http-server)), so
|
|
113
|
+
validation, authorization and error handling exist once, in the routes. The MCP
|
|
114
|
+
request's headers — what `createMcpRouter`'s `getApiHeaders` produced — are
|
|
115
|
+
forwarded onto the dispatched request.
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
import {
|
|
119
|
+
createInProcessCallApi,
|
|
120
|
+
registerOpenApiTools,
|
|
121
|
+
} from '@ttoss/http-server-mcp-openapi';
|
|
122
|
+
|
|
123
|
+
registerOpenApiTools({
|
|
124
|
+
server,
|
|
125
|
+
spec,
|
|
126
|
+
callApi: createInProcessCallApi({
|
|
127
|
+
app, // or () => app, when the app is built after the tools
|
|
128
|
+
headers: () => ({ 'x-via': 'mcp' }), // optional, added to every call
|
|
129
|
+
}),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const router = createMcpRouter(server, {
|
|
133
|
+
getApiHeaders: (ctx) => ({ authorization: ctx.headers.authorization ?? '' }),
|
|
134
|
+
});
|
|
135
|
+
app.use(router.routes());
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
A 2xx answers its body. Anything else throws, so the client sees a tool error
|
|
139
|
+
rather than an error body rendered as a result: the message is read from a
|
|
140
|
+
string body, `{ error: '…' }`, `{ error: { code, message } }` (as
|
|
141
|
+
`code: message`) or `{ message: '…' }` (exported as `errorMessageOf`), falling
|
|
142
|
+
back to `HTTP <status>`. Pass `toError` to build the error yourself.
|
|
143
|
+
|
|
107
144
|
## `openApiToToolDefinitions`
|
|
108
145
|
|
|
109
146
|
Use the lower-level function when you want the tool definitions without
|
|
@@ -137,6 +174,8 @@ registerOpenApiTools({
|
|
|
137
174
|
serverManagedExtension: 'x-mcp-server-managed', // or several: ['x-a', 'x-b']
|
|
138
175
|
argumentNames: 'camelCase', // or 'verbatim'
|
|
139
176
|
documents: { './tags.yaml': tagsDocument }, // targets of cross-file $refs
|
|
177
|
+
schemaDetail: 'full', // or 'compact' (default)
|
|
178
|
+
describe: ({ operation, method, pathTemplate }) => operation.summary ?? '',
|
|
140
179
|
},
|
|
141
180
|
});
|
|
142
181
|
```
|
|
@@ -153,6 +192,31 @@ registerOpenApiTools({
|
|
|
153
192
|
a sibling resolve against that sibling. A ref to a file missing from the map
|
|
154
193
|
resolves to an empty schema, which accepts any value.
|
|
155
194
|
|
|
195
|
+
- **`schemaDetail`** (default `compact`) — see [Schema detail](#schema-detail).
|
|
196
|
+
- **`describe`** — builds each tool's description from `{ operation, method, pathTemplate }`
|
|
197
|
+
(method uppercase). The default is the operation's `description` flattened to one line.
|
|
198
|
+
|
|
199
|
+
### Schema detail
|
|
200
|
+
|
|
201
|
+
`compact` gives each top-level argument its `type`, `items` and `description`,
|
|
202
|
+
with descriptions flattened to one line. It is the smallest surface, and the
|
|
203
|
+
model learns nothing about which values are allowed.
|
|
204
|
+
|
|
205
|
+
`full` gives each argument its whole schema: `enum`, `format`, `pattern`,
|
|
206
|
+
`minimum`/`maximum`, `default`, nested `properties` and `required`, `oneOf`,
|
|
207
|
+
`additionalProperties`, and descriptions verbatim. It changes only what JSON
|
|
208
|
+
Schema cannot express:
|
|
209
|
+
|
|
210
|
+
- `allOf` is merged — the properties and `required` of an object composition,
|
|
211
|
+
or a single referenced scalar with the wrapper's own `description` winning;
|
|
212
|
+
- `nullable: true` adds `'null'` to the `type`, and `null` to an `enum`;
|
|
213
|
+
- OpenAPI-only keywords (`example`, `discriminator`, `xml`, `externalDocs`) and
|
|
214
|
+
`x-` extensions are dropped;
|
|
215
|
+
- a path or query parameter's own `description` wins over its schema's.
|
|
216
|
+
|
|
217
|
+
`properties` is always present in `full`, even when empty. The same
|
|
218
|
+
transformation is exported as `toToolSchema`, for a schema you derive yourself.
|
|
219
|
+
|
|
156
220
|
### Server-managed values
|
|
157
221
|
|
|
158
222
|
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
|
|
766
|
-
parameters,
|
|
767
|
-
spec: args.spec,
|
|
768
|
-
documents,
|
|
769
|
-
toArgName,
|
|
770
|
-
serverManagedExtension
|
|
771
|
-
});
|
|
772
|
-
const queryParams = extractQueryParams({
|
|
1005
|
+
const paramArgs = {
|
|
773
1006
|
parameters,
|
|
774
1007
|
spec: args.spec,
|
|
775
1008
|
documents,
|
|
776
1009
|
toArgName,
|
|
777
1010
|
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
|
};
|
|
@@ -969,10 +1214,13 @@ exports.DEFAULT_EXCLUDE_EXTENSION = DEFAULT_EXCLUDE_EXTENSION;
|
|
|
969
1214
|
exports.DEFAULT_SERVER_MANAGED_EXTENSION = DEFAULT_SERVER_MANAGED_EXTENSION;
|
|
970
1215
|
exports.NO_CONTENT_TEXT = NO_CONTENT_TEXT;
|
|
971
1216
|
exports.buildBodyFn = buildBodyFn;
|
|
1217
|
+
exports.buildFullInputSchema = buildFullInputSchema;
|
|
972
1218
|
exports.buildInputSchema = buildInputSchema;
|
|
973
1219
|
exports.buildPathFn = buildPathFn;
|
|
974
1220
|
exports.buildQueryFn = buildQueryFn;
|
|
1221
|
+
exports.createInProcessCallApi = createInProcessCallApi;
|
|
975
1222
|
exports.dereferenceSchema = dereferenceSchema;
|
|
1223
|
+
exports.errorMessageOf = errorMessageOf;
|
|
976
1224
|
exports.extractAcceptedBodyFields = extractAcceptedBodyFields;
|
|
977
1225
|
exports.extractBodyProps = extractBodyProps;
|
|
978
1226
|
exports.extractPathParams = extractPathParams;
|
|
@@ -986,4 +1234,5 @@ exports.processPath = processPath;
|
|
|
986
1234
|
exports.registerOpenApiTools = registerOpenApiTools;
|
|
987
1235
|
exports.resolveParameter = resolveParameter;
|
|
988
1236
|
exports.resolveSchema = resolveSchema;
|
|
989
|
-
exports.snakeToCamel = snakeToCamel;
|
|
1237
|
+
exports.snakeToCamel = snakeToCamel;
|
|
1238
|
+
exports.toToolSchema = toToolSchema;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
import { JsonObjectSchema, McpServer } from "@ttoss/http-server-mcp";
|
|
3
|
+
import { App, InProcessResponse } from "@ttoss/http-server";
|
|
3
4
|
|
|
4
5
|
//#region src/types.d.ts
|
|
5
6
|
/** The JSON Schema primitive types this generator can derive from an OpenAPI `type`. */
|
|
@@ -121,6 +122,7 @@ type RequestBodySpec = {
|
|
|
121
122
|
};
|
|
122
123
|
interface OperationSpec {
|
|
123
124
|
operationId?: string;
|
|
125
|
+
summary?: string;
|
|
124
126
|
description?: string;
|
|
125
127
|
parameters?: Array<{
|
|
126
128
|
name?: string;
|
|
@@ -183,9 +185,39 @@ interface OpenApiToToolsOptions {
|
|
|
183
185
|
* to an empty schema, which accepts any value.
|
|
184
186
|
*/
|
|
185
187
|
documents?: OpenApiDocuments;
|
|
188
|
+
/**
|
|
189
|
+
* How much of each parameter's and body property's schema reaches the
|
|
190
|
+
* tool's `inputSchema`.
|
|
191
|
+
*
|
|
192
|
+
* - `'compact'` keeps the `type`, `items` and `description` of each
|
|
193
|
+
* top-level argument, with descriptions flattened to one line.
|
|
194
|
+
* - `'full'` keeps the whole schema — `enum`, `format`, `pattern`,
|
|
195
|
+
* `minimum`, `default`, nested `properties` and `required`, `oneOf` — and
|
|
196
|
+
* changes only what JSON Schema cannot say: `allOf` is merged, `nullable`
|
|
197
|
+
* becomes a `'null'` type (and joins an `enum`), and OpenAPI-only keywords
|
|
198
|
+
* and `x-` extensions are dropped. Descriptions stay verbatim.
|
|
199
|
+
*
|
|
200
|
+
* @default 'compact'
|
|
201
|
+
*/
|
|
202
|
+
schemaDetail?: 'compact' | 'full';
|
|
203
|
+
/**
|
|
204
|
+
* Builds each tool's description from its operation. The default is the
|
|
205
|
+
* operation's `description`, flattened to one line.
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```typescript
|
|
209
|
+
* describe: ({ operation, method, pathTemplate }) =>
|
|
210
|
+
* `${operation.summary}\n\n${operation.description}\n\n(${method} ${pathTemplate})`,
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
describe?: (args: {
|
|
214
|
+
operation: OperationSpec; /** Uppercase HTTP method. */
|
|
215
|
+
method: string;
|
|
216
|
+
pathTemplate: string;
|
|
217
|
+
}) => string;
|
|
186
218
|
}
|
|
187
219
|
/** {@link OpenApiToToolsOptions} with every default applied. */
|
|
188
|
-
type ResolvedToolOptions = Required<Omit<OpenApiToToolsOptions, 'documents'>> & Pick<OpenApiToToolsOptions, 'documents'>;
|
|
220
|
+
type ResolvedToolOptions = Required<Omit<OpenApiToToolsOptions, 'documents' | 'describe' | 'schemaDetail'>> & Pick<OpenApiToToolsOptions, 'documents' | 'describe' | 'schemaDetail'>;
|
|
189
221
|
declare const DEFAULT_EXCLUDE_EXTENSION = "x-mcp-exclude";
|
|
190
222
|
declare const DEFAULT_SERVER_MANAGED_EXTENSION = "x-mcp-server-managed";
|
|
191
223
|
//#endregion
|
|
@@ -217,6 +249,8 @@ type ExtractParamsArgs = {
|
|
|
217
249
|
declare const extractPathParams: (args: ExtractParamsArgs) => Array<{
|
|
218
250
|
name: string;
|
|
219
251
|
argName: string;
|
|
252
|
+
description?: string; /** The parameter's schema with every `$ref` inlined. */
|
|
253
|
+
schema?: Record<string, unknown>;
|
|
220
254
|
serverManaged: boolean;
|
|
221
255
|
pinnedValue?: string;
|
|
222
256
|
}>;
|
|
@@ -225,7 +259,8 @@ declare const extractQueryParams: (args: ExtractParamsArgs) => Array<{
|
|
|
225
259
|
argName: string;
|
|
226
260
|
description: string;
|
|
227
261
|
required: boolean;
|
|
228
|
-
type: string;
|
|
262
|
+
type: string; /** The parameter's schema with every `$ref` inlined. */
|
|
263
|
+
schema?: Record<string, unknown>;
|
|
229
264
|
style?: string;
|
|
230
265
|
explode?: boolean;
|
|
231
266
|
serverManaged: boolean;
|
|
@@ -258,7 +293,8 @@ declare const extractBodyProps: (args: {
|
|
|
258
293
|
nullable: boolean;
|
|
259
294
|
oneOf?: unknown[];
|
|
260
295
|
anyOf?: unknown[];
|
|
261
|
-
allOf?: unknown[];
|
|
296
|
+
allOf?: unknown[]; /** The property's whole schema, with every `$ref` inlined. */
|
|
297
|
+
schema: Record<string, unknown>;
|
|
262
298
|
}>;
|
|
263
299
|
/**
|
|
264
300
|
* The value each server-managed body property pins, typed by its schema and
|
|
@@ -273,6 +309,39 @@ declare const extractPinnedBody: (args: {
|
|
|
273
309
|
operationId: string;
|
|
274
310
|
}) => Record<string, string | number | boolean>;
|
|
275
311
|
//#endregion
|
|
312
|
+
//#region src/fullSchema.d.ts
|
|
313
|
+
/**
|
|
314
|
+
* Turns an already-dereferenced OpenAPI schema into the JSON Schema a tool
|
|
315
|
+
* argument carries, keeping every constraint it declares — `enum`, `format`,
|
|
316
|
+
* `pattern`, `minimum`, `default`, nested `properties` and `required`,
|
|
317
|
+
* `oneOf` — and changing only what JSON Schema cannot express: `allOf` is
|
|
318
|
+
* merged, `nullable` becomes a `'null'` type, and OpenAPI-only keywords and
|
|
319
|
+
* `x-` extensions are dropped. Descriptions are kept verbatim.
|
|
320
|
+
*/
|
|
321
|
+
declare const toToolSchema: (value: unknown) => unknown;
|
|
322
|
+
type FullParam = {
|
|
323
|
+
argName: string;
|
|
324
|
+
required?: boolean;
|
|
325
|
+
description?: string;
|
|
326
|
+
schema?: unknown;
|
|
327
|
+
serverManaged?: boolean;
|
|
328
|
+
};
|
|
329
|
+
type FullBodyProp = {
|
|
330
|
+
argName: string;
|
|
331
|
+
required: boolean;
|
|
332
|
+
schema: Record<string, unknown>;
|
|
333
|
+
};
|
|
334
|
+
/**
|
|
335
|
+
* The `inputSchema` of a tool under `schemaDetail: 'full'`: every parameter
|
|
336
|
+
* and body property with its whole schema. `properties` is always present,
|
|
337
|
+
* even when empty, because some clients refuse an object schema without it.
|
|
338
|
+
*/
|
|
339
|
+
declare const buildFullInputSchema: (args: {
|
|
340
|
+
pathParams: FullParam[];
|
|
341
|
+
queryParams: FullParam[];
|
|
342
|
+
bodyProps: FullBodyProp[];
|
|
343
|
+
}) => JsonObjectSchema;
|
|
344
|
+
//#endregion
|
|
276
345
|
//#region src/registerOpenApiTools.d.ts
|
|
277
346
|
/** The resolved HTTP request a tool call maps to, before transport concerns. */
|
|
278
347
|
interface ResolvedRequest {
|
|
@@ -360,6 +429,58 @@ declare const NO_CONTENT_TEXT = "Succeeded. The operation returned no content.";
|
|
|
360
429
|
*/
|
|
361
430
|
declare const registerOpenApiTools: (args: RegisterOpenApiToolsArgs) => ToolDefinition[];
|
|
362
431
|
//#endregion
|
|
432
|
+
//#region src/inProcessCallApi.d.ts
|
|
433
|
+
interface CreateInProcessCallApiArgs {
|
|
434
|
+
/**
|
|
435
|
+
* The Koa app serving the REST API the tools were generated from, or a
|
|
436
|
+
* function returning it — for an app that mounts the MCP router itself and
|
|
437
|
+
* so is not built yet when the tools are registered.
|
|
438
|
+
*/
|
|
439
|
+
app: App | (() => App | Promise<App>);
|
|
440
|
+
/**
|
|
441
|
+
* Headers added to every dispatched request, after the ones the MCP request
|
|
442
|
+
* carried — e.g. a marker that tells a request log the call came from a tool.
|
|
443
|
+
*/
|
|
444
|
+
headers?: (request: ResolvedRequest) => Record<string, string | undefined>;
|
|
445
|
+
/**
|
|
446
|
+
* Builds the error a non-2xx response throws. The default reads the message
|
|
447
|
+
* out of the common error envelopes (see {@link errorMessageOf}).
|
|
448
|
+
*/
|
|
449
|
+
toError?: (response: InProcessResponse, request: ResolvedRequest) => Error;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* The message an error response carries, read from the envelopes REST APIs
|
|
453
|
+
* commonly answer with: a plain string, `{ error: '…' }`,
|
|
454
|
+
* `{ error: { code, message } }` (as `code: message`) or `{ message: '…' }`.
|
|
455
|
+
* `null` when the body is none of them.
|
|
456
|
+
*/
|
|
457
|
+
declare const errorMessageOf: (body: unknown) => string | null;
|
|
458
|
+
/**
|
|
459
|
+
* A `callApi` for {@link registerOpenApiTools} that serves each tool call
|
|
460
|
+
* against the REST app in this same process, with no socket: validation,
|
|
461
|
+
* authorization and error handling run once, in the routes, for both
|
|
462
|
+
* surfaces.
|
|
463
|
+
*
|
|
464
|
+
* The MCP request's headers (what `createMcpRouter`'s `getApiHeaders`
|
|
465
|
+
* produced — typically the caller's `Authorization`) are forwarded onto the
|
|
466
|
+
* dispatched request. A 2xx answers its body; anything else throws, so the
|
|
467
|
+
* client sees a tool error rather than an error body rendered as a result.
|
|
468
|
+
*
|
|
469
|
+
* @example
|
|
470
|
+
* ```typescript
|
|
471
|
+
* registerOpenApiTools({
|
|
472
|
+
* server,
|
|
473
|
+
* spec,
|
|
474
|
+
* callApi: createInProcessCallApi({ app }),
|
|
475
|
+
* });
|
|
476
|
+
* ```
|
|
477
|
+
*/
|
|
478
|
+
declare const createInProcessCallApi: ({
|
|
479
|
+
app,
|
|
480
|
+
headers,
|
|
481
|
+
toError
|
|
482
|
+
}: CreateInProcessCallApiArgs) => ((request: ResolvedRequest) => Promise<unknown>);
|
|
483
|
+
//#endregion
|
|
363
484
|
//#region src/schema.d.ts
|
|
364
485
|
type ResolvedSchema = {
|
|
365
486
|
type?: string;
|
|
@@ -498,4 +619,4 @@ declare const openApiToToolDefinitions: (args: {
|
|
|
498
619
|
options?: OpenApiToToolsOptions;
|
|
499
620
|
}) => ToolDefinition[];
|
|
500
621
|
//#endregion
|
|
501
|
-
export { DEFAULT_EXCLUDE_EXTENSION, DEFAULT_SERVER_MANAGED_EXTENSION, type JsonSchemaProperty, NO_CONTENT_TEXT, type OpenApiDocuments, type OpenApiSpec, type OpenApiToToolsOptions, type OperationSpec, type QueryParamSerialization, type RegisterOpenApiToolsArgs, type RequestBodySpec, type ResolvedParameter, type ResolvedRequest, type ResolvedToolOptions, type ServerManagedParameter, type ToolDefinition, buildBodyFn, buildInputSchema, buildPathFn, buildQueryFn, dereferenceSchema, extractAcceptedBodyFields, extractBodyProps, extractPathParams, extractPinnedBody, extractQueryParams, getJsonSchemaType, openApiToToolDefinitions, operationIdToToolName, processOperation, processPath, registerOpenApiTools, resolveParameter, resolveSchema, snakeToCamel };
|
|
622
|
+
export { type CreateInProcessCallApiArgs, DEFAULT_EXCLUDE_EXTENSION, DEFAULT_SERVER_MANAGED_EXTENSION, type JsonSchemaProperty, NO_CONTENT_TEXT, type OpenApiDocuments, type OpenApiSpec, type OpenApiToToolsOptions, type OperationSpec, type QueryParamSerialization, type RegisterOpenApiToolsArgs, 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, operationIdToToolName, processOperation, processPath, registerOpenApiTools, resolveParameter, resolveSchema, snakeToCamel, toToolSchema };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
|
|
2
|
+
import { App, InProcessResponse } from "@ttoss/http-server";
|
|
2
3
|
import { JsonObjectSchema, McpServer } from "@ttoss/http-server-mcp";
|
|
3
4
|
|
|
4
5
|
//#region src/types.d.ts
|
|
@@ -121,6 +122,7 @@ type RequestBodySpec = {
|
|
|
121
122
|
};
|
|
122
123
|
interface OperationSpec {
|
|
123
124
|
operationId?: string;
|
|
125
|
+
summary?: string;
|
|
124
126
|
description?: string;
|
|
125
127
|
parameters?: Array<{
|
|
126
128
|
name?: string;
|
|
@@ -183,9 +185,39 @@ interface OpenApiToToolsOptions {
|
|
|
183
185
|
* to an empty schema, which accepts any value.
|
|
184
186
|
*/
|
|
185
187
|
documents?: OpenApiDocuments;
|
|
188
|
+
/**
|
|
189
|
+
* How much of each parameter's and body property's schema reaches the
|
|
190
|
+
* tool's `inputSchema`.
|
|
191
|
+
*
|
|
192
|
+
* - `'compact'` keeps the `type`, `items` and `description` of each
|
|
193
|
+
* top-level argument, with descriptions flattened to one line.
|
|
194
|
+
* - `'full'` keeps the whole schema — `enum`, `format`, `pattern`,
|
|
195
|
+
* `minimum`, `default`, nested `properties` and `required`, `oneOf` — and
|
|
196
|
+
* changes only what JSON Schema cannot say: `allOf` is merged, `nullable`
|
|
197
|
+
* becomes a `'null'` type (and joins an `enum`), and OpenAPI-only keywords
|
|
198
|
+
* and `x-` extensions are dropped. Descriptions stay verbatim.
|
|
199
|
+
*
|
|
200
|
+
* @default 'compact'
|
|
201
|
+
*/
|
|
202
|
+
schemaDetail?: 'compact' | 'full';
|
|
203
|
+
/**
|
|
204
|
+
* Builds each tool's description from its operation. The default is the
|
|
205
|
+
* operation's `description`, flattened to one line.
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```typescript
|
|
209
|
+
* describe: ({ operation, method, pathTemplate }) =>
|
|
210
|
+
* `${operation.summary}\n\n${operation.description}\n\n(${method} ${pathTemplate})`,
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
describe?: (args: {
|
|
214
|
+
operation: OperationSpec; /** Uppercase HTTP method. */
|
|
215
|
+
method: string;
|
|
216
|
+
pathTemplate: string;
|
|
217
|
+
}) => string;
|
|
186
218
|
}
|
|
187
219
|
/** {@link OpenApiToToolsOptions} with every default applied. */
|
|
188
|
-
type ResolvedToolOptions = Required<Omit<OpenApiToToolsOptions, 'documents'>> & Pick<OpenApiToToolsOptions, 'documents'>;
|
|
220
|
+
type ResolvedToolOptions = Required<Omit<OpenApiToToolsOptions, 'documents' | 'describe' | 'schemaDetail'>> & Pick<OpenApiToToolsOptions, 'documents' | 'describe' | 'schemaDetail'>;
|
|
189
221
|
declare const DEFAULT_EXCLUDE_EXTENSION = "x-mcp-exclude";
|
|
190
222
|
declare const DEFAULT_SERVER_MANAGED_EXTENSION = "x-mcp-server-managed";
|
|
191
223
|
//#endregion
|
|
@@ -217,6 +249,8 @@ type ExtractParamsArgs = {
|
|
|
217
249
|
declare const extractPathParams: (args: ExtractParamsArgs) => Array<{
|
|
218
250
|
name: string;
|
|
219
251
|
argName: string;
|
|
252
|
+
description?: string; /** The parameter's schema with every `$ref` inlined. */
|
|
253
|
+
schema?: Record<string, unknown>;
|
|
220
254
|
serverManaged: boolean;
|
|
221
255
|
pinnedValue?: string;
|
|
222
256
|
}>;
|
|
@@ -225,7 +259,8 @@ declare const extractQueryParams: (args: ExtractParamsArgs) => Array<{
|
|
|
225
259
|
argName: string;
|
|
226
260
|
description: string;
|
|
227
261
|
required: boolean;
|
|
228
|
-
type: string;
|
|
262
|
+
type: string; /** The parameter's schema with every `$ref` inlined. */
|
|
263
|
+
schema?: Record<string, unknown>;
|
|
229
264
|
style?: string;
|
|
230
265
|
explode?: boolean;
|
|
231
266
|
serverManaged: boolean;
|
|
@@ -258,7 +293,8 @@ declare const extractBodyProps: (args: {
|
|
|
258
293
|
nullable: boolean;
|
|
259
294
|
oneOf?: unknown[];
|
|
260
295
|
anyOf?: unknown[];
|
|
261
|
-
allOf?: unknown[];
|
|
296
|
+
allOf?: unknown[]; /** The property's whole schema, with every `$ref` inlined. */
|
|
297
|
+
schema: Record<string, unknown>;
|
|
262
298
|
}>;
|
|
263
299
|
/**
|
|
264
300
|
* The value each server-managed body property pins, typed by its schema and
|
|
@@ -273,6 +309,39 @@ declare const extractPinnedBody: (args: {
|
|
|
273
309
|
operationId: string;
|
|
274
310
|
}) => Record<string, string | number | boolean>;
|
|
275
311
|
//#endregion
|
|
312
|
+
//#region src/fullSchema.d.ts
|
|
313
|
+
/**
|
|
314
|
+
* Turns an already-dereferenced OpenAPI schema into the JSON Schema a tool
|
|
315
|
+
* argument carries, keeping every constraint it declares — `enum`, `format`,
|
|
316
|
+
* `pattern`, `minimum`, `default`, nested `properties` and `required`,
|
|
317
|
+
* `oneOf` — and changing only what JSON Schema cannot express: `allOf` is
|
|
318
|
+
* merged, `nullable` becomes a `'null'` type, and OpenAPI-only keywords and
|
|
319
|
+
* `x-` extensions are dropped. Descriptions are kept verbatim.
|
|
320
|
+
*/
|
|
321
|
+
declare const toToolSchema: (value: unknown) => unknown;
|
|
322
|
+
type FullParam = {
|
|
323
|
+
argName: string;
|
|
324
|
+
required?: boolean;
|
|
325
|
+
description?: string;
|
|
326
|
+
schema?: unknown;
|
|
327
|
+
serverManaged?: boolean;
|
|
328
|
+
};
|
|
329
|
+
type FullBodyProp = {
|
|
330
|
+
argName: string;
|
|
331
|
+
required: boolean;
|
|
332
|
+
schema: Record<string, unknown>;
|
|
333
|
+
};
|
|
334
|
+
/**
|
|
335
|
+
* The `inputSchema` of a tool under `schemaDetail: 'full'`: every parameter
|
|
336
|
+
* and body property with its whole schema. `properties` is always present,
|
|
337
|
+
* even when empty, because some clients refuse an object schema without it.
|
|
338
|
+
*/
|
|
339
|
+
declare const buildFullInputSchema: (args: {
|
|
340
|
+
pathParams: FullParam[];
|
|
341
|
+
queryParams: FullParam[];
|
|
342
|
+
bodyProps: FullBodyProp[];
|
|
343
|
+
}) => JsonObjectSchema;
|
|
344
|
+
//#endregion
|
|
276
345
|
//#region src/registerOpenApiTools.d.ts
|
|
277
346
|
/** The resolved HTTP request a tool call maps to, before transport concerns. */
|
|
278
347
|
interface ResolvedRequest {
|
|
@@ -360,6 +429,58 @@ declare const NO_CONTENT_TEXT = "Succeeded. The operation returned no content.";
|
|
|
360
429
|
*/
|
|
361
430
|
declare const registerOpenApiTools: (args: RegisterOpenApiToolsArgs) => ToolDefinition[];
|
|
362
431
|
//#endregion
|
|
432
|
+
//#region src/inProcessCallApi.d.ts
|
|
433
|
+
interface CreateInProcessCallApiArgs {
|
|
434
|
+
/**
|
|
435
|
+
* The Koa app serving the REST API the tools were generated from, or a
|
|
436
|
+
* function returning it — for an app that mounts the MCP router itself and
|
|
437
|
+
* so is not built yet when the tools are registered.
|
|
438
|
+
*/
|
|
439
|
+
app: App | (() => App | Promise<App>);
|
|
440
|
+
/**
|
|
441
|
+
* Headers added to every dispatched request, after the ones the MCP request
|
|
442
|
+
* carried — e.g. a marker that tells a request log the call came from a tool.
|
|
443
|
+
*/
|
|
444
|
+
headers?: (request: ResolvedRequest) => Record<string, string | undefined>;
|
|
445
|
+
/**
|
|
446
|
+
* Builds the error a non-2xx response throws. The default reads the message
|
|
447
|
+
* out of the common error envelopes (see {@link errorMessageOf}).
|
|
448
|
+
*/
|
|
449
|
+
toError?: (response: InProcessResponse, request: ResolvedRequest) => Error;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* The message an error response carries, read from the envelopes REST APIs
|
|
453
|
+
* commonly answer with: a plain string, `{ error: '…' }`,
|
|
454
|
+
* `{ error: { code, message } }` (as `code: message`) or `{ message: '…' }`.
|
|
455
|
+
* `null` when the body is none of them.
|
|
456
|
+
*/
|
|
457
|
+
declare const errorMessageOf: (body: unknown) => string | null;
|
|
458
|
+
/**
|
|
459
|
+
* A `callApi` for {@link registerOpenApiTools} that serves each tool call
|
|
460
|
+
* against the REST app in this same process, with no socket: validation,
|
|
461
|
+
* authorization and error handling run once, in the routes, for both
|
|
462
|
+
* surfaces.
|
|
463
|
+
*
|
|
464
|
+
* The MCP request's headers (what `createMcpRouter`'s `getApiHeaders`
|
|
465
|
+
* produced — typically the caller's `Authorization`) are forwarded onto the
|
|
466
|
+
* dispatched request. A 2xx answers its body; anything else throws, so the
|
|
467
|
+
* client sees a tool error rather than an error body rendered as a result.
|
|
468
|
+
*
|
|
469
|
+
* @example
|
|
470
|
+
* ```typescript
|
|
471
|
+
* registerOpenApiTools({
|
|
472
|
+
* server,
|
|
473
|
+
* spec,
|
|
474
|
+
* callApi: createInProcessCallApi({ app }),
|
|
475
|
+
* });
|
|
476
|
+
* ```
|
|
477
|
+
*/
|
|
478
|
+
declare const createInProcessCallApi: ({
|
|
479
|
+
app,
|
|
480
|
+
headers,
|
|
481
|
+
toError
|
|
482
|
+
}: CreateInProcessCallApiArgs) => ((request: ResolvedRequest) => Promise<unknown>);
|
|
483
|
+
//#endregion
|
|
363
484
|
//#region src/schema.d.ts
|
|
364
485
|
type ResolvedSchema = {
|
|
365
486
|
type?: string;
|
|
@@ -498,4 +619,4 @@ declare const openApiToToolDefinitions: (args: {
|
|
|
498
619
|
options?: OpenApiToToolsOptions;
|
|
499
620
|
}) => ToolDefinition[];
|
|
500
621
|
//#endregion
|
|
501
|
-
export { DEFAULT_EXCLUDE_EXTENSION, DEFAULT_SERVER_MANAGED_EXTENSION, type JsonSchemaProperty, NO_CONTENT_TEXT, type OpenApiDocuments, type OpenApiSpec, type OpenApiToToolsOptions, type OperationSpec, type QueryParamSerialization, type RegisterOpenApiToolsArgs, type RequestBodySpec, type ResolvedParameter, type ResolvedRequest, type ResolvedToolOptions, type ServerManagedParameter, type ToolDefinition, buildBodyFn, buildInputSchema, buildPathFn, buildQueryFn, dereferenceSchema, extractAcceptedBodyFields, extractBodyProps, extractPathParams, extractPinnedBody, extractQueryParams, getJsonSchemaType, openApiToToolDefinitions, operationIdToToolName, processOperation, processPath, registerOpenApiTools, resolveParameter, resolveSchema, snakeToCamel };
|
|
622
|
+
export { type CreateInProcessCallApiArgs, DEFAULT_EXCLUDE_EXTENSION, DEFAULT_SERVER_MANAGED_EXTENSION, type JsonSchemaProperty, NO_CONTENT_TEXT, type OpenApiDocuments, type OpenApiSpec, type OpenApiToToolsOptions, type OperationSpec, type QueryParamSerialization, type RegisterOpenApiToolsArgs, 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, operationIdToToolName, processOperation, processPath, registerOpenApiTools, resolveParameter, resolveSchema, snakeToCamel, toToolSchema };
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
|
|
2
|
+
import { dispatchInProcess } from "@ttoss/http-server";
|
|
2
3
|
import { getApiHeaders, registerToolFromSchema } from "@ttoss/http-server-mcp";
|
|
3
4
|
|
|
4
5
|
//#region src/schema.ts
|
|
5
|
-
var isRecord = value => {
|
|
6
|
+
var isRecord$1 = value => {
|
|
6
7
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
8
|
};
|
|
8
9
|
var getAlternativeSchemas = schema => {
|
|
@@ -39,7 +40,7 @@ var followPointer = args => {
|
|
|
39
40
|
const tokens = args.pointer.split("/").slice(1);
|
|
40
41
|
let current = args.root;
|
|
41
42
|
for (const rawToken of tokens) {
|
|
42
|
-
if (!isRecord(current) && !Array.isArray(current)) return void 0;
|
|
43
|
+
if (!isRecord$1(current) && !Array.isArray(current)) return void 0;
|
|
43
44
|
const token = decodeURIComponent(rawToken).replace(/~1/g, "/").replace(/~0/g, "~");
|
|
44
45
|
current = current[token];
|
|
45
46
|
}
|
|
@@ -98,7 +99,7 @@ var dereferenceValue = args => {
|
|
|
98
99
|
seenRefs
|
|
99
100
|
});
|
|
100
101
|
});
|
|
101
|
-
if (isRecord(value)) {
|
|
102
|
+
if (isRecord$1(value)) {
|
|
102
103
|
if (typeof value.$ref === "string") {
|
|
103
104
|
const target = resolveRef({
|
|
104
105
|
ref: value.$ref,
|
|
@@ -178,7 +179,7 @@ var resolveParameter = (param, spec, documents) => {
|
|
|
178
179
|
documents
|
|
179
180
|
}
|
|
180
181
|
});
|
|
181
|
-
return isRecord(target?.value) ? target.value : {};
|
|
182
|
+
return isRecord$1(target?.value) ? target.value : {};
|
|
182
183
|
}
|
|
183
184
|
return param;
|
|
184
185
|
};
|
|
@@ -217,7 +218,7 @@ var appendDeepObject = args => {
|
|
|
217
218
|
});
|
|
218
219
|
return;
|
|
219
220
|
}
|
|
220
|
-
if (isRecord(args.value)) {
|
|
221
|
+
if (isRecord$1(args.value)) {
|
|
221
222
|
for (const [property, propertyValue] of Object.entries(args.value)) appendDeepObject({
|
|
222
223
|
search: args.search,
|
|
223
224
|
key: `${args.key}[${property}]`,
|
|
@@ -269,7 +270,7 @@ var appendQueryValue = args => {
|
|
|
269
270
|
});
|
|
270
271
|
return;
|
|
271
272
|
}
|
|
272
|
-
if (isRecord(args.value)) {
|
|
273
|
+
if (isRecord$1(args.value)) {
|
|
273
274
|
appendObjectValue({
|
|
274
275
|
...serialization,
|
|
275
276
|
value: args.value
|
|
@@ -498,6 +499,8 @@ var extractPathParams = args => {
|
|
|
498
499
|
return {
|
|
499
500
|
name: p.name || "",
|
|
500
501
|
argName: toArgName(p.name || ""),
|
|
502
|
+
description: p.description,
|
|
503
|
+
schema: dereferenceSchema(p.schema, args.spec, args.documents),
|
|
501
504
|
...managedFields(readServerManaged({
|
|
502
505
|
node: p,
|
|
503
506
|
extension: flag
|
|
@@ -519,6 +522,7 @@ var extractQueryParams = args => {
|
|
|
519
522
|
description: p.description || "",
|
|
520
523
|
required: p.required || false,
|
|
521
524
|
type: p.schema?.type || "string",
|
|
525
|
+
schema: dereferenceSchema(p.schema, args.spec, args.documents),
|
|
522
526
|
style: p.style,
|
|
523
527
|
explode: p.explode,
|
|
524
528
|
...managedFields(readServerManaged({
|
|
@@ -611,7 +615,8 @@ var extractBodyProps = args => {
|
|
|
611
615
|
nullable: val.nullable === true,
|
|
612
616
|
oneOf: Array.isArray(val.oneOf) ? val.oneOf : void 0,
|
|
613
617
|
anyOf: Array.isArray(val.anyOf) ? val.anyOf : void 0,
|
|
614
|
-
allOf: Array.isArray(val.allOf) ? val.allOf : void 0
|
|
618
|
+
allOf: Array.isArray(val.allOf) ? val.allOf : void 0,
|
|
619
|
+
schema: value
|
|
615
620
|
};
|
|
616
621
|
});
|
|
617
622
|
};
|
|
@@ -641,6 +646,209 @@ var extractPinnedBody = args => {
|
|
|
641
646
|
return pinned;
|
|
642
647
|
};
|
|
643
648
|
|
|
649
|
+
//#endregion
|
|
650
|
+
//#region src/fullSchema.ts
|
|
651
|
+
var isRecord = value => {
|
|
652
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
653
|
+
};
|
|
654
|
+
var OPENAPI_ONLY_KEYWORDS = new Set(["discriminator", "example", "externalDocs", "xml"]);
|
|
655
|
+
var SCHEMA_MAPS = new Set(["$defs", "definitions", "dependentSchemas", "patternProperties", "properties"]);
|
|
656
|
+
var SCHEMA_VALUES = new Set(["additionalProperties", "contains", "else", "if", "items", "not", "propertyNames", "then", "unevaluatedItems", "unevaluatedProperties"]);
|
|
657
|
+
var SCHEMA_LISTS = new Set(["anyOf", "oneOf", "prefixItems"]);
|
|
658
|
+
/**
|
|
659
|
+
* OpenAPI 3.0's `nullable: true` in JSON Schema: `'null'` joins the `type`,
|
|
660
|
+
* and `null` joins an `enum`, which would otherwise still refuse it.
|
|
661
|
+
*/
|
|
662
|
+
var withNull = schema => {
|
|
663
|
+
const result = {
|
|
664
|
+
...schema
|
|
665
|
+
};
|
|
666
|
+
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"];
|
|
667
|
+
if (Array.isArray(result.enum) && !result.enum.includes(null)) result.enum = [...result.enum, null];
|
|
668
|
+
return result;
|
|
669
|
+
};
|
|
670
|
+
/**
|
|
671
|
+
* Merges an `allOf` into one schema, the shape a tool argument needs.
|
|
672
|
+
*
|
|
673
|
+
* Members that declare `properties` are an object composition — a write body
|
|
674
|
+
* that is a patch plus two fields — and merge their properties and `required`.
|
|
675
|
+
* Otherwise the `allOf` only wraps a referenced scalar so it can carry its own
|
|
676
|
+
* `description` (the one way OpenAPI 3.0 allows beside a `$ref`), and the
|
|
677
|
+
* members are merged as they are. Sibling keys win in both, because the
|
|
678
|
+
* wrapper exists to say something the referenced schema does not.
|
|
679
|
+
*/
|
|
680
|
+
var mergeAllOf = args => {
|
|
681
|
+
const {
|
|
682
|
+
members,
|
|
683
|
+
siblings
|
|
684
|
+
} = args;
|
|
685
|
+
if (!members.some(member => {
|
|
686
|
+
return member.properties !== void 0;
|
|
687
|
+
})) return Object.assign({}, ...members, siblings);
|
|
688
|
+
const properties = {};
|
|
689
|
+
const required = [];
|
|
690
|
+
for (const member of members) {
|
|
691
|
+
Object.assign(properties, member.properties);
|
|
692
|
+
if (Array.isArray(member.required)) required.push(...member.required);
|
|
693
|
+
}
|
|
694
|
+
return {
|
|
695
|
+
...siblings,
|
|
696
|
+
type: "object",
|
|
697
|
+
properties,
|
|
698
|
+
...(required.length > 0 ? {
|
|
699
|
+
required: [...new Set(required)]
|
|
700
|
+
} : {})
|
|
701
|
+
};
|
|
702
|
+
};
|
|
703
|
+
var normalizeChildren = args => {
|
|
704
|
+
const {
|
|
705
|
+
schema,
|
|
706
|
+
next
|
|
707
|
+
} = args;
|
|
708
|
+
const result = {};
|
|
709
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
710
|
+
if (OPENAPI_ONLY_KEYWORDS.has(key) || key.startsWith("x-")) continue;
|
|
711
|
+
if (SCHEMA_MAPS.has(key) && isRecord(value)) result[key] = Object.fromEntries(Object.entries(value).map(([name, child]) => {
|
|
712
|
+
return [name, next(child)];
|
|
713
|
+
}));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;
|
|
714
|
+
}
|
|
715
|
+
return result;
|
|
716
|
+
};
|
|
717
|
+
/**
|
|
718
|
+
* Turns an already-dereferenced OpenAPI schema into the JSON Schema a tool
|
|
719
|
+
* argument carries, keeping every constraint it declares — `enum`, `format`,
|
|
720
|
+
* `pattern`, `minimum`, `default`, nested `properties` and `required`,
|
|
721
|
+
* `oneOf` — and changing only what JSON Schema cannot express: `allOf` is
|
|
722
|
+
* merged, `nullable` becomes a `'null'` type, and OpenAPI-only keywords and
|
|
723
|
+
* `x-` extensions are dropped. Descriptions are kept verbatim.
|
|
724
|
+
*/
|
|
725
|
+
var toToolSchema = value => {
|
|
726
|
+
if (!isRecord(value)) return value;
|
|
727
|
+
const {
|
|
728
|
+
allOf,
|
|
729
|
+
nullable,
|
|
730
|
+
...rest
|
|
731
|
+
} = value;
|
|
732
|
+
const own = normalizeChildren({
|
|
733
|
+
schema: rest,
|
|
734
|
+
next: toToolSchema
|
|
735
|
+
});
|
|
736
|
+
const merged = Array.isArray(allOf) ? mergeAllOf({
|
|
737
|
+
members: allOf.filter(isRecord).map(member => {
|
|
738
|
+
return toToolSchema(member);
|
|
739
|
+
}),
|
|
740
|
+
siblings: own
|
|
741
|
+
}) : own;
|
|
742
|
+
return nullable === true ? withNull(merged) : merged;
|
|
743
|
+
};
|
|
744
|
+
/** A path or query parameter's schema, with the parameter's own description. */
|
|
745
|
+
var paramProperty = param => {
|
|
746
|
+
return {
|
|
747
|
+
...(toToolSchema(param.schema) ?? {}),
|
|
748
|
+
...(param.description ? {
|
|
749
|
+
description: param.description
|
|
750
|
+
} : {})
|
|
751
|
+
};
|
|
752
|
+
};
|
|
753
|
+
/**
|
|
754
|
+
* The `inputSchema` of a tool under `schemaDetail: 'full'`: every parameter
|
|
755
|
+
* and body property with its whole schema. `properties` is always present,
|
|
756
|
+
* even when empty, because some clients refuse an object schema without it.
|
|
757
|
+
*/
|
|
758
|
+
var buildFullInputSchema = args => {
|
|
759
|
+
const params = [...args.pathParams, ...args.queryParams].filter(param => {
|
|
760
|
+
return !param.serverManaged;
|
|
761
|
+
});
|
|
762
|
+
const properties = {};
|
|
763
|
+
const required = [];
|
|
764
|
+
for (const param of params) {
|
|
765
|
+
properties[param.argName] = paramProperty(param);
|
|
766
|
+
if (param.required) required.push(param.argName);
|
|
767
|
+
}
|
|
768
|
+
for (const prop of args.bodyProps) {
|
|
769
|
+
properties[prop.argName] = toToolSchema(prop.schema);
|
|
770
|
+
if (prop.required) required.push(prop.argName);
|
|
771
|
+
}
|
|
772
|
+
return {
|
|
773
|
+
type: "object",
|
|
774
|
+
properties,
|
|
775
|
+
...(required.length > 0 ? {
|
|
776
|
+
required: [...new Set(required)]
|
|
777
|
+
} : {})
|
|
778
|
+
};
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
//#endregion
|
|
782
|
+
//#region src/inProcessCallApi.ts
|
|
783
|
+
var nestedMessageOf = error => {
|
|
784
|
+
if (!error || typeof error !== "object") return null;
|
|
785
|
+
const {
|
|
786
|
+
code,
|
|
787
|
+
message
|
|
788
|
+
} = error;
|
|
789
|
+
if (typeof message !== "string") return null;
|
|
790
|
+
return typeof code === "string" ? `${code}: ${message}` : message;
|
|
791
|
+
};
|
|
792
|
+
/**
|
|
793
|
+
* The message an error response carries, read from the envelopes REST APIs
|
|
794
|
+
* commonly answer with: a plain string, `{ error: '…' }`,
|
|
795
|
+
* `{ error: { code, message } }` (as `code: message`) or `{ message: '…' }`.
|
|
796
|
+
* `null` when the body is none of them.
|
|
797
|
+
*/
|
|
798
|
+
var errorMessageOf = body => {
|
|
799
|
+
if (typeof body === "string") return body || null;
|
|
800
|
+
if (!body || typeof body !== "object") return null;
|
|
801
|
+
const {
|
|
802
|
+
error,
|
|
803
|
+
message
|
|
804
|
+
} = body;
|
|
805
|
+
if (typeof error === "string") return error;
|
|
806
|
+
return nestedMessageOf(error) ?? (typeof message === "string" ? message : null);
|
|
807
|
+
};
|
|
808
|
+
var defaultToError = response => {
|
|
809
|
+
return new Error(errorMessageOf(response.body) ?? `HTTP ${response.status}`);
|
|
810
|
+
};
|
|
811
|
+
/**
|
|
812
|
+
* A `callApi` for {@link registerOpenApiTools} that serves each tool call
|
|
813
|
+
* against the REST app in this same process, with no socket: validation,
|
|
814
|
+
* authorization and error handling run once, in the routes, for both
|
|
815
|
+
* surfaces.
|
|
816
|
+
*
|
|
817
|
+
* The MCP request's headers (what `createMcpRouter`'s `getApiHeaders`
|
|
818
|
+
* produced — typically the caller's `Authorization`) are forwarded onto the
|
|
819
|
+
* dispatched request. A 2xx answers its body; anything else throws, so the
|
|
820
|
+
* client sees a tool error rather than an error body rendered as a result.
|
|
821
|
+
*
|
|
822
|
+
* @example
|
|
823
|
+
* ```typescript
|
|
824
|
+
* registerOpenApiTools({
|
|
825
|
+
* server,
|
|
826
|
+
* spec,
|
|
827
|
+
* callApi: createInProcessCallApi({ app }),
|
|
828
|
+
* });
|
|
829
|
+
* ```
|
|
830
|
+
*/
|
|
831
|
+
var createInProcessCallApi = ({
|
|
832
|
+
app,
|
|
833
|
+
headers,
|
|
834
|
+
toError = defaultToError
|
|
835
|
+
}) => {
|
|
836
|
+
return async request => {
|
|
837
|
+
const response = await dispatchInProcess({
|
|
838
|
+
app: typeof app === "function" ? await app() : app,
|
|
839
|
+
method: request.method,
|
|
840
|
+
path: request.url,
|
|
841
|
+
headers: {
|
|
842
|
+
...request.headers,
|
|
843
|
+
...headers?.(request)
|
|
844
|
+
},
|
|
845
|
+
body: request.body
|
|
846
|
+
});
|
|
847
|
+
if (response.status < 200 || response.status >= 300) throw toError(response, request);
|
|
848
|
+
return response.body;
|
|
849
|
+
};
|
|
850
|
+
};
|
|
851
|
+
|
|
644
852
|
//#endregion
|
|
645
853
|
//#region src/toolDefinitions.ts
|
|
646
854
|
/** Converts a camelCase `operationId` to a kebab-case tool name. */
|
|
@@ -657,6 +865,11 @@ var getJsonSchemaType = schemaType => {
|
|
|
657
865
|
var sanitizeDescription = description => {
|
|
658
866
|
return (description || "").replace(/'/g, "\\'").replace(/\n/g, " ").trim();
|
|
659
867
|
};
|
|
868
|
+
var defaultDescribe = ({
|
|
869
|
+
operation
|
|
870
|
+
}) => {
|
|
871
|
+
return sanitizeDescription(operation.description);
|
|
872
|
+
};
|
|
660
873
|
/**
|
|
661
874
|
* Forwards a property's `oneOf` / `anyOf` / multi-entry `allOf` verbatim, or
|
|
662
875
|
* returns `undefined` when it declares none.
|
|
@@ -713,8 +926,7 @@ var buildInputSchema = (pathParams, queryParams, bodyProps) => {
|
|
|
713
926
|
const modelQueryParams = queryParams.filter(p => {
|
|
714
927
|
return !p.serverManaged;
|
|
715
928
|
});
|
|
716
|
-
|
|
717
|
-
if (allParams.length === 0) return {
|
|
929
|
+
if ([...modelPathParams, ...modelQueryParams, ...bodyProps].length === 0) return {
|
|
718
930
|
type: "object"
|
|
719
931
|
};
|
|
720
932
|
const requiredFields = [...modelPathParams.map(p => {
|
|
@@ -729,10 +941,11 @@ var buildInputSchema = (pathParams, queryParams, bodyProps) => {
|
|
|
729
941
|
return p.argName;
|
|
730
942
|
})];
|
|
731
943
|
const properties = {};
|
|
732
|
-
for (const param of
|
|
944
|
+
for (const param of modelPathParams) properties[param.argName] = {
|
|
733
945
|
type: "string",
|
|
734
946
|
description: ""
|
|
735
947
|
};
|
|
948
|
+
for (const param of [...modelQueryParams, ...bodyProps]) properties[param.argName] = normalizeNullable(buildTypedProperty(param));
|
|
736
949
|
return {
|
|
737
950
|
type: "object",
|
|
738
951
|
properties,
|
|
@@ -741,6 +954,35 @@ var buildInputSchema = (pathParams, queryParams, bodyProps) => {
|
|
|
741
954
|
} : {})
|
|
742
955
|
};
|
|
743
956
|
};
|
|
957
|
+
var SUPPORTED_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
958
|
+
/** The tool description, from `options.describe` or the default. */
|
|
959
|
+
var describeOperation = args => {
|
|
960
|
+
const {
|
|
961
|
+
options,
|
|
962
|
+
operation,
|
|
963
|
+
method,
|
|
964
|
+
pathTemplate
|
|
965
|
+
} = args;
|
|
966
|
+
return (options.describe ?? defaultDescribe)({
|
|
967
|
+
operation,
|
|
968
|
+
method,
|
|
969
|
+
pathTemplate
|
|
970
|
+
});
|
|
971
|
+
};
|
|
972
|
+
/** The `inputSchema` at the detail {@link OpenApiToToolsOptions.schemaDetail} asks for. */
|
|
973
|
+
var selectInputSchema = args => {
|
|
974
|
+
if (args.schemaDetail !== "full") return buildInputSchema(args.pathParams, args.queryParams, args.bodyProps);
|
|
975
|
+
return buildFullInputSchema({
|
|
976
|
+
pathParams: args.pathParams.map(param => {
|
|
977
|
+
return {
|
|
978
|
+
...param,
|
|
979
|
+
required: true
|
|
980
|
+
};
|
|
981
|
+
}),
|
|
982
|
+
queryParams: args.queryParams,
|
|
983
|
+
bodyProps: args.bodyProps
|
|
984
|
+
});
|
|
985
|
+
};
|
|
744
986
|
/** Collects every `x-` prefixed extension declared on the operation. */
|
|
745
987
|
var extractExtensions = operation => {
|
|
746
988
|
const extensions = {};
|
|
@@ -749,9 +991,7 @@ var extractExtensions = operation => {
|
|
|
749
991
|
};
|
|
750
992
|
var processOperation = args => {
|
|
751
993
|
const httpMethod = args.method.toUpperCase();
|
|
752
|
-
if (!
|
|
753
|
-
if (!args.operation.operationId) return null;
|
|
754
|
-
if (args.operation[args.options.excludeExtension]) return null;
|
|
994
|
+
if (!SUPPORTED_METHODS.includes(httpMethod) || !args.operation.operationId || args.operation[args.options.excludeExtension]) return null;
|
|
755
995
|
const toolName = operationIdToToolName(args.operation.operationId);
|
|
756
996
|
const parameters = [...(args.pathItemParameters ?? []), ...(args.operation.parameters ?? [])];
|
|
757
997
|
const {
|
|
@@ -759,20 +999,15 @@ var processOperation = args => {
|
|
|
759
999
|
serverManagedExtension
|
|
760
1000
|
} = args.options;
|
|
761
1001
|
const toArgName = argNameMapper(args.options.argumentNames);
|
|
762
|
-
const
|
|
763
|
-
parameters,
|
|
764
|
-
spec: args.spec,
|
|
765
|
-
documents,
|
|
766
|
-
toArgName,
|
|
767
|
-
serverManagedExtension
|
|
768
|
-
});
|
|
769
|
-
const queryParams = extractQueryParams({
|
|
1002
|
+
const paramArgs = {
|
|
770
1003
|
parameters,
|
|
771
1004
|
spec: args.spec,
|
|
772
1005
|
documents,
|
|
773
1006
|
toArgName,
|
|
774
1007
|
serverManagedExtension
|
|
775
|
-
}
|
|
1008
|
+
};
|
|
1009
|
+
const pathParams = extractPathParams(paramArgs);
|
|
1010
|
+
const queryParams = extractQueryParams(paramArgs);
|
|
776
1011
|
const bodyArgs = {
|
|
777
1012
|
requestBody: args.operation.requestBody,
|
|
778
1013
|
spec: args.spec,
|
|
@@ -783,7 +1018,12 @@ var processOperation = args => {
|
|
|
783
1018
|
...bodyArgs,
|
|
784
1019
|
toArgName
|
|
785
1020
|
});
|
|
786
|
-
const inputSchema =
|
|
1021
|
+
const inputSchema = selectInputSchema({
|
|
1022
|
+
schemaDetail: args.options.schemaDetail,
|
|
1023
|
+
pathParams,
|
|
1024
|
+
queryParams,
|
|
1025
|
+
bodyProps
|
|
1026
|
+
});
|
|
787
1027
|
const serverManagedParameters = collectServerManagedParameters({
|
|
788
1028
|
pathParams,
|
|
789
1029
|
queryParams
|
|
@@ -791,7 +1031,10 @@ var processOperation = args => {
|
|
|
791
1031
|
const pinned = pinnedArgs(serverManagedParameters);
|
|
792
1032
|
return {
|
|
793
1033
|
name: toolName,
|
|
794
|
-
description:
|
|
1034
|
+
description: describeOperation({
|
|
1035
|
+
...args,
|
|
1036
|
+
method: httpMethod
|
|
1037
|
+
}),
|
|
795
1038
|
inputSchema,
|
|
796
1039
|
method: httpMethod,
|
|
797
1040
|
pathTemplate: args.pathTemplate,
|
|
@@ -840,6 +1083,8 @@ var resolveOptions = (options = {}) => {
|
|
|
840
1083
|
excludeExtension: options.excludeExtension ?? "x-mcp-exclude",
|
|
841
1084
|
serverManagedExtension: options.serverManagedExtension ?? "x-mcp-server-managed",
|
|
842
1085
|
argumentNames: options.argumentNames ?? "camelCase",
|
|
1086
|
+
schemaDetail: options.schemaDetail,
|
|
1087
|
+
describe: options.describe,
|
|
843
1088
|
documents: options.documents
|
|
844
1089
|
};
|
|
845
1090
|
};
|
|
@@ -962,4 +1207,4 @@ var registerOpenApiTools = args => {
|
|
|
962
1207
|
};
|
|
963
1208
|
|
|
964
1209
|
//#endregion
|
|
965
|
-
export { DEFAULT_EXCLUDE_EXTENSION, DEFAULT_SERVER_MANAGED_EXTENSION, NO_CONTENT_TEXT, buildBodyFn, buildInputSchema, buildPathFn, buildQueryFn, dereferenceSchema, extractAcceptedBodyFields, extractBodyProps, extractPathParams, extractPinnedBody, extractQueryParams, getJsonSchemaType, openApiToToolDefinitions, operationIdToToolName, processOperation, processPath, registerOpenApiTools, resolveParameter, resolveSchema, snakeToCamel };
|
|
1210
|
+
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, registerOpenApiTools, 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.6.0",
|
|
4
4
|
"description": "Generate Model Context Protocol (MCP) tools from an OpenAPI specification for @ttoss/http-server-mcp",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -35,15 +35,15 @@
|
|
|
35
35
|
"dist"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@ttoss/http-server
|
|
38
|
+
"@ttoss/http-server": "^0.11.0",
|
|
39
|
+
"@ttoss/http-server-mcp": "^0.32.0"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
42
|
"@modelcontextprotocol/server": "^2.0.0",
|
|
42
43
|
"jest": "^30.4.2",
|
|
43
44
|
"supertest": "^7.2.2",
|
|
44
45
|
"tsdown": "^0.22.2",
|
|
45
|
-
"@ttoss/config": "^1.
|
|
46
|
-
"@ttoss/http-server": "^0.10.3"
|
|
46
|
+
"@ttoss/config": "^1.40.0"
|
|
47
47
|
},
|
|
48
48
|
"publishConfig": {
|
|
49
49
|
"access": "public",
|