@typeship-ax/mcp 0.22.0 → 0.23.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.
Files changed (55) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +1 -1
  3. package/api.md +1 -1
  4. package/dist/arguments.d.ts +9 -2
  5. package/dist/arguments.d.ts.map +1 -1
  6. package/dist/arguments.js +17 -6
  7. package/dist/fields.d.ts +8 -1
  8. package/dist/fields.d.ts.map +1 -1
  9. package/dist/fields.js +91 -5
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.js +3 -3
  12. package/dist/mcp-protocol.d.ts +21 -0
  13. package/dist/mcp-protocol.d.ts.map +1 -1
  14. package/dist/mcp-protocol.js +280 -58
  15. package/dist/mcp.d.ts.map +1 -1
  16. package/dist/mcp.js +4 -3
  17. package/dist/ops.d.ts +5 -0
  18. package/dist/ops.d.ts.map +1 -1
  19. package/dist/ops.js +66 -6
  20. package/dist/resources/deliveries.d.ts +1 -1
  21. package/dist/resources/deliveries.d.ts.map +1 -1
  22. package/dist/resources/deliveries.js +1 -1
  23. package/dist/resources/drafts.d.ts +1 -1
  24. package/dist/resources/drafts.d.ts.map +1 -1
  25. package/dist/resources/drafts.js +1 -1
  26. package/dist/resources/generations.d.ts +2 -2
  27. package/dist/resources/generations.d.ts.map +1 -1
  28. package/dist/resources/generations.js +2 -2
  29. package/dist/resources/releases.d.ts +1 -1
  30. package/dist/resources/releases.d.ts.map +1 -1
  31. package/dist/resources/releases.js +1 -1
  32. package/dist/resources/spec-revisions.d.ts +1 -1
  33. package/dist/resources/spec-revisions.d.ts.map +1 -1
  34. package/dist/resources/spec-revisions.js +1 -1
  35. package/dist/resources/targets.d.ts +1 -1
  36. package/dist/resources/targets.d.ts.map +1 -1
  37. package/dist/resources/targets.js +1 -1
  38. package/dist/type-docs.d.ts +61 -0
  39. package/dist/type-docs.d.ts.map +1 -0
  40. package/dist/type-docs.js +174 -0
  41. package/package.json +1 -1
  42. package/server.json +2 -2
  43. package/src/arguments.ts +19 -7
  44. package/src/fields.ts +81 -5
  45. package/src/index.ts +3 -3
  46. package/src/mcp-protocol.ts +279 -58
  47. package/src/mcp.ts +4 -3
  48. package/src/ops.ts +69 -6
  49. package/src/resources/deliveries.ts +2 -2
  50. package/src/resources/drafts.ts +2 -2
  51. package/src/resources/generations.ts +4 -4
  52. package/src/resources/releases.ts +2 -2
  53. package/src/resources/spec-revisions.ts +2 -2
  54. package/src/resources/targets.ts +2 -2
  55. package/src/type-docs.ts +205 -0
@@ -16,10 +16,11 @@
16
16
  * Spec: https://modelcontextprotocol.io/specification/2026-07-28
17
17
  */
18
18
 
19
- import { checkValue, closestName, isMeReference, normalizeName, userShapedReference, type ArgumentIssue } from "./arguments.js";
19
+ import { checkValue, closestName, isMeReference, meIdentityField, normalizeName, userShapedReference, type ArgumentIssue } from "./arguments.js";
20
20
  import { docsReadTarget, resolveDocsContentUrl, searchConnectedGuides } from "./docs.js";
21
21
  import { projectFields, unmatchedFields, unmatchedFieldsMessage } from "./fields.js";
22
22
  import { SEARCH_PAGE_SIZE, rankOperations } from "./search.js";
23
+ import { argumentPathText, findInputType, inputTypeText, namedTypesIn, type InputTypes } from "./type-docs.js";
23
24
 
24
25
  export { projectFields } from "./fields.js";
25
26
 
@@ -589,8 +590,8 @@ export const SEARCH_DOCS_TOOL: ToolDefinition = {
589
590
 
590
591
  export const READ_DOCS_TOOL: ToolDefinition = {
591
592
  name: "read_docs",
592
- description: 'Read a documentation page: an operation reference (a tool name, or dotted "resource.method") or a docs-site guide page by name or URL. Long pages come in parts; pass the offset a part ends with to continue.',
593
- inputSchema: { type: "object", properties: { page: { type: "string" }, schema: { type: "boolean", description: "Also return the operation's complete input and output JSON Schemas, default false" }, offset: { type: "integer", minimum: 0, description: "Character offset to continue a long page from, default 0" } }, required: ["page"] },
593
+ description: 'Read a documentation page: an operation reference (a tool name, or dotted "resource.method"), a named input type such as a filter, or a docs-site guide page by name or URL. Long pages come in parts; pass the offset a part ends with to continue.',
594
+ inputSchema: { type: "object", properties: { page: { type: "string" }, path: { type: "string", description: "A dotted argument path within the operation or type, e.g. \"filter.team.key\": that field's type, with its named type's fields or values" }, schema: { type: "boolean", description: "Also return the operation's complete input and output JSON Schemas, default false" }, offset: { type: "integer", minimum: 0, description: "Character offset to continue a long page from, default 0" } }, required: ["page"] },
594
595
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
595
596
  };
596
597
 
@@ -709,7 +710,7 @@ export function toolDefinitions(ops: OpLike[], mode: "operations" | "meta", omit
709
710
  : "";
710
711
  return [
711
712
  { ...SEARCH_DOCS_TOOL, description: "Search this API's " + ops.length + " generated operations and, when a docs site is configured, its guides. Start here to find the operation you need." + coverage },
712
- { ...READ_DOCS_TOOL, description: "Read an operation's reference (arguments, authentication, safety, an example and the result's shape) by tool name, or a docs-site guide page. schema: true adds the complete JSON Schemas." },
713
+ { ...READ_DOCS_TOOL, description: "Read an operation's reference (arguments, authentication, safety, an example and the result's shape) by tool name, a named input type, or a docs-site guide page. path narrows to one nested argument, e.g. \"filter.team.key\"; schema: true adds the complete JSON Schemas." },
713
714
  execute,
714
715
  ];
715
716
  }
@@ -730,6 +731,55 @@ export function missingArguments(op: OpLike, args: Record<string, unknown>): str
730
731
 
731
732
  // ---- server instructions ----------------------------------------------------------
732
733
 
734
+ /** Identity fields that name the caller for a login-shaped "me" (GitHub's
735
+ * users_get_authenticated returns login). */
736
+ const IDENTITY_LOGIN_FIELDS = ["login", "username", "handle"];
737
+
738
+ const MAX_ME_SCHEMA_DEPTH = 6;
739
+
740
+ function acceptsString(schema: unknown): boolean {
741
+ if (!schema || typeof schema !== "object") return false;
742
+ const record = schema as Record<string, unknown>;
743
+ const type = record.type;
744
+ if (type === "string" || (Array.isArray(type) && type.includes("string"))) return true;
745
+ const variants = record.anyOf ?? record.oneOf;
746
+ return Array.isArray(variants) && variants.some(acceptsString);
747
+ }
748
+
749
+ function objectVariants(schema: unknown): Record<string, unknown>[] {
750
+ if (!schema || typeof schema !== "object") return [];
751
+ const record = schema as Record<string, unknown>;
752
+ const variants = record.anyOf ?? record.oneOf;
753
+ return [record, ...(Array.isArray(variants) ? variants.flatMap(objectVariants) : [])]
754
+ .filter((candidate) => candidate.properties && typeof candidate.properties === "object");
755
+ }
756
+
757
+ /** Every argument path where the server resolves "me": a user-shaped string
758
+ * (or a list or union that takes one) at the top level or inside an object
759
+ * argument. The instructions name exactly these. */
760
+ export function meReferenceArguments(ops: OpLike[]): string[] {
761
+ const found = new Set<string>();
762
+ const visit = (name: string, schema: unknown, path: string, depth: number): void => {
763
+ if (!schema || typeof schema !== "object" || depth > MAX_ME_SCHEMA_DEPTH) return;
764
+ const record = schema as Record<string, unknown>;
765
+ const items = record.items ?? (Array.isArray(record.anyOf ?? record.oneOf) ? ((record.anyOf ?? record.oneOf) as Record<string, unknown>[]).find((variant) => variant && variant.items)?.items : undefined);
766
+ if (userShapedReference(name) && (acceptsString(record) || acceptsString(items))) found.add(path);
767
+ const nested: [Record<string, unknown>[], string][] = [[objectVariants(record), path], [objectVariants(items), path + "[]"]];
768
+ for (const [variants, prefix] of nested) {
769
+ for (const variant of variants) {
770
+ for (const [key, child] of Object.entries(variant.properties as Record<string, unknown>)) visit(key, child, prefix + "." + key, depth + 1);
771
+ }
772
+ }
773
+ };
774
+ for (const op of ops) {
775
+ const properties = (op.inputSchema.properties ?? {}) as Record<string, unknown>;
776
+ for (const param of op.params) {
777
+ if (param.resolve !== false) visit(param.name, properties[param.name], param.name, 0);
778
+ }
779
+ }
780
+ return [...found].sort((a, b) => a.split(".").length - b.split(".").length || a.localeCompare(b));
781
+ }
782
+
733
783
  export interface InstructionsInput {
734
784
  title: string;
735
785
  /** Callable operations, used to advertise only capabilities the surface has. */
@@ -770,8 +820,9 @@ export function serverInstructions(input: InstructionsInput): string {
770
820
  }
771
821
  parts.push("Arguments use the API's wire names; an unknown, mistyped or missing argument returns an isError result listing each problem (nothing is dropped silently), and obvious forms are coerced (\"true\" to boolean, \"3\" to number, enum case).");
772
822
  if (input.referenceResolution) parts.push("Reference arguments marked in their schema accept either an ID or an exact case-insensitive name, slug, key or email; the server resolves one match through the named list tool, reports multiple candidates, and never guesses fuzzily.");
773
- if (input.identityTool && input.ops.some((op) => op.params.some((param) => param.type === "string" && param.resolve !== false && userShapedReference(param.name)))) {
774
- parts.push("User-shaped reference arguments also accept \"me\", resolved through " + input.identityTool + ".");
823
+ const meArguments = input.identityTool ? meReferenceArguments(input.ops) : [];
824
+ if (meArguments.length > 0) {
825
+ parts.push("These arguments also accept \"me\" for the caller, resolved through " + input.identityTool + " (ID arguments take its id, others its login when it has one): " + meArguments.join(", ") + ".");
775
826
  }
776
827
  parts.push("Paginated tools return items, hasMore and nextPage (the exact arguments for the following page). Pass fields (dotted paths) to keep only the result keys you need; oversized results are cut to whole items or keys with a truncated note saying how to ask for less.");
777
828
  parts.push("Errors carry error, code, message, status, the API's body and next_steps.");
@@ -876,7 +927,7 @@ export function prepareCall(
876
927
  // Patterns apply inside objects and arrays. A top-level argument's own
877
928
  // pattern is left to the API: its documented example values do not all
878
929
  // satisfy it yet, and a name or "me" is only resolved to an ID later.
879
- args[name] = checkValue(value, propSchema, name, issues, { skipPattern: true });
930
+ args[name] = checkValue(value, propSchema, name, issues, { skipPattern: true, meName: name });
880
931
  }
881
932
 
882
933
  // A path argument that is the configured credential's username (Twilio's
@@ -1008,27 +1059,36 @@ export async function resolveReferences(
1008
1059
  const args = { ...preparedArgs };
1009
1060
  const maxPages = Math.max(1, Math.floor(options.maxPages ?? 5));
1010
1061
  const identity = options.identityTool ? findOperation(options.ops, options.identityTool) : undefined;
1011
- /** The caller's ID for "me", fetched once per cache. */
1012
- const callerId = async (argument: string): Promise<{ id: string | number } | { outcome: ToolOutcome }> => {
1013
- const cacheKey = "me:" + identity!.tool;
1062
+ let identityBody: Record<string, unknown> | undefined;
1063
+ /** The caller's ID, or login for a login-shaped name (GitHub's owner and
1064
+ * assignees), for "me": one identity call per cache. */
1065
+ const callerId = async (argument: string, key: string): Promise<{ id: string | number } | { outcome: ToolOutcome }> => {
1066
+ const field = meIdentityField(key);
1067
+ const cacheKey = "me:" + identity!.tool + (field === "id" ? "" : ":" + field);
1014
1068
  const cached = options.cache.get(cacheKey);
1015
1069
  if (cached !== undefined) return { id: cached };
1016
- const outcome = await options.runOperation(identity!, {});
1017
- if (outcome.isError) return { outcome };
1018
- const body = outcomeValue(outcome);
1019
- const id = body && typeof body === "object" && !Array.isArray(body) ? (body as Record<string, unknown>).id : undefined;
1070
+ if (!identityBody) {
1071
+ const outcome = await options.runOperation(identity!, {});
1072
+ if (outcome.isError) return { outcome };
1073
+ const body = outcomeValue(outcome);
1074
+ identityBody = body && typeof body === "object" && !Array.isArray(body) ? body as Record<string, unknown> : {};
1075
+ }
1076
+ const login = IDENTITY_LOGIN_FIELDS.map((name) => identityBody![name]).find((value) => typeof value === "string" && value !== "");
1077
+ const id = field === "login" && login !== undefined ? login : identityBody.id;
1020
1078
  if (typeof id !== "string" && typeof id !== "number") {
1021
- return { outcome: referenceError("NOT_AVAILABLE", `${identity!.tool} did not return a top-level id, so "me" cannot be resolved for ${argument}.`, argument, ["Pass the caller's exact ID instead."]) };
1079
+ const expected = field === "id" ? "a top-level id" : "a top-level " + IDENTITY_LOGIN_FIELDS.join(", ") + " or id";
1080
+ return { outcome: referenceError("NOT_AVAILABLE", `${identity!.tool} did not return ${expected}, so "me" cannot be resolved for ${argument}.`, argument, [field === "id" ? "Pass the caller's exact ID instead." : "Pass the caller's exact login instead."]) };
1022
1081
  }
1023
1082
  putReferenceCache(options.cache, cacheKey, id);
1024
1083
  return { id };
1025
1084
  };
1026
- /** "me" inside an object argument (a GraphQL input's assigneeId, or each
1027
- * of its subscriberIds): the same resolution, keyed by property name. */
1085
+ /** "me" inside an object or array argument (a GraphQL input's assigneeId,
1086
+ * each of its subscriberIds, each of GitHub's assignees): the same
1087
+ * resolution, keyed by property name. */
1028
1088
  const resolveNestedMe = async (value: unknown, key: string, path: string): Promise<{ value: unknown } | { outcome: ToolOutcome }> => {
1029
1089
  if (typeof value === "string") {
1030
1090
  if (!isMeReference(key, value)) return { value };
1031
- const caller = await callerId(path);
1091
+ const caller = await callerId(path, key);
1032
1092
  return "outcome" in caller ? caller : { value: caller.id };
1033
1093
  }
1034
1094
  if (Array.isArray(value)) {
@@ -1063,7 +1123,7 @@ export async function resolveReferences(
1063
1123
  const value = raw.trim();
1064
1124
 
1065
1125
  if (identity && isMeReference(param.name, value)) {
1066
- const caller = await callerId(param.name);
1126
+ const caller = await callerId(param.name, param.name);
1067
1127
  if ("outcome" in caller) return { ok: false, outcome: caller.outcome };
1068
1128
  args[param.name] = caller.id;
1069
1129
  continue;
@@ -1161,6 +1221,10 @@ export interface ResultOptions {
1161
1221
  /** Paths the result may lack, such as a server's default projection:
1162
1222
  * they are left out rather than reported. */
1163
1223
  optionalFields?: string[];
1224
+ /** The tool's outputSchema. A fields path it declares may be absent from
1225
+ * the result (an optional key) without being reported; a page checks its
1226
+ * items against the schema's items. */
1227
+ outputSchema?: Record<string, unknown>;
1164
1228
  /** Arguments nextPage must repeat from args (required path parameters
1165
1229
  * such as owner and repo; fields is repeated too): the next-page
1166
1230
  * parameters alone, a page_url for instance, would fail validation when
@@ -1169,31 +1233,52 @@ export interface ResultOptions {
1169
1233
  }
1170
1234
 
1171
1235
  /** A fields path that selects nothing is an error that names the keys
1172
- * that exist, not a silent {}. The API call already happened, so the error
1173
- * says so, and a write's unprojected result comes back with it. */
1174
- function unmatchedFieldsOutcome(value: unknown, fields: string[][] | null, perItem: boolean, options: ResultOptions): ToolOutcome | null {
1236
+ * that exist, not a silent {}. A path the response schema declares is not
1237
+ * one: an optional key no item has is simply absent. The API call already
1238
+ * happened, so the error says so and carries the unprojected result (as
1239
+ * much as fits under half the cap), so neither a read nor a write has to
1240
+ * run again to see it. */
1241
+ function unmatchedFieldsOutcome(value: unknown, fields: string[][] | null, perItem: boolean, options: ResultOptions, schema: unknown, page?: { nextPage: Record<string, unknown> | null }): ToolOutcome | null {
1175
1242
  if (fields === null) return null;
1176
1243
  const optional = new Set(options.optionalFields ?? []);
1177
- const unmatched = unmatchedFields(value, fields).filter((u) => !optional.has(u.path));
1244
+ const unmatched = unmatchedFields(value, fields, schema).filter((u) => !optional.has(u.path));
1178
1245
  if (unmatched.length === 0) return null;
1179
1246
  const write = options.safety !== "read";
1180
1247
  const maxChars = options.maxChars ?? DEFAULT_MAX_RESULT_CHARS;
1181
- const full = write ? JSON.stringify(value) : undefined;
1248
+ const retained = retainedResult(value, Math.floor(maxChars / 2));
1249
+ const steps: string[] = [];
1250
+ if (write) steps.push("This operation has already run; do not call it again to change fields." + (retained.whole ? " Its full result is in result." : retained.result !== undefined ? " The first items of its result are in result." : ""));
1251
+ else if (retained.whole) steps.push("The full result is in result; use it rather than calling again.");
1252
+ else if (retained.result !== undefined) steps.push("result holds the first " + (retained.result as unknown[]).length + " of " + (value as unknown[]).length + " items; the rest are over the size cap.");
1253
+ steps.push((write ? "Next time, use" : "To project a later call, use") + " fields from the available keys" + (perItem ? " (fields apply to each item)" : "") + ", or omit fields for the whole result.");
1182
1254
  const structured = {
1183
1255
  error: "UnmatchedFields",
1184
1256
  code: "FIELDS_UNMATCHED",
1185
1257
  message: unmatchedFieldsMessage(unmatched, perItem),
1186
1258
  unmatched,
1187
- ...(full !== undefined && full.length <= maxChars / 2 ? { result: value } : {}),
1188
- next_steps: [
1189
- ...(write
1190
- ? ["This operation has already run; do not call it again to change fields." + (full !== undefined && full.length <= maxChars / 2 ? " Its full result is in result." : "")]
1191
- : ["Call again with fields from the available keys" + (perItem ? " (fields apply to each item)" : "") + ", or omit fields for the whole result."]),
1192
- ],
1259
+ ...(retained.result !== undefined ? { result: retained.result } : {}),
1260
+ ...(retained.omitted ? { result_omitted: retained.omitted } : {}),
1261
+ ...(page ? { hasMore: page.nextPage !== null, ...(page.nextPage !== null ? { nextPage: page.nextPage } : {}) } : {}),
1262
+ ...(options.requestId ? { request_id: options.requestId } : {}),
1263
+ next_steps: steps,
1193
1264
  };
1194
1265
  return { text: JSON.stringify(structured), isError: true, structured };
1195
1266
  }
1196
1267
 
1268
+ /** The unprojected result an unmatched-fields error carries: whole when it
1269
+ * fits the budget, the leading items of a list when only some do, and
1270
+ * nothing for an object over the budget. */
1271
+ function retainedResult(value: unknown, budget: number): { result?: unknown; whole: boolean; omitted?: number } {
1272
+ const text = JSON.stringify(value);
1273
+ if (text === undefined) return { whole: false };
1274
+ if (text.length <= budget) return { result: value, whole: true };
1275
+ if (!Array.isArray(value)) return { whole: false };
1276
+ const k = itemsThatFit(value, budget);
1277
+ const head = value.slice(0, k);
1278
+ if (JSON.stringify(head).length > budget) return { whole: false };
1279
+ return { result: head, whole: false, omitted: value.length - k };
1280
+ }
1281
+
1197
1282
  const fieldsHint = (perItem: boolean) => "Pass fields (dotted paths" + (perItem ? ", applied per item" : "") + ") to keep only the keys you need.";
1198
1283
 
1199
1284
  /** How many leading items fit under `budget` characters once serialized
@@ -1228,7 +1313,8 @@ export function pageOutcome(items: unknown[], nextPage: Record<string, unknown>
1228
1313
  }
1229
1314
  const fields = options.fields ?? null;
1230
1315
  const maxChars = options.maxChars ?? DEFAULT_MAX_RESULT_CHARS;
1231
- const unmatched = unmatchedFieldsOutcome(items, fields, true, options);
1316
+ const pageSchema = options.outputSchema?.properties as Record<string, { items?: unknown }> | undefined;
1317
+ const unmatched = unmatchedFieldsOutcome(items, fields, true, options, pageSchema?.items, { nextPage });
1232
1318
  if (unmatched) return unmatched;
1233
1319
  const shown = projectFields(items, fields) as unknown[];
1234
1320
  const full = {
@@ -1287,7 +1373,7 @@ const omittedMarker = (key: string, chars: number, maxChars: number) => chars >
1287
1373
  export function dataOutcome(data: unknown, options: ResultOptions = {}): ToolOutcome {
1288
1374
  const fields = options.fields ?? null;
1289
1375
  const maxChars = options.maxChars ?? DEFAULT_MAX_RESULT_CHARS;
1290
- const unmatched = data !== undefined && data !== null ? unmatchedFieldsOutcome(data, fields, Array.isArray(data), options) : null;
1376
+ const unmatched = data !== undefined && data !== null ? unmatchedFieldsOutcome(data, fields, Array.isArray(data), options, options.outputSchema) : null;
1291
1377
  if (unmatched) return unmatched;
1292
1378
  const value = data !== undefined && data !== null ? projectFields(data, fields) : { ok: true };
1293
1379
  if (typeof value === "string") {
@@ -1444,6 +1530,28 @@ function payloadFailureCode(code: unknown): ErrorCode | undefined {
1444
1530
  return undefined;
1445
1531
  }
1446
1532
 
1533
+ /** The vendor's own code on an in-band GraphQL error: the first error's
1534
+ * extensions.code, or its type (GitHub). */
1535
+ function graphqlVendorCode(error: unknown): string | undefined {
1536
+ const first = (error as { errors?: { extensions?: { code?: unknown }; type?: unknown }[] } | null)?.errors?.[0];
1537
+ const code = first?.extensions?.code ?? first?.type;
1538
+ return typeof code === "string" && code !== "" ? code : undefined;
1539
+ }
1540
+
1541
+ /** GraphQL error codes whose meaning is settled (Apollo's standard codes,
1542
+ * GitHub's types, Linear's codes), by recovery class. Any other code is
1543
+ * passed through as is, with no guessed next step. The CLI's
1544
+ * classification (cli-agent.ts) uses the same table. */
1545
+ function graphqlErrorClass(code: string): "not_found" | "unauthenticated" | "forbidden" | "bad_input" | "rate_limited" | undefined {
1546
+ const c = code.toUpperCase();
1547
+ if (c === "NOT_FOUND") return "not_found";
1548
+ if (c === "UNAUTHENTICATED" || c === "AUTHENTICATION_ERROR") return "unauthenticated";
1549
+ if (c === "FORBIDDEN") return "forbidden";
1550
+ if (c === "BAD_USER_INPUT" || c === "GRAPHQL_VALIDATION_FAILED" || c === "GRAPHQL_PARSE_FAILED" || c === "INPUT_ERROR") return "bad_input";
1551
+ if (c === "RATE_LIMITED" || c === "RATELIMITED") return "rate_limited";
1552
+ return undefined;
1553
+ }
1554
+
1447
1555
  /** Classify an SDK error result by status: the code and what to do next. */
1448
1556
  export function classifyError(error: unknown, context: ErrorContext = {}): { code: ErrorCode; nextSteps: string[] } {
1449
1557
  const e = (error ?? {}) as { name?: string; message?: string; status?: number; code?: unknown; body?: unknown; violations?: unknown; rateLimit?: { retryAt?: Date } };
@@ -1467,13 +1575,24 @@ export function classifyError(error: unknown, context: ErrorContext = {}): { cod
1467
1575
  if (reported === "RATE_LIMITED") return { code: "RATE_LIMITED", nextSteps: [rateLimitNextStep(undefined)] };
1468
1576
  return { code: "CALL_FAILED", nextSteps: ["The API reported a failure in a successful response; body says why. Do not treat the call as done."] };
1469
1577
  }
1470
- if (status === 401) {
1471
- return context.hadCredential
1472
- ? { code: "AUTH_INVALID", nextSteps: ["The credential was rejected; it may be expired or for another environment." + (auth ? " " + auth : "")] }
1473
- : { code: "NO_AUTH", nextSteps: ["No credential was sent." + (auth ? " " + auth : "")] };
1578
+ const unauthenticated = (): { code: ErrorCode; nextSteps: string[] } => context.hadCredential
1579
+ ? { code: "AUTH_INVALID", nextSteps: ["The credential was rejected; it may be expired or for another environment." + (auth ? " " + auth : "")] }
1580
+ : { code: "NO_AUTH", nextSteps: ["No credential was sent." + (auth ? " " + auth : "")] };
1581
+ const forbidden = (): { code: ErrorCode; nextSteps: string[] } => scopes.length ? scopeFailure : { code: "AUTH_INVALID", nextSteps: ["The credential lacks access to this operation; it is not a retryable error."] };
1582
+ // An in-band GraphQL error (HTTP 200): classified by the vendor's code.
1583
+ if (e.name === "GraphQLRequestError") {
1584
+ const vendor = graphqlVendorCode(error);
1585
+ switch (vendor === undefined ? undefined : graphqlErrorClass(vendor)) {
1586
+ case "not_found": return { code: "NOT_FOUND", nextSteps: ["Check the id in the arguments; list the resource first to find the right one."] };
1587
+ case "unauthenticated": return unauthenticated();
1588
+ case "forbidden": return forbidden();
1589
+ case "bad_input": return { code: "INVALID_REQUEST", nextSteps: ["The API rejected an argument or the selection; the errors in body name it (message, path). Fix that and call again."] };
1590
+ case "rate_limited": return { code: "RATE_LIMITED", nextSteps: [rateLimitNextStep(undefined)] };
1591
+ default: return { code: "CALL_FAILED", nextSteps: [] };
1592
+ }
1474
1593
  }
1475
- if (status === 403 && scopes.length) return scopeFailure;
1476
- if (status === 403) return { code: "AUTH_INVALID", nextSteps: ["The credential lacks access to this operation; it is not a retryable error."] };
1594
+ if (status === 401) return unauthenticated();
1595
+ if (status === 403) return forbidden();
1477
1596
  if (status === 402) return { code: "PLAN_LIMIT", nextSteps: ["The account's plan stops here; the body may name where to lift the limit. Do not retry the same call as is."] };
1478
1597
  if (status === 404) return { code: "NOT_FOUND", nextSteps: notFoundNextSteps(message, e.body) };
1479
1598
  if (status === 422 && body?.errors?.[0]?.code === "spec_error") {
@@ -1531,9 +1650,12 @@ export function errorOutcome(error: unknown, context: ErrorContext = {}): ToolOu
1531
1650
  ? (e.body as Record<string, unknown>).request_id ?? (e.body as Record<string, unknown>).requestId
1532
1651
  : undefined;
1533
1652
  const requestId = e?.response?.requestId ?? (typeof bodyRequestId === "string" ? bodyRequestId : undefined);
1653
+ // The vendor's identity for the failure, next to the normalized code.
1654
+ const vendorCode = e?.name === "GraphQLRequestError" ? graphqlVendorCode(error) : undefined;
1534
1655
  const structured = {
1535
1656
  error: e?.name ?? "Error",
1536
1657
  code,
1658
+ ...(vendorCode ? { vendor_code: vendorCode } : {}),
1537
1659
  message: e?.message,
1538
1660
  ...(typeof e?.status === "number" ? { status: e.status } : {}),
1539
1661
  ...(requestId ? { request_id: requestId } : {}),
@@ -1570,6 +1692,8 @@ export interface DocsSource {
1570
1692
  hiddenOps?: HiddenOperation[];
1571
1693
  /** Count generated before runtime surface filters. */
1572
1694
  generatedOperationCount?: number;
1695
+ /** Named input types for type and argument-path lookups. */
1696
+ inputTypes?: InputTypes;
1573
1697
  /** Base URL of the docs site, or null when none is configured. */
1574
1698
  docsUrl(): string | null;
1575
1699
  /** Exact llms.txt URL when it is not at <docsUrl>/llms.txt. */
@@ -1617,10 +1741,56 @@ const REFERENCE_ARGUMENT_BUDGET = 6_000;
1617
1741
  /** Enum values listed inline; longer enums are cut with a count. */
1618
1742
  const REFERENCE_ENUM_VALUES = 30;
1619
1743
 
1620
- /** Prose for one description: one line, markdown links reduced to their text. */
1621
- function referenceProse(text: unknown): string {
1744
+ /** HTML that API descriptions carry for formatting only. Anything else
1745
+ * in angle brackets (a <placeholder>) is text and stays. */
1746
+ const FORMATTING_TAGS = /<\/?(?:a|abbr|b|br|code|div|em|i|li|ol|p|pre|small|span|strong|sub|sup|u|ul)(?:\s[^<>]*)?\/?>/gi;
1747
+ /** A Markdown link or image: [text](url "title"), where the URL may hold
1748
+ * one level of parentheses (Wikipedia's Foo_(bar)). */
1749
+ const MARKDOWN_LINK = /!?\[([^\[\]]*(?:\[[^\[\]]*\][^\[\]]*)*)\]\((?:[^()\s]|\([^()\s]*\))*(?:\s+"[^"]*")?\)/g;
1750
+
1751
+ /** Prose for one description, as one line: Markdown and HTML links reduced
1752
+ * to their visible text, formatting tags and emphasis dropped, <code> as
1753
+ * inline code (kept, because agents copy it). */
1754
+ export function referenceProse(text: unknown): string {
1622
1755
  if (typeof text !== "string") return "";
1623
- return text.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\s+/g, " ").trim();
1756
+ return text
1757
+ .replace(/<code>([\s\S]*?)<\/code>/gi, "`$1`")
1758
+ .replace(/<br\s*\/?>/gi, " ")
1759
+ .replace(FORMATTING_TAGS, "")
1760
+ .replace(MARKDOWN_LINK, "$1")
1761
+ .replace(/\[([^\[\]]+)\]\[[^\[\]]*\]/g, "$1")
1762
+ .replace(/(\*\*|__)(?=\S)([^*_]*?\S)\1/g, "$2")
1763
+ .replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, "\"").replace(/&#39;/g, "'").replace(/&amp;/g, "&")
1764
+ .replace(/\s+/g, " ")
1765
+ .trim();
1766
+ }
1767
+
1768
+ /** Abbreviations whose period does not end a sentence. */
1769
+ const ABBREVIATION = /(?:^|[\s(])(?:e\.g|i\.e|etc|vs|approx|incl|cf|no|min|max)\.$/i;
1770
+
1771
+ /** A description's sentences, every character kept: a sentence ends at
1772
+ * . ! or ? (closing quotes and brackets included) before whitespace, not
1773
+ * after an abbreviation, and never inside inline code. */
1774
+ function sentencesOf(prose: string): string[] {
1775
+ const sentences: string[] = [];
1776
+ let current = "";
1777
+ for (const part of prose.split(/(?<=[.!?]["')\]]?)\s+/)) {
1778
+ current = current ? current + " " + part : part;
1779
+ const openCode = (current.match(/`/g) ?? []).length % 2 === 1;
1780
+ if (!openCode && !ABBREVIATION.test(current)) {
1781
+ sentences.push(current);
1782
+ current = "";
1783
+ }
1784
+ }
1785
+ if (current) sentences.push(current);
1786
+ return sentences;
1787
+ }
1788
+
1789
+ /** Cut one long sentence at a word boundary, never inside inline code. */
1790
+ function cutSentence(sentence: string, limit: number): string {
1791
+ let cut = sentence.slice(0, limit).replace(/\s+\S*$/, "");
1792
+ if ((cut.match(/`/g) ?? []).length % 2 === 1) cut = cut.slice(0, cut.lastIndexOf("`")).trimEnd();
1793
+ return cut.replace(/[\s,;:(]+$/, "") + "…";
1624
1794
  }
1625
1795
 
1626
1796
  /** Sentences the generator appends to an argument's description because a
@@ -1633,18 +1803,16 @@ const REFERENCE_ARGUMENT_PROSE = 160;
1633
1803
  /** An argument's description, cut to its leading sentences within the
1634
1804
  * budget plus every note the generator added. The full text is in the
1635
1805
  * schema (schema: true). */
1636
- function argumentProse(text: unknown): string {
1806
+ export function argumentProse(text: unknown): string {
1637
1807
  const prose = referenceProse(text);
1638
1808
  if (prose.length <= REFERENCE_ARGUMENT_PROSE) return prose;
1639
- const sentences = prose.match(/[^.!?]+(?:[.!?]+(?=\s|$)|$)\s*/g) ?? [prose];
1640
1809
  const kept: string[] = [];
1641
1810
  let used = 0;
1642
1811
  let cut = false;
1643
- for (const raw of sentences) {
1644
- const sentence = raw.trim();
1812
+ for (const sentence of sentencesOf(prose)) {
1645
1813
  if (REFERENCE_NOTE.test(sentence)) { kept.push(sentence); continue; }
1646
1814
  if (kept.length === 0 || used + sentence.length <= REFERENCE_ARGUMENT_PROSE) {
1647
- kept.push(sentence.length > REFERENCE_ARGUMENT_PROSE * 2 ? sentence.slice(0, REFERENCE_ARGUMENT_PROSE * 2).replace(/\s+\S*$/, "") + "…" : sentence);
1815
+ kept.push(sentence.length > REFERENCE_ARGUMENT_PROSE * 2 ? cutSentence(sentence, REFERENCE_ARGUMENT_PROSE * 2) : sentence);
1648
1816
  used += sentence.length;
1649
1817
  } else {
1650
1818
  cut = true;
@@ -1688,9 +1856,13 @@ function referenceObject(schema: Record<string, unknown>): Record<string, unknow
1688
1856
  return undefined;
1689
1857
  }
1690
1858
 
1859
+ /** Where an argument line sits: the operation, the dotted path to it,
1860
+ * and the named input type of the object that holds it, when known. */
1861
+ interface ArgumentContext { tool: string; types?: InputTypes; path: string[]; typeName?: string }
1862
+
1691
1863
  /** One line per argument, nested fields indented beneath their object.
1692
1864
  * Each top-level argument is spelled out as deep as fits its budget. */
1693
- function referenceArguments(schema: Record<string, unknown>, lines: string[]): void {
1865
+ function referenceArguments(schema: Record<string, unknown>, lines: string[], context: ArgumentContext): void {
1694
1866
  const properties = (schema.properties ?? {}) as Record<string, Record<string, unknown>>;
1695
1867
  for (const name of Object.keys(properties)) {
1696
1868
  const only = { ...schema, properties: { [name]: properties[name] } };
@@ -1699,14 +1871,14 @@ function referenceArguments(schema: Record<string, unknown>, lines: string[]): v
1699
1871
  // below the cut; then one level with names only.
1700
1872
  for (const [depth, names] of [[REFERENCE_DEPTH, true], [2, true], [2, false], [1, true]] as const) {
1701
1873
  block = [];
1702
- referenceArgumentLines(only, 0, depth, names, " ", block);
1874
+ referenceArgumentLines(only, 0, depth, names, " ", block, context);
1703
1875
  if (block.join("\n").length <= REFERENCE_ARGUMENT_BUDGET) break;
1704
1876
  }
1705
1877
  lines.push(...block);
1706
1878
  }
1707
1879
  }
1708
1880
 
1709
- function referenceArgumentLines(schema: Record<string, unknown>, depth: number, maxDepth: number, names: boolean, indent: string, lines: string[]): void {
1881
+ function referenceArgumentLines(schema: Record<string, unknown>, depth: number, maxDepth: number, names: boolean, indent: string, lines: string[], context: ArgumentContext): void {
1710
1882
  const properties = (schema.properties ?? {}) as Record<string, Record<string, unknown>>;
1711
1883
  const required = new Set(Array.isArray(schema.required) ? schema.required as string[] : []);
1712
1884
  for (const [name, child] of Object.entries(properties)) {
@@ -1716,16 +1888,27 @@ function referenceArgumentLines(schema: Record<string, unknown>, depth: number,
1716
1888
  ...(child.default !== undefined && !/\bdefault\b/i.test(description) ? ["default " + JSON.stringify(child.default)] : []),
1717
1889
  ...(child.deprecated === true && !/deprecated/i.test(description) ? ["deprecated"] : []),
1718
1890
  ];
1719
- lines.push(indent + name + " (" + referenceType(child) + (required.has(name) ? ", required" : "") + (extras.length ? ", " + extras.join(", ") : "") + ")" + (description ? ": " + description : ""));
1720
1891
  const nested = referenceObject(child);
1892
+ // An object argument reads as its named type (IssueFilter), which
1893
+ // read_docs can look up; everything else keeps its inline type.
1894
+ const expression = context.path.length === 0
1895
+ ? context.types?.args[context.tool]?.[name]
1896
+ : context.typeName ? context.types?.types[context.typeName]?.fields?.[name]?.type : undefined;
1897
+ const named = nested ? namedTypesIn(context.types, expression) : [];
1898
+ const type = named.length > 0 ? expression! : referenceType(child);
1899
+ lines.push(indent + name + " (" + type + (required.has(name) ? ", required" : "") + (extras.length ? ", " + extras.join(", ") : "") + ")" + (description ? ": " + description : ""));
1721
1900
  if (!nested) continue;
1901
+ const path = [...context.path, name];
1902
+ const objects = named.filter((typeName) => context.types!.types[typeName]!.fields);
1903
+ const inner: ArgumentContext = { ...context, path, typeName: objects.length === 1 ? objects[0] : undefined };
1722
1904
  if (depth + 1 < maxDepth) {
1723
- referenceArgumentLines(nested, depth + 1, maxDepth, names, indent + " ", lines);
1905
+ referenceArgumentLines(nested, depth + 1, maxDepth, names, indent + " ", lines, inner);
1724
1906
  continue;
1725
1907
  }
1726
1908
  if (!names) continue;
1727
1909
  const keys = Object.keys(nested.properties as object);
1728
- lines.push(indent + " fields: " + keys.slice(0, 40).join(", ") + (keys.length > 40 ? ", … " + (keys.length - 40) + " more" : "") + " (types with schema: true)");
1910
+ // Field types (and any cut names) are one path lookup away.
1911
+ lines.push(indent + " fields: " + keys.slice(0, 40).join(", ") + (keys.length > 40 ? ", … " + (keys.length - 40) + " more: read_docs " + JSON.stringify({ page: context.tool, path: path.join(".") }) + " lists all" : ""));
1729
1912
  }
1730
1913
  }
1731
1914
 
@@ -1758,7 +1941,7 @@ function referenceShape(schema: Record<string, unknown> | undefined, depth = 0):
1758
1941
  * Schemas come with `schema: true`, as the CLI's `docs --schema` does; they
1759
1942
  * cost several times the rest and an agent rarely needs them to call.
1760
1943
  */
1761
- export function referenceText(op: OpLike, options: { schema?: boolean } = {}): string {
1944
+ export function referenceText(op: OpLike, options: { schema?: boolean; types?: InputTypes } = {}): string {
1762
1945
  const safety = operationSafety(op);
1763
1946
  // A credential-defaulted argument (Twilio's AccountSid) is left out, so the
1764
1947
  // example shows the call an agent should make.
@@ -1776,7 +1959,7 @@ export function referenceText(op: OpLike, options: { schema?: boolean } = {}): s
1776
1959
  const input = toolInputSchema(op);
1777
1960
  if (Object.keys((input.properties ?? {}) as object).length > 0) {
1778
1961
  lines.push("", "Arguments:");
1779
- referenceArguments(input, lines);
1962
+ referenceArguments(input, lines, { tool: op.tool, types: options.types, path: [] });
1780
1963
  }
1781
1964
  lines.push("", "Example arguments: " + JSON.stringify(example));
1782
1965
  if (op.outputSchema) {
@@ -1786,6 +1969,15 @@ export function referenceText(op: OpLike, options: { schema?: boolean } = {}): s
1786
1969
  lines.push("", "Input schema: " + JSON.stringify(input));
1787
1970
  if (op.outputSchema) lines.push("", "Output schema: " + JSON.stringify(op.outputSchema));
1788
1971
  } else {
1972
+ // Say how to drill into an object argument: the page above stops at a
1973
+ // depth and a size, and the schema is the whole graph at once.
1974
+ const nested = Object.entries((input.properties ?? {}) as Record<string, Record<string, unknown>>)
1975
+ .find(([, child]) => child && typeof child === "object" && referenceObject(child))?.[0];
1976
+ if (nested) {
1977
+ const named = namedTypesIn(options.types, options.types?.args[op.tool]?.[nested])[0];
1978
+ lines.push("", "Nested arguments: read_docs " + JSON.stringify({ page: op.tool, path: nested }) + " gives an argument's type and all its fields; extend the path (\"" + nested + ".<field>\") to go deeper."
1979
+ + (named ? " Named types read the same way: read_docs " + JSON.stringify({ page: named }) + "." : ""));
1980
+ }
1789
1981
  lines.push("", "Full input and output JSON Schemas: read_docs " + JSON.stringify({ page: op.tool, schema: true }) + ".");
1790
1982
  }
1791
1983
  return lines.join("\n");
@@ -1857,20 +2049,48 @@ export const READ_DOCS_LIMIT = 20_000;
1857
2049
 
1858
2050
  /** One part of a long page, ending with how to read the next part. */
1859
2051
  function docsPart(page: string, text: string, offset: number, schema = false): string {
2052
+ return docsPartFor({ page, ...(schema ? { schema: true } : {}) }, text, offset);
2053
+ }
2054
+
2055
+ /** docsPart for any read_docs arguments (a page, a path, schema). */
2056
+ function docsPartFor(request: Record<string, unknown>, text: string, offset: number): string {
1860
2057
  if (offset <= 0 && text.length <= READ_DOCS_LIMIT) return text;
1861
2058
  const start = Math.min(Math.max(0, offset), text.length);
1862
2059
  const end = Math.min(text.length, start + READ_DOCS_LIMIT);
1863
2060
  const more = end < text.length
1864
- ? "\n\n[Characters " + start + "-" + end + " of " + text.length + ". Continue with read_docs " + JSON.stringify({ page, ...(schema ? { schema: true } : {}), offset: end }) + ".]"
2061
+ ? "\n\n[Characters " + start + "-" + end + " of " + text.length + ". Continue with read_docs " + JSON.stringify({ ...request, offset: end }) + ".]"
1865
2062
  : "\n\n[Characters " + start + "-" + end + " of " + text.length + "; end of page.]";
1866
2063
  return text.slice(start, end) + more;
1867
2064
  }
1868
2065
 
1869
- export async function docsRead(source: DocsSource, page: string, offset = 0, options: { schema?: boolean } = {}): Promise<ToolOutcome> {
2066
+ /** How read_docs names a type lookup, for the pages that mention types. */
2067
+ function readDocsTypeHint(name: string): string {
2068
+ return "read_docs " + JSON.stringify({ page: name });
2069
+ }
2070
+
2071
+ /** One argument path within an operation or a named type. */
2072
+ function docsPathOutcome(source: DocsSource, root: { tool: string; inputSchema: Record<string, unknown> } | { type: string }, page: string, path: string, offset: number): ToolOutcome {
2073
+ const found = argumentPathText(source.inputTypes, root, path, readDocsTypeHint);
2074
+ if (found.ok) return { text: docsPartFor({ page, path }, found.text, offset), isError: false };
2075
+ const structured = {
2076
+ error: "NotFoundError", code: "NOT_FOUND", message: found.message,
2077
+ ...(found.available.length > 0 ? { available: found.available } : {}),
2078
+ next_steps: [found.available.length > 0 ? "Pass one of the available fields as the next path segment." : "read_docs " + JSON.stringify({ page }) + " lists the arguments."],
2079
+ };
2080
+ return { text: JSON.stringify(structured), isError: true, structured };
2081
+ }
2082
+
2083
+ export async function docsRead(source: DocsSource, page: string, offset = 0, options: { schema?: boolean; path?: string } = {}): Promise<ToolOutcome> {
1870
2084
  const opMatch = findOperation(source.ops, page);
1871
- if (opMatch) return { text: docsPart(page, referenceText(opMatch, options), offset, options.schema === true), isError: false };
2085
+ const typeMatch = opMatch ? undefined : findInputType(source.inputTypes, page);
2086
+ if (options.path !== undefined && options.path.trim() !== "") {
2087
+ if (opMatch) return docsPathOutcome(source, opMatch, page, options.path, offset);
2088
+ if (typeMatch) return docsPathOutcome(source, { type: typeMatch }, page, options.path, offset);
2089
+ }
2090
+ if (opMatch) return { text: docsPart(page, referenceText(opMatch, { schema: options.schema, types: source.inputTypes }), offset, options.schema === true), isError: false };
1872
2091
  const omittedMatch = findOperation(source.omittedOps ?? [], page);
1873
2092
  if (omittedMatch) return omittedPlanLimit(source, [omittedMatch], omittedMatch.tool);
2093
+ if (typeMatch) return { text: docsPartFor({ page }, inputTypeText(source.inputTypes!, typeMatch, readDocsTypeHint), offset), isError: false };
1874
2094
  let target = page;
1875
2095
  if (!/^https?:\/\//.test(target)) {
1876
2096
  const index = await fetchDocs(source, "llms.txt");
@@ -1909,7 +2129,8 @@ export async function callSharedTool(
1909
2129
  const hidden = findHidden(source, args.page);
1910
2130
  if (hidden) return hiddenOutcome(hidden);
1911
2131
  }
1912
- return typeof args.page === "string" ? docsRead(source, args.page, offset, { schema }) : argumentsError({ tool: name }, [{ code: "MISSING_ARGUMENT", argument: "page", message: "read_docs requires a page string." }]);
2132
+ const path = typeof args.path === "string" ? args.path : undefined;
2133
+ return typeof args.page === "string" ? docsRead(source, args.page, offset, { schema, path }) : argumentsError({ tool: name }, [{ code: "MISSING_ARGUMENT", argument: "page", message: "read_docs requires a page string." }]);
1913
2134
  }
1914
2135
  if (name === "execute") {
1915
2136
  if (typeof args.operation !== "string") return argumentsError({ tool: name }, [{ code: "MISSING_ARGUMENT", argument: "operation", message: "execute requires an operation name." }]);
package/src/mcp.ts CHANGED
@@ -23,7 +23,7 @@ import { basename, join } from "node:path";
23
23
  import { fileURLToPath } from "node:url";
24
24
  import { TypeshipClient, formatDebugEvent, type ClientOptions, type DebugEvent } from "./index.js";
25
25
  import { asApiResult, mediaTypeForPath } from "./core/http.js";
26
- import { GLOBALS, OMITTED_OPS, OPS, buildArgs, type OpSpec } from "./ops.js";
26
+ import { GLOBALS, INPUT_TYPES, OMITTED_OPS, OPS, buildArgs, type OpSpec } from "./ops.js";
27
27
  import {
28
28
  DEFAULT_MAX_RESULT_CHARS, SUPPORTED_PROTOCOL_VERSIONS, McpAccountLinkRequired, argumentsError, asJsonRpc, binaryOutcome, callSharedTool, checkRequestHeaders,
29
29
  createStdioRpcHandler, dataOutcome, errorOutcome, handleRpc, hiddenOperations, isRpcOutcome, pageOutcome, parseIncludeList, prepareCall, resolveReferences, serverInstructions,
@@ -43,7 +43,7 @@ export { McpAccountLinkRequired } from "./mcp-protocol.js";
43
43
  const BIN = "typeship";
44
44
  const PKG_NAME = "@typeship-ax/mcp";
45
45
  const SERVER_NAME = "typeship-mcp";
46
- const SERVER_VERSION = "0.22.0";
46
+ const SERVER_VERSION = "0.23.0";
47
47
  /** The MCP client's announced name (clientInfo in request _meta), for the User-Agent. */
48
48
  let MCP_CLIENT_NAME: string | null = null;
49
49
  function noteClientInfo(message: unknown): void {
@@ -264,7 +264,7 @@ async function callOperationRaw(op: OpSpec, rawArgs: Record<string, unknown>, re
264
264
  hadCredential: !!remote || CLIENT_CREDENTIALS.get(client) === true,
265
265
  requiredScopes: requiredScopes(op.security),
266
266
  };
267
- const shape = { fields, optionalFields, maxChars, pagination: op.pagination, args, safety: op.safety };
267
+ const shape = { fields, optionalFields, maxChars, pagination: op.pagination, args, safety: op.safety, outputSchema: op.outputSchema };
268
268
  try {
269
269
  const target = (client as unknown as Record<string, Record<string, (...a: unknown[]) => unknown>>)[op.resource]!;
270
270
  let result = await asApiResult(target[op.method]!(...callArgs) as Promise<unknown>);
@@ -377,6 +377,7 @@ function saveBinary(bytes: Uint8Array, _mediaType: string, suggestedName: string
377
377
  const docsSource: DocsSource = {
378
378
  ops: MCP_OPS as unknown as OpLike[],
379
379
  omittedOps: OMITTED_OPS as unknown as OpLike[],
380
+ inputTypes: INPUT_TYPES,
380
381
  hiddenOps: hiddenOperations(OPS as unknown as OpLike[], { readOnly: READ_ONLY, include: INCLUDE, uploads: LOCAL_PROCESS }, { readOnly: READ_ONLY_SWITCH, include: TOOLS_SWITCH }),
381
382
  generatedOperationCount: OPS.length,
382
383
  docsUrl: () => readJson<{ docsUrl?: string }>("config.json")?.docsUrl ?? DOCS_URL_DEFAULT,