@typeship-ax/mcp 0.21.0 → 0.22.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 (129) hide show
  1. package/AGENTS.md +15 -11
  2. package/README.md +22 -53
  3. package/api.json +9998 -10118
  4. package/api.md +8983 -9120
  5. package/dist/arguments.d.ts +47 -0
  6. package/dist/arguments.d.ts.map +1 -0
  7. package/dist/arguments.js +254 -0
  8. package/dist/core/http.d.ts +162 -19
  9. package/dist/core/http.d.ts.map +1 -1
  10. package/dist/core/http.js +381 -48
  11. package/dist/core/pagination.d.ts +42 -6
  12. package/dist/core/pagination.d.ts.map +1 -1
  13. package/dist/core/pagination.js +111 -17
  14. package/dist/credential-storage.d.ts +10 -3
  15. package/dist/credential-storage.d.ts.map +1 -1
  16. package/dist/credential-storage.js +15 -6
  17. package/dist/dates.d.ts +1 -1
  18. package/dist/dates.js +1 -1
  19. package/dist/errors.d.ts +20 -84
  20. package/dist/errors.d.ts.map +1 -1
  21. package/dist/errors.js +20 -108
  22. package/dist/fields.d.ts +29 -0
  23. package/dist/fields.d.ts.map +1 -0
  24. package/dist/fields.js +101 -0
  25. package/dist/index.d.ts +28 -18
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +35 -25
  28. package/dist/mcp-authorization.d.ts.map +1 -1
  29. package/dist/mcp-authorization.js +34 -10
  30. package/dist/mcp-protocol.d.ts +87 -44
  31. package/dist/mcp-protocol.d.ts.map +1 -1
  32. package/dist/mcp-protocol.js +552 -478
  33. package/dist/mcp.d.ts.map +1 -1
  34. package/dist/mcp.js +127 -28
  35. package/dist/named-credentials.d.ts +19 -0
  36. package/dist/named-credentials.d.ts.map +1 -1
  37. package/dist/named-credentials.js +81 -1
  38. package/dist/oauth-request.d.ts +7 -1
  39. package/dist/oauth-request.d.ts.map +1 -1
  40. package/dist/oauth-request.js +26 -4
  41. package/dist/oauth-session.d.ts +13 -1
  42. package/dist/oauth-session.d.ts.map +1 -1
  43. package/dist/oauth-session.js +34 -18
  44. package/dist/ops.d.ts +53 -5
  45. package/dist/ops.d.ts.map +1 -1
  46. package/dist/ops.js +49 -40
  47. package/dist/resources/api-keys.d.ts +10 -7
  48. package/dist/resources/api-keys.d.ts.map +1 -1
  49. package/dist/resources/api-keys.js +10 -31
  50. package/dist/resources/deliveries.d.ts +88 -4
  51. package/dist/resources/deliveries.d.ts.map +1 -1
  52. package/dist/resources/deliveries.js +95 -18
  53. package/dist/resources/drafts.d.ts +15 -15
  54. package/dist/resources/drafts.d.ts.map +1 -1
  55. package/dist/resources/drafts.js +11 -64
  56. package/dist/resources/files.d.ts +4 -4
  57. package/dist/resources/files.d.ts.map +1 -1
  58. package/dist/resources/files.js +3 -12
  59. package/dist/resources/generations.d.ts +14 -14
  60. package/dist/resources/generations.d.ts.map +1 -1
  61. package/dist/resources/generations.js +21 -45
  62. package/dist/resources/organization.d.ts +4 -4
  63. package/dist/resources/organization.d.ts.map +1 -1
  64. package/dist/resources/organization.js +3 -10
  65. package/dist/resources/{generate.d.ts → packages.d.ts} +16 -16
  66. package/dist/resources/packages.d.ts.map +1 -0
  67. package/dist/resources/{generate.js → packages.js} +13 -29
  68. package/dist/resources/projects.d.ts +50 -50
  69. package/dist/resources/projects.d.ts.map +1 -1
  70. package/dist/resources/projects.js +60 -116
  71. package/dist/resources/releases.d.ts +21 -16
  72. package/dist/resources/releases.d.ts.map +1 -1
  73. package/dist/resources/releases.js +18 -39
  74. package/dist/resources/spec-revisions.d.ts +15 -6
  75. package/dist/resources/spec-revisions.d.ts.map +1 -1
  76. package/dist/resources/spec-revisions.js +6 -28
  77. package/dist/resources/specs.d.ts +7 -7
  78. package/dist/resources/specs.d.ts.map +1 -1
  79. package/dist/resources/specs.js +6 -34
  80. package/dist/resources/targets.d.ts +48 -48
  81. package/dist/resources/targets.d.ts.map +1 -1
  82. package/dist/resources/targets.js +58 -114
  83. package/dist/schemas.d.ts.map +1 -1
  84. package/dist/schemas.js +78 -76
  85. package/dist/search.d.ts +54 -0
  86. package/dist/search.d.ts.map +1 -0
  87. package/dist/search.js +421 -0
  88. package/dist/types.d.ts +499 -339
  89. package/dist/types.d.ts.map +1 -1
  90. package/dist/types.js +18 -18
  91. package/dist/worker.js +2 -2
  92. package/package.json +5 -2
  93. package/server.json +5 -5
  94. package/src/arguments.ts +242 -0
  95. package/src/core/http.ts +457 -58
  96. package/src/core/pagination.ts +129 -18
  97. package/src/credential-storage.ts +16 -6
  98. package/src/dates.ts +1 -1
  99. package/src/errors.ts +46 -115
  100. package/src/fields.ts +91 -0
  101. package/src/index.ts +45 -28
  102. package/src/mcp-authorization.ts +29 -9
  103. package/src/mcp-protocol.ts +580 -428
  104. package/src/mcp.ts +113 -26
  105. package/src/named-credentials.ts +66 -1
  106. package/src/oauth-request.ts +32 -6
  107. package/src/oauth-session.ts +37 -19
  108. package/src/ops.ts +82 -44
  109. package/src/resources/api-keys.ts +34 -48
  110. package/src/resources/deliveries.ts +211 -30
  111. package/src/resources/drafts.ts +60 -107
  112. package/src/resources/files.ts +19 -20
  113. package/src/resources/generations.ts +57 -75
  114. package/src/resources/organization.ts +11 -16
  115. package/src/resources/{generate.ts → packages.ts} +43 -51
  116. package/src/resources/projects.ts +145 -200
  117. package/src/resources/releases.ts +48 -65
  118. package/src/resources/spec-revisions.ts +38 -47
  119. package/src/resources/specs.ts +39 -59
  120. package/src/resources/targets.ts +143 -193
  121. package/src/schemas.ts +78 -76
  122. package/src/search.ts +434 -0
  123. package/src/types.ts +538 -357
  124. package/src/worker.ts +2 -2
  125. package/dist/resources/generate.d.ts.map +0 -1
  126. package/dist/resources/publications.d.ts +0 -47
  127. package/dist/resources/publications.d.ts.map +0 -1
  128. package/dist/resources/publications.js +0 -70
  129. package/src/resources/publications.ts +0 -140
@@ -1,22 +1,25 @@
1
1
  /**
2
2
  * MCP tools server with a 2025-11-25 handshake adapter and a 2026-07-28 core.
3
- * Generated by typeship — https://typeship.dev
3
+ * Generated by Typeship — https://typeship.dev
4
4
  *
5
- * The protocol layer shared by the MCP server in every generated package and
6
- * by typeship's hosted endpoint: JSON-RPC shape checks, the per-request _meta
7
- * rules, Streamable HTTP header validation, result wrapping, and the tool
5
+ * The protocol layer of the MCP server, shared by its stdio and HTTP
6
+ * transports: JSON-RPC shape checks, the per-request _meta rules,
7
+ * Streamable HTTP header validation, result wrapping, and the tool
8
8
  * surface (per-operation tools or the three-tool "meta" shape, plus the
9
9
  * search_docs / read_docs pair). Also the agent-facing contract of a tool
10
10
  * call: argument validation and coercion against the tool's input schema
11
- * (relative dates via ./dates), field projection, a size cap on results,
11
+ * (via ./arguments, shared with the CLI), field projection, a size cap on results,
12
12
  * and error results that carry a stable code and next steps. Transport,
13
13
  * credentials, and how a tool call reaches the API stay with the caller.
14
14
  * No external dependencies.
15
15
  *
16
16
  * Spec: https://modelcontextprotocol.io/specification/2026-07-28
17
17
  */
18
- import { dateKindOf, relativeDate } from "./dates.js";
18
+ import { checkValue, closestName, isMeReference, normalizeName, userShapedReference } from "./arguments.js";
19
19
  import { docsReadTarget, resolveDocsContentUrl, searchConnectedGuides } from "./docs.js";
20
+ import { projectFields, unmatchedFields, unmatchedFieldsMessage } from "./fields.js";
21
+ import { SEARCH_PAGE_SIZE, rankOperations } from "./search.js";
22
+ export { projectFields } from "./fields.js";
20
23
  export const MCP_PROTOCOL_VERSION = "2026-07-28";
21
24
  export const LEGACY_PROTOCOL_VERSION = "2025-11-25";
22
25
  /** The modern model and the supported initialize-handshake wire revision. */
@@ -52,7 +55,7 @@ export class McpAccountLinkRequired extends Error {
52
55
  Object.freeze(this);
53
56
  }
54
57
  }
55
- const API_LINK_INPUT = "typeship_api_account";
58
+ const API_LINK_INPUT = "api_account";
56
59
  /** Does the operation take a file (multipart form or raw binary body)? */
57
60
  export function isUploadOp(op) {
58
61
  return op.bodyKind === "multipart" || op.bodyKind === "binary";
@@ -127,17 +130,16 @@ export function checkRequestHeaders(headers, message) {
127
130
  const id = message.id;
128
131
  const version = headers.get("mcp-protocol-version");
129
132
  const bodyVersion = message.params?._meta?.[META_VERSION];
130
- if (version !== null && !SUPPORTED_PROTOCOL_VERSIONS.includes(version))
131
- return rpcError(id, -32022, "Unsupported protocol version", 400, { supported: SUPPORTED_PROTOCOL_VERSIONS, requested: version });
132
133
  if (message.method === "initialize" || version === LEGACY_PROTOCOL_VERSION) {
133
134
  if (bodyVersion !== undefined || headers.get("mcp-method") !== null || headers.get("mcp-name") !== null) {
134
135
  return rpcError(id, -32020, "Header mismatch: initialize-handshake requests cannot carry modern protocol metadata or routing headers.", 400);
135
136
  }
136
- if (version !== null && version !== LEGACY_PROTOCOL_VERSION) {
137
- return rpcError(id, -32022, "Unsupported initialize-handshake protocol version", 400, { supported: [LEGACY_PROTOCOL_VERSION], requested: version });
138
- }
137
+ // initialize negotiates the revision in its body, so a header naming an
138
+ // older one is not an error here.
139
139
  return null;
140
140
  }
141
+ if (version !== null && !SUPPORTED_PROTOCOL_VERSIONS.includes(version))
142
+ return rpcError(id, -32022, "Unsupported protocol version", 400, { supported: SUPPORTED_PROTOCOL_VERSIONS, requested: version });
141
143
  if (version === null)
142
144
  return rpcError(id, -32020, "Header mismatch: MCP-Protocol-Version header is required", 400);
143
145
  if (typeof bodyVersion === "string" && version !== bodyVersion) {
@@ -226,8 +228,10 @@ export async function handleRpc(server, incoming, protocolVersion) {
226
228
  !info || typeof info !== "object" || Array.isArray(info) || typeof info.name !== "string" || typeof info.version !== "string") {
227
229
  return rpcError(id, -32602, "initialize requires protocolVersion, capabilities, and clientInfo with name and version.", 400);
228
230
  }
229
- if (params.protocolVersion !== LEGACY_PROTOCOL_VERSION)
230
- return rpcError(id, -32022, "Unsupported initialize-handshake protocol version", 400, { supported: [LEGACY_PROTOCOL_VERSION], requested: params.protocolVersion });
231
+ // Version negotiation: the server answers with a revision it supports
232
+ // (the requested one when it can), and a client that cannot speak it
233
+ // disconnects. So an older request (2025-06-18, 2025-03-26, 2024-11-05)
234
+ // gets 2025-11-25 rather than an error.
231
235
  return { status: 200, message: { jsonrpc: "2.0", id, result: {
232
236
  protocolVersion: LEGACY_PROTOCOL_VERSION,
233
237
  capabilities: { tools: {} },
@@ -385,132 +389,6 @@ export function operationSafety(op) {
385
389
  ? "destructive"
386
390
  : "write";
387
391
  }
388
- /** A deterministic, schema-valid-enough example for documentation and
389
- * agent calls. Prefer facts supplied by the API author, then conservative
390
- * values based on formats and field names. Only required object fields are
391
- * included, keeping examples useful instead of manufacturing giant bodies. */
392
- export function exampleFromSchema(schema, field = "value", depth = 0) {
393
- if (!schema || typeof schema !== "object" || depth > 8)
394
- return null;
395
- const node = schema;
396
- if (node.const !== undefined)
397
- return node.const;
398
- if (node.example !== undefined)
399
- return node.example;
400
- if (Array.isArray(node.examples) && node.examples.length > 0)
401
- return node.examples[0];
402
- if (node.default !== undefined)
403
- return node.default;
404
- if (Array.isArray(node.enum) && node.enum.length > 0) {
405
- if (typeof node.pattern === "string") {
406
- try {
407
- const pattern = new RegExp(node.pattern);
408
- const matching = node.enum.find((value) => typeof value === "string" && pattern.test(value));
409
- if (matching !== undefined)
410
- return matching;
411
- }
412
- catch { /* malformed patterns are ignored for examples */ }
413
- }
414
- return node.enum.find((v) => v !== null) ?? node.enum[0];
415
- }
416
- const variants = (Array.isArray(node.oneOf) ? node.oneOf : Array.isArray(node.anyOf) ? node.anyOf : null);
417
- if (variants) {
418
- const useful = variants.find((v) => v && typeof v === "object" && v.type !== "null") ?? variants[0];
419
- return exampleFromSchema(useful, field, depth + 1);
420
- }
421
- const type = Array.isArray(node.type) ? node.type.find((v) => v !== "null") : node.type;
422
- if (type === "object" || node.properties || node.additionalProperties) {
423
- const properties = (node.properties ?? {});
424
- const required = new Set(Array.isArray(node.required) ? node.required.filter((v) => typeof v === "string") : []);
425
- // At the operation root, optional really means optional: the most honest
426
- // runnable example is `{}`. Inside a required object, one representative
427
- // optional field still makes an otherwise empty nested shape legible.
428
- const names = required.size > 0 ? [...required] : depth === 0 ? [] : Object.keys(properties).slice(0, 1);
429
- const value = {};
430
- for (const name of names) {
431
- if (properties[name] !== undefined)
432
- value[name] = exampleFromSchema(properties[name], name, depth + 1);
433
- }
434
- if (Object.keys(value).length === 0 && node.additionalProperties && typeof node.additionalProperties === "object") {
435
- value.key = exampleFromSchema(node.additionalProperties, "key", depth + 1);
436
- }
437
- return value;
438
- }
439
- if (type === "array" || node.items) {
440
- const count = typeof node.minItems === "number" && node.minItems > 1 ? Math.min(node.minItems, 3) : 1;
441
- return Array.from({ length: count }, () => exampleFromSchema(node.items, field, depth + 1));
442
- }
443
- if (type === "integer" || type === "number") {
444
- if (typeof node.minimum === "number")
445
- return node.minimum;
446
- if (typeof node.exclusiveMinimum === "number")
447
- return node.exclusiveMinimum + 1;
448
- return 1;
449
- }
450
- if (type === "boolean")
451
- return true;
452
- if (type === "string" || type === undefined) {
453
- const format = typeof node.format === "string" ? node.format : "";
454
- const lower = field.toLowerCase();
455
- const min = typeof node.minLength === "number" ? node.minLength : 0;
456
- const max = typeof node.maxLength === "number" ? node.maxLength : undefined;
457
- const patterned = typeof node.pattern === "string" ? exampleMatchingPattern(node.pattern, min, max) : null;
458
- let value = patterned ?? (format === "date-time" ? "2026-01-15T12:00:00Z"
459
- : format === "date" ? "2026-01-15"
460
- : format === "email" || lower.includes("email") ? "person@example.com"
461
- : (format === "uri" || format === "url" || lower.endsWith("url")) && (lower.includes("webhook") || lower.includes("callback")) ? "https://example.com/webhook"
462
- : format === "uri" || format === "url" || lower.endsWith("url") ? "https://example.com"
463
- : format === "uuid" ? "00000000-0000-4000-8000-000000000000"
464
- : lower.includes("repository") || lower === "repo" ? "acme/api"
465
- : lower.includes("path") ? "openapi.yaml"
466
- : lower.includes("version") ? "1.0.0"
467
- : /(^|_)id$|Id$/.test(field) ? (lower === "id" ? "id" : field.replace(/[_-]?id$/i, "")) + "_123"
468
- : lower.includes("name") ? "example"
469
- : "value");
470
- while (value.length < min)
471
- value += "x";
472
- if (max !== undefined)
473
- value = value.slice(0, max);
474
- return value;
475
- }
476
- return null;
477
- }
478
- /** A useful value for the common API-id pattern (`^agt_`, `^src_[a-z0-9]+$`).
479
- * Full regex generation would be surprising and heavyweight; an anchored
480
- * literal prefix plus ordinary id suffix covers the schemas that use a
481
- * pattern to communicate a typed identifier. Every candidate is checked by
482
- * the actual RegExp before it is returned. */
483
- function exampleMatchingPattern(pattern, minLength, maxLength) {
484
- let regex;
485
- try {
486
- regex = new RegExp(pattern);
487
- }
488
- catch {
489
- return null;
490
- }
491
- const match = /^\^((?:\\.|[A-Za-z0-9_-])+)/.exec(pattern);
492
- const prefix = match?.[1]?.replace(/\\(.)/g, "$1") ?? "";
493
- const fit = (candidate) => {
494
- let value = candidate;
495
- while (value.length < minLength)
496
- value += "x";
497
- if (maxLength !== undefined)
498
- value = value.slice(0, maxLength);
499
- return value;
500
- };
501
- const candidates = [prefix + "123", prefix + "example", prefix, "resource.method", "example.value", "example_123", "example", "value"];
502
- for (const candidate of candidates) {
503
- const value = fit(candidate);
504
- regex.lastIndex = 0;
505
- if (regex.test(value))
506
- return value;
507
- }
508
- return null;
509
- }
510
- export function exampleArgumentsFromSchema(inputSchema) {
511
- const value = exampleFromSchema(inputSchema, "arguments");
512
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
513
- }
514
392
  /** The `fields` argument every tool takes unless the API already has one:
515
393
  * dotted paths to keep in the result (per item for paginated tools). */
516
394
  export const FIELDS_ARGUMENT = "fields";
@@ -550,23 +428,22 @@ export function operationTool(op) {
550
428
  ...(op.outputSchema ? { outputSchema: op.outputSchema } : {}),
551
429
  };
552
430
  }
553
- /** Reference matches per search_docs page. */
554
- export const SEARCH_PAGE_SIZE = 15;
431
+ export { SEARCH_PAGE_SIZE } from "./search.js";
555
432
  export const SEARCH_DOCS_TOOL = {
556
433
  name: "search_docs",
557
434
  description: "Search this API's reference (operations, parameters) and, when a docs site is configured, its guides. Best matches first; page through with page.",
558
- inputSchema: { type: "object", properties: { query: { type: "string" }, page: { type: "integer", minimum: 1, description: "Page of reference matches (15 per page), default 1" } }, required: ["query"] },
435
+ inputSchema: { type: "object", properties: { query: { type: "string", description: "The task in a few words, e.g. \"assign issue\" or \"list calls\"" }, page: { type: "integer", minimum: 1, description: "Page of reference matches (" + SEARCH_PAGE_SIZE + " per page), default 1" } }, required: ["query"] },
559
436
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
560
437
  };
561
438
  export const READ_DOCS_TOOL = {
562
439
  name: "read_docs",
563
- 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.',
564
- inputSchema: { type: "object", properties: { page: { type: "string" } }, required: ["page"] },
440
+ 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.',
441
+ 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"] },
565
442
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
566
443
  };
567
444
  export const EXECUTE_TOOL = {
568
445
  name: "execute",
569
- description: "Execute an API operation by name. Discover it with search_docs, then read_docs <operation> for its full schema, example, and safety classification. Destructive operations require confirm: true.",
446
+ description: "Execute an API operation by name. Discover it with search_docs, then read_docs <operation> for its arguments, example, and safety classification. Destructive operations require confirm: true.",
570
447
  inputSchema: {
571
448
  type: "object",
572
449
  properties: {
@@ -578,6 +455,8 @@ export const EXECUTE_TOOL = {
578
455
  },
579
456
  annotations: { openWorldHint: false },
580
457
  };
458
+ /** The keys execute itself takes; everything else is an operation argument. */
459
+ const EXECUTE_KEYS = ["operation", "arguments", "confirm"];
581
460
  /** The operations a server serves under these options: the callable set,
582
461
  * not just the listed one, so a hidden write is not reachable by name or
583
462
  * through execute either. Deterministic (spec) order. */
@@ -587,12 +466,49 @@ export function visibleOps(ops, options = {}) {
587
466
  out = out.filter(isReadOperation);
588
467
  if (options.include && options.include.length > 0) {
589
468
  const wanted = new Set(options.include.map((s) => s.trim().toLowerCase()).filter(Boolean));
590
- out = out.filter((op) => wanted.has(op.tool.toLowerCase()) ||
591
- (op.resource !== undefined && wanted.has(op.resource.toLowerCase())) ||
592
- (op.resource !== undefined && op.method !== undefined && wanted.has((op.resource + "." + op.method).toLowerCase())));
469
+ out = out.filter((op) => includedBy(op, wanted));
593
470
  }
594
471
  return out;
595
472
  }
473
+ function includedBy(op, wanted) {
474
+ return wanted.has(op.tool.toLowerCase()) ||
475
+ (op.resource !== undefined && wanted.has(op.resource.toLowerCase())) ||
476
+ (op.resource !== undefined && op.method !== undefined && wanted.has((op.resource + "." + op.method).toLowerCase()));
477
+ }
478
+ /** Entries of an include list that name no operation (a typo such as
479
+ * "pull" for "pulls"). Matched against every generated operation, so an
480
+ * entry the other switches hide still counts as a name. */
481
+ export function unmatchedIncludes(ops, include) {
482
+ return (include ?? []).filter((entry) => !ops.some((op) => includedBy(op, new Set([entry.trim().toLowerCase()]))));
483
+ }
484
+ /** The exposed operations that readOnly or include hide, each explained in
485
+ * terms of the switch the operator set (`switches` names them as the
486
+ * operator wrote them, such as "--read-only"). */
487
+ export function hiddenOperations(ops, options, switches) {
488
+ const visible = new Set(visibleOps(ops, options));
489
+ return ops.filter((op) => !visible.has(op) && mcpExposed(op, { uploads: options.uploads })).map((op) => {
490
+ const included = !options.include?.length || includedBy(op, new Set(options.include.map((entry) => entry.trim().toLowerCase())));
491
+ return included
492
+ ? {
493
+ op,
494
+ message: op.tool + " is a write, and this server is read-only (" + switches.readOnly + "), so it cannot be called here.",
495
+ nextSteps: ["Writes need a server started without " + switches.readOnly + "; tell the user if this operation is required."],
496
+ }
497
+ : {
498
+ op,
499
+ message: op.tool + " is not enabled on this server: " + switches.include + " limits it to " + (options.include ?? []).join(", ") + ".",
500
+ nextSteps: ["The operation needs a server whose " + switches.include + " includes " + (op.resource ?? op.tool) + "; tell the user if it is required."],
501
+ };
502
+ });
503
+ }
504
+ function findHidden(source, wanted) {
505
+ const hidden = source.hiddenOps ?? [];
506
+ const op = findOperation(hidden.map((h) => h.op), wanted);
507
+ return op ? hidden.find((h) => h.op === op) : undefined;
508
+ }
509
+ function hiddenOutcome(hidden) {
510
+ return textError(hidden.message, "NOT_AVAILABLE", hidden.nextSteps);
511
+ }
596
512
  /** Parse a comma-separated include list (from an env var or a flag). */
597
513
  export function parseIncludeList(value) {
598
514
  const list = (value ?? "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -607,14 +523,15 @@ export function toolDefinitions(ops, mode, omittedOps = []) {
607
523
  if (mode === "meta") {
608
524
  const execute = structuredClone(EXECUTE_TOOL);
609
525
  const operation = execute.inputSchema.properties.operation;
610
- if (ops[0])
611
- operation.examples = [ops[0].tool];
526
+ const example = ops.find((op) => op.showcase) ?? ops[0];
527
+ if (example)
528
+ operation.examples = [example.tool];
612
529
  const coverage = omittedOps.length > 0
613
- ? " Generated " + ops.length + " of " + (ops.length + omittedOps.length) + " operations; search_docs names operations omitted by the plan limit."
530
+ ? " This build includes " + ops.length + " of " + (ops.length + omittedOps.length) + " operations."
614
531
  : "";
615
532
  return [
616
533
  { ...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 },
617
- { ...READ_DOCS_TOOL, description: "Read an operation's full reference (arguments, schemas, authentication, safety and example) by tool name, or a docs-site guide page." },
534
+ { ...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." },
618
535
  execute,
619
536
  ];
620
537
  }
@@ -639,7 +556,7 @@ export function serverInstructions(input) {
639
556
  : input.title + " as MCP tools: one tool per operation (" + input.toolCount + "), plus search_docs to find operations and read_docs <tool> for an operation's full argument reference.");
640
557
  if (input.omittedOps?.length) {
641
558
  const generated = input.generatedOperationCount ?? input.toolCount;
642
- parts.push("Plan-limited generation: generated " + generated + " of " + (generated + input.omittedOps.length) + " operations. Omitted operations: " + input.omittedOps.map((op) => op.tool + " (" + op.httpMethod + " " + op.path + ")").join(", ") + ". Calling or searching for one returns PLAN_LIMIT with upgrade next_steps.");
559
+ parts.push("This build includes " + generated + " of " + (generated + input.omittedOps.length) + " operations; calling or searching for one of the others returns PLAN_LIMIT.");
643
560
  }
644
561
  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).");
645
562
  if (input.referenceResolution)
@@ -662,160 +579,31 @@ export function serverInstructions(input) {
662
579
  parts.push(input.custom.trim());
663
580
  return parts.join(" ");
664
581
  }
665
- const normalizeName = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, "");
666
- function editDistance(a, b) {
667
- const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
668
- for (let i = 1; i <= a.length; i++) {
669
- let diag = prev[0];
670
- prev[0] = i;
671
- for (let j = 1; j <= b.length; j++) {
672
- const tmp = prev[j];
673
- prev[j] = Math.min(prev[j] + 1, prev[j - 1] + 1, diag + (a[i - 1] === b[j - 1] ? 0 : 1));
674
- diag = tmp;
675
- }
676
- }
677
- return prev[b.length];
678
- }
679
- /** The closest accepted name: same letters ignoring case/punctuation first,
680
- * then a small edit distance. Undefined when nothing is close. */
681
- export function closestName(name, known) {
682
- const exact = known.filter((k) => normalizeName(k) === normalizeName(name));
683
- if (exact.length === 1)
684
- return exact[0];
685
- if (exact.length > 1)
686
- return undefined;
687
- let best;
688
- for (const k of known) {
689
- const d = editDistance(name.toLowerCase(), k.toLowerCase());
690
- if (d <= Math.max(1, Math.floor(k.length / 4)) && (best === undefined || d < best.d))
691
- best = { name: k, d };
692
- }
693
- return best?.name;
694
- }
695
- function schemaTypes(schema) {
696
- const t = schema.type;
697
- if (typeof t === "string")
698
- return [t];
699
- if (Array.isArray(t))
700
- return t.filter((x) => typeof x === "string");
701
- return [];
702
- }
703
- /**
704
- * Coerce one value toward its schema when the intent is unambiguous: the
705
- * strings agents produce for booleans and numbers, a JSON string for an
706
- * object or array, a scalar for a one-element array, an enum member in the
707
- * wrong case. Returns the value to send, or a message when it can't be made
708
- * to fit. Untyped schemas (unions, anything) pass through.
709
- */
710
- export function coerceValue(value, schema) {
711
- if (value === null || value === undefined)
712
- return { value };
713
- // Date-shaped arguments take relative forms (-P7D, 7 days ago, today),
714
- // resolved here so the API sees an absolute value.
715
- const dateKind = dateKindOf(schema.format);
716
- if (dateKind && typeof value === "string") {
717
- const resolved = relativeDate(value, dateKind);
718
- if (resolved && "error" in resolved)
719
- return { error: resolved.error };
720
- if (resolved)
721
- value = resolved.value;
722
- }
723
- const types = schemaTypes(schema);
724
- const enumValues = Array.isArray(schema.enum) ? schema.enum : undefined;
725
- const accepts = (t) => types.length === 0 || types.includes(t);
726
- const kind = Array.isArray(value) ? "array" : typeof value;
727
- let out = value;
728
- if (types.length > 0) {
729
- if (kind === "boolean" && !accepts("boolean")) {
730
- if (accepts("string"))
731
- out = String(value);
732
- else
733
- return { error: "expected " + types.join(" or ") + ", got boolean" };
734
- }
735
- else if (kind === "number" && !accepts("number") && !accepts("integer")) {
736
- if (accepts("string"))
737
- out = String(value);
738
- else if (accepts("array"))
739
- out = [value];
740
- else
741
- return { error: "expected " + types.join(" or ") + ", got number" };
742
- }
743
- else if (kind === "number" && accepts("integer") && !accepts("number") && !Number.isInteger(value)) {
744
- return { error: "expected an integer, got " + String(value) };
745
- }
746
- else if (kind === "string" && !accepts("string")) {
747
- const s = value.trim();
748
- if (accepts("boolean") && /^(true|false|yes|no|1|0)$/i.test(s))
749
- out = /^(true|yes|1)$/i.test(s);
750
- else if ((accepts("integer") || accepts("number")) && s !== "" && !Number.isNaN(Number(s))) {
751
- const n = Number(s);
752
- if (accepts("integer") && !accepts("number") && !Number.isInteger(n))
753
- return { error: "expected an integer, got \"" + s + "\"" };
754
- out = n;
755
- }
756
- else if ((accepts("object") || accepts("array")) && /^[[{]/.test(s)) {
757
- try {
758
- const parsed = JSON.parse(s);
759
- const parsedKind = Array.isArray(parsed) ? "array" : parsed === null ? "null" : typeof parsed;
760
- if (!accepts(parsedKind))
761
- return { error: "expected " + types.join(" or ") + ", got a JSON " + parsedKind + " in a string" };
762
- out = parsed;
763
- }
764
- catch {
765
- return { error: "expected " + types.join(" or ") + ", got a string that is not valid JSON" };
766
- }
767
- }
768
- else if (accepts("array")) {
769
- out = [value];
770
- }
771
- else {
772
- return { error: "expected " + types.join(" or ") + ", got string" };
773
- }
774
- }
775
- else if (kind === "object" && !accepts("object")) {
776
- if (accepts("array"))
777
- out = [value];
778
- else
779
- return { error: "expected " + types.join(" or ") + ", got object" };
780
- }
781
- else if (kind === "array" && !accepts("array")) {
782
- return { error: "expected " + types.join(" or ") + ", got array" };
783
- }
784
- }
785
- // Array items: coerce each against the items schema when it has one.
786
- if (Array.isArray(out) && schema.items && typeof schema.items === "object" && !Array.isArray(schema.items)) {
787
- const itemSchema = schema.items;
788
- const items = [];
789
- for (let i = 0; i < out.length; i++) {
790
- const r = coerceValue(out[i], itemSchema);
791
- if ("error" in r)
792
- return { error: "item " + i + ": " + r.error };
793
- items.push(r.value);
794
- }
795
- out = items;
796
- }
797
- if (enumValues && typeof out === "string" && !enumValues.includes(out)) {
798
- const match = enumValues.filter((e) => typeof e === "string" && e.toLowerCase() === out.toLowerCase());
799
- if (match.length === 1)
800
- out = match[0];
801
- else
802
- return { error: "must be one of " + enumValues.map((e) => JSON.stringify(e)).join(", ") + ", got " + JSON.stringify(out) };
803
- }
804
- return { value: out };
805
- }
582
+ // ---- argument validation + coercion ---------------------------------------------
583
+ // The checks themselves live in ./arguments, shared with the CLI.
584
+ export { checkValue, closestName, coerceValue } from "./arguments.js";
585
+ /** Field paths, and which of them may be absent: a leading "?" marks a
586
+ * path the result may lack, as reference resolution's list calls ask for
587
+ * match keys an API can omit. Any other path must match something. */
806
588
  function parseFields(value) {
807
589
  const raw = typeof value === "string" ? value.split(",") : Array.isArray(value) ? value : null;
808
590
  if (raw === null)
809
591
  return { error: "expected an array of field paths, e.g. [\"id\",\"name\"]" };
810
592
  const paths = [];
593
+ const optional = [];
811
594
  for (const entry of raw) {
812
595
  if (typeof entry !== "string")
813
596
  return { error: "expected an array of strings" };
814
- const path = entry.trim();
597
+ let path = entry.trim();
598
+ if (path.startsWith("?")) {
599
+ path = path.slice(1).trim();
600
+ if (path !== "")
601
+ optional.push(path);
602
+ }
815
603
  if (path !== "")
816
604
  paths.push(path.split("."));
817
605
  }
818
- return paths;
606
+ return { paths, optional };
819
607
  }
820
608
  /**
821
609
  * Check a tool call's arguments against the tool's input schema before
@@ -852,23 +640,40 @@ export function prepareCall(op, rawArgs, options = {}) {
852
640
  });
853
641
  }
854
642
  let fields = null;
643
+ let optionalFields = [];
855
644
  if (!hasOwnFieldsParam(op) && args[FIELDS_ARGUMENT] !== undefined) {
856
645
  const parsed = parseFields(args[FIELDS_ARGUMENT]);
857
646
  if ("error" in parsed)
858
647
  issues.push({ code: "INVALID_ARGUMENT", argument: FIELDS_ARGUMENT, message: "fields: " + parsed.error });
859
- else
860
- fields = parsed.length > 0 ? parsed : null;
648
+ else if (parsed.paths.length > 0)
649
+ ({ paths: fields, optional: optionalFields } = parsed);
861
650
  delete args[FIELDS_ARGUMENT];
862
651
  }
863
652
  for (const [name, value] of Object.entries(args)) {
864
653
  const propSchema = properties[name];
865
654
  if (!propSchema)
866
655
  continue;
867
- const r = coerceValue(value, propSchema);
868
- if ("error" in r)
869
- issues.push({ code: "INVALID_ARGUMENT", argument: name, message: name + ": " + r.error });
870
- else
871
- args[name] = r.value;
656
+ // Patterns apply inside objects and arrays. A top-level argument's own
657
+ // pattern is left to the API: its documented example values do not all
658
+ // satisfy it yet, and a name or "me" is only resolved to an ID later.
659
+ args[name] = checkValue(value, propSchema, name, issues, { skipPattern: true });
660
+ }
661
+ // A path argument that is the configured credential's username (Twilio's
662
+ // AccountSid) defaults to it; without one it is an ordinary missing argument
663
+ // whose message says where the default would come from.
664
+ for (const param of op.params) {
665
+ if (!param.credential || args[param.name] !== undefined)
666
+ continue;
667
+ const username = options.credentialUsername ?? undefined;
668
+ if (username !== undefined && username !== "") {
669
+ args[param.name] = username;
670
+ continue;
671
+ }
672
+ issues.push({
673
+ code: "MISSING_ARGUMENT",
674
+ argument: param.name,
675
+ message: "Missing required argument \"" + param.name + "\": it defaults to the Basic-auth username" + (param.credential.env ? " (" + param.credential.env + ")" : "") + ", and none is configured. Pass " + param.name + ", or configure the credential.",
676
+ });
872
677
  }
873
678
  const required = Array.isArray(schema.required) ? schema.required : [];
874
679
  for (const name of required) {
@@ -878,7 +683,7 @@ export function prepareCall(op, rawArgs, options = {}) {
878
683
  }
879
684
  if (issues.length > 0)
880
685
  return { ok: false, outcome: argumentsError(op, issues) };
881
- return { ok: true, call: { args, fields, maxChars: options.maxChars ?? DEFAULT_MAX_RESULT_CHARS } };
686
+ return { ok: true, call: { args, fields, optionalFields, maxChars: options.maxChars ?? DEFAULT_MAX_RESULT_CHARS } };
882
687
  }
883
688
  function referenceError(code, message, argument, nextSteps, candidates) {
884
689
  const structured = {
@@ -921,10 +726,6 @@ function referencePage(value) {
921
726
  : null,
922
727
  };
923
728
  }
924
- function userShapedReference(name) {
925
- const normalized = name.toLowerCase().replace(/[^a-z0-9]/g, "").replace(/id$/, "");
926
- return ["user", "assignee", "owner", "member", "actor", "creator", "account", "profile"].includes(normalized);
927
- }
928
729
  function referenceMatchLabel(fields) {
929
730
  return fields.length === 1 ? fields[0] : fields.slice(0, -1).join(", ") + " or " + fields.at(-1);
930
731
  }
@@ -961,32 +762,73 @@ function candidateRecord(item, resolver) {
961
762
  export async function resolveReferences(op, preparedArgs, options) {
962
763
  const args = { ...preparedArgs };
963
764
  const maxPages = Math.max(1, Math.floor(options.maxPages ?? 5));
765
+ const identity = options.identityTool ? findOperation(options.ops, options.identityTool) : undefined;
766
+ /** The caller's ID for "me", fetched once per cache. */
767
+ const callerId = async (argument) => {
768
+ const cacheKey = "me:" + identity.tool;
769
+ const cached = options.cache.get(cacheKey);
770
+ if (cached !== undefined)
771
+ return { id: cached };
772
+ const outcome = await options.runOperation(identity, {});
773
+ if (outcome.isError)
774
+ return { outcome };
775
+ const body = outcomeValue(outcome);
776
+ const id = body && typeof body === "object" && !Array.isArray(body) ? body.id : undefined;
777
+ if (typeof id !== "string" && typeof id !== "number") {
778
+ 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."]) };
779
+ }
780
+ putReferenceCache(options.cache, cacheKey, id);
781
+ return { id };
782
+ };
783
+ /** "me" inside an object argument (a GraphQL input's assigneeId, or each
784
+ * of its subscriberIds): the same resolution, keyed by property name. */
785
+ const resolveNestedMe = async (value, key, path) => {
786
+ if (typeof value === "string") {
787
+ if (!isMeReference(key, value))
788
+ return { value };
789
+ const caller = await callerId(path);
790
+ return "outcome" in caller ? caller : { value: caller.id };
791
+ }
792
+ if (Array.isArray(value)) {
793
+ const items = [];
794
+ for (let i = 0; i < value.length; i++) {
795
+ const item = await resolveNestedMe(value[i], key, path + "[" + i + "]");
796
+ if ("outcome" in item)
797
+ return item;
798
+ items.push(item.value);
799
+ }
800
+ return { value: items };
801
+ }
802
+ if (value && typeof value === "object") {
803
+ const out = {};
804
+ for (const [name, entry] of Object.entries(value)) {
805
+ const resolved = await resolveNestedMe(entry, name, path + "." + name);
806
+ if ("outcome" in resolved)
807
+ return resolved;
808
+ out[name] = resolved.value;
809
+ }
810
+ return { value: out };
811
+ }
812
+ return { value };
813
+ };
964
814
  for (const param of op.params) {
965
815
  const raw = args[param.name];
816
+ if (identity && raw && typeof raw === "object" && param.resolve !== false) {
817
+ const resolved = await resolveNestedMe(raw, param.name, param.name);
818
+ if ("outcome" in resolved)
819
+ return { ok: false, outcome: resolved.outcome };
820
+ args[param.name] = resolved.value;
821
+ continue;
822
+ }
966
823
  if (typeof raw !== "string" || param.resolve === false)
967
824
  continue;
968
825
  const value = raw.trim();
969
- if (value.toLowerCase() === "me" && userShapedReference(param.name) && options.identityTool) {
970
- const identity = findOperation(options.ops, options.identityTool);
971
- if (identity) {
972
- const cacheKey = "me:" + identity.tool;
973
- const cached = options.cache.get(cacheKey);
974
- if (cached !== undefined) {
975
- args[param.name] = cached;
976
- continue;
977
- }
978
- const outcome = await options.runOperation(identity, {});
979
- if (outcome.isError)
980
- return { ok: false, outcome };
981
- const body = outcomeValue(outcome);
982
- const id = body && typeof body === "object" && !Array.isArray(body) ? body.id : undefined;
983
- if (typeof id !== "string" && typeof id !== "number") {
984
- return { ok: false, outcome: referenceError("NOT_AVAILABLE", `${identity.tool} did not return a top-level id, so "me" cannot be resolved for ${param.name}.`, param.name, ["Pass the caller's exact ID instead."]) };
985
- }
986
- putReferenceCache(options.cache, cacheKey, id);
987
- args[param.name] = id;
988
- continue;
989
- }
826
+ if (identity && isMeReference(param.name, value)) {
827
+ const caller = await callerId(param.name);
828
+ if ("outcome" in caller)
829
+ return { ok: false, outcome: caller.outcome };
830
+ args[param.name] = caller.id;
831
+ continue;
990
832
  }
991
833
  const resolver = param.resolve && typeof param.resolve === "object" ? param.resolve : undefined;
992
834
  if (!resolver || looksLikeIdentifier(value, resolver))
@@ -1009,7 +851,7 @@ export async function resolveReferences(op, preparedArgs, options) {
1009
851
  if (resolver.filterParam)
1010
852
  pageArgs[resolver.filterParam] = value;
1011
853
  if (!hasOwnFieldsParam(source))
1012
- pageArgs.fields = [resolver.id, ...resolver.match];
854
+ pageArgs.fields = [resolver.id, ...resolver.match.map((field) => "?" + field)];
1013
855
  let exhausted = false;
1014
856
  for (let pageNumber = 1; pageNumber <= maxPages; pageNumber++) {
1015
857
  const outcome = await options.runOperation(source, pageArgs);
@@ -1064,38 +906,32 @@ export function argumentsError(op, issues) {
1064
906
  };
1065
907
  return { text: JSON.stringify(structured), isError: true, structured };
1066
908
  }
1067
- // ---- results: projection, size cap, envelopes ------------------------------------
1068
- /** Keep only `paths` of a value: arrays item by item, objects by dotted
1069
- * path, including paths through arrays (`items.id` keeps each item's id);
1070
- * scalars untouched. Same rule as the CLI's --fields. */
1071
- export function projectFields(value, paths) {
1072
- if (paths === null)
1073
- return value;
1074
- if (Array.isArray(value))
1075
- return value.map((v) => projectFields(v, paths));
1076
- if (value === null || typeof value !== "object")
1077
- return value;
1078
- const groups = new Map();
1079
- for (const [key, ...rest] of paths) {
1080
- if (key === undefined)
1081
- continue;
1082
- const group = groups.get(key);
1083
- if (group)
1084
- group.push(rest);
1085
- else
1086
- groups.set(key, [rest]);
1087
- }
1088
- const out = {};
1089
- for (const [key, rests] of groups) {
1090
- const child = value[key];
1091
- if (child === undefined)
1092
- continue;
1093
- if (rests.some((rest) => rest.length === 0))
1094
- out[key] = child;
1095
- else if (child !== null && typeof child === "object")
1096
- out[key] = projectFields(child, rests);
1097
- }
1098
- return out;
909
+ /** A fields path that selects nothing is an error that names the keys
910
+ * that exist, not a silent {}. The API call already happened, so the error
911
+ * says so, and a write's unprojected result comes back with it. */
912
+ function unmatchedFieldsOutcome(value, fields, perItem, options) {
913
+ if (fields === null)
914
+ return null;
915
+ const optional = new Set(options.optionalFields ?? []);
916
+ const unmatched = unmatchedFields(value, fields).filter((u) => !optional.has(u.path));
917
+ if (unmatched.length === 0)
918
+ return null;
919
+ const write = options.safety !== "read";
920
+ const maxChars = options.maxChars ?? DEFAULT_MAX_RESULT_CHARS;
921
+ const full = write ? JSON.stringify(value) : undefined;
922
+ const structured = {
923
+ error: "UnmatchedFields",
924
+ code: "FIELDS_UNMATCHED",
925
+ message: unmatchedFieldsMessage(unmatched, perItem),
926
+ unmatched,
927
+ ...(full !== undefined && full.length <= maxChars / 2 ? { result: value } : {}),
928
+ next_steps: [
929
+ ...(write
930
+ ? ["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." : "")]
931
+ : ["Call again with fields from the available keys" + (perItem ? " (fields apply to each item)" : "") + ", or omit fields for the whole result."]),
932
+ ],
933
+ };
934
+ return { text: JSON.stringify(structured), isError: true, structured };
1099
935
  }
1100
936
  const fieldsHint = (perItem) => "Pass fields (dotted paths" + (perItem ? ", applied per item" : "") + ") to keep only the keys you need.";
1101
937
  /** How many leading items fit under `budget` characters once serialized
@@ -1118,8 +954,21 @@ function itemsThatFit(items, budget) {
1118
954
  * last-id styles resume at the cut, cursor and page styles say what the
1119
955
  * caller must do instead. */
1120
956
  export function pageOutcome(items, nextPage, options = {}) {
957
+ if (nextPage !== null && options.carryArgs) {
958
+ const args = options.args ?? {};
959
+ const carried = options.carryArgs.filter((name) => args[name] !== undefined && !(name in nextPage));
960
+ nextPage = {
961
+ ...Object.fromEntries(carried.map((name) => [name, args[name]])),
962
+ // The same projection on the next page.
963
+ ...(options.fields && !("fields" in nextPage) ? { fields: options.fields.map((path) => path.join(".")) } : {}),
964
+ ...nextPage,
965
+ };
966
+ }
1121
967
  const fields = options.fields ?? null;
1122
968
  const maxChars = options.maxChars ?? DEFAULT_MAX_RESULT_CHARS;
969
+ const unmatched = unmatchedFieldsOutcome(items, fields, true, options);
970
+ if (unmatched)
971
+ return unmatched;
1123
972
  const shown = projectFields(items, fields);
1124
973
  const full = {
1125
974
  items: shown,
@@ -1177,7 +1026,10 @@ const omittedMarker = (key, chars, maxChars) => chars > maxChars
1177
1026
  export function dataOutcome(data, options = {}) {
1178
1027
  const fields = options.fields ?? null;
1179
1028
  const maxChars = options.maxChars ?? DEFAULT_MAX_RESULT_CHARS;
1180
- const value = data === undefined || data === null ? { ok: true } : projectFields(data, fields);
1029
+ const unmatched = data !== undefined && data !== null ? unmatchedFieldsOutcome(data, fields, Array.isArray(data), options) : null;
1030
+ if (unmatched)
1031
+ return unmatched;
1032
+ const value = data !== undefined && data !== null ? projectFields(data, fields) : { ok: true };
1181
1033
  if (typeof value === "string") {
1182
1034
  if (value.length <= maxChars)
1183
1035
  return { text: value, isError: false, structured: value };
@@ -1286,13 +1138,23 @@ export async function binaryOutcome(blob, options = {}) {
1286
1138
  isError: false,
1287
1139
  };
1288
1140
  }
1289
- function extractRetryAfter(e) {
1290
- const headers = e.headers;
1291
- const fromHeader = headers?.get?.("retry-after");
1292
- if (fromHeader)
1293
- return fromHeader;
1294
- const match = /retry after (\d+)s/i.exec(JSON.stringify(e.body ?? ""));
1295
- return match?.[1];
1141
+ function rateLimitNextStep(retryAt) {
1142
+ if (retryAt instanceof Date && !Number.isNaN(retryAt.getTime())) {
1143
+ const at = new Date(Math.ceil(retryAt.getTime() / 1000) * 1000).toISOString().replace(/\.\d{3}Z$/, "Z");
1144
+ return "Rate limited: wait until " + at + ", then call again. This is not a credential problem.";
1145
+ }
1146
+ return "Rate limited: back off, then call again once; the request already honored any Retry-After within its ceiling.";
1147
+ }
1148
+ /** Error codes APIs report inside a 2xx body (Slack's `error`), mapped to
1149
+ * the stable codes when their meaning is unambiguous. */
1150
+ function payloadFailureCode(code) {
1151
+ if (typeof code !== "string")
1152
+ return undefined;
1153
+ if (/^(not_authed|invalid_auth|token_revoked|token_expired|account_inactive|unauthorized|unauthenticated|forbidden|access_denied|missing_scope)$/i.test(code))
1154
+ return "AUTH_INVALID";
1155
+ if (/^(ratelimited|rate_limited|rate_limit_exceeded|too_many_requests)$/i.test(code))
1156
+ return "RATE_LIMITED";
1157
+ return undefined;
1296
1158
  }
1297
1159
  /** Classify an SDK error result by status: the code and what to do next. */
1298
1160
  export function classifyError(error, context = {}) {
@@ -1300,27 +1162,41 @@ export function classifyError(error, context = {}) {
1300
1162
  const message = e.message ?? String(error);
1301
1163
  if (e.violations !== undefined)
1302
1164
  return { code: "VALIDATION_FAILED", nextSteps: ["Fix the fields named in violations and call again."] };
1303
- if (e.name === "TransportError" || (typeof e.status !== "number" && /fetch|ECONN|ENOTFOUND|timed out|TLS|abort/i.test(message))) {
1165
+ if (e.name === "TransportError" || (e.name !== "PaginationError" && typeof e.status !== "number" && /fetch|ECONN|ENOTFOUND|timed out|TLS|abort/i.test(message))) {
1304
1166
  return { code: "NETWORK_ERROR", nextSteps: ["The API could not be reached (network, DNS, TLS or timeout). Retry once with backoff; do not loop."] };
1305
1167
  }
1306
1168
  const status = typeof e.status === "number" ? e.status : 0;
1307
1169
  const body = e.body;
1308
1170
  const auth = context.authHint ? context.authHint.trim().replace(/[.]?$/, ".") : null;
1171
+ // A rate limit can arrive as a 403 (GitHub); the SDK marks it either way.
1172
+ if (status === 429 || e.rateLimit !== undefined)
1173
+ return { code: "RATE_LIMITED", nextSteps: [rateLimitNextStep(e.rateLimit?.retryAt)] };
1174
+ const scopes = context.requiredScopes ?? [];
1175
+ const scopeFailure = { code: "INSUFFICIENT_SCOPE", nextSteps: ["This operation requires the OAuth scopes: " + scopes.join(", ") + ". Use a credential granted them (with the CLI: login --scopes " + scopes.join(",") + "). If it already has them, the account may lack access to this resource."] };
1176
+ // A failure the API reported inside a 2xx body.
1177
+ if (e.name === "PayloadError") {
1178
+ if (scopes.length && /^missing_scope$/i.test(String(e.code)))
1179
+ return scopeFailure;
1180
+ const reported = payloadFailureCode(e.code);
1181
+ if (reported === "AUTH_INVALID")
1182
+ return { code: "AUTH_INVALID", nextSteps: ["The API rejected the credential (" + String(e.code) + ")." + (auth ? " " + auth : "")] };
1183
+ if (reported === "RATE_LIMITED")
1184
+ return { code: "RATE_LIMITED", nextSteps: [rateLimitNextStep(undefined)] };
1185
+ return { code: "CALL_FAILED", nextSteps: ["The API reported a failure in a successful response; body says why. Do not treat the call as done."] };
1186
+ }
1309
1187
  if (status === 401) {
1310
1188
  return context.hadCredential
1311
1189
  ? { code: "AUTH_INVALID", nextSteps: ["The credential was rejected; it may be expired or for another environment." + (auth ? " " + auth : "")] }
1312
1190
  : { code: "NO_AUTH", nextSteps: ["No credential was sent." + (auth ? " " + auth : "")] };
1313
1191
  }
1192
+ if (status === 403 && scopes.length)
1193
+ return scopeFailure;
1314
1194
  if (status === 403)
1315
1195
  return { code: "AUTH_INVALID", nextSteps: ["The credential lacks access to this operation; it is not a retryable error."] };
1316
1196
  if (status === 402)
1317
1197
  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."] };
1318
1198
  if (status === 404)
1319
1199
  return { code: "NOT_FOUND", nextSteps: notFoundNextSteps(message, e.body) };
1320
- if (status === 429) {
1321
- const retryAfter = extractRetryAfter(e);
1322
- return { code: "RATE_LIMITED", nextSteps: [retryAfter ? "Wait " + retryAfter + " seconds, then call again." : "Back off and retry once; the request was already retried with the server's Retry-After."] };
1323
- }
1324
1200
  if (status === 422 && body?.errors?.[0]?.code === "spec_error") {
1325
1201
  return {
1326
1202
  code: "SPEC_INVALID",
@@ -1355,7 +1231,22 @@ function notFoundNextSteps(message, body) {
1355
1231
  * message, status, the API's body, where to read more, and what to do. */
1356
1232
  export function errorOutcome(error, context = {}) {
1357
1233
  const e = error;
1234
+ // A GraphQL response with data and errors is a partial success: the agent
1235
+ // gets the data it can use and the errors that explain what is missing.
1236
+ const partial = error;
1237
+ if (partial?.name === "GraphQLRequestError" && partial.data !== undefined && partial.data !== null) {
1238
+ const structured = { data: partial.data, errors: partial.errors ?? [], partial: true };
1239
+ return { text: JSON.stringify(structured), isError: false, structured };
1240
+ }
1241
+ // The SDK raises NotModifiedError for a 304: a conditional request
1242
+ // matched. That is a result, not a failure; the CLI prints the same shape.
1243
+ const notModified = error;
1244
+ if (notModified?.name === "NotModifiedError") {
1245
+ const structured = { ok: true, not_modified: true, ...(notModified.etag ? { etag: notModified.etag } : {}) };
1246
+ return { text: JSON.stringify(structured), isError: false, structured };
1247
+ }
1358
1248
  const { code, nextSteps } = classifyError(error, context);
1249
+ const retryAt = e?.rateLimit?.retryAt instanceof Date ? new Date(Math.ceil(e.rateLimit.retryAt.getTime() / 1000) * 1000).toISOString().replace(/\.\d{3}Z$/, "Z") : undefined;
1359
1250
  const bodyRequestId = e?.body && typeof e.body === "object" && !Array.isArray(e.body)
1360
1251
  ? e.body.request_id ?? e.body.requestId
1361
1252
  : undefined;
@@ -1366,12 +1257,23 @@ export function errorOutcome(error, context = {}) {
1366
1257
  message: e?.message,
1367
1258
  ...(typeof e?.status === "number" ? { status: e.status } : {}),
1368
1259
  ...(requestId ? { request_id: requestId } : {}),
1260
+ ...(retryAt ? { retry_at: retryAt } : {}),
1369
1261
  ...(e?.body !== undefined ? { body: e.body } : {}),
1370
1262
  ...(context.docsUrl ? { docs_url: context.docsUrl } : {}),
1371
1263
  next_steps: nextSteps,
1372
1264
  };
1373
1265
  return { text: JSON.stringify(structured), isError: true, structured };
1374
1266
  }
1267
+ /** A required operation whose schemes this server cannot send fails before
1268
+ * any request, instead of calling the API without credentials. */
1269
+ export function unsupportedAuthOutcome(op) {
1270
+ if (op.auth !== "required" || !op.credentialOptions || op.credentialOptions.some((alternative) => alternative.length > 0))
1271
+ return null;
1272
+ const schemes = [...new Set((op.security ?? []).flatMap((requirement) => Object.keys(requirement)))];
1273
+ return textError(`${op.tool} requires ${schemes.length ? schemes.join(" or ") : "an authentication scheme"} authentication, which this MCP server cannot send.`, "NO_AUTH", [
1274
+ "Call this operation from an SDK with a custom HTTP client that adds the credential, or ask the API owner to add a supported scheme.",
1275
+ ]);
1276
+ }
1375
1277
  export function textError(text, code = "CALL_FAILED", nextSteps = []) {
1376
1278
  const structured = { error: "Error", code, message: text, next_steps: nextSteps };
1377
1279
  return { text: JSON.stringify(structured), isError: true, structured };
@@ -1380,114 +1282,236 @@ async function fetchDocs(source, pathOrFile) {
1380
1282
  const url = resolveDocsContentUrl(source.docsUrl(), source.docsIndexUrl?.() ?? null, pathOrFile);
1381
1283
  return url === null ? null : source.fetchText(url);
1382
1284
  }
1383
- function coverageText(source) {
1384
- const omitted = source.omittedOps ?? [];
1385
- const generated = source.generatedOperationCount ?? source.ops.length;
1386
- return omitted.length > 0
1387
- ? "Coverage: generated " + generated + " of " + (generated + omitted.length) + " operations. Omitted by the plan limit: " + omitted.map((op) => op.tool + " (" + op.httpMethod + " " + op.path + ")").join(", ") + "."
1388
- : null;
1285
+ /** "GraphQL mutation issueCreate" or "POST /v1/issues". */
1286
+ export function operationLabel(op) {
1287
+ return op.graphql?.field ? "GraphQL " + op.graphql.kind + " " + op.graphql.field : op.httpMethod + " " + op.path;
1389
1288
  }
1390
- function omittedPlanLimit(ops, requested) {
1289
+ function omittedEntry(op) {
1290
+ return op.graphql?.field
1291
+ ? { tool: op.tool, graphql: op.graphql.kind + " " + op.graphql.field }
1292
+ : { tool: op.tool, method: op.httpMethod, path: op.path };
1293
+ }
1294
+ function omittedPlanLimit(source, ops, requested) {
1295
+ const generated = source.generatedOperationCount ?? source.ops.length;
1296
+ const total = generated + (source.omittedOps?.length ?? 0);
1391
1297
  const structured = {
1392
1298
  error: "PlanLimitError",
1393
1299
  code: "PLAN_LIMIT",
1394
- message: requested
1395
- ? "The operation " + requested + " exists in the Spec but was omitted from this generated package by its plan limit."
1396
- : "Matching operations exist in the Spec but were omitted from this generated package by its plan limit.",
1397
- omitted_operations: ops.map((op) => ({ tool: op.tool, method: op.httpMethod, path: op.path })),
1398
- next_steps: ["Upgrade at https://typeship.dev/pricing and regenerate the package without the operation cap.", "Do not invent or retry an omitted operation against this generated package."],
1300
+ message: (requested
1301
+ ? "The operation " + requested + " is in the API but not in this package"
1302
+ : "Matching operations are in the API but not in this package") + ", which was generated with " + generated + " of its " + total + " operations.",
1303
+ omitted_operations: ops.slice(0, SEARCH_PAGE_SIZE).map(omittedEntry),
1304
+ next_steps: ["The package's publisher can regenerate it with every operation.", "Do not invent or retry an omitted operation against this package."],
1399
1305
  };
1400
1306
  return { text: JSON.stringify(structured), isError: true, structured };
1401
1307
  }
1402
- export function referenceText(op) {
1308
+ /** Nesting read_docs spells out before pointing at schema: true. */
1309
+ const REFERENCE_DEPTH = 3;
1310
+ /** Characters one top-level argument's nested fields may take; a deep
1311
+ * filter object is shown shallower until it fits. */
1312
+ const REFERENCE_ARGUMENT_BUDGET = 6_000;
1313
+ /** Enum values listed inline; longer enums are cut with a count. */
1314
+ const REFERENCE_ENUM_VALUES = 30;
1315
+ /** Prose for one description: one line, markdown links reduced to their text. */
1316
+ function referenceProse(text) {
1317
+ if (typeof text !== "string")
1318
+ return "";
1319
+ return text.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\s+/g, " ").trim();
1320
+ }
1321
+ /** Sentences the generator appends to an argument's description because a
1322
+ * call depends on them: enum meanings, defaults, deprecation, reference
1323
+ * and date forms, file paths. They survive the cut. */
1324
+ const REFERENCE_NOTE = /^(Values:|Default|Deprecated|Accepts|Markdown|Paths? of (a )?local file|Must|Required|Only one of)/;
1325
+ /** Leading prose kept per argument before its notes. */
1326
+ const REFERENCE_ARGUMENT_PROSE = 160;
1327
+ /** An argument's description, cut to its leading sentences within the
1328
+ * budget plus every note the generator added. The full text is in the
1329
+ * schema (schema: true). */
1330
+ function argumentProse(text) {
1331
+ const prose = referenceProse(text);
1332
+ if (prose.length <= REFERENCE_ARGUMENT_PROSE)
1333
+ return prose;
1334
+ const sentences = prose.match(/[^.!?]+(?:[.!?]+(?=\s|$)|$)\s*/g) ?? [prose];
1335
+ const kept = [];
1336
+ let used = 0;
1337
+ let cut = false;
1338
+ for (const raw of sentences) {
1339
+ const sentence = raw.trim();
1340
+ if (REFERENCE_NOTE.test(sentence)) {
1341
+ kept.push(sentence);
1342
+ continue;
1343
+ }
1344
+ if (kept.length === 0 || used + sentence.length <= REFERENCE_ARGUMENT_PROSE) {
1345
+ kept.push(sentence.length > REFERENCE_ARGUMENT_PROSE * 2 ? sentence.slice(0, REFERENCE_ARGUMENT_PROSE * 2).replace(/\s+\S*$/, "") + "…" : sentence);
1346
+ used += sentence.length;
1347
+ }
1348
+ else {
1349
+ cut = true;
1350
+ }
1351
+ }
1352
+ return kept.join(" ") + (cut ? " …" : "");
1353
+ }
1354
+ /** A schema's type as an agent writes it: string, integer[], "a"|"b", object. */
1355
+ function referenceType(schema) {
1356
+ if (Array.isArray(schema.enum)) {
1357
+ const values = schema.enum.slice(0, REFERENCE_ENUM_VALUES).map((v) => JSON.stringify(v));
1358
+ return values.join("|") + (schema.enum.length > REFERENCE_ENUM_VALUES ? "|… (" + (schema.enum.length - REFERENCE_ENUM_VALUES) + " more)" : "");
1359
+ }
1360
+ if (schema.const !== undefined)
1361
+ return JSON.stringify(schema.const);
1362
+ const variants = (schema.anyOf ?? schema.oneOf);
1363
+ if (Array.isArray(variants))
1364
+ return [...new Set(variants.map((v) => referenceType(v)))].join("|");
1365
+ const types = Array.isArray(schema.type) ? schema.type : typeof schema.type === "string" ? [schema.type] : [];
1366
+ const one = (t) => {
1367
+ if (t === "array") {
1368
+ const items = schema.items && typeof schema.items === "object" ? referenceType(schema.items) : "any";
1369
+ return (/[| ]/.test(items) ? "(" + items + ")" : items) + "[]";
1370
+ }
1371
+ return t + (t === "string" && typeof schema.format === "string" ? " (" + schema.format + ")" : "");
1372
+ };
1373
+ if (types.length === 0)
1374
+ return schema.properties ? "object" : "any";
1375
+ return types.map(one).join("|");
1376
+ }
1377
+ /** The object whose fields an argument lists: itself, its array's items,
1378
+ * or the one object of a nullable union. */
1379
+ function referenceObject(schema) {
1380
+ if (schema.properties && typeof schema.properties === "object")
1381
+ return schema;
1382
+ const items = schema.items;
1383
+ if (items && typeof items === "object")
1384
+ return referenceObject(items);
1385
+ const variants = (schema.anyOf ?? schema.oneOf);
1386
+ if (Array.isArray(variants)) {
1387
+ const objects = variants.map(referenceObject).filter((v) => v !== undefined);
1388
+ if (objects.length === 1)
1389
+ return objects[0];
1390
+ }
1391
+ return undefined;
1392
+ }
1393
+ /** One line per argument, nested fields indented beneath their object.
1394
+ * Each top-level argument is spelled out as deep as fits its budget. */
1395
+ function referenceArguments(schema, lines) {
1396
+ const properties = (schema.properties ?? {});
1397
+ for (const name of Object.keys(properties)) {
1398
+ const only = { ...schema, properties: { [name]: properties[name] } };
1399
+ let block = [];
1400
+ // Deepest first; then the same depth without listing the field names
1401
+ // below the cut; then one level with names only.
1402
+ for (const [depth, names] of [[REFERENCE_DEPTH, true], [2, true], [2, false], [1, true]]) {
1403
+ block = [];
1404
+ referenceArgumentLines(only, 0, depth, names, " ", block);
1405
+ if (block.join("\n").length <= REFERENCE_ARGUMENT_BUDGET)
1406
+ break;
1407
+ }
1408
+ lines.push(...block);
1409
+ }
1410
+ }
1411
+ function referenceArgumentLines(schema, depth, maxDepth, names, indent, lines) {
1412
+ const properties = (schema.properties ?? {});
1413
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
1414
+ for (const [name, child] of Object.entries(properties)) {
1415
+ if (!child || typeof child !== "object")
1416
+ continue;
1417
+ const description = argumentProse(child.description);
1418
+ const extras = [
1419
+ ...(child.default !== undefined && !/\bdefault\b/i.test(description) ? ["default " + JSON.stringify(child.default)] : []),
1420
+ ...(child.deprecated === true && !/deprecated/i.test(description) ? ["deprecated"] : []),
1421
+ ];
1422
+ lines.push(indent + name + " (" + referenceType(child) + (required.has(name) ? ", required" : "") + (extras.length ? ", " + extras.join(", ") : "") + ")" + (description ? ": " + description : ""));
1423
+ const nested = referenceObject(child);
1424
+ if (!nested)
1425
+ continue;
1426
+ if (depth + 1 < maxDepth) {
1427
+ referenceArgumentLines(nested, depth + 1, maxDepth, names, indent + " ", lines);
1428
+ continue;
1429
+ }
1430
+ if (!names)
1431
+ continue;
1432
+ const keys = Object.keys(nested.properties);
1433
+ lines.push(indent + " fields: " + keys.slice(0, 40).join(", ") + (keys.length > 40 ? ", … " + (keys.length - 40) + " more" : "") + " (types with schema: true)");
1434
+ }
1435
+ }
1436
+ /** A result's shape in one line: keys and types, one level into objects. */
1437
+ function referenceShape(schema, depth = 0) {
1438
+ if (!schema || typeof schema !== "object")
1439
+ return "any";
1440
+ const object = schema.properties && typeof schema.properties === "object" ? schema : undefined;
1441
+ if (object) {
1442
+ if (depth >= 2)
1443
+ return "{…}";
1444
+ const entries = Object.entries(object.properties);
1445
+ const shown = entries.slice(0, 40).map(([key, child]) => key + ": " + referenceShape(child, depth + 1));
1446
+ return "{" + shown.join(", ") + (entries.length > 40 ? ", … " + (entries.length - 40) + " more" : "") + "}";
1447
+ }
1448
+ const items = schema.items;
1449
+ if (items && typeof items === "object" && (items.properties || items.items)) {
1450
+ return "[" + referenceShape(items, depth) + "]";
1451
+ }
1452
+ if (Array.isArray(schema.enum) && schema.enum.length > 8) {
1453
+ return schema.enum.slice(0, 8).map((v) => JSON.stringify(v)).join("|") + "|…";
1454
+ }
1455
+ // Nullability and formats matter for sending, not for reading a result.
1456
+ const type = referenceType({ ...schema, format: undefined });
1457
+ return type.split("|").filter((t) => t !== "null").join("|") || type;
1458
+ }
1459
+ /**
1460
+ * An operation's reference as read_docs returns it: what it does, whether
1461
+ * it is safe, every argument in prose (types, enums, required, notes, and
1462
+ * nested fields), one example and the result's shape. The complete JSON
1463
+ * Schemas come with `schema: true`, as the CLI's `docs --schema` does; they
1464
+ * cost several times the rest and an agent rarely needs them to call.
1465
+ */
1466
+ export function referenceText(op, options = {}) {
1403
1467
  const safety = operationSafety(op);
1404
- const example = op.exampleArguments ?? exampleArgumentsFromSchema(op.inputSchema);
1468
+ // A credential-defaulted argument (Twilio's AccountSid) is left out, so the
1469
+ // example shows the call an agent should make.
1470
+ const example = Object.fromEntries(Object.entries(op.exampleArguments ?? {})
1471
+ .filter(([name]) => !op.params.some((p) => p.name === name && p.credential)));
1405
1472
  const lines = [
1406
- op.tool + ": " + op.httpMethod + " " + op.path + (op.paginated ? " (paginated)" : ""),
1473
+ op.tool + ": " + operationLabel(op) + (op.paginated ? " (paginated)" : ""),
1407
1474
  ...(op.summary ? [op.summary] : []),
1408
- ...(op.description ? ["", op.description.trim()] : []),
1475
+ ...(op.description && op.description.trim() !== op.summary?.trim() ? ["", op.description.trim()] : []),
1409
1476
  "",
1410
1477
  "Safety: " + safety + (safety === "destructive" ? " (execute requires confirm: true)" : ""),
1411
- ...(op.auth ? ["Authentication: " + op.auth] : []),
1478
+ ...(op.auth ? ["Authentication: " + (op.authNotDeclared ? "not declared by the API Spec" : op.auth)] : []),
1479
+ ...(requiredScopes(op.security).length ? ["Required OAuth scopes: " + requiredScopes(op.security).join(", ")] : []),
1412
1480
  ];
1413
- if (op.params.length > 0) {
1481
+ const input = toolInputSchema(op);
1482
+ if (Object.keys((input.properties ?? {})).length > 0) {
1414
1483
  lines.push("", "Arguments:");
1415
- for (const p of op.params) {
1416
- lines.push(" " + p.name + " (" + (p.enum ? p.enum.join("|") : p.type) + (p.required ? ", required" : "") + ")" +
1417
- (p.description ? ": " + p.description.trim().split("\n")[0] : ""));
1418
- }
1484
+ referenceArguments(input, lines);
1419
1485
  }
1420
- if (!hasOwnFieldsParam(op)) {
1421
- lines.push("", "Also: fields (array of dotted paths) keeps only those keys of the result" + (op.paginated ? ", per item" : "") + ".");
1486
+ lines.push("", "Example arguments: " + JSON.stringify(example));
1487
+ if (op.outputSchema) {
1488
+ lines.push("", (op.paginated ? "Returns one page: " : "Returns: ") + referenceShape(op.outputSchema));
1489
+ }
1490
+ if (options.schema) {
1491
+ lines.push("", "Input schema: " + JSON.stringify(input));
1492
+ if (op.outputSchema)
1493
+ lines.push("", "Output schema: " + JSON.stringify(op.outputSchema));
1494
+ }
1495
+ else {
1496
+ lines.push("", "Full input and output JSON Schemas: read_docs " + JSON.stringify({ page: op.tool, schema: true }) + ".");
1422
1497
  }
1423
- lines.push("", "Input schema:", "```json", JSON.stringify(toolInputSchema(op), null, 2), "```");
1424
- lines.push("", "Example arguments:", "```json", JSON.stringify(example, null, 2), "```");
1425
- if (op.outputSchema)
1426
- lines.push("", "Output schema:", "```json", JSON.stringify(op.outputSchema, null, 2), "```");
1427
1498
  return lines.join("\n");
1428
1499
  }
1429
- /** Query terms: lowercase words of two or more characters, with the
1430
- * snake/kebab/camel seams split so "createAccount" finds accounts_create. */
1431
- function searchTerms(query) {
1432
- return [...new Set(query.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 2))];
1433
- }
1434
- /** Relevance of one operation to the terms: the tool name counts most,
1435
- * then summary, path and argument names, then the description. The whole
1436
- * query as a phrase in the name or summary is a strong signal. */
1437
- export function searchScore(op, query) {
1438
- const terms = searchTerms(query);
1439
- if (terms.length === 0)
1440
- return 0;
1441
- const tool = op.tool.toLowerCase();
1442
- const toolWords = tool.split("_");
1443
- const summary = (op.summary ?? "").toLowerCase();
1444
- const path = op.path.toLowerCase();
1445
- const params = op.params.map((p) => p.name.toLowerCase());
1446
- const description = (op.description ?? "").toLowerCase();
1447
- let score = 0;
1448
- for (const term of terms) {
1449
- if (toolWords.includes(term))
1450
- score += 10;
1451
- else if (tool.includes(term))
1452
- score += 6;
1453
- if (summary.split(/[^a-z0-9]+/).includes(term))
1454
- score += 5;
1455
- else if (summary.includes(term))
1456
- score += 3;
1457
- if (path.includes(term))
1458
- score += 3;
1459
- if (params.some((p) => p === term))
1460
- score += 3;
1461
- else if (params.some((p) => p.includes(term)))
1462
- score += 1;
1463
- if (description.includes(term))
1464
- score += 1;
1465
- }
1466
- const phrase = query.trim().toLowerCase();
1467
- if (phrase.length >= 3 && (tool.includes(phrase.replace(/[^a-z0-9]+/g, "_")) || summary.includes(phrase)))
1468
- score += 8;
1469
- return score;
1470
- }
1471
1500
  export async function docsSearch(source, query, page = 1) {
1472
1501
  const sections = [];
1473
- const ranked = source.ops
1474
- .map((op) => ({ op, score: searchScore(op, query) }))
1475
- .filter((r) => r.score > 0)
1476
- .sort((a, b) => b.score - a.score || a.op.tool.localeCompare(b.op.tool));
1477
- const omittedRanked = (source.omittedOps ?? [])
1478
- .map((op) => ({ op, score: searchScore(op, query) }))
1479
- .filter((result) => result.score > 0)
1480
- .sort((a, b) => b.score - a.score || a.op.tool.localeCompare(b.op.tool));
1502
+ const ranked = rankOperations(source.ops, query);
1503
+ const omittedRanked = rankOperations(source.omittedOps ?? [], query);
1481
1504
  const exactOmitted = findOperation(source.omittedOps ?? [], query);
1482
1505
  if (exactOmitted)
1483
- return omittedPlanLimit([exactOmitted], exactOmitted.tool);
1506
+ return omittedPlanLimit(source, [exactOmitted], exactOmitted.tool);
1484
1507
  const pageIndex = Math.max(1, Math.floor(page)) - 1;
1485
1508
  const slice = ranked.slice(pageIndex * SEARCH_PAGE_SIZE, (pageIndex + 1) * SEARCH_PAGE_SIZE);
1509
+ const more = ranked.length - (pageIndex + 1) * SEARCH_PAGE_SIZE;
1486
1510
  if (slice.length > 0) {
1487
- const more = ranked.length - (pageIndex + 1) * SEARCH_PAGE_SIZE;
1488
1511
  sections.push("Reference matches (best first" + (ranked.length > SEARCH_PAGE_SIZE ? ", page " + (pageIndex + 1) + " of " + Math.ceil(ranked.length / SEARCH_PAGE_SIZE) : "") + "):\n" +
1489
- slice.map((r) => "- " + r.op.tool + ": " + (r.op.summary ?? r.op.httpMethod + " " + r.op.path)).join("\n") +
1490
- (more > 0 ? "\n(" + more + " more; pass page: " + (pageIndex + 2) + ")" : ""));
1512
+ slice.map((r) => "- " + r.op.tool + ": " + searchLabel(r.op)).join("\n") +
1513
+ (more > 0 ? "\n(" + more + " more; pass page: " + (pageIndex + 2) + ")" : "") +
1514
+ "\nread_docs {\"page\": \"<tool>\"} gives an operation's arguments and example.");
1491
1515
  }
1492
1516
  else if (ranked.length > 0) {
1493
1517
  sections.push("No reference matches on page " + (pageIndex + 1) + "; there are " + Math.ceil(ranked.length / SEARCH_PAGE_SIZE) + " pages.");
@@ -1496,40 +1520,63 @@ export async function docsSearch(source, query, page = 1) {
1496
1520
  const guideSlice = guides.slice(pageIndex * SEARCH_PAGE_SIZE, (pageIndex + 1) * SEARCH_PAGE_SIZE);
1497
1521
  const proseMatchCount = guides.length;
1498
1522
  if (guideSlice.length > 0) {
1499
- sections.push("Guide matches (best first, " + guides.length + " pages):\n" + guideSlice.map((match) => "- [" + match.title + (match.section ? " / " + match.section : "") + "](" + match.url + "): " + match.excerpt + "\n read_docs " + JSON.stringify({ page: match.url })).join("\n"));
1523
+ sections.push("Guide matches (best first, " + guides.length + " pages; read_docs {\"page\": \"<url>\"} reads one):\n" + guideSlice.map((match) => "- [" + match.title + (match.section ? " / " + match.section : "") + "](" + match.url + "): " + match.excerpt).join("\n"));
1500
1524
  }
1501
1525
  if (status === "unavailable")
1502
1526
  sections.push("The docs site is unavailable; the API reference was still searched.");
1527
+ // Lean on purpose: the text above is what most clients show the model,
1528
+ // and this mirrors it rather than repeating the read_docs call per hit.
1503
1529
  const structured = {
1504
- schema_version: "1", query, page: pageIndex + 1,
1505
- reference: slice.map(({ op }) => ({ tool: op.tool, method: op.httpMethod, path: op.path, ...(op.summary ? { summary: op.summary } : {}), read_tool: { name: "read_docs", arguments: { page: op.tool } } })),
1506
- guides: guideSlice.map((match) => ({ ...match, read_tool: { name: "read_docs", arguments: { page: match.url } } })),
1530
+ schema_version: "2", query, page: pageIndex + 1,
1531
+ reference: slice.map(({ op }) => ({ tool: op.tool, summary: searchLabel(op), ...(searchDeprecated(op) ? { deprecated: true } : {}) })),
1532
+ guides: guideSlice,
1507
1533
  totals: { reference: ranked.length, guides: guides.length }, guides_status: status,
1534
+ ...(slice.length > 0 || guideSlice.length > 0 ? { read_docs: { page: "<tool name or guide url>" } } : {}),
1535
+ ...(more > 0 ? { next_page: pageIndex + 2 } : {}),
1508
1536
  };
1509
1537
  if (omittedRanked.length > 0 && ranked.length === 0 && proseMatchCount === 0) {
1510
- return omittedPlanLimit(omittedRanked.map((result) => result.op));
1538
+ return omittedPlanLimit(source, omittedRanked.map((result) => result.op));
1511
1539
  }
1512
1540
  if (omittedRanked.length > 0) {
1513
- sections.push("Operations omitted by the plan limit:\n" + omittedRanked.slice(0, SEARCH_PAGE_SIZE).map((result) => "- " + result.op.tool + ": " + (result.op.summary ?? result.op.httpMethod + " " + result.op.path)).join("\n"));
1541
+ const generated = source.generatedOperationCount ?? source.ops.length;
1542
+ sections.push("Also in the API but not in this package (it includes " + generated + " of " + (generated + (source.omittedOps?.length ?? 0)) + " operations; these return PLAN_LIMIT):\n" + omittedRanked.slice(0, SEARCH_PAGE_SIZE).map((result) => "- " + result.op.tool + ": " + (result.op.summary ?? operationLabel(result.op))).join("\n"));
1514
1543
  }
1515
- const coverage = coverageText(source);
1516
1544
  if (sections.length === 0) {
1517
1545
  return {
1518
- text: (coverage ? coverage + "\n\n" : "") + "No matches for: " + query + (source.docsUrl() === null && (source.docsIndexUrl?.() ?? null) === null ? " (a docs URL was not provided at generate time; only the API reference was searched)" : ""),
1546
+ text: "No matches for: " + query + (source.docsUrl() === null && (source.docsIndexUrl?.() ?? null) === null ? " (a docs URL was not provided at generate time; only the API reference was searched)" : ""),
1519
1547
  isError: false,
1520
1548
  structured,
1521
1549
  };
1522
1550
  }
1523
- return { text: [...(coverage ? [coverage] : []), ...sections].join("\n\n"), isError: false, structured };
1524
- }
1525
- export async function docsRead(source, page) {
1551
+ return { text: sections.join("\n\n"), isError: false, structured };
1552
+ }
1553
+ function searchDeprecated(op) {
1554
+ return op.deprecated === true;
1555
+ }
1556
+ /** One line per hit: the summary (or the wire call), flagged when deprecated. */
1557
+ function searchLabel(op) {
1558
+ return (searchDeprecated(op) ? "(deprecated) " : "") + (op.summary ?? operationLabel(op));
1559
+ }
1560
+ /** Characters one read_docs call returns; the rest is paged by offset. */
1561
+ export const READ_DOCS_LIMIT = 20_000;
1562
+ /** One part of a long page, ending with how to read the next part. */
1563
+ function docsPart(page, text, offset, schema = false) {
1564
+ if (offset <= 0 && text.length <= READ_DOCS_LIMIT)
1565
+ return text;
1566
+ const start = Math.min(Math.max(0, offset), text.length);
1567
+ const end = Math.min(text.length, start + READ_DOCS_LIMIT);
1568
+ const more = end < text.length
1569
+ ? "\n\n[Characters " + start + "-" + end + " of " + text.length + ". Continue with read_docs " + JSON.stringify({ page, ...(schema ? { schema: true } : {}), offset: end }) + ".]"
1570
+ : "\n\n[Characters " + start + "-" + end + " of " + text.length + "; end of page.]";
1571
+ return text.slice(start, end) + more;
1572
+ }
1573
+ export async function docsRead(source, page, offset = 0, options = {}) {
1526
1574
  const opMatch = findOperation(source.ops, page);
1527
- const coverage = coverageText(source);
1528
1575
  if (opMatch)
1529
- return { text: [...(coverage ? [coverage] : []), referenceText(opMatch)].join("\n\n"), isError: false };
1576
+ return { text: docsPart(page, referenceText(opMatch, options), offset, options.schema === true), isError: false };
1530
1577
  const omittedMatch = findOperation(source.omittedOps ?? [], page);
1531
1578
  if (omittedMatch)
1532
- return omittedPlanLimit([omittedMatch], omittedMatch.tool);
1579
+ return omittedPlanLimit(source, [omittedMatch], omittedMatch.tool);
1533
1580
  let target = page;
1534
1581
  if (!/^https?:\/\//.test(target)) {
1535
1582
  const index = await fetchDocs(source, "llms.txt");
@@ -1541,7 +1588,7 @@ export async function docsRead(source, page) {
1541
1588
  ? "A docs URL was not provided at generate time, and no generated operation matches \"" + page + "\"."
1542
1589
  : "Couldn't fetch \"" + page + "\". Use search_docs to find pages.", "NOT_FOUND", ["search_docs finds operations and guide pages."]);
1543
1590
  }
1544
- return { text: [...(coverage ? [coverage] : []), text].join("\n\n"), isError: false };
1591
+ return { text: docsPart(page, text, offset), isError: false };
1545
1592
  }
1546
1593
  /**
1547
1594
  * Dispatch for the shared tools (search_docs, read_docs, execute); returns
@@ -1557,17 +1604,38 @@ export async function callSharedTool(name, args, source, runOperation) {
1557
1604
  return docsSearch(source, args.query, page);
1558
1605
  }
1559
1606
  if (name === "read_docs") {
1560
- return typeof args.page === "string" ? docsRead(source, args.page) : argumentsError({ tool: name }, [{ code: "MISSING_ARGUMENT", argument: "page", message: "read_docs requires a page string." }]);
1607
+ const offset = typeof args.offset === "number" ? args.offset : typeof args.offset === "string" && /^\d+$/.test(args.offset) ? Number(args.offset) : 0;
1608
+ const schema = args.schema === true || args.schema === "true";
1609
+ if (typeof args.page === "string" && !findOperation(source.ops, args.page)) {
1610
+ const hidden = findHidden(source, args.page);
1611
+ if (hidden)
1612
+ return hiddenOutcome(hidden);
1613
+ }
1614
+ 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." }]);
1561
1615
  }
1562
1616
  if (name === "execute") {
1563
1617
  if (typeof args.operation !== "string")
1564
1618
  return argumentsError({ tool: name }, [{ code: "MISSING_ARGUMENT", argument: "operation", message: "execute requires an operation name." }]);
1619
+ // Operation arguments (fields included) belong inside arguments; a
1620
+ // stray top-level key would otherwise be ignored without a word.
1621
+ const stray = Object.keys(args).filter((key) => !EXECUTE_KEYS.includes(key) && args[key] !== undefined);
1622
+ if (stray.length > 0) {
1623
+ return argumentsError({ tool: name }, stray.map((key) => ({
1624
+ code: "UNKNOWN_ARGUMENT",
1625
+ argument: key,
1626
+ message: "Unknown argument \"" + key + "\" to execute, which takes " + EXECUTE_KEYS.join(", ") + ". Put operation arguments, including fields, inside arguments: {\"operation\": \"" + args.operation + "\", \"arguments\": {\"" + key + "\": …}}.",
1627
+ })));
1628
+ }
1565
1629
  const target = findOperation(source.ops, args.operation);
1566
1630
  if (!target) {
1567
1631
  const omitted = findOperation(source.omittedOps ?? [], args.operation);
1568
- return omitted
1569
- ? omittedPlanLimit([omitted], omitted.tool)
1570
- : textError("Unknown operation: " + args.operation + ".", "NOT_FOUND", ["search_docs finds operations by name, path or description."]);
1632
+ if (omitted)
1633
+ return omittedPlanLimit(source, [omitted], omitted.tool);
1634
+ const hidden = findHidden(source, args.operation);
1635
+ if (hidden)
1636
+ return hiddenOutcome(hidden);
1637
+ const suggestions = rankOperations(source.ops, args.operation.replace(/[_.]+/g, " ")).slice(0, 3).map((r) => r.op.tool);
1638
+ return textError("Unknown operation: " + args.operation + "." + (suggestions.length > 0 ? " Did you mean " + suggestions.join(", ") + "?" : ""), "NOT_FOUND", [...(suggestions.length > 0 ? ["read_docs {\"page\": \"" + suggestions[0] + "\"} gives its arguments."] : []), "search_docs finds operations by name, path or description."]);
1571
1639
  }
1572
1640
  if (operationSafety(target) === "destructive" && args.confirm !== true) {
1573
1641
  return textError("The destructive operation " + target.tool + " requires explicit confirmation.", "CONFIRMATION_REQUIRED", ["Review read_docs " + target.tool + ", then retry execute with confirm: true if the destructive effect is intended."]);
@@ -1577,5 +1645,11 @@ export async function callSharedTool(name, args, source, runOperation) {
1577
1645
  : {};
1578
1646
  return runOperation(target, opArgs);
1579
1647
  }
1580
- return undefined;
1648
+ // An operation tool the server's switches hide (operations mode).
1649
+ const hidden = source.ops.some((op) => op.tool === name) ? undefined : (source.hiddenOps ?? []).find((h) => h.op.tool === name);
1650
+ return hidden ? hiddenOutcome(hidden) : undefined;
1651
+ }
1652
+ /** Every OAuth scope an operation's security requirements name, in order. */
1653
+ export function requiredScopes(security) {
1654
+ return [...new Set((security ?? []).flatMap((requirement) => Object.values(requirement).flat()))];
1581
1655
  }