@typeship-ax/mcp 0.8.0 → 0.10.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 (103) hide show
  1. package/AGENTS.md +31 -0
  2. package/README.md +66 -9
  3. package/api.json +3153 -761
  4. package/api.md +9796 -382
  5. package/dist/api-identity.d.ts +40 -0
  6. package/dist/api-identity.d.ts.map +1 -0
  7. package/dist/api-identity.js +128 -0
  8. package/dist/auth-profiles.d.ts +30 -0
  9. package/dist/auth-profiles.d.ts.map +1 -0
  10. package/dist/auth-profiles.js +138 -0
  11. package/dist/core/http.d.ts +17 -2
  12. package/dist/core/http.d.ts.map +1 -1
  13. package/dist/core/http.js +78 -17
  14. package/dist/credential-storage.d.ts +24 -0
  15. package/dist/credential-storage.d.ts.map +1 -0
  16. package/dist/credential-storage.js +207 -0
  17. package/dist/docs.d.ts +25 -0
  18. package/dist/docs.d.ts.map +1 -1
  19. package/dist/docs.js +144 -0
  20. package/dist/errors.d.ts +18 -10
  21. package/dist/errors.d.ts.map +1 -1
  22. package/dist/errors.js +24 -14
  23. package/dist/index.d.ts +10 -3
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +20 -4
  26. package/dist/mcp-authorization.d.ts +52 -0
  27. package/dist/mcp-authorization.d.ts.map +1 -0
  28. package/dist/mcp-authorization.js +232 -0
  29. package/dist/mcp-protocol.d.ts +51 -2
  30. package/dist/mcp-protocol.d.ts.map +1 -1
  31. package/dist/mcp-protocol.js +249 -37
  32. package/dist/mcp.d.ts +21 -3
  33. package/dist/mcp.d.ts.map +1 -1
  34. package/dist/mcp.js +185 -68
  35. package/dist/named-credentials.d.ts +21 -0
  36. package/dist/named-credentials.d.ts.map +1 -0
  37. package/dist/named-credentials.js +86 -0
  38. package/dist/oauth-request.d.ts +21 -0
  39. package/dist/oauth-request.d.ts.map +1 -0
  40. package/dist/oauth-request.js +119 -0
  41. package/dist/oauth-session.d.ts +106 -0
  42. package/dist/oauth-session.d.ts.map +1 -0
  43. package/dist/oauth-session.js +244 -0
  44. package/dist/ops.d.ts +14 -1
  45. package/dist/ops.d.ts.map +1 -1
  46. package/dist/ops.js +34 -30
  47. package/dist/resources/account.d.ts +2 -2
  48. package/dist/resources/account.d.ts.map +1 -1
  49. package/dist/resources/account.js +1 -0
  50. package/dist/resources/api-keys.d.ts +3 -3
  51. package/dist/resources/api-keys.d.ts.map +1 -1
  52. package/dist/resources/api-keys.js +2 -0
  53. package/dist/resources/definition-revisions.d.ts +5 -5
  54. package/dist/resources/definition-revisions.d.ts.map +1 -1
  55. package/dist/resources/definition-revisions.js +4 -0
  56. package/dist/resources/definitions.d.ts +15 -4
  57. package/dist/resources/definitions.d.ts.map +1 -1
  58. package/dist/resources/definitions.js +11 -2
  59. package/dist/resources/generate.d.ts +14 -3
  60. package/dist/resources/generate.d.ts.map +1 -1
  61. package/dist/resources/generate.js +10 -2
  62. package/dist/resources/generations.d.ts +3 -3
  63. package/dist/resources/generations.d.ts.map +1 -1
  64. package/dist/resources/generations.js +2 -0
  65. package/dist/resources/projects.d.ts +56 -20
  66. package/dist/resources/projects.d.ts.map +1 -1
  67. package/dist/resources/projects.js +40 -4
  68. package/dist/resources/targets.d.ts +84 -10
  69. package/dist/resources/targets.d.ts.map +1 -1
  70. package/dist/resources/targets.js +127 -2
  71. package/dist/schemas.d.ts.map +1 -1
  72. package/dist/schemas.js +55 -26
  73. package/dist/types.d.ts +602 -123
  74. package/dist/types.d.ts.map +1 -1
  75. package/dist/types.js +24 -0
  76. package/dist/worker.js +4 -4
  77. package/package.json +11 -1
  78. package/server.json +42 -0
  79. package/src/api-identity.ts +98 -0
  80. package/src/auth-profiles.ts +114 -0
  81. package/src/core/http.ts +88 -19
  82. package/src/credential-storage.ts +183 -0
  83. package/src/docs.ts +138 -0
  84. package/src/errors.ts +26 -15
  85. package/src/index.ts +29 -4
  86. package/src/mcp-authorization.ts +211 -0
  87. package/src/mcp-protocol.ts +287 -38
  88. package/src/mcp.ts +186 -72
  89. package/src/named-credentials.ts +74 -0
  90. package/src/oauth-request.ts +90 -0
  91. package/src/oauth-session.ts +258 -0
  92. package/src/ops.ts +48 -31
  93. package/src/resources/account.ts +3 -0
  94. package/src/resources/api-keys.ts +5 -0
  95. package/src/resources/definition-revisions.ts +9 -0
  96. package/src/resources/definitions.ts +25 -0
  97. package/src/resources/generate.ts +23 -0
  98. package/src/resources/generations.ts +5 -0
  99. package/src/resources/projects.ts +95 -7
  100. package/src/resources/targets.ts +241 -0
  101. package/src/schemas.ts +55 -26
  102. package/src/types.ts +640 -123
  103. package/src/worker.ts +4 -4
@@ -16,7 +16,7 @@
16
16
  */
17
17
 
18
18
  import { dateKindOf, relativeDate } from "./dates.js";
19
- import { resolveDocsContentUrl } from "./docs.js";
19
+ import { docsReadTarget, resolveDocsContentUrl, searchConnectedGuides } from "./docs.js";
20
20
 
21
21
  export const MCP_PROTOCOL_VERSION = "2026-07-28";
22
22
  /** Revisions served. Legacy (initialize-handshake) revisions are not; an
@@ -77,6 +77,28 @@ export interface ToolOutcome {
77
77
  content?: ContentBlock[];
78
78
  }
79
79
 
80
+ /** Throw only from an application-owned credential resolver, before calling
81
+ * the API. The URL must show a sign-in/linking page, never a pre-authenticated
82
+ * resource, token or personal information. The page must verify the same user
83
+ * before linking. The runtime rechecks credentials on every subsequent call. */
84
+ export class McpAccountLinkRequired extends Error {
85
+ readonly url: string;
86
+ constructor(url: string) {
87
+ super("Connect your API account in the browser to continue.");
88
+ this.name = "McpAccountLinkRequired";
89
+ try {
90
+ const parsed = new URL(url);
91
+ if (url.length > 2048 || url !== url.trim() || /[\u0000-\u0020\u007F"\\]/.test(url) || parsed.username || parsed.password || parsed.hash ||
92
+ !(parsed.protocol === "https:" || parsed.protocol === "http:" && ["127.0.0.1", "[::1]", "localhost"].includes(parsed.hostname)) ||
93
+ [...parsed.searchParams.keys()].some(key => /^(access_token|refresh_token|client_secret|api_key|password|authorization)$/i.test(key))) throw new Error();
94
+ } catch { throw new Error("Provide a public HTTPS account-linking page without credentials or a fragment (HTTP loopback is allowed for development)."); }
95
+ this.url = url;
96
+ Object.freeze(this);
97
+ }
98
+ }
99
+
100
+ const API_LINK_INPUT = "typeship_api_account";
101
+
80
102
  /** The subset of an operation spec (ops.ts / the hosted manifest) the
81
103
  * protocol layer reads. */
82
104
  export interface OpLike {
@@ -93,7 +115,14 @@ export interface OpLike {
93
115
  /** GraphQL ops accept a raw selection-set override. */
94
116
  select: boolean;
95
117
  graphql?: { kind: string };
96
- params: { name: string; type: string; required: boolean; enum?: string[]; description?: string }[];
118
+ params: {
119
+ name: string;
120
+ type: string;
121
+ required: boolean;
122
+ enum?: string[];
123
+ description?: string;
124
+ resolve?: false | ReferenceResolver;
125
+ }[];
97
126
  inputSchema: Record<string, unknown>;
98
127
  outputSchema?: Record<string, unknown>;
99
128
  /** Canonical effect classification shared by generated docs, CLI, and MCP. */
@@ -110,6 +139,16 @@ export interface OpLike {
110
139
  bodyKind?: string | null;
111
140
  }
112
141
 
142
+ /** Fully proved lookup metadata carried in ops.ts / the hosted manifest. */
143
+ export interface ReferenceResolver {
144
+ via: string;
145
+ match: string[];
146
+ id: string;
147
+ idPattern?: string;
148
+ filterParam?: string;
149
+ inferred?: boolean;
150
+ }
151
+
113
152
  /** Does the operation take a file (multipart form or raw binary body)? */
114
153
  export function isUploadOp(op: OpLike): boolean {
115
154
  return op.bodyKind === "multipart" || op.bodyKind === "binary";
@@ -152,7 +191,8 @@ export interface McpServer {
152
191
  listTools(): ToolDefinition[];
153
192
  /**
154
193
  * Run a tool. Return undefined for an unknown tool (the client gets
155
- * -32602), a ToolOutcome otherwise. Throwing yields -32603.
194
+ * -32602), a ToolOutcome otherwise. McpAccountLinkRequired requests a
195
+ * browser interaction; other exceptions yield -32603.
156
196
  */
157
197
  callTool(name: string, args: Record<string, unknown>): Promise<ToolOutcome | undefined>;
158
198
  /** Optional actionable wording for an unknown tool name. The protocol
@@ -312,10 +352,37 @@ export async function handleRpc(server: McpServer, incoming: unknown): Promise<R
312
352
  if (args === null || typeof args !== "object" || Array.isArray(args)) {
313
353
  return rpcError(id, -32602, "tools/call arguments must be an object", 400);
314
354
  }
355
+ const responses = request.params?.inputResponses;
356
+ if (responses !== undefined && (responses === null || typeof responses !== "object" || Array.isArray(responses))) return rpcError(id, -32602, "inputResponses must be an object", 400);
357
+ const response = responses && Object.hasOwn(responses, API_LINK_INPUT) ? (responses as Record<string, unknown>)[API_LINK_INPUT] : undefined;
358
+ if (response !== undefined) {
359
+ if (!response || typeof response !== "object" || Array.isArray(response) || !["accept", "decline", "cancel"].includes((response as { action: string }).action)) return rpcError(id, -32602, "Invalid API account-link response", 400);
360
+ if ((response as { action: string }).action !== "accept") {
361
+ const cancelled = textError("API account linking was cancelled. No API request was made.", "ACCOUNT_LINK_CANCELLED");
362
+ return complete({ content: [{ type: "text", text: cancelled.text }], isError: true });
363
+ }
364
+ // A client acknowledgment is not authorization. Only a fresh lookup
365
+ // of the server's linked credentials can let the operation proceed.
366
+ }
315
367
  const denied = server.beforeToolCall ? await server.beforeToolCall(name) : null;
316
368
  if (denied) return denied;
317
369
  const started = Date.now();
318
- const outcome = await server.callTool(name, args);
370
+ let outcome: ToolOutcome | undefined;
371
+ try { outcome = await server.callTool(name, args); }
372
+ catch (error) {
373
+ if (!(error instanceof McpAccountLinkRequired)) throw error;
374
+ const caps = (request.params?._meta as Record<string, unknown>)[META_CLIENT_CAPS] as { elicitation?: { url?: unknown } };
375
+ const urlMode = caps.elicitation?.url;
376
+ if (urlMode === null || typeof urlMode !== "object" || Array.isArray(urlMode)) {
377
+ const unsupported = textError("Connect your API account using an MCP client that supports browser account-linking prompts (URL-mode elicitation), then retry.", "ACCOUNT_LINK_REQUIRED");
378
+ return complete({ content: [{ type: "text", text: unsupported.text }], isError: true });
379
+ }
380
+ return { status: 200, message: { jsonrpc: "2.0", id, result: {
381
+ resultType: "input_required",
382
+ inputRequests: { [API_LINK_INPUT]: { method: "elicitation/create", params: { mode: "url", url: error.url, message: "Connect your API account in the browser to continue." } } },
383
+ _meta: { [META_SERVER_INFO]: server.serverInfo },
384
+ } } };
385
+ }
319
386
  if (outcome === undefined) return rpcError(id, -32602, server.unknownToolMessage?.(name) ?? "Unknown tool: " + name, 400);
320
387
  if (server.afterToolCall) await server.afterToolCall(name, outcome, Date.now() - started);
321
388
  // A tool that declares an outputSchema MUST return structuredContent,
@@ -484,7 +551,7 @@ function exampleMatchingPattern(pattern: string, minLength: number, maxLength?:
484
551
  if (maxLength !== undefined) value = value.slice(0, maxLength);
485
552
  return value;
486
553
  };
487
- const candidates = [prefix + "123", prefix + "example", prefix, "example", "value"];
554
+ const candidates = [prefix + "123", prefix + "example", prefix, "resource.method", "example.value", "example_123", "example", "value"];
488
555
  for (const candidate of candidates) {
489
556
  const value = fit(candidate);
490
557
  regex.lastIndex = 0;
@@ -643,6 +710,8 @@ export function missingArguments(op: OpLike, args: Record<string, unknown>): str
643
710
 
644
711
  export interface InstructionsInput {
645
712
  title: string;
713
+ /** Callable operations, used to advertise only capabilities the surface has. */
714
+ ops: OpLike[];
646
715
  /** Operations the server serves (after read-only / include filtering). */
647
716
  toolCount: number;
648
717
  /** Operations present in the Definition but absent from this capped generation. */
@@ -657,6 +726,8 @@ export interface InstructionsInput {
657
726
  authHint?: string | null;
658
727
  /** The tool that returns the caller (the CLI's whoami target), when the API has one. */
659
728
  identityTool?: string | null;
729
+ /** At least one argument accepts an exact human reference as well as an ID. */
730
+ referenceResolution?: boolean;
660
731
  /** Upload operations are exposed (local server): their file arguments take paths. */
661
732
  uploads?: boolean;
662
733
  /** Project-supplied text, appended verbatim. */
@@ -676,6 +747,10 @@ export function serverInstructions(input: InstructionsInput): string {
676
747
  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.");
677
748
  }
678
749
  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).");
750
+ 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.");
751
+ if (input.identityTool && input.ops.some((op) => op.params.some((param) => param.type === "string" && param.resolve !== false && userShapedReference(param.name)))) {
752
+ parts.push("User-shaped reference arguments also accept \"me\", resolved through " + input.identityTool + ".");
753
+ }
679
754
  parts.push("Paginated tools return items, hasMore and nextPage (the exact arguments for the following page). Pass fields (dotted paths) to keep only the result keys you need; oversized results are cut to whole items or keys with a truncated note saying how to ask for less.");
680
755
  parts.push("Errors carry error, code, message, status, the API's body and next_steps.");
681
756
  if (input.authHint) parts.push(input.authHint.trim().replace(/[.]?$/, "."));
@@ -903,6 +978,194 @@ export function prepareCall(
903
978
  return { ok: true, call: { args, fields, maxChars: options.maxChars ?? DEFAULT_MAX_RESULT_CHARS } };
904
979
  }
905
980
 
981
+ /** One caller/session's resolved names. Hosted transports must scope this
982
+ * map by caller credential; generated stdio servers naturally have one map
983
+ * per process. */
984
+ export type ReferenceCache = Map<string, string | number>;
985
+
986
+ export interface ResolveReferencesOptions {
987
+ /** Operations available to act as declared/inferred resolvers. */
988
+ ops: OpLike[];
989
+ /** Operation returning the authenticated caller, for user-shaped "me". */
990
+ identityTool?: string | null;
991
+ cache: ReferenceCache;
992
+ /** Raw operation runner: validates and calls, but does not resolve again. */
993
+ runOperation(op: OpLike, args: Record<string, unknown>): Promise<ToolOutcome>;
994
+ /** Hard bound for list scans. Default 5. */
995
+ maxPages?: number;
996
+ }
997
+
998
+ export type ResolveReferencesResult =
999
+ | { ok: true; args: Record<string, unknown> }
1000
+ | { ok: false; outcome: ToolOutcome };
1001
+
1002
+ function referenceError(
1003
+ code: "REFERENCE_NOT_FOUND" | "REFERENCE_AMBIGUOUS" | "REFERENCE_SCAN_LIMIT" | "NOT_AVAILABLE",
1004
+ message: string,
1005
+ argument: string,
1006
+ nextSteps: string[],
1007
+ candidates?: Record<string, unknown>[],
1008
+ ): ToolOutcome {
1009
+ const structured = {
1010
+ error: code === "REFERENCE_NOT_FOUND" ? "ReferenceNotFound" : code === "REFERENCE_AMBIGUOUS" ? "AmbiguousReference" : code === "REFERENCE_SCAN_LIMIT" ? "ReferenceScanLimit" : "ReferenceResolutionUnavailable",
1011
+ code,
1012
+ message,
1013
+ argument,
1014
+ ...(candidates ? { candidates } : {}),
1015
+ next_steps: nextSteps,
1016
+ };
1017
+ return { text: JSON.stringify(structured), isError: true, structured };
1018
+ }
1019
+
1020
+ function outcomeValue(outcome: ToolOutcome): unknown {
1021
+ if (outcome.structured !== undefined) return outcome.structured;
1022
+ try { return JSON.parse(outcome.text); } catch { return undefined; }
1023
+ }
1024
+
1025
+ function referencePage(value: unknown): { items: Record<string, unknown>[]; nextPage: Record<string, unknown> | null } | null {
1026
+ if (Array.isArray(value)) {
1027
+ return { items: value.filter((item): item is Record<string, unknown> => Boolean(item) && typeof item === "object" && !Array.isArray(item)), nextPage: null };
1028
+ }
1029
+ if (!value || typeof value !== "object") return null;
1030
+ const record = value as Record<string, unknown>;
1031
+ const direct = Array.isArray(record.items) ? record.items : undefined;
1032
+ const arrays = direct ? [direct] : Object.entries(record)
1033
+ .filter(([name, entry]) => name !== "request_id" && name !== "requestId" && Array.isArray(entry))
1034
+ .map(([, entry]) => entry as unknown[]);
1035
+ if (arrays.length !== 1) return null;
1036
+ return {
1037
+ items: arrays[0]!.filter((item): item is Record<string, unknown> => Boolean(item) && typeof item === "object" && !Array.isArray(item)),
1038
+ nextPage: record.nextPage && typeof record.nextPage === "object" && !Array.isArray(record.nextPage)
1039
+ ? record.nextPage as Record<string, unknown>
1040
+ : null,
1041
+ };
1042
+ }
1043
+
1044
+ function userShapedReference(name: string): boolean {
1045
+ const normalized = name.toLowerCase().replace(/[^a-z0-9]/g, "").replace(/id$/, "");
1046
+ return ["user", "assignee", "owner", "member", "actor", "creator", "account", "profile"].includes(normalized);
1047
+ }
1048
+
1049
+ function referenceMatchLabel(fields: string[]): string {
1050
+ return fields.length === 1 ? fields[0]! : fields.slice(0, -1).join(", ") + " or " + fields.at(-1);
1051
+ }
1052
+
1053
+ function looksLikeIdentifier(value: string, resolver: ReferenceResolver): boolean {
1054
+ if (/\s/.test(value)) return false;
1055
+ if (resolver.idPattern !== undefined) {
1056
+ try { return new RegExp(resolver.idPattern).test(value); } catch { return false; }
1057
+ }
1058
+ return /^\d+$/.test(value) ||
1059
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value) ||
1060
+ /^[a-z][a-z0-9]*_[a-z0-9][a-z0-9_-]*$/i.test(value) ||
1061
+ /^[A-Z][A-Z0-9]{1,9}-\d+$/.test(value) ||
1062
+ /^[A-Za-z0-9_-]{20,}$/.test(value);
1063
+ }
1064
+
1065
+ function putReferenceCache(cache: ReferenceCache, key: string, value: string | number): void {
1066
+ cache.set(key, value);
1067
+ while (cache.size > 256) cache.delete(cache.keys().next().value!);
1068
+ }
1069
+
1070
+ function candidateRecord(item: Record<string, unknown>, resolver: ReferenceResolver): Record<string, unknown> {
1071
+ return Object.fromEntries([resolver.id, ...resolver.match]
1072
+ .filter((name, index, all) => all.indexOf(name) === index && item[name] !== undefined)
1073
+ .map((name) => [name, item[name]]));
1074
+ }
1075
+
1076
+ /** Resolve every eligible reference after validation/coercion and before the
1077
+ * requested API call. Matching is exact (case-insensitive for strings),
1078
+ * never fuzzy. Zero matches fail before the requested API call. */
1079
+ export async function resolveReferences(
1080
+ op: OpLike,
1081
+ preparedArgs: Record<string, unknown>,
1082
+ options: ResolveReferencesOptions,
1083
+ ): Promise<ResolveReferencesResult> {
1084
+ const args = { ...preparedArgs };
1085
+ const maxPages = Math.max(1, Math.floor(options.maxPages ?? 5));
1086
+ for (const param of op.params) {
1087
+ const raw = args[param.name];
1088
+ if (typeof raw !== "string" || param.resolve === false) continue;
1089
+ const value = raw.trim();
1090
+
1091
+ if (value.toLowerCase() === "me" && userShapedReference(param.name) && options.identityTool) {
1092
+ const identity = findOperation(options.ops, options.identityTool);
1093
+ if (identity) {
1094
+ const cacheKey = "me:" + identity.tool;
1095
+ const cached = options.cache.get(cacheKey);
1096
+ if (cached !== undefined) {
1097
+ args[param.name] = cached;
1098
+ continue;
1099
+ }
1100
+ const outcome = await options.runOperation(identity, {});
1101
+ if (outcome.isError) return { ok: false, outcome };
1102
+ const body = outcomeValue(outcome);
1103
+ const id = body && typeof body === "object" && !Array.isArray(body) ? (body as Record<string, unknown>).id : undefined;
1104
+ if (typeof id !== "string" && typeof id !== "number") {
1105
+ 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."]) };
1106
+ }
1107
+ putReferenceCache(options.cache, cacheKey, id);
1108
+ args[param.name] = id;
1109
+ continue;
1110
+ }
1111
+ }
1112
+
1113
+ const resolver = param.resolve && typeof param.resolve === "object" ? param.resolve : undefined;
1114
+ if (!resolver || looksLikeIdentifier(value, resolver)) continue;
1115
+ const source = findOperation(options.ops, resolver.via);
1116
+ if (!source) continue;
1117
+ const cacheKey = source.tool + ":" + resolver.id + ":" + resolver.match.join(",") + ":" + value.toLowerCase();
1118
+ const cached = options.cache.get(cacheKey);
1119
+ if (cached !== undefined) {
1120
+ args[param.name] = cached;
1121
+ continue;
1122
+ }
1123
+
1124
+ const matches = new Map<string, { id: string | number; item: Record<string, unknown> }>();
1125
+ let pageArgs: Record<string, unknown> = {};
1126
+ for (const sourceParam of source.params) {
1127
+ if (args[sourceParam.name] !== undefined && sourceParam.name !== param.name) pageArgs[sourceParam.name] = args[sourceParam.name];
1128
+ }
1129
+ if (resolver.filterParam) pageArgs[resolver.filterParam] = value;
1130
+ if (!hasOwnFieldsParam(source)) pageArgs.fields = [resolver.id, ...resolver.match];
1131
+
1132
+ let exhausted = false;
1133
+ for (let pageNumber = 1; pageNumber <= maxPages; pageNumber++) {
1134
+ const outcome = await options.runOperation(source, pageArgs);
1135
+ if (outcome.isError) return { ok: false, outcome };
1136
+ const page = referencePage(outcomeValue(outcome));
1137
+ if (!page) {
1138
+ return { ok: false, outcome: referenceError("NOT_AVAILABLE", `${source.tool} did not return one recognizable item array, so ${param.name} cannot be resolved by name.`, param.name, ["Pass the exact ID instead.", `Check the resolver hint for ${source.tool}.`]) };
1139
+ }
1140
+ for (const item of page.items) {
1141
+ const id = item[resolver.id];
1142
+ if (typeof id !== "string" && typeof id !== "number") continue;
1143
+ const hit = resolver.match.some((field) => typeof item[field] === "string" && (item[field] as string).toLowerCase() === value.toLowerCase());
1144
+ if (hit) matches.set(typeof id + ":" + String(id), { id, item });
1145
+ }
1146
+ if (matches.size > 1) {
1147
+ const candidates = [...matches.values()].map(({ item }) => candidateRecord(item, resolver));
1148
+ return { ok: false, outcome: referenceError("REFERENCE_AMBIGUOUS", `${JSON.stringify(raw)} matches multiple candidates for ${param.name}; nothing was sent to ${op.tool}.`, param.name, ["Choose one candidate ID and call again."], candidates) };
1149
+ }
1150
+ if (!page.nextPage) {
1151
+ exhausted = true;
1152
+ break;
1153
+ }
1154
+ pageArgs = { ...pageArgs, ...page.nextPage };
1155
+ }
1156
+ if (!exhausted) {
1157
+ return { ok: false, outcome: referenceError("REFERENCE_SCAN_LIMIT", `${source.tool} still had more results after ${maxPages} pages, so ${JSON.stringify(raw)} could not be resolved unambiguously.`, param.name, ["Pass the exact ID instead.", `Narrow ${source.tool} with its filters, or declare a more selective resolver.`]) };
1158
+ }
1159
+ const match = [...matches.values()][0];
1160
+ if (!match) {
1161
+ return { ok: false, outcome: referenceError("REFERENCE_NOT_FOUND", `${JSON.stringify(raw)} did not exactly match any ${referenceMatchLabel(resolver.match)} from ${source.tool}; nothing was sent to ${op.tool}.`, param.name, [`Call ${source.tool} to choose an exact ${referenceMatchLabel(resolver.match)} or ID, then call again.`]) };
1162
+ }
1163
+ putReferenceCache(options.cache, cacheKey, match.id);
1164
+ args[param.name] = match.id;
1165
+ }
1166
+ return { ok: true, args };
1167
+ }
1168
+
906
1169
  /** The isError result for bad arguments: one stable code, one issue per
907
1170
  * argument, and the way out. */
908
1171
  export function argumentsError(op: { tool: string }, issues: ArgumentIssue[]): ToolOutcome {
@@ -1162,7 +1425,9 @@ export async function binaryOutcome(blob: Blob, options: BinaryOptions = {}): Pr
1162
1425
  * generated CLI's error envelope). Additive only. */
1163
1426
  export type ErrorCode =
1164
1427
  | "NO_AUTH" | "AUTH_INVALID" | "PLAN_LIMIT" | "NOT_FOUND" | "INVALID_REQUEST" | "RATE_LIMITED"
1165
- | "SPEC_INVALID" | "SERVER_ERROR" | "NETWORK_ERROR" | "VALIDATION_FAILED" | "INVALID_ARGUMENTS" | "CONFIRMATION_REQUIRED" | "NOT_AVAILABLE" | "CALL_FAILED";
1428
+ | "SPEC_INVALID" | "SERVER_ERROR" | "NETWORK_ERROR" | "VALIDATION_FAILED" | "INVALID_ARGUMENTS" | "CONFIRMATION_REQUIRED"
1429
+ | "REFERENCE_NOT_FOUND" | "REFERENCE_AMBIGUOUS" | "REFERENCE_SCAN_LIMIT" | "NOT_AVAILABLE" | "CALL_FAILED"
1430
+ | "ACCOUNT_LINK_REQUIRED" | "ACCOUNT_LINK_CANCELLED";
1166
1431
 
1167
1432
  export interface ErrorContext {
1168
1433
  /** One sentence on how to supply a credential on this transport. */
@@ -1368,8 +1633,6 @@ export function searchScore(op: OpLike, query: string): number {
1368
1633
  }
1369
1634
 
1370
1635
  export async function docsSearch(source: DocsSource, query: string, page = 1): Promise<ToolOutcome> {
1371
- const term = query.trim().toLowerCase();
1372
- const terms = searchTerms(query);
1373
1636
  const sections: string[] = [];
1374
1637
  const ranked = source.ops
1375
1638
  .map((op) => ({ op, score: searchScore(op, query) }))
@@ -1391,33 +1654,20 @@ export async function docsSearch(source: DocsSource, query: string, page = 1): P
1391
1654
  } else if (ranked.length > 0) {
1392
1655
  sections.push("No reference matches on page " + (pageIndex + 1) + "; there are " + Math.ceil(ranked.length / SEARCH_PAGE_SIZE) + " pages.");
1393
1656
  }
1394
- const prose = await fetchDocs(source, "llms-full.txt");
1395
- let proseMatchCount = 0;
1396
- if (prose !== null) {
1397
- let heading = "";
1398
- const proseMatches: { heading: string; excerpt: string; score: number }[] = [];
1399
- for (const line of prose.split("\n")) {
1400
- if (/^#{1,3} /.test(line)) heading = line.replace(/^#+ /, "").trim();
1401
- else {
1402
- const lowerHeading = heading.toLowerCase();
1403
- const lowerLine = line.toLowerCase();
1404
- const matched = terms.filter((word) => lowerHeading.includes(word) || lowerLine.includes(word));
1405
- if (matched.length > 0) {
1406
- const allTerms = matched.length === terms.length;
1407
- proseMatches.push({
1408
- heading,
1409
- excerpt: line.trim().slice(0, 160),
1410
- score: matched.length * 10 + (allTerms ? 50 : 0) + (lowerHeading.includes(term) || lowerLine.includes(term) ? 25 : 0),
1411
- });
1412
- }
1413
- }
1414
- }
1415
- proseMatches.sort((a, b) => b.score - a.score || a.heading.localeCompare(b.heading) || a.excerpt.localeCompare(b.excerpt));
1416
- proseMatchCount = proseMatches.length;
1417
- if (proseMatches.length > 0) {
1418
- sections.push("Guide matches (best first):\n" + proseMatches.slice(0, 15).map((match) => "- [" + match.heading + "] " + match.excerpt).join("\n"));
1419
- }
1657
+ const { guides, status } = await searchConnectedGuides(source.docsUrl(), source.docsIndexUrl?.() ?? null, (path) => fetchDocs(source, path), query);
1658
+ const guideSlice = guides.slice(pageIndex * SEARCH_PAGE_SIZE, (pageIndex + 1) * SEARCH_PAGE_SIZE);
1659
+ const proseMatchCount = guides.length;
1660
+ if (guideSlice.length > 0) {
1661
+ sections.push("Guide matches (best first, " + guides.length + " pages):\n" + guideSlice.map((match) =>
1662
+ "- [" + match.title + (match.section ? " / " + match.section : "") + "](" + match.url + "): " + match.excerpt + "\n read_docs " + JSON.stringify({ page: match.url })).join("\n"));
1420
1663
  }
1664
+ if (status === "unavailable") sections.push("The docs site is unavailable; the API reference was still searched.");
1665
+ const structured = {
1666
+ schema_version: "1", query, page: pageIndex + 1,
1667
+ 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 } } })),
1668
+ guides: guideSlice.map((match) => ({ ...match, read_tool: { name: "read_docs", arguments: { page: match.url } } })),
1669
+ totals: { reference: ranked.length, guides: guides.length }, guides_status: status,
1670
+ };
1421
1671
  if (omittedRanked.length > 0 && ranked.length === 0 && proseMatchCount === 0) {
1422
1672
  return omittedPlanLimit(omittedRanked.map((result) => result.op));
1423
1673
  }
@@ -1429,9 +1679,10 @@ export async function docsSearch(source: DocsSource, query: string, page = 1): P
1429
1679
  return {
1430
1680
  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)" : ""),
1431
1681
  isError: false,
1682
+ structured,
1432
1683
  };
1433
1684
  }
1434
- return { text: [...(coverage ? [coverage] : []), ...sections].join("\n\n"), isError: false };
1685
+ return { text: [...(coverage ? [coverage] : []), ...sections].join("\n\n"), isError: false, structured };
1435
1686
  }
1436
1687
 
1437
1688
  export async function docsRead(source: DocsSource, page: string): Promise<ToolOutcome> {
@@ -1443,9 +1694,7 @@ export async function docsRead(source: DocsSource, page: string): Promise<ToolOu
1443
1694
  let target = page;
1444
1695
  if (!/^https?:\/\//.test(target)) {
1445
1696
  const index = await fetchDocs(source, "llms.txt");
1446
- const linked = index?.match(/\((https?:[^)]+)\)/g)?.map((m) => m.slice(1, -1)) ?? [];
1447
- const hit = linked.find((u) => u.toLowerCase().includes(target.toLowerCase()));
1448
- if (hit !== undefined) target = hit;
1697
+ target = docsReadTarget(index, source.docsUrl(), source.docsIndexUrl?.() ?? null, target);
1449
1698
  }
1450
1699
  const text = await fetchDocs(source, target);
1451
1700
  if (text === null) {