@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 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
- const allParams = [...modelPathParams, ...modelQueryParams, ...bodyProps];
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 allParams) properties[param.argName] = "description" in param ? normalizeNullable(buildTypedProperty(param)) : {
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 (!["GET", "POST", "PUT", "PATCH", "DELETE"].includes(httpMethod)) return null;
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 pathParams = extractPathParams({
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 = buildInputSchema(pathParams, queryParams, bodyProps);
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: sanitizeDescription(args.operation.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
- const allParams = [...modelPathParams, ...modelQueryParams, ...bodyProps];
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 allParams) properties[param.argName] = "description" in param ? normalizeNullable(buildTypedProperty(param)) : {
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 (!["GET", "POST", "PUT", "PATCH", "DELETE"].includes(httpMethod)) return null;
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 pathParams = extractPathParams({
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 = buildInputSchema(pathParams, queryParams, bodyProps);
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: sanitizeDescription(args.operation.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.5.6",
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-mcp": "^0.31.0"
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.39.1",
46
- "@ttoss/http-server": "^0.10.3"
46
+ "@ttoss/config": "^1.40.0"
47
47
  },
48
48
  "publishConfig": {
49
49
  "access": "public",