@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,14 +1,14 @@
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.
@@ -16,8 +16,12 @@
16
16
  * Spec: https://modelcontextprotocol.io/specification/2026-07-28
17
17
  */
18
18
 
19
- import { dateKindOf, relativeDate } from "./dates.js";
19
+ import { checkValue, closestName, isMeReference, normalizeName, userShapedReference, type ArgumentIssue } from "./arguments.js";
20
20
  import { docsReadTarget, resolveDocsContentUrl, searchConnectedGuides } from "./docs.js";
21
+ import { projectFields, unmatchedFields, unmatchedFieldsMessage } from "./fields.js";
22
+ import { SEARCH_PAGE_SIZE, rankOperations } from "./search.js";
23
+
24
+ export { projectFields } from "./fields.js";
21
25
 
22
26
  export const MCP_PROTOCOL_VERSION = "2026-07-28";
23
27
  export const LEGACY_PROTOCOL_VERSION = "2025-11-25";
@@ -97,7 +101,7 @@ export class McpAccountLinkRequired extends Error {
97
101
  }
98
102
  }
99
103
 
100
- const API_LINK_INPUT = "typeship_api_account";
104
+ const API_LINK_INPUT = "api_account";
101
105
 
102
106
  /** The subset of an operation spec (ops.ts / the hosted manifest) the
103
107
  * protocol layer reads. */
@@ -114,7 +118,7 @@ export interface OpLike {
114
118
  paginated: boolean;
115
119
  /** GraphQL ops accept a raw selection-set override. */
116
120
  select: boolean;
117
- graphql?: { kind: string };
121
+ graphql?: { kind: string; field?: string };
118
122
  params: {
119
123
  name: string;
120
124
  type: string;
@@ -122,6 +126,10 @@ export interface OpLike {
122
126
  enum?: string[];
123
127
  description?: string;
124
128
  resolve?: false | ReferenceResolver;
129
+ /** A path argument that is the Basic-auth username (Twilio's AccountSid):
130
+ * optional, defaulting to the configured credential. `env` names the
131
+ * variable that supplies it, when the target has one. */
132
+ credential?: { from: "username"; env?: string };
125
133
  }[];
126
134
  inputSchema: Record<string, unknown>;
127
135
  outputSchema?: Record<string, unknown>;
@@ -129,14 +137,24 @@ export interface OpLike {
129
137
  safety?: "read" | "write" | "destructive";
130
138
  /** Whether the operation accepts or requires an API credential. */
131
139
  auth?: "required" | "optional" | "none";
140
+ /** The API Spec declares no security; the generic token is optional. */
141
+ authNotDeclared?: boolean;
142
+ /** Complete credential alternatives this runtime can send; empty when the
143
+ * operation's only schemes are unsupported (digest, mutual TLS, …). */
144
+ credentialOptions?: string[][];
145
+ security?: Record<string, string[]>[];
132
146
  /** Schema-derived, valid wire arguments for examples and agent discovery. */
133
147
  exampleArguments?: Record<string, unknown>;
148
+ /** The operation generated examples lead with: a safe read. */
149
+ showcase?: true;
134
150
  /** Page-walking config (same shape as the SDK's PageConfig), when paginated. */
135
151
  pagination?: { style: string; itemsField: string; cursorParam?: string; idField?: string; pageParam?: string; offsetParam?: string; limitParam?: string };
136
152
  /** Success body is a text/event-stream. */
137
153
  sse?: boolean;
138
154
  /** Wire encoding of the request body. */
139
155
  bodyKind?: string | null;
156
+ /** The spec marks the operation deprecated: search ranks it last. */
157
+ deprecated?: boolean;
140
158
  }
141
159
 
142
160
  /** Fully proved lookup metadata carried in ops.ts / the hosted manifest. */
@@ -271,16 +289,15 @@ export function checkRequestHeaders(headers: { get(name: string): string | null
271
289
  const id = message.id;
272
290
  const version = headers.get("mcp-protocol-version");
273
291
  const bodyVersion = (message.params?._meta as Record<string, unknown> | undefined)?.[META_VERSION];
274
- if (version !== null && !SUPPORTED_PROTOCOL_VERSIONS.includes(version)) return rpcError(id, -32022, "Unsupported protocol version", 400, { supported: SUPPORTED_PROTOCOL_VERSIONS, requested: version });
275
292
  if (message.method === "initialize" || version === LEGACY_PROTOCOL_VERSION) {
276
293
  if (bodyVersion !== undefined || headers.get("mcp-method") !== null || headers.get("mcp-name") !== null) {
277
294
  return rpcError(id, -32020, "Header mismatch: initialize-handshake requests cannot carry modern protocol metadata or routing headers.", 400);
278
295
  }
279
- if (version !== null && version !== LEGACY_PROTOCOL_VERSION) {
280
- return rpcError(id, -32022, "Unsupported initialize-handshake protocol version", 400, { supported: [LEGACY_PROTOCOL_VERSION], requested: version });
281
- }
296
+ // initialize negotiates the revision in its body, so a header naming an
297
+ // older one is not an error here.
282
298
  return null;
283
299
  }
300
+ if (version !== null && !SUPPORTED_PROTOCOL_VERSIONS.includes(version)) return rpcError(id, -32022, "Unsupported protocol version", 400, { supported: SUPPORTED_PROTOCOL_VERSIONS, requested: version });
284
301
  if (version === null) return rpcError(id, -32020, "Header mismatch: MCP-Protocol-Version header is required", 400);
285
302
  if (typeof bodyVersion === "string" && version !== bodyVersion) {
286
303
  return rpcError(id, -32020, "Header mismatch: MCP-Protocol-Version header '" + version + "' does not match body value '" + bodyVersion + "'", 400);
@@ -364,7 +381,10 @@ export async function handleRpc(server: McpServer, incoming: unknown, protocolVe
364
381
  !info || typeof info !== "object" || Array.isArray(info) || typeof info.name !== "string" || typeof info.version !== "string") {
365
382
  return rpcError(id, -32602, "initialize requires protocolVersion, capabilities, and clientInfo with name and version.", 400);
366
383
  }
367
- if (params.protocolVersion !== LEGACY_PROTOCOL_VERSION) return rpcError(id, -32022, "Unsupported initialize-handshake protocol version", 400, { supported: [LEGACY_PROTOCOL_VERSION], requested: params.protocolVersion });
384
+ // Version negotiation: the server answers with a revision it supports
385
+ // (the requested one when it can), and a client that cannot speak it
386
+ // disconnects. So an older request (2025-06-18, 2025-03-26, 2024-11-05)
387
+ // gets 2025-11-25 rather than an error.
368
388
  return { status: 200, message: { jsonrpc: "2.0", id, result: {
369
389
  protocolVersion: LEGACY_PROTOCOL_VERSION,
370
390
  capabilities: { tools: {} },
@@ -515,114 +535,6 @@ export function operationSafety(op: { httpMethod: string; method?: string; graph
515
535
  : "write";
516
536
  }
517
537
 
518
- /** A deterministic, schema-valid-enough example for documentation and
519
- * agent calls. Prefer facts supplied by the API author, then conservative
520
- * values based on formats and field names. Only required object fields are
521
- * included, keeping examples useful instead of manufacturing giant bodies. */
522
- export function exampleFromSchema(schema: unknown, field = "value", depth = 0): unknown {
523
- if (!schema || typeof schema !== "object" || depth > 8) return null;
524
- const node = schema as Record<string, unknown>;
525
- if (node.const !== undefined) return node.const;
526
- if (node.example !== undefined) return node.example;
527
- if (Array.isArray(node.examples) && node.examples.length > 0) return node.examples[0];
528
- if (node.default !== undefined) return node.default;
529
- if (Array.isArray(node.enum) && node.enum.length > 0) {
530
- if (typeof node.pattern === "string") {
531
- try {
532
- const pattern = new RegExp(node.pattern);
533
- const matching = node.enum.find((value) => typeof value === "string" && pattern.test(value));
534
- if (matching !== undefined) return matching;
535
- } catch { /* malformed patterns are ignored for examples */ }
536
- }
537
- return node.enum.find((v) => v !== null) ?? node.enum[0];
538
- }
539
- const variants = (Array.isArray(node.oneOf) ? node.oneOf : Array.isArray(node.anyOf) ? node.anyOf : null) as unknown[] | null;
540
- if (variants) {
541
- const useful = variants.find((v) => v && typeof v === "object" && (v as Record<string, unknown>).type !== "null") ?? variants[0];
542
- return exampleFromSchema(useful, field, depth + 1);
543
- }
544
- const type = Array.isArray(node.type) ? node.type.find((v) => v !== "null") : node.type;
545
- if (type === "object" || node.properties || node.additionalProperties) {
546
- const properties = (node.properties ?? {}) as Record<string, unknown>;
547
- const required = new Set(Array.isArray(node.required) ? node.required.filter((v): v is string => typeof v === "string") : []);
548
- // At the operation root, optional really means optional: the most honest
549
- // runnable example is `{}`. Inside a required object, one representative
550
- // optional field still makes an otherwise empty nested shape legible.
551
- const names = required.size > 0 ? [...required] : depth === 0 ? [] : Object.keys(properties).slice(0, 1);
552
- const value: Record<string, unknown> = {};
553
- for (const name of names) {
554
- if (properties[name] !== undefined) value[name] = exampleFromSchema(properties[name], name, depth + 1);
555
- }
556
- if (Object.keys(value).length === 0 && node.additionalProperties && typeof node.additionalProperties === "object") {
557
- value.key = exampleFromSchema(node.additionalProperties, "key", depth + 1);
558
- }
559
- return value;
560
- }
561
- if (type === "array" || node.items) {
562
- const count = typeof node.minItems === "number" && node.minItems > 1 ? Math.min(node.minItems, 3) : 1;
563
- return Array.from({ length: count }, () => exampleFromSchema(node.items, field, depth + 1));
564
- }
565
- if (type === "integer" || type === "number") {
566
- if (typeof node.minimum === "number") return node.minimum;
567
- if (typeof node.exclusiveMinimum === "number") return node.exclusiveMinimum + 1;
568
- return 1;
569
- }
570
- if (type === "boolean") return true;
571
- if (type === "string" || type === undefined) {
572
- const format = typeof node.format === "string" ? node.format : "";
573
- const lower = field.toLowerCase();
574
- const min = typeof node.minLength === "number" ? node.minLength : 0;
575
- const max = typeof node.maxLength === "number" ? node.maxLength : undefined;
576
- const patterned = typeof node.pattern === "string" ? exampleMatchingPattern(node.pattern, min, max) : null;
577
- let value = patterned ?? (format === "date-time" ? "2026-01-15T12:00:00Z"
578
- : format === "date" ? "2026-01-15"
579
- : format === "email" || lower.includes("email") ? "person@example.com"
580
- : (format === "uri" || format === "url" || lower.endsWith("url")) && (lower.includes("webhook") || lower.includes("callback")) ? "https://example.com/webhook"
581
- : format === "uri" || format === "url" || lower.endsWith("url") ? "https://example.com"
582
- : format === "uuid" ? "00000000-0000-4000-8000-000000000000"
583
- : lower.includes("repository") || lower === "repo" ? "acme/api"
584
- : lower.includes("path") ? "openapi.yaml"
585
- : lower.includes("version") ? "1.0.0"
586
- : /(^|_)id$|Id$/.test(field) ? (lower === "id" ? "id" : field.replace(/[_-]?id$/i, "")) + "_123"
587
- : lower.includes("name") ? "example"
588
- : "value");
589
- while (value.length < min) value += "x";
590
- if (max !== undefined) value = value.slice(0, max);
591
- return value;
592
- }
593
- return null;
594
- }
595
-
596
- /** A useful value for the common API-id pattern (`^agt_`, `^src_[a-z0-9]+$`).
597
- * Full regex generation would be surprising and heavyweight; an anchored
598
- * literal prefix plus ordinary id suffix covers the schemas that use a
599
- * pattern to communicate a typed identifier. Every candidate is checked by
600
- * the actual RegExp before it is returned. */
601
- function exampleMatchingPattern(pattern: string, minLength: number, maxLength?: number): string | null {
602
- let regex: RegExp;
603
- try { regex = new RegExp(pattern); } catch { return null; }
604
- const match = /^\^((?:\\.|[A-Za-z0-9_-])+)/.exec(pattern);
605
- const prefix = match?.[1]?.replace(/\\(.)/g, "$1") ?? "";
606
- const fit = (candidate: string): string => {
607
- let value = candidate;
608
- while (value.length < minLength) value += "x";
609
- if (maxLength !== undefined) value = value.slice(0, maxLength);
610
- return value;
611
- };
612
- const candidates = [prefix + "123", prefix + "example", prefix, "resource.method", "example.value", "example_123", "example", "value"];
613
- for (const candidate of candidates) {
614
- const value = fit(candidate);
615
- regex.lastIndex = 0;
616
- if (regex.test(value)) return value;
617
- }
618
- return null;
619
- }
620
-
621
- export function exampleArgumentsFromSchema(inputSchema: Record<string, unknown>): Record<string, unknown> {
622
- const value = exampleFromSchema(inputSchema, "arguments");
623
- return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
624
- }
625
-
626
538
  /** The `fields` argument every tool takes unless the API already has one:
627
539
  * dotted paths to keep in the result (per item for paginated tools). */
628
540
  export const FIELDS_ARGUMENT = "fields";
@@ -666,26 +578,25 @@ export function operationTool(op: OpLike): ToolDefinition {
666
578
  };
667
579
  }
668
580
 
669
- /** Reference matches per search_docs page. */
670
- export const SEARCH_PAGE_SIZE = 15;
581
+ export { SEARCH_PAGE_SIZE } from "./search.js";
671
582
 
672
583
  export const SEARCH_DOCS_TOOL: ToolDefinition = {
673
584
  name: "search_docs",
674
585
  description: "Search this API's reference (operations, parameters) and, when a docs site is configured, its guides. Best matches first; page through with page.",
675
- inputSchema: { type: "object", properties: { query: { type: "string" }, page: { type: "integer", minimum: 1, description: "Page of reference matches (15 per page), default 1" } }, required: ["query"] },
586
+ 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"] },
676
587
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
677
588
  };
678
589
 
679
590
  export const READ_DOCS_TOOL: ToolDefinition = {
680
591
  name: "read_docs",
681
- 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.',
682
- inputSchema: { type: "object", properties: { page: { type: "string" } }, required: ["page"] },
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"] },
683
594
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
684
595
  };
685
596
 
686
597
  export const EXECUTE_TOOL: ToolDefinition = {
687
598
  name: "execute",
688
- 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.",
599
+ 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.",
689
600
  inputSchema: {
690
601
  type: "object",
691
602
  properties: {
@@ -698,6 +609,9 @@ export const EXECUTE_TOOL: ToolDefinition = {
698
609
  annotations: { openWorldHint: false },
699
610
  };
700
611
 
612
+ /** The keys execute itself takes; everything else is an operation argument. */
613
+ const EXECUTE_KEYS = ["operation", "arguments", "confirm"];
614
+
701
615
  /** Which operations a server exposes, beyond the streams rule. */
702
616
  export interface SurfaceOptions {
703
617
  /** Hide every operation that is not a read (isReadOperation). */
@@ -716,14 +630,63 @@ export function visibleOps<T extends OpLike>(ops: T[], options: SurfaceOptions =
716
630
  if (options.readOnly) out = out.filter(isReadOperation);
717
631
  if (options.include && options.include.length > 0) {
718
632
  const wanted = new Set(options.include.map((s) => s.trim().toLowerCase()).filter(Boolean));
719
- out = out.filter((op) =>
720
- wanted.has(op.tool.toLowerCase()) ||
721
- (op.resource !== undefined && wanted.has(op.resource.toLowerCase())) ||
722
- (op.resource !== undefined && op.method !== undefined && wanted.has((op.resource + "." + op.method).toLowerCase())));
633
+ out = out.filter((op) => includedBy(op, wanted));
723
634
  }
724
635
  return out;
725
636
  }
726
637
 
638
+ function includedBy(op: OpLike, wanted: Set<string>): boolean {
639
+ return wanted.has(op.tool.toLowerCase()) ||
640
+ (op.resource !== undefined && wanted.has(op.resource.toLowerCase())) ||
641
+ (op.resource !== undefined && op.method !== undefined && wanted.has((op.resource + "." + op.method).toLowerCase()));
642
+ }
643
+
644
+ /** Entries of an include list that name no operation (a typo such as
645
+ * "pull" for "pulls"). Matched against every generated operation, so an
646
+ * entry the other switches hide still counts as a name. */
647
+ export function unmatchedIncludes(ops: OpLike[], include: string[] | undefined): string[] {
648
+ return (include ?? []).filter((entry) => !ops.some((op) => includedBy(op, new Set([entry.trim().toLowerCase()]))));
649
+ }
650
+
651
+ /** An operation the server's own switches hide, with the reason, so a call
652
+ * to it says why instead of reading like a typo. */
653
+ export interface HiddenOperation {
654
+ op: OpLike;
655
+ message: string;
656
+ nextSteps: string[];
657
+ }
658
+
659
+ /** The exposed operations that readOnly or include hide, each explained in
660
+ * terms of the switch the operator set (`switches` names them as the
661
+ * operator wrote them, such as "--read-only"). */
662
+ export function hiddenOperations(ops: OpLike[], options: SurfaceOptions, switches: { readOnly: string; include: string }): HiddenOperation[] {
663
+ const visible = new Set(visibleOps(ops, options));
664
+ return ops.filter((op) => !visible.has(op) && mcpExposed(op, { uploads: options.uploads })).map((op) => {
665
+ const included = !options.include?.length || includedBy(op, new Set(options.include.map((entry) => entry.trim().toLowerCase())));
666
+ return included
667
+ ? {
668
+ op,
669
+ message: op.tool + " is a write, and this server is read-only (" + switches.readOnly + "), so it cannot be called here.",
670
+ nextSteps: ["Writes need a server started without " + switches.readOnly + "; tell the user if this operation is required."],
671
+ }
672
+ : {
673
+ op,
674
+ message: op.tool + " is not enabled on this server: " + switches.include + " limits it to " + (options.include ?? []).join(", ") + ".",
675
+ nextSteps: ["The operation needs a server whose " + switches.include + " includes " + (op.resource ?? op.tool) + "; tell the user if it is required."],
676
+ };
677
+ });
678
+ }
679
+
680
+ function findHidden(source: DocsSource, wanted: string): HiddenOperation | undefined {
681
+ const hidden = source.hiddenOps ?? [];
682
+ const op = findOperation(hidden.map((h) => h.op), wanted);
683
+ return op ? hidden.find((h) => h.op === op) : undefined;
684
+ }
685
+
686
+ function hiddenOutcome(hidden: HiddenOperation): ToolOutcome {
687
+ return textError(hidden.message, "NOT_AVAILABLE", hidden.nextSteps);
688
+ }
689
+
727
690
  /** Parse a comma-separated include list (from an env var or a flag). */
728
691
  export function parseIncludeList(value: string | undefined | null): string[] | undefined {
729
692
  const list = (value ?? "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -739,13 +702,14 @@ export function toolDefinitions(ops: OpLike[], mode: "operations" | "meta", omit
739
702
  if (mode === "meta") {
740
703
  const execute = structuredClone(EXECUTE_TOOL);
741
704
  const operation = (execute.inputSchema.properties as Record<string, Record<string, unknown>>).operation!;
742
- if (ops[0]) operation.examples = [ops[0].tool];
705
+ const example = ops.find((op) => op.showcase) ?? ops[0];
706
+ if (example) operation.examples = [example.tool];
743
707
  const coverage = omittedOps.length > 0
744
- ? " Generated " + ops.length + " of " + (ops.length + omittedOps.length) + " operations; search_docs names operations omitted by the plan limit."
708
+ ? " This build includes " + ops.length + " of " + (ops.length + omittedOps.length) + " operations."
745
709
  : "";
746
710
  return [
747
711
  { ...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 },
748
- { ...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." },
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." },
749
713
  execute,
750
714
  ];
751
715
  }
@@ -802,7 +766,7 @@ export function serverInstructions(input: InstructionsInput): string {
802
766
  : 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.");
803
767
  if (input.omittedOps?.length) {
804
768
  const generated = input.generatedOperationCount ?? input.toolCount;
805
- 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.");
769
+ parts.push("This build includes " + generated + " of " + (generated + input.omittedOps.length) + " operations; calling or searching for one of the others returns PLAN_LIMIT.");
806
770
  }
807
771
  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).");
808
772
  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.");
@@ -823,11 +787,8 @@ export function serverInstructions(input: InstructionsInput): string {
823
787
 
824
788
  // ---- argument validation + coercion ---------------------------------------------
825
789
 
826
- export interface ArgumentIssue {
827
- code: "UNKNOWN_ARGUMENT" | "INVALID_ARGUMENT" | "MISSING_ARGUMENT";
828
- argument: string;
829
- message: string;
830
- }
790
+ // The checks themselves live in ./arguments, shared with the CLI.
791
+ export { checkValue, closestName, coerceValue, type ArgumentIssue } from "./arguments.js";
831
792
 
832
793
  /** Everything a tool call needs after its arguments were checked: the
833
794
  * arguments to send (coerced, projection stripped), and how to shape the
@@ -836,138 +797,29 @@ export interface PreparedCall {
836
797
  args: Record<string, unknown>;
837
798
  /** Dotted paths from the `fields` argument, null for the whole result. */
838
799
  fields: string[][] | null;
800
+ /** Of those, the dotted paths a result may lack (written "?path"). */
801
+ optionalFields: string[];
839
802
  maxChars: number;
840
803
  }
841
804
 
842
- const normalizeName = (name: string): string => name.toLowerCase().replace(/[^a-z0-9]/g, "");
843
-
844
- function editDistance(a: string, b: string): number {
845
- const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
846
- for (let i = 1; i <= a.length; i++) {
847
- let diag = prev[0]!;
848
- prev[0] = i;
849
- for (let j = 1; j <= b.length; j++) {
850
- const tmp = prev[j]!;
851
- prev[j] = Math.min(prev[j]! + 1, prev[j - 1]! + 1, diag + (a[i - 1] === b[j - 1] ? 0 : 1));
852
- diag = tmp;
853
- }
854
- }
855
- return prev[b.length]!;
856
- }
857
-
858
- /** The closest accepted name: same letters ignoring case/punctuation first,
859
- * then a small edit distance. Undefined when nothing is close. */
860
- export function closestName(name: string, known: string[]): string | undefined {
861
- const exact = known.filter((k) => normalizeName(k) === normalizeName(name));
862
- if (exact.length === 1) return exact[0];
863
- if (exact.length > 1) return undefined;
864
- let best: { name: string; d: number } | undefined;
865
- for (const k of known) {
866
- const d = editDistance(name.toLowerCase(), k.toLowerCase());
867
- if (d <= Math.max(1, Math.floor(k.length / 4)) && (best === undefined || d < best.d)) best = { name: k, d };
868
- }
869
- return best?.name;
870
- }
871
-
872
- function schemaTypes(schema: Record<string, unknown>): string[] {
873
- const t = schema.type;
874
- if (typeof t === "string") return [t];
875
- if (Array.isArray(t)) return t.filter((x): x is string => typeof x === "string");
876
- return [];
877
- }
878
-
879
- /**
880
- * Coerce one value toward its schema when the intent is unambiguous: the
881
- * strings agents produce for booleans and numbers, a JSON string for an
882
- * object or array, a scalar for a one-element array, an enum member in the
883
- * wrong case. Returns the value to send, or a message when it can't be made
884
- * to fit. Untyped schemas (unions, anything) pass through.
885
- */
886
- export function coerceValue(value: unknown, schema: Record<string, unknown>): { value: unknown } | { error: string } {
887
- if (value === null || value === undefined) return { value };
888
- // Date-shaped arguments take relative forms (-P7D, 7 days ago, today),
889
- // resolved here so the API sees an absolute value.
890
- const dateKind = dateKindOf(schema.format);
891
- if (dateKind && typeof value === "string") {
892
- const resolved = relativeDate(value, dateKind);
893
- if (resolved && "error" in resolved) return { error: resolved.error };
894
- if (resolved) value = resolved.value;
895
- }
896
- const types = schemaTypes(schema);
897
- const enumValues = Array.isArray(schema.enum) ? schema.enum : undefined;
898
- const accepts = (t: string) => types.length === 0 || types.includes(t);
899
- const kind = Array.isArray(value) ? "array" : typeof value;
900
-
901
- let out: unknown = value;
902
- if (types.length > 0) {
903
- if (kind === "boolean" && !accepts("boolean")) {
904
- if (accepts("string")) out = String(value);
905
- else return { error: "expected " + types.join(" or ") + ", got boolean" };
906
- } else if (kind === "number" && !accepts("number") && !accepts("integer")) {
907
- if (accepts("string")) out = String(value);
908
- else if (accepts("array")) out = [value];
909
- else return { error: "expected " + types.join(" or ") + ", got number" };
910
- } else if (kind === "number" && accepts("integer") && !accepts("number") && !Number.isInteger(value)) {
911
- return { error: "expected an integer, got " + String(value) };
912
- } else if (kind === "string" && !accepts("string")) {
913
- const s = (value as string).trim();
914
- if (accepts("boolean") && /^(true|false|yes|no|1|0)$/i.test(s)) out = /^(true|yes|1)$/i.test(s);
915
- else if ((accepts("integer") || accepts("number")) && s !== "" && !Number.isNaN(Number(s))) {
916
- const n = Number(s);
917
- if (accepts("integer") && !accepts("number") && !Number.isInteger(n)) return { error: "expected an integer, got \"" + s + "\"" };
918
- out = n;
919
- } else if ((accepts("object") || accepts("array")) && /^[[{]/.test(s)) {
920
- try {
921
- const parsed: unknown = JSON.parse(s);
922
- const parsedKind = Array.isArray(parsed) ? "array" : parsed === null ? "null" : typeof parsed;
923
- if (!accepts(parsedKind)) return { error: "expected " + types.join(" or ") + ", got a JSON " + parsedKind + " in a string" };
924
- out = parsed;
925
- } catch {
926
- return { error: "expected " + types.join(" or ") + ", got a string that is not valid JSON" };
927
- }
928
- } else if (accepts("array")) {
929
- out = [value];
930
- } else {
931
- return { error: "expected " + types.join(" or ") + ", got string" };
932
- }
933
- } else if (kind === "object" && !accepts("object")) {
934
- if (accepts("array")) out = [value];
935
- else return { error: "expected " + types.join(" or ") + ", got object" };
936
- } else if (kind === "array" && !accepts("array")) {
937
- return { error: "expected " + types.join(" or ") + ", got array" };
938
- }
939
- }
940
-
941
- // Array items: coerce each against the items schema when it has one.
942
- if (Array.isArray(out) && schema.items && typeof schema.items === "object" && !Array.isArray(schema.items)) {
943
- const itemSchema = schema.items as Record<string, unknown>;
944
- const items: unknown[] = [];
945
- for (let i = 0; i < out.length; i++) {
946
- const r = coerceValue(out[i], itemSchema);
947
- if ("error" in r) return { error: "item " + i + ": " + r.error };
948
- items.push(r.value);
949
- }
950
- out = items;
951
- }
952
-
953
- if (enumValues && typeof out === "string" && !enumValues.includes(out)) {
954
- const match = enumValues.filter((e) => typeof e === "string" && e.toLowerCase() === (out as string).toLowerCase());
955
- if (match.length === 1) out = match[0];
956
- else return { error: "must be one of " + enumValues.map((e) => JSON.stringify(e)).join(", ") + ", got " + JSON.stringify(out) };
957
- }
958
- return { value: out };
959
- }
960
-
961
- function parseFields(value: unknown): string[][] | { error: string } {
805
+ /** Field paths, and which of them may be absent: a leading "?" marks a
806
+ * path the result may lack, as reference resolution's list calls ask for
807
+ * match keys an API can omit. Any other path must match something. */
808
+ function parseFields(value: unknown): { paths: string[][]; optional: string[] } | { error: string } {
962
809
  const raw = typeof value === "string" ? value.split(",") : Array.isArray(value) ? value : null;
963
810
  if (raw === null) return { error: "expected an array of field paths, e.g. [\"id\",\"name\"]" };
964
811
  const paths: string[][] = [];
812
+ const optional: string[] = [];
965
813
  for (const entry of raw) {
966
814
  if (typeof entry !== "string") return { error: "expected an array of strings" };
967
- const path = entry.trim();
815
+ let path = entry.trim();
816
+ if (path.startsWith("?")) {
817
+ path = path.slice(1).trim();
818
+ if (path !== "") optional.push(path);
819
+ }
968
820
  if (path !== "") paths.push(path.split("."));
969
821
  }
970
- return paths;
822
+ return { paths, optional };
971
823
  }
972
824
 
973
825
  /**
@@ -982,7 +834,7 @@ function parseFields(value: unknown): string[][] | { error: string } {
982
834
  export function prepareCall(
983
835
  op: OpLike,
984
836
  rawArgs: Record<string, unknown>,
985
- options: { maxChars?: number } = {},
837
+ options: { maxChars?: number; credentialUsername?: string | null } = {},
986
838
  ): { ok: true; call: PreparedCall } | { ok: false; outcome: ToolOutcome } {
987
839
  const schema = toolInputSchema(op);
988
840
  const properties = (schema.properties ?? {}) as Record<string, Record<string, unknown>>;
@@ -1010,19 +862,38 @@ export function prepareCall(
1010
862
  }
1011
863
 
1012
864
  let fields: string[][] | null = null;
865
+ let optionalFields: string[] = [];
1013
866
  if (!hasOwnFieldsParam(op) && args[FIELDS_ARGUMENT] !== undefined) {
1014
867
  const parsed = parseFields(args[FIELDS_ARGUMENT]);
1015
868
  if ("error" in parsed) issues.push({ code: "INVALID_ARGUMENT", argument: FIELDS_ARGUMENT, message: "fields: " + parsed.error });
1016
- else fields = parsed.length > 0 ? parsed : null;
869
+ else if (parsed.paths.length > 0) ({ paths: fields, optional: optionalFields } = parsed);
1017
870
  delete args[FIELDS_ARGUMENT];
1018
871
  }
1019
872
 
1020
873
  for (const [name, value] of Object.entries(args)) {
1021
874
  const propSchema = properties[name];
1022
875
  if (!propSchema) continue;
1023
- const r = coerceValue(value, propSchema);
1024
- if ("error" in r) issues.push({ code: "INVALID_ARGUMENT", argument: name, message: name + ": " + r.error });
1025
- else args[name] = r.value;
876
+ // Patterns apply inside objects and arrays. A top-level argument's own
877
+ // pattern is left to the API: its documented example values do not all
878
+ // 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 });
880
+ }
881
+
882
+ // A path argument that is the configured credential's username (Twilio's
883
+ // AccountSid) defaults to it; without one it is an ordinary missing argument
884
+ // whose message says where the default would come from.
885
+ for (const param of op.params) {
886
+ if (!param.credential || args[param.name] !== undefined) continue;
887
+ const username = options.credentialUsername ?? undefined;
888
+ if (username !== undefined && username !== "") {
889
+ args[param.name] = username;
890
+ continue;
891
+ }
892
+ issues.push({
893
+ code: "MISSING_ARGUMENT",
894
+ argument: param.name,
895
+ 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.",
896
+ });
1026
897
  }
1027
898
 
1028
899
  const required = Array.isArray(schema.required) ? (schema.required as string[]) : [];
@@ -1033,7 +904,7 @@ export function prepareCall(
1033
904
  }
1034
905
 
1035
906
  if (issues.length > 0) return { ok: false, outcome: argumentsError(op, issues) };
1036
- return { ok: true, call: { args, fields, maxChars: options.maxChars ?? DEFAULT_MAX_RESULT_CHARS } };
907
+ return { ok: true, call: { args, fields, optionalFields, maxChars: options.maxChars ?? DEFAULT_MAX_RESULT_CHARS } };
1037
908
  }
1038
909
 
1039
910
  /** One caller/session's resolved names. Hosted transports must scope this
@@ -1099,11 +970,6 @@ function referencePage(value: unknown): { items: Record<string, unknown>[]; next
1099
970
  };
1100
971
  }
1101
972
 
1102
- function userShapedReference(name: string): boolean {
1103
- const normalized = name.toLowerCase().replace(/[^a-z0-9]/g, "").replace(/id$/, "");
1104
- return ["user", "assignee", "owner", "member", "actor", "creator", "account", "profile"].includes(normalized);
1105
- }
1106
-
1107
973
  function referenceMatchLabel(fields: string[]): string {
1108
974
  return fields.length === 1 ? fields[0]! : fields.slice(0, -1).join(", ") + " or " + fields.at(-1);
1109
975
  }
@@ -1141,31 +1007,66 @@ export async function resolveReferences(
1141
1007
  ): Promise<ResolveReferencesResult> {
1142
1008
  const args = { ...preparedArgs };
1143
1009
  const maxPages = Math.max(1, Math.floor(options.maxPages ?? 5));
1010
+ 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;
1014
+ const cached = options.cache.get(cacheKey);
1015
+ 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;
1020
+ 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."]) };
1022
+ }
1023
+ putReferenceCache(options.cache, cacheKey, id);
1024
+ return { id };
1025
+ };
1026
+ /** "me" inside an object argument (a GraphQL input's assigneeId, or each
1027
+ * of its subscriberIds): the same resolution, keyed by property name. */
1028
+ const resolveNestedMe = async (value: unknown, key: string, path: string): Promise<{ value: unknown } | { outcome: ToolOutcome }> => {
1029
+ if (typeof value === "string") {
1030
+ if (!isMeReference(key, value)) return { value };
1031
+ const caller = await callerId(path);
1032
+ return "outcome" in caller ? caller : { value: caller.id };
1033
+ }
1034
+ if (Array.isArray(value)) {
1035
+ const items: unknown[] = [];
1036
+ for (let i = 0; i < value.length; i++) {
1037
+ const item = await resolveNestedMe(value[i], key, path + "[" + i + "]");
1038
+ if ("outcome" in item) return item;
1039
+ items.push(item.value);
1040
+ }
1041
+ return { value: items };
1042
+ }
1043
+ if (value && typeof value === "object") {
1044
+ const out: Record<string, unknown> = {};
1045
+ for (const [name, entry] of Object.entries(value as Record<string, unknown>)) {
1046
+ const resolved = await resolveNestedMe(entry, name, path + "." + name);
1047
+ if ("outcome" in resolved) return resolved;
1048
+ out[name] = resolved.value;
1049
+ }
1050
+ return { value: out };
1051
+ }
1052
+ return { value };
1053
+ };
1144
1054
  for (const param of op.params) {
1145
1055
  const raw = args[param.name];
1056
+ if (identity && raw && typeof raw === "object" && param.resolve !== false) {
1057
+ const resolved = await resolveNestedMe(raw, param.name, param.name);
1058
+ if ("outcome" in resolved) return { ok: false, outcome: resolved.outcome };
1059
+ args[param.name] = resolved.value;
1060
+ continue;
1061
+ }
1146
1062
  if (typeof raw !== "string" || param.resolve === false) continue;
1147
1063
  const value = raw.trim();
1148
1064
 
1149
- if (value.toLowerCase() === "me" && userShapedReference(param.name) && options.identityTool) {
1150
- const identity = findOperation(options.ops, options.identityTool);
1151
- if (identity) {
1152
- const cacheKey = "me:" + identity.tool;
1153
- const cached = options.cache.get(cacheKey);
1154
- if (cached !== undefined) {
1155
- args[param.name] = cached;
1156
- continue;
1157
- }
1158
- const outcome = await options.runOperation(identity, {});
1159
- if (outcome.isError) return { ok: false, outcome };
1160
- const body = outcomeValue(outcome);
1161
- const id = body && typeof body === "object" && !Array.isArray(body) ? (body as Record<string, unknown>).id : undefined;
1162
- if (typeof id !== "string" && typeof id !== "number") {
1163
- 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."]) };
1164
- }
1165
- putReferenceCache(options.cache, cacheKey, id);
1166
- args[param.name] = id;
1167
- continue;
1168
- }
1065
+ if (identity && isMeReference(param.name, value)) {
1066
+ const caller = await callerId(param.name);
1067
+ if ("outcome" in caller) return { ok: false, outcome: caller.outcome };
1068
+ args[param.name] = caller.id;
1069
+ continue;
1169
1070
  }
1170
1071
 
1171
1072
  const resolver = param.resolve && typeof param.resolve === "object" ? param.resolve : undefined;
@@ -1185,7 +1086,7 @@ export async function resolveReferences(
1185
1086
  if (args[sourceParam.name] !== undefined && sourceParam.name !== param.name) pageArgs[sourceParam.name] = args[sourceParam.name];
1186
1087
  }
1187
1088
  if (resolver.filterParam) pageArgs[resolver.filterParam] = value;
1188
- if (!hasOwnFieldsParam(source)) pageArgs.fields = [resolver.id, ...resolver.match];
1089
+ if (!hasOwnFieldsParam(source)) pageArgs.fields = [resolver.id, ...resolver.match.map((field) => "?" + field)];
1189
1090
 
1190
1091
  let exhausted = false;
1191
1092
  for (let pageNumber = 1; pageNumber <= maxPages; pageNumber++) {
@@ -1242,29 +1143,6 @@ export function argumentsError(op: { tool: string }, issues: ArgumentIssue[]): T
1242
1143
 
1243
1144
  // ---- results: projection, size cap, envelopes ------------------------------------
1244
1145
 
1245
- /** Keep only `paths` of a value: arrays item by item, objects by dotted
1246
- * path, including paths through arrays (`items.id` keeps each item's id);
1247
- * scalars untouched. Same rule as the CLI's --fields. */
1248
- export function projectFields(value: unknown, paths: string[][] | null): unknown {
1249
- if (paths === null) return value;
1250
- if (Array.isArray(value)) return value.map((v) => projectFields(v, paths));
1251
- if (value === null || typeof value !== "object") return value;
1252
- const groups = new Map<string, string[][]>();
1253
- for (const [key, ...rest] of paths) {
1254
- if (key === undefined) continue;
1255
- const group = groups.get(key);
1256
- if (group) group.push(rest); else groups.set(key, [rest]);
1257
- }
1258
- const out: Record<string, unknown> = {};
1259
- for (const [key, rests] of groups) {
1260
- const child = (value as Record<string, unknown>)[key];
1261
- if (child === undefined) continue;
1262
- if (rests.some((rest) => rest.length === 0)) out[key] = child;
1263
- else if (child !== null && typeof child === "object") out[key] = projectFields(child, rests);
1264
- }
1265
- return out;
1266
- }
1267
-
1268
1146
  /** How a result was shaped; every field optional so callers can pass what they have. */
1269
1147
  export interface ResultOptions {
1270
1148
  fields?: string[][] | null;
@@ -1276,6 +1154,44 @@ export interface ResultOptions {
1276
1154
  * the call's arguments let some styles resume exactly where the cut fell. */
1277
1155
  pagination?: OpLike["pagination"];
1278
1156
  args?: Record<string, unknown>;
1157
+ /** The operation's effect. When fields match nothing, a write's full
1158
+ * result rides along in the error so the agent never repeats the call to
1159
+ * see what it did. */
1160
+ safety?: "read" | "write" | "destructive";
1161
+ /** Paths the result may lack, such as a server's default projection:
1162
+ * they are left out rather than reported. */
1163
+ optionalFields?: string[];
1164
+ /** Arguments nextPage must repeat from args (required path parameters
1165
+ * such as owner and repo; fields is repeated too): the next-page
1166
+ * parameters alone, a page_url for instance, would fail validation when
1167
+ * passed back. */
1168
+ carryArgs?: string[];
1169
+ }
1170
+
1171
+ /** 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 {
1175
+ if (fields === null) return null;
1176
+ const optional = new Set(options.optionalFields ?? []);
1177
+ const unmatched = unmatchedFields(value, fields).filter((u) => !optional.has(u.path));
1178
+ if (unmatched.length === 0) return null;
1179
+ const write = options.safety !== "read";
1180
+ const maxChars = options.maxChars ?? DEFAULT_MAX_RESULT_CHARS;
1181
+ const full = write ? JSON.stringify(value) : undefined;
1182
+ const structured = {
1183
+ error: "UnmatchedFields",
1184
+ code: "FIELDS_UNMATCHED",
1185
+ message: unmatchedFieldsMessage(unmatched, perItem),
1186
+ 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
+ ],
1193
+ };
1194
+ return { text: JSON.stringify(structured), isError: true, structured };
1279
1195
  }
1280
1196
 
1281
1197
  const fieldsHint = (perItem: boolean) => "Pass fields (dotted paths" + (perItem ? ", applied per item" : "") + ") to keep only the keys you need.";
@@ -1300,8 +1216,20 @@ function itemsThatFit(items: unknown[], budget: number): number {
1300
1216
  * last-id styles resume at the cut, cursor and page styles say what the
1301
1217
  * caller must do instead. */
1302
1218
  export function pageOutcome(items: unknown[], nextPage: Record<string, unknown> | null, options: ResultOptions = {}): ToolOutcome {
1219
+ if (nextPage !== null && options.carryArgs) {
1220
+ const args = options.args ?? {};
1221
+ const carried = options.carryArgs.filter((name) => args[name] !== undefined && !(name in nextPage!));
1222
+ nextPage = {
1223
+ ...Object.fromEntries(carried.map((name) => [name, args[name]])),
1224
+ // The same projection on the next page.
1225
+ ...(options.fields && !("fields" in nextPage) ? { fields: options.fields.map((path) => path.join(".")) } : {}),
1226
+ ...nextPage,
1227
+ };
1228
+ }
1303
1229
  const fields = options.fields ?? null;
1304
1230
  const maxChars = options.maxChars ?? DEFAULT_MAX_RESULT_CHARS;
1231
+ const unmatched = unmatchedFieldsOutcome(items, fields, true, options);
1232
+ if (unmatched) return unmatched;
1305
1233
  const shown = projectFields(items, fields) as unknown[];
1306
1234
  const full = {
1307
1235
  items: shown,
@@ -1359,7 +1287,9 @@ const omittedMarker = (key: string, chars: number, maxChars: number) => chars >
1359
1287
  export function dataOutcome(data: unknown, options: ResultOptions = {}): ToolOutcome {
1360
1288
  const fields = options.fields ?? null;
1361
1289
  const maxChars = options.maxChars ?? DEFAULT_MAX_RESULT_CHARS;
1362
- const value = data === undefined || data === null ? { ok: true } : projectFields(data, fields);
1290
+ const unmatched = data !== undefined && data !== null ? unmatchedFieldsOutcome(data, fields, Array.isArray(data), options) : null;
1291
+ if (unmatched) return unmatched;
1292
+ const value = data !== undefined && data !== null ? projectFields(data, fields) : { ok: true };
1363
1293
  if (typeof value === "string") {
1364
1294
  if (value.length <= maxChars) return { text: value, isError: false, structured: value };
1365
1295
  return { text: value.slice(0, maxChars) + "\n\n[truncated: " + (value.length - maxChars).toLocaleString("en-US") + " more characters; results are capped at " + maxChars.toLocaleString("en-US") + ".]", isError: false };
@@ -1481,10 +1411,10 @@ export async function binaryOutcome(blob: Blob, options: BinaryOptions = {}): Pr
1481
1411
  /** Stable codes an agent can branch on (the same vocabulary as the
1482
1412
  * generated CLI's error envelope). Additive only. */
1483
1413
  export type ErrorCode =
1484
- | "NO_AUTH" | "AUTH_INVALID" | "PLAN_LIMIT" | "NOT_FOUND" | "INVALID_REQUEST" | "RATE_LIMITED"
1414
+ | "NO_AUTH" | "AUTH_INVALID" | "INSUFFICIENT_SCOPE" | "PLAN_LIMIT" | "NOT_FOUND" | "INVALID_REQUEST" | "RATE_LIMITED"
1485
1415
  | "SPEC_INVALID" | "SERVER_ERROR" | "NETWORK_ERROR" | "VALIDATION_FAILED" | "INVALID_ARGUMENTS" | "CONFIRMATION_REQUIRED"
1486
1416
  | "REFERENCE_NOT_FOUND" | "REFERENCE_AMBIGUOUS" | "REFERENCE_SCAN_LIMIT" | "NOT_AVAILABLE" | "CALL_FAILED"
1487
- | "ACCOUNT_LINK_REQUIRED" | "ACCOUNT_LINK_CANCELLED";
1417
+ | "ACCOUNT_LINK_REQUIRED" | "ACCOUNT_LINK_CANCELLED" | "FIELDS_UNMATCHED";
1488
1418
 
1489
1419
  export interface ErrorContext {
1490
1420
  /** One sentence on how to supply a credential on this transport. */
@@ -1493,39 +1423,59 @@ export interface ErrorContext {
1493
1423
  docsUrl?: string | null;
1494
1424
  /** True when a credential was sent with the request (401 means it was rejected). */
1495
1425
  hadCredential?: boolean;
1426
+ /** OAuth scopes the operation requires; a 403 then names them. */
1427
+ requiredScopes?: string[];
1428
+ }
1429
+
1430
+ function rateLimitNextStep(retryAt: unknown): string {
1431
+ if (retryAt instanceof Date && !Number.isNaN(retryAt.getTime())) {
1432
+ const at = new Date(Math.ceil(retryAt.getTime() / 1000) * 1000).toISOString().replace(/\.\d{3}Z$/, "Z");
1433
+ return "Rate limited: wait until " + at + ", then call again. This is not a credential problem.";
1434
+ }
1435
+ return "Rate limited: back off, then call again once; the request already honored any Retry-After within its ceiling.";
1496
1436
  }
1497
1437
 
1498
- function extractRetryAfter(e: { headers?: unknown; body?: unknown }): string | undefined {
1499
- const headers = e.headers as { get?: (name: string) => string | null } | undefined;
1500
- const fromHeader = headers?.get?.("retry-after");
1501
- if (fromHeader) return fromHeader;
1502
- const match = /retry after (\d+)s/i.exec(JSON.stringify(e.body ?? ""));
1503
- return match?.[1];
1438
+ /** Error codes APIs report inside a 2xx body (Slack's `error`), mapped to
1439
+ * the stable codes when their meaning is unambiguous. */
1440
+ function payloadFailureCode(code: unknown): ErrorCode | undefined {
1441
+ if (typeof code !== "string") return undefined;
1442
+ if (/^(not_authed|invalid_auth|token_revoked|token_expired|account_inactive|unauthorized|unauthenticated|forbidden|access_denied|missing_scope)$/i.test(code)) return "AUTH_INVALID";
1443
+ if (/^(ratelimited|rate_limited|rate_limit_exceeded|too_many_requests)$/i.test(code)) return "RATE_LIMITED";
1444
+ return undefined;
1504
1445
  }
1505
1446
 
1506
1447
  /** Classify an SDK error result by status: the code and what to do next. */
1507
1448
  export function classifyError(error: unknown, context: ErrorContext = {}): { code: ErrorCode; nextSteps: string[] } {
1508
- const e = (error ?? {}) as { name?: string; message?: string; status?: number; body?: unknown; violations?: unknown };
1449
+ const e = (error ?? {}) as { name?: string; message?: string; status?: number; code?: unknown; body?: unknown; violations?: unknown; rateLimit?: { retryAt?: Date } };
1509
1450
  const message = e.message ?? String(error);
1510
1451
  if (e.violations !== undefined) return { code: "VALIDATION_FAILED", nextSteps: ["Fix the fields named in violations and call again."] };
1511
- if (e.name === "TransportError" || (typeof e.status !== "number" && /fetch|ECONN|ENOTFOUND|timed out|TLS|abort/i.test(message))) {
1452
+ if (e.name === "TransportError" || (e.name !== "PaginationError" && typeof e.status !== "number" && /fetch|ECONN|ENOTFOUND|timed out|TLS|abort/i.test(message))) {
1512
1453
  return { code: "NETWORK_ERROR", nextSteps: ["The API could not be reached (network, DNS, TLS or timeout). Retry once with backoff; do not loop."] };
1513
1454
  }
1514
1455
  const status = typeof e.status === "number" ? e.status : 0;
1515
1456
  const body = e.body as { errors?: { code?: string; message?: string }[] } | undefined;
1516
1457
  const auth = context.authHint ? context.authHint.trim().replace(/[.]?$/, ".") : null;
1458
+ // A rate limit can arrive as a 403 (GitHub); the SDK marks it either way.
1459
+ if (status === 429 || e.rateLimit !== undefined) return { code: "RATE_LIMITED", nextSteps: [rateLimitNextStep(e.rateLimit?.retryAt)] };
1460
+ const scopes = context.requiredScopes ?? [];
1461
+ const scopeFailure = { code: "INSUFFICIENT_SCOPE" as const, 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."] };
1462
+ // A failure the API reported inside a 2xx body.
1463
+ if (e.name === "PayloadError") {
1464
+ if (scopes.length && /^missing_scope$/i.test(String(e.code))) return scopeFailure;
1465
+ const reported = payloadFailureCode(e.code);
1466
+ if (reported === "AUTH_INVALID") return { code: "AUTH_INVALID", nextSteps: ["The API rejected the credential (" + String(e.code) + ")." + (auth ? " " + auth : "")] };
1467
+ if (reported === "RATE_LIMITED") return { code: "RATE_LIMITED", nextSteps: [rateLimitNextStep(undefined)] };
1468
+ 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
+ }
1517
1470
  if (status === 401) {
1518
1471
  return context.hadCredential
1519
1472
  ? { code: "AUTH_INVALID", nextSteps: ["The credential was rejected; it may be expired or for another environment." + (auth ? " " + auth : "")] }
1520
1473
  : { code: "NO_AUTH", nextSteps: ["No credential was sent." + (auth ? " " + auth : "")] };
1521
1474
  }
1475
+ if (status === 403 && scopes.length) return scopeFailure;
1522
1476
  if (status === 403) return { code: "AUTH_INVALID", nextSteps: ["The credential lacks access to this operation; it is not a retryable error."] };
1523
1477
  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."] };
1524
1478
  if (status === 404) return { code: "NOT_FOUND", nextSteps: notFoundNextSteps(message, e.body) };
1525
- if (status === 429) {
1526
- const retryAfter = extractRetryAfter(e);
1527
- 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."] };
1528
- }
1529
1479
  if (status === 422 && body?.errors?.[0]?.code === "spec_error") {
1530
1480
  return {
1531
1481
  code: "SPEC_INVALID",
@@ -1560,8 +1510,23 @@ function notFoundNextSteps(message: string, body: unknown): string[] {
1560
1510
  /** A typed API error as the agent should see it: name, a stable code, the
1561
1511
  * message, status, the API's body, where to read more, and what to do. */
1562
1512
  export function errorOutcome(error: unknown, context: ErrorContext = {}): ToolOutcome {
1563
- const e = error as { name?: string; message?: string; status?: number; body?: unknown; response?: { requestId?: string } };
1513
+ const e = error as { name?: string; message?: string; status?: number; body?: unknown; rateLimit?: { retryAt?: Date }; response?: { requestId?: string } };
1514
+ // A GraphQL response with data and errors is a partial success: the agent
1515
+ // gets the data it can use and the errors that explain what is missing.
1516
+ const partial = error as { name?: string; data?: unknown; errors?: unknown[] } | null;
1517
+ if (partial?.name === "GraphQLRequestError" && partial.data !== undefined && partial.data !== null) {
1518
+ const structured = { data: partial.data, errors: partial.errors ?? [], partial: true };
1519
+ return { text: JSON.stringify(structured), isError: false, structured };
1520
+ }
1521
+ // The SDK raises NotModifiedError for a 304: a conditional request
1522
+ // matched. That is a result, not a failure; the CLI prints the same shape.
1523
+ const notModified = error as { name?: string; etag?: string } | null;
1524
+ if (notModified?.name === "NotModifiedError") {
1525
+ const structured = { ok: true, not_modified: true, ...(notModified.etag ? { etag: notModified.etag } : {}) };
1526
+ return { text: JSON.stringify(structured), isError: false, structured };
1527
+ }
1564
1528
  const { code, nextSteps } = classifyError(error, context);
1529
+ const retryAt = e?.rateLimit?.retryAt instanceof Date ? new Date(Math.ceil(e.rateLimit.retryAt.getTime() / 1000) * 1000).toISOString().replace(/\.\d{3}Z$/, "Z") : undefined;
1565
1530
  const bodyRequestId = e?.body && typeof e.body === "object" && !Array.isArray(e.body)
1566
1531
  ? (e.body as Record<string, unknown>).request_id ?? (e.body as Record<string, unknown>).requestId
1567
1532
  : undefined;
@@ -1572,6 +1537,7 @@ export function errorOutcome(error: unknown, context: ErrorContext = {}): ToolOu
1572
1537
  message: e?.message,
1573
1538
  ...(typeof e?.status === "number" ? { status: e.status } : {}),
1574
1539
  ...(requestId ? { request_id: requestId } : {}),
1540
+ ...(retryAt ? { retry_at: retryAt } : {}),
1575
1541
  ...(e?.body !== undefined ? { body: e.body } : {}),
1576
1542
  ...(context.docsUrl ? { docs_url: context.docsUrl } : {}),
1577
1543
  next_steps: nextSteps,
@@ -1579,6 +1545,16 @@ export function errorOutcome(error: unknown, context: ErrorContext = {}): ToolOu
1579
1545
  return { text: JSON.stringify(structured), isError: true, structured };
1580
1546
  }
1581
1547
 
1548
+ /** A required operation whose schemes this server cannot send fails before
1549
+ * any request, instead of calling the API without credentials. */
1550
+ export function unsupportedAuthOutcome(op: OpLike): ToolOutcome | null {
1551
+ if (op.auth !== "required" || !op.credentialOptions || op.credentialOptions.some((alternative) => alternative.length > 0)) return null;
1552
+ const schemes = [...new Set((op.security ?? []).flatMap((requirement) => Object.keys(requirement)))];
1553
+ return textError(`${op.tool} requires ${schemes.length ? schemes.join(" or ") : "an authentication scheme"} authentication, which this MCP server cannot send.`, "NO_AUTH", [
1554
+ "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.",
1555
+ ]);
1556
+ }
1557
+
1582
1558
  export function textError(text: string, code: ErrorCode = "CALL_FAILED", nextSteps: string[] = []): ToolOutcome {
1583
1559
  const structured = { error: "Error", code, message: text, next_steps: nextSteps };
1584
1560
  return { text: JSON.stringify(structured), isError: true, structured };
@@ -1590,6 +1566,8 @@ export interface DocsSource {
1590
1566
  ops: OpLike[];
1591
1567
  /** Operations omitted from a capped generation. */
1592
1568
  omittedOps?: OpLike[];
1569
+ /** Operations this server's switches hide (read-only, a tools subset). */
1570
+ hiddenOps?: HiddenOperation[];
1593
1571
  /** Count generated before runtime surface filters. */
1594
1572
  generatedOperationCount?: number;
1595
1573
  /** Base URL of the docs site, or null when none is configured. */
@@ -1605,109 +1583,228 @@ async function fetchDocs(source: DocsSource, pathOrFile: string): Promise<string
1605
1583
  return url === null ? null : source.fetchText(url);
1606
1584
  }
1607
1585
 
1608
- function coverageText(source: DocsSource): string | null {
1609
- const omitted = source.omittedOps ?? [];
1610
- const generated = source.generatedOperationCount ?? source.ops.length;
1611
- return omitted.length > 0
1612
- ? "Coverage: generated " + generated + " of " + (generated + omitted.length) + " operations. Omitted by the plan limit: " + omitted.map((op) => op.tool + " (" + op.httpMethod + " " + op.path + ")").join(", ") + "."
1613
- : null;
1586
+ /** "GraphQL mutation issueCreate" or "POST /v1/issues". */
1587
+ export function operationLabel(op: Pick<OpLike, "httpMethod" | "path" | "graphql">): string {
1588
+ return op.graphql?.field ? "GraphQL " + op.graphql.kind + " " + op.graphql.field : op.httpMethod + " " + op.path;
1589
+ }
1590
+
1591
+ function omittedEntry(op: OpLike): Record<string, string> {
1592
+ return op.graphql?.field
1593
+ ? { tool: op.tool, graphql: op.graphql.kind + " " + op.graphql.field }
1594
+ : { tool: op.tool, method: op.httpMethod, path: op.path };
1614
1595
  }
1615
1596
 
1616
- function omittedPlanLimit(ops: OpLike[], requested?: string): ToolOutcome {
1597
+ function omittedPlanLimit(source: DocsSource, ops: OpLike[], requested?: string): ToolOutcome {
1598
+ const generated = source.generatedOperationCount ?? source.ops.length;
1599
+ const total = generated + (source.omittedOps?.length ?? 0);
1617
1600
  const structured = {
1618
1601
  error: "PlanLimitError",
1619
1602
  code: "PLAN_LIMIT",
1620
- message: requested
1621
- ? "The operation " + requested + " exists in the Spec but was omitted from this generated package by its plan limit."
1622
- : "Matching operations exist in the Spec but were omitted from this generated package by its plan limit.",
1623
- omitted_operations: ops.map((op) => ({ tool: op.tool, method: op.httpMethod, path: op.path })),
1624
- 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."],
1603
+ message: (requested
1604
+ ? "The operation " + requested + " is in the API but not in this package"
1605
+ : "Matching operations are in the API but not in this package") + ", which was generated with " + generated + " of its " + total + " operations.",
1606
+ omitted_operations: ops.slice(0, SEARCH_PAGE_SIZE).map(omittedEntry),
1607
+ next_steps: ["The package's publisher can regenerate it with every operation.", "Do not invent or retry an omitted operation against this package."],
1625
1608
  };
1626
1609
  return { text: JSON.stringify(structured), isError: true, structured };
1627
1610
  }
1628
1611
 
1629
- export function referenceText(op: OpLike): string {
1612
+ /** Nesting read_docs spells out before pointing at schema: true. */
1613
+ const REFERENCE_DEPTH = 3;
1614
+ /** Characters one top-level argument's nested fields may take; a deep
1615
+ * filter object is shown shallower until it fits. */
1616
+ const REFERENCE_ARGUMENT_BUDGET = 6_000;
1617
+ /** Enum values listed inline; longer enums are cut with a count. */
1618
+ const REFERENCE_ENUM_VALUES = 30;
1619
+
1620
+ /** Prose for one description: one line, markdown links reduced to their text. */
1621
+ function referenceProse(text: unknown): string {
1622
+ if (typeof text !== "string") return "";
1623
+ return text.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\s+/g, " ").trim();
1624
+ }
1625
+
1626
+ /** Sentences the generator appends to an argument's description because a
1627
+ * call depends on them: enum meanings, defaults, deprecation, reference
1628
+ * and date forms, file paths. They survive the cut. */
1629
+ const REFERENCE_NOTE = /^(Values:|Default|Deprecated|Accepts|Markdown|Paths? of (a )?local file|Must|Required|Only one of)/;
1630
+ /** Leading prose kept per argument before its notes. */
1631
+ const REFERENCE_ARGUMENT_PROSE = 160;
1632
+
1633
+ /** An argument's description, cut to its leading sentences within the
1634
+ * budget plus every note the generator added. The full text is in the
1635
+ * schema (schema: true). */
1636
+ function argumentProse(text: unknown): string {
1637
+ const prose = referenceProse(text);
1638
+ if (prose.length <= REFERENCE_ARGUMENT_PROSE) return prose;
1639
+ const sentences = prose.match(/[^.!?]+(?:[.!?]+(?=\s|$)|$)\s*/g) ?? [prose];
1640
+ const kept: string[] = [];
1641
+ let used = 0;
1642
+ let cut = false;
1643
+ for (const raw of sentences) {
1644
+ const sentence = raw.trim();
1645
+ if (REFERENCE_NOTE.test(sentence)) { kept.push(sentence); continue; }
1646
+ 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);
1648
+ used += sentence.length;
1649
+ } else {
1650
+ cut = true;
1651
+ }
1652
+ }
1653
+ return kept.join(" ") + (cut ? " …" : "");
1654
+ }
1655
+
1656
+ /** A schema's type as an agent writes it: string, integer[], "a"|"b", object. */
1657
+ function referenceType(schema: Record<string, unknown>): string {
1658
+ if (Array.isArray(schema.enum)) {
1659
+ const values = schema.enum.slice(0, REFERENCE_ENUM_VALUES).map((v) => JSON.stringify(v));
1660
+ return values.join("|") + (schema.enum.length > REFERENCE_ENUM_VALUES ? "|… (" + (schema.enum.length - REFERENCE_ENUM_VALUES) + " more)" : "");
1661
+ }
1662
+ if (schema.const !== undefined) return JSON.stringify(schema.const);
1663
+ const variants = (schema.anyOf ?? schema.oneOf) as Record<string, unknown>[] | undefined;
1664
+ if (Array.isArray(variants)) return [...new Set(variants.map((v) => referenceType(v)))].join("|");
1665
+ const types = Array.isArray(schema.type) ? schema.type as string[] : typeof schema.type === "string" ? [schema.type] : [];
1666
+ const one = (t: string): string => {
1667
+ if (t === "array") {
1668
+ const items = schema.items && typeof schema.items === "object" ? referenceType(schema.items as Record<string, unknown>) : "any";
1669
+ return (/[| ]/.test(items) ? "(" + items + ")" : items) + "[]";
1670
+ }
1671
+ return t + (t === "string" && typeof schema.format === "string" ? " (" + schema.format + ")" : "");
1672
+ };
1673
+ if (types.length === 0) return schema.properties ? "object" : "any";
1674
+ return types.map(one).join("|");
1675
+ }
1676
+
1677
+ /** The object whose fields an argument lists: itself, its array's items,
1678
+ * or the one object of a nullable union. */
1679
+ function referenceObject(schema: Record<string, unknown>): Record<string, unknown> | undefined {
1680
+ if (schema.properties && typeof schema.properties === "object") return schema;
1681
+ const items = schema.items as Record<string, unknown> | undefined;
1682
+ if (items && typeof items === "object") return referenceObject(items);
1683
+ const variants = (schema.anyOf ?? schema.oneOf) as Record<string, unknown>[] | undefined;
1684
+ if (Array.isArray(variants)) {
1685
+ const objects = variants.map(referenceObject).filter((v) => v !== undefined);
1686
+ if (objects.length === 1) return objects[0];
1687
+ }
1688
+ return undefined;
1689
+ }
1690
+
1691
+ /** One line per argument, nested fields indented beneath their object.
1692
+ * Each top-level argument is spelled out as deep as fits its budget. */
1693
+ function referenceArguments(schema: Record<string, unknown>, lines: string[]): void {
1694
+ const properties = (schema.properties ?? {}) as Record<string, Record<string, unknown>>;
1695
+ for (const name of Object.keys(properties)) {
1696
+ const only = { ...schema, properties: { [name]: properties[name] } };
1697
+ let block: string[] = [];
1698
+ // Deepest first; then the same depth without listing the field names
1699
+ // below the cut; then one level with names only.
1700
+ for (const [depth, names] of [[REFERENCE_DEPTH, true], [2, true], [2, false], [1, true]] as const) {
1701
+ block = [];
1702
+ referenceArgumentLines(only, 0, depth, names, " ", block);
1703
+ if (block.join("\n").length <= REFERENCE_ARGUMENT_BUDGET) break;
1704
+ }
1705
+ lines.push(...block);
1706
+ }
1707
+ }
1708
+
1709
+ function referenceArgumentLines(schema: Record<string, unknown>, depth: number, maxDepth: number, names: boolean, indent: string, lines: string[]): void {
1710
+ const properties = (schema.properties ?? {}) as Record<string, Record<string, unknown>>;
1711
+ const required = new Set(Array.isArray(schema.required) ? schema.required as string[] : []);
1712
+ for (const [name, child] of Object.entries(properties)) {
1713
+ if (!child || typeof child !== "object") continue;
1714
+ const description = argumentProse(child.description);
1715
+ const extras = [
1716
+ ...(child.default !== undefined && !/\bdefault\b/i.test(description) ? ["default " + JSON.stringify(child.default)] : []),
1717
+ ...(child.deprecated === true && !/deprecated/i.test(description) ? ["deprecated"] : []),
1718
+ ];
1719
+ lines.push(indent + name + " (" + referenceType(child) + (required.has(name) ? ", required" : "") + (extras.length ? ", " + extras.join(", ") : "") + ")" + (description ? ": " + description : ""));
1720
+ const nested = referenceObject(child);
1721
+ if (!nested) continue;
1722
+ if (depth + 1 < maxDepth) {
1723
+ referenceArgumentLines(nested, depth + 1, maxDepth, names, indent + " ", lines);
1724
+ continue;
1725
+ }
1726
+ if (!names) continue;
1727
+ 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)");
1729
+ }
1730
+ }
1731
+
1732
+ /** A result's shape in one line: keys and types, one level into objects. */
1733
+ function referenceShape(schema: Record<string, unknown> | undefined, depth = 0): string {
1734
+ if (!schema || typeof schema !== "object") return "any";
1735
+ const object = schema.properties && typeof schema.properties === "object" ? schema : undefined;
1736
+ if (object) {
1737
+ if (depth >= 2) return "{…}";
1738
+ const entries = Object.entries(object.properties as Record<string, Record<string, unknown>>);
1739
+ const shown = entries.slice(0, 40).map(([key, child]) => key + ": " + referenceShape(child, depth + 1));
1740
+ return "{" + shown.join(", ") + (entries.length > 40 ? ", … " + (entries.length - 40) + " more" : "") + "}";
1741
+ }
1742
+ const items = schema.items as Record<string, unknown> | undefined;
1743
+ if (items && typeof items === "object" && (items.properties || items.items)) {
1744
+ return "[" + referenceShape(items, depth) + "]";
1745
+ }
1746
+ if (Array.isArray(schema.enum) && schema.enum.length > 8) {
1747
+ return schema.enum.slice(0, 8).map((v) => JSON.stringify(v)).join("|") + "|…";
1748
+ }
1749
+ // Nullability and formats matter for sending, not for reading a result.
1750
+ const type = referenceType({ ...schema, format: undefined });
1751
+ return type.split("|").filter((t) => t !== "null").join("|") || type;
1752
+ }
1753
+
1754
+ /**
1755
+ * An operation's reference as read_docs returns it: what it does, whether
1756
+ * it is safe, every argument in prose (types, enums, required, notes, and
1757
+ * nested fields), one example and the result's shape. The complete JSON
1758
+ * Schemas come with `schema: true`, as the CLI's `docs --schema` does; they
1759
+ * cost several times the rest and an agent rarely needs them to call.
1760
+ */
1761
+ export function referenceText(op: OpLike, options: { schema?: boolean } = {}): string {
1630
1762
  const safety = operationSafety(op);
1631
- const example = op.exampleArguments ?? exampleArgumentsFromSchema(op.inputSchema);
1763
+ // A credential-defaulted argument (Twilio's AccountSid) is left out, so the
1764
+ // example shows the call an agent should make.
1765
+ const example = Object.fromEntries(Object.entries(op.exampleArguments ?? {})
1766
+ .filter(([name]) => !op.params.some((p) => p.name === name && p.credential)));
1632
1767
  const lines = [
1633
- op.tool + ": " + op.httpMethod + " " + op.path + (op.paginated ? " (paginated)" : ""),
1768
+ op.tool + ": " + operationLabel(op) + (op.paginated ? " (paginated)" : ""),
1634
1769
  ...(op.summary ? [op.summary] : []),
1635
- ...(op.description ? ["", op.description.trim()] : []),
1770
+ ...(op.description && op.description.trim() !== op.summary?.trim() ? ["", op.description.trim()] : []),
1636
1771
  "",
1637
1772
  "Safety: " + safety + (safety === "destructive" ? " (execute requires confirm: true)" : ""),
1638
- ...(op.auth ? ["Authentication: " + op.auth] : []),
1773
+ ...(op.auth ? ["Authentication: " + (op.authNotDeclared ? "not declared by the API Spec" : op.auth)] : []),
1774
+ ...(requiredScopes(op.security).length ? ["Required OAuth scopes: " + requiredScopes(op.security).join(", ")] : []),
1639
1775
  ];
1640
- if (op.params.length > 0) {
1776
+ const input = toolInputSchema(op);
1777
+ if (Object.keys((input.properties ?? {}) as object).length > 0) {
1641
1778
  lines.push("", "Arguments:");
1642
- for (const p of op.params) {
1643
- lines.push(
1644
- " " + p.name + " (" + (p.enum ? p.enum.join("|") : p.type) + (p.required ? ", required" : "") + ")" +
1645
- (p.description ? ": " + p.description.trim().split("\n")[0] : ""),
1646
- );
1647
- }
1779
+ referenceArguments(input, lines);
1648
1780
  }
1649
- if (!hasOwnFieldsParam(op)) {
1650
- lines.push("", "Also: fields (array of dotted paths) keeps only those keys of the result" + (op.paginated ? ", per item" : "") + ".");
1781
+ lines.push("", "Example arguments: " + JSON.stringify(example));
1782
+ if (op.outputSchema) {
1783
+ lines.push("", (op.paginated ? "Returns one page: " : "Returns: ") + referenceShape(op.outputSchema));
1784
+ }
1785
+ if (options.schema) {
1786
+ lines.push("", "Input schema: " + JSON.stringify(input));
1787
+ if (op.outputSchema) lines.push("", "Output schema: " + JSON.stringify(op.outputSchema));
1788
+ } else {
1789
+ lines.push("", "Full input and output JSON Schemas: read_docs " + JSON.stringify({ page: op.tool, schema: true }) + ".");
1651
1790
  }
1652
- lines.push("", "Input schema:", "```json", JSON.stringify(toolInputSchema(op), null, 2), "```");
1653
- lines.push("", "Example arguments:", "```json", JSON.stringify(example, null, 2), "```");
1654
- if (op.outputSchema) lines.push("", "Output schema:", "```json", JSON.stringify(op.outputSchema, null, 2), "```");
1655
1791
  return lines.join("\n");
1656
1792
  }
1657
1793
 
1658
- /** Query terms: lowercase words of two or more characters, with the
1659
- * snake/kebab/camel seams split so "createAccount" finds accounts_create. */
1660
- function searchTerms(query: string): string[] {
1661
- return [...new Set(query.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 2))];
1662
- }
1663
-
1664
- /** Relevance of one operation to the terms: the tool name counts most,
1665
- * then summary, path and argument names, then the description. The whole
1666
- * query as a phrase in the name or summary is a strong signal. */
1667
- export function searchScore(op: OpLike, query: string): number {
1668
- const terms = searchTerms(query);
1669
- if (terms.length === 0) return 0;
1670
- const tool = op.tool.toLowerCase();
1671
- const toolWords = tool.split("_");
1672
- const summary = (op.summary ?? "").toLowerCase();
1673
- const path = op.path.toLowerCase();
1674
- const params = op.params.map((p) => p.name.toLowerCase());
1675
- const description = (op.description ?? "").toLowerCase();
1676
- let score = 0;
1677
- for (const term of terms) {
1678
- if (toolWords.includes(term)) score += 10;
1679
- else if (tool.includes(term)) score += 6;
1680
- if (summary.split(/[^a-z0-9]+/).includes(term)) score += 5;
1681
- else if (summary.includes(term)) score += 3;
1682
- if (path.includes(term)) score += 3;
1683
- if (params.some((p) => p === term)) score += 3;
1684
- else if (params.some((p) => p.includes(term))) score += 1;
1685
- if (description.includes(term)) score += 1;
1686
- }
1687
- const phrase = query.trim().toLowerCase();
1688
- if (phrase.length >= 3 && (tool.includes(phrase.replace(/[^a-z0-9]+/g, "_")) || summary.includes(phrase))) score += 8;
1689
- return score;
1690
- }
1691
-
1692
1794
  export async function docsSearch(source: DocsSource, query: string, page = 1): Promise<ToolOutcome> {
1693
1795
  const sections: string[] = [];
1694
- const ranked = source.ops
1695
- .map((op) => ({ op, score: searchScore(op, query) }))
1696
- .filter((r) => r.score > 0)
1697
- .sort((a, b) => b.score - a.score || a.op.tool.localeCompare(b.op.tool));
1698
- const omittedRanked = (source.omittedOps ?? [])
1699
- .map((op) => ({ op, score: searchScore(op, query) }))
1700
- .filter((result) => result.score > 0)
1701
- .sort((a, b) => b.score - a.score || a.op.tool.localeCompare(b.op.tool));
1796
+ const ranked = rankOperations(source.ops, query);
1797
+ const omittedRanked = rankOperations(source.omittedOps ?? [], query);
1702
1798
  const exactOmitted = findOperation(source.omittedOps ?? [], query);
1703
- if (exactOmitted) return omittedPlanLimit([exactOmitted], exactOmitted.tool);
1799
+ if (exactOmitted) return omittedPlanLimit(source, [exactOmitted], exactOmitted.tool);
1704
1800
  const pageIndex = Math.max(1, Math.floor(page)) - 1;
1705
1801
  const slice = ranked.slice(pageIndex * SEARCH_PAGE_SIZE, (pageIndex + 1) * SEARCH_PAGE_SIZE);
1802
+ const more = ranked.length - (pageIndex + 1) * SEARCH_PAGE_SIZE;
1706
1803
  if (slice.length > 0) {
1707
- const more = ranked.length - (pageIndex + 1) * SEARCH_PAGE_SIZE;
1708
1804
  sections.push("Reference matches (best first" + (ranked.length > SEARCH_PAGE_SIZE ? ", page " + (pageIndex + 1) + " of " + Math.ceil(ranked.length / SEARCH_PAGE_SIZE) : "") + "):\n" +
1709
- slice.map((r) => "- " + r.op.tool + ": " + (r.op.summary ?? r.op.httpMethod + " " + r.op.path)).join("\n") +
1710
- (more > 0 ? "\n(" + more + " more; pass page: " + (pageIndex + 2) + ")" : ""));
1805
+ slice.map((r) => "- " + r.op.tool + ": " + searchLabel(r.op)).join("\n") +
1806
+ (more > 0 ? "\n(" + more + " more; pass page: " + (pageIndex + 2) + ")" : "") +
1807
+ "\nread_docs {\"page\": \"<tool>\"} gives an operation's arguments and example.");
1711
1808
  } else if (ranked.length > 0) {
1712
1809
  sections.push("No reference matches on page " + (pageIndex + 1) + "; there are " + Math.ceil(ranked.length / SEARCH_PAGE_SIZE) + " pages.");
1713
1810
  }
@@ -1715,39 +1812,65 @@ export async function docsSearch(source: DocsSource, query: string, page = 1): P
1715
1812
  const guideSlice = guides.slice(pageIndex * SEARCH_PAGE_SIZE, (pageIndex + 1) * SEARCH_PAGE_SIZE);
1716
1813
  const proseMatchCount = guides.length;
1717
1814
  if (guideSlice.length > 0) {
1718
- sections.push("Guide matches (best first, " + guides.length + " pages):\n" + guideSlice.map((match) =>
1719
- "- [" + match.title + (match.section ? " / " + match.section : "") + "](" + match.url + "): " + match.excerpt + "\n read_docs " + JSON.stringify({ page: match.url })).join("\n"));
1815
+ sections.push("Guide matches (best first, " + guides.length + " pages; read_docs {\"page\": \"<url>\"} reads one):\n" + guideSlice.map((match) =>
1816
+ "- [" + match.title + (match.section ? " / " + match.section : "") + "](" + match.url + "): " + match.excerpt).join("\n"));
1720
1817
  }
1721
1818
  if (status === "unavailable") sections.push("The docs site is unavailable; the API reference was still searched.");
1819
+ // Lean on purpose: the text above is what most clients show the model,
1820
+ // and this mirrors it rather than repeating the read_docs call per hit.
1722
1821
  const structured = {
1723
- schema_version: "1", query, page: pageIndex + 1,
1724
- 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 } } })),
1725
- guides: guideSlice.map((match) => ({ ...match, read_tool: { name: "read_docs", arguments: { page: match.url } } })),
1822
+ schema_version: "2", query, page: pageIndex + 1,
1823
+ reference: slice.map(({ op }) => ({ tool: op.tool, summary: searchLabel(op), ...(searchDeprecated(op) ? { deprecated: true } : {}) })),
1824
+ guides: guideSlice,
1726
1825
  totals: { reference: ranked.length, guides: guides.length }, guides_status: status,
1826
+ ...(slice.length > 0 || guideSlice.length > 0 ? { read_docs: { page: "<tool name or guide url>" } } : {}),
1827
+ ...(more > 0 ? { next_page: pageIndex + 2 } : {}),
1727
1828
  };
1728
1829
  if (omittedRanked.length > 0 && ranked.length === 0 && proseMatchCount === 0) {
1729
- return omittedPlanLimit(omittedRanked.map((result) => result.op));
1830
+ return omittedPlanLimit(source, omittedRanked.map((result) => result.op));
1730
1831
  }
1731
1832
  if (omittedRanked.length > 0) {
1732
- 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"));
1833
+ const generated = source.generatedOperationCount ?? source.ops.length;
1834
+ 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"));
1733
1835
  }
1734
- const coverage = coverageText(source);
1735
1836
  if (sections.length === 0) {
1736
1837
  return {
1737
- 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)" : ""),
1838
+ 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)" : ""),
1738
1839
  isError: false,
1739
1840
  structured,
1740
1841
  };
1741
1842
  }
1742
- return { text: [...(coverage ? [coverage] : []), ...sections].join("\n\n"), isError: false, structured };
1843
+ return { text: sections.join("\n\n"), isError: false, structured };
1844
+ }
1845
+
1846
+ function searchDeprecated(op: OpLike): boolean {
1847
+ return op.deprecated === true;
1848
+ }
1849
+
1850
+ /** One line per hit: the summary (or the wire call), flagged when deprecated. */
1851
+ function searchLabel(op: OpLike): string {
1852
+ return (searchDeprecated(op) ? "(deprecated) " : "") + (op.summary ?? operationLabel(op));
1743
1853
  }
1744
1854
 
1745
- export async function docsRead(source: DocsSource, page: string): Promise<ToolOutcome> {
1855
+ /** Characters one read_docs call returns; the rest is paged by offset. */
1856
+ export const READ_DOCS_LIMIT = 20_000;
1857
+
1858
+ /** One part of a long page, ending with how to read the next part. */
1859
+ function docsPart(page: string, text: string, offset: number, schema = false): string {
1860
+ if (offset <= 0 && text.length <= READ_DOCS_LIMIT) return text;
1861
+ const start = Math.min(Math.max(0, offset), text.length);
1862
+ const end = Math.min(text.length, start + READ_DOCS_LIMIT);
1863
+ 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 }) + ".]"
1865
+ : "\n\n[Characters " + start + "-" + end + " of " + text.length + "; end of page.]";
1866
+ return text.slice(start, end) + more;
1867
+ }
1868
+
1869
+ export async function docsRead(source: DocsSource, page: string, offset = 0, options: { schema?: boolean } = {}): Promise<ToolOutcome> {
1746
1870
  const opMatch = findOperation(source.ops, page);
1747
- const coverage = coverageText(source);
1748
- if (opMatch) return { text: [...(coverage ? [coverage] : []), referenceText(opMatch)].join("\n\n"), isError: false };
1871
+ if (opMatch) return { text: docsPart(page, referenceText(opMatch, options), offset, options.schema === true), isError: false };
1749
1872
  const omittedMatch = findOperation(source.omittedOps ?? [], page);
1750
- if (omittedMatch) return omittedPlanLimit([omittedMatch], omittedMatch.tool);
1873
+ if (omittedMatch) return omittedPlanLimit(source, [omittedMatch], omittedMatch.tool);
1751
1874
  let target = page;
1752
1875
  if (!/^https?:\/\//.test(target)) {
1753
1876
  const index = await fetchDocs(source, "llms.txt");
@@ -1759,7 +1882,7 @@ export async function docsRead(source: DocsSource, page: string): Promise<ToolOu
1759
1882
  ? "A docs URL was not provided at generate time, and no generated operation matches \"" + page + "\"."
1760
1883
  : "Couldn't fetch \"" + page + "\". Use search_docs to find pages.", "NOT_FOUND", ["search_docs finds operations and guide pages."]);
1761
1884
  }
1762
- return { text: [...(coverage ? [coverage] : []), text].join("\n\n"), isError: false };
1885
+ return { text: docsPart(page, text, offset), isError: false };
1763
1886
  }
1764
1887
 
1765
1888
  /**
@@ -1780,16 +1903,38 @@ export async function callSharedTool(
1780
1903
  return docsSearch(source, args.query, page);
1781
1904
  }
1782
1905
  if (name === "read_docs") {
1783
- return typeof args.page === "string" ? docsRead(source, args.page) : argumentsError({ tool: name }, [{ code: "MISSING_ARGUMENT", argument: "page", message: "read_docs requires a page string." }]);
1906
+ const offset = typeof args.offset === "number" ? args.offset : typeof args.offset === "string" && /^\d+$/.test(args.offset) ? Number(args.offset) : 0;
1907
+ const schema = args.schema === true || args.schema === "true";
1908
+ if (typeof args.page === "string" && !findOperation(source.ops, args.page)) {
1909
+ const hidden = findHidden(source, args.page);
1910
+ if (hidden) return hiddenOutcome(hidden);
1911
+ }
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." }]);
1784
1913
  }
1785
1914
  if (name === "execute") {
1786
1915
  if (typeof args.operation !== "string") return argumentsError({ tool: name }, [{ code: "MISSING_ARGUMENT", argument: "operation", message: "execute requires an operation name." }]);
1916
+ // Operation arguments (fields included) belong inside arguments; a
1917
+ // stray top-level key would otherwise be ignored without a word.
1918
+ const stray = Object.keys(args).filter((key) => !EXECUTE_KEYS.includes(key) && args[key] !== undefined);
1919
+ if (stray.length > 0) {
1920
+ return argumentsError({ tool: name }, stray.map((key) => ({
1921
+ code: "UNKNOWN_ARGUMENT" as const,
1922
+ argument: key,
1923
+ message: "Unknown argument \"" + key + "\" to execute, which takes " + EXECUTE_KEYS.join(", ") + ". Put operation arguments, including fields, inside arguments: {\"operation\": \"" + args.operation + "\", \"arguments\": {\"" + key + "\": …}}.",
1924
+ })));
1925
+ }
1787
1926
  const target = findOperation(source.ops, args.operation);
1788
1927
  if (!target) {
1789
1928
  const omitted = findOperation(source.omittedOps ?? [], args.operation);
1790
- return omitted
1791
- ? omittedPlanLimit([omitted], omitted.tool)
1792
- : textError("Unknown operation: " + args.operation + ".", "NOT_FOUND", ["search_docs finds operations by name, path or description."]);
1929
+ if (omitted) return omittedPlanLimit(source, [omitted], omitted.tool);
1930
+ const hidden = findHidden(source, args.operation);
1931
+ if (hidden) return hiddenOutcome(hidden);
1932
+ const suggestions = rankOperations(source.ops, args.operation.replace(/[_.]+/g, " ")).slice(0, 3).map((r) => r.op.tool);
1933
+ return textError(
1934
+ "Unknown operation: " + args.operation + "." + (suggestions.length > 0 ? " Did you mean " + suggestions.join(", ") + "?" : ""),
1935
+ "NOT_FOUND",
1936
+ [...(suggestions.length > 0 ? ["read_docs {\"page\": \"" + suggestions[0] + "\"} gives its arguments."] : []), "search_docs finds operations by name, path or description."],
1937
+ );
1793
1938
  }
1794
1939
  if (operationSafety(target) === "destructive" && args.confirm !== true) {
1795
1940
  return textError(
@@ -1803,5 +1948,12 @@ export async function callSharedTool(
1803
1948
  : {};
1804
1949
  return runOperation(target, opArgs);
1805
1950
  }
1806
- return undefined;
1951
+ // An operation tool the server's switches hide (operations mode).
1952
+ const hidden = source.ops.some((op) => op.tool === name) ? undefined : (source.hiddenOps ?? []).find((h) => h.op.tool === name);
1953
+ return hidden ? hiddenOutcome(hidden) : undefined;
1954
+ }
1955
+
1956
+ /** Every OAuth scope an operation's security requirements name, in order. */
1957
+ export function requiredScopes(security: Record<string, string[]>[] | undefined): string[] {
1958
+ return [...new Set((security ?? []).flatMap((requirement) => Object.values(requirement).flat()))];
1807
1959
  }