@ttoss/http-server-mcp-openapi 0.5.7 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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({
1002
+ const paramArgs = {
763
1003
  parameters,
764
1004
  spec: args.spec,
765
1005
  documents,
766
1006
  toArgName,
767
1007
  serverManagedExtension
768
- });
769
- const queryParams = extractQueryParams({
770
- parameters,
771
- spec: args.spec,
772
- documents,
773
- toArgName,
774
- 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
  };
@@ -935,6 +1180,9 @@ var registerOpenApiTools = args => {
935
1180
  name: tool.name,
936
1181
  description: tool.description,
937
1182
  inputSchema: tool.inputSchema,
1183
+ _meta: args.toolMeta?.({
1184
+ tool
1185
+ }),
938
1186
  handler: async rawArgs => {
939
1187
  const headers = getApiHeaders();
940
1188
  const handlerArgs = await applyServerParameters({
@@ -944,17 +1192,25 @@ var registerOpenApiTools = args => {
944
1192
  serverParameters: args.serverParameters
945
1193
  });
946
1194
  const url = tool.path(handlerArgs) + (tool.query ? tool.query(handlerArgs) : "");
1195
+ const data = await args.callApi({
1196
+ method: tool.method,
1197
+ url,
1198
+ body: tool.body ? tool.body(handlerArgs) : void 0,
1199
+ tool,
1200
+ headers
1201
+ });
1202
+ const structuredContent = args.toStructuredContent?.({
1203
+ data,
1204
+ tool
1205
+ });
947
1206
  return {
948
1207
  content: [{
949
1208
  type: "text",
950
- text: toText(await args.callApi({
951
- method: tool.method,
952
- url,
953
- body: tool.body ? tool.body(handlerArgs) : void 0,
954
- tool,
955
- headers
956
- }))
957
- }]
1209
+ text: toText(data)
1210
+ }],
1211
+ ...(structuredContent === void 0 ? {} : {
1212
+ structuredContent
1213
+ })
958
1214
  };
959
1215
  }
960
1216
  });
@@ -962,4 +1218,4 @@ var registerOpenApiTools = args => {
962
1218
  };
963
1219
 
964
1220
  //#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 };
1221
+ export { DEFAULT_EXCLUDE_EXTENSION, DEFAULT_SERVER_MANAGED_EXTENSION, NO_CONTENT_TEXT, buildBodyFn, buildFullInputSchema, buildInputSchema, buildPathFn, buildQueryFn, createInProcessCallApi, dereferenceSchema, errorMessageOf, extractAcceptedBodyFields, extractBodyProps, extractPathParams, extractPinnedBody, extractQueryParams, getJsonSchemaType, openApiToToolDefinitions, operationIdToToolName, processOperation, processPath, 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.7",
3
+ "version": "0.7.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.1"
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/http-server": "^0.10.4",
46
- "@ttoss/config": "^1.39.1"
46
+ "@ttoss/config": "^1.40.0"
47
47
  },
48
48
  "publishConfig": {
49
49
  "access": "public",