@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
@@ -15,7 +15,7 @@
15
15
  * Spec: https://modelcontextprotocol.io/specification/2026-07-28
16
16
  */
17
17
  import { dateKindOf, relativeDate } from "./dates.js";
18
- import { resolveDocsContentUrl } from "./docs.js";
18
+ import { docsReadTarget, resolveDocsContentUrl, searchConnectedGuides } from "./docs.js";
19
19
  export const MCP_PROTOCOL_VERSION = "2026-07-28";
20
20
  /** Revisions served. Legacy (initialize-handshake) revisions are not; an
21
21
  * initialize request gets an error naming this list, as the spec asks of
@@ -29,6 +29,30 @@ export const META_SERVER_INFO = "io.modelcontextprotocol/serverInfo";
29
29
  * ask for less. Roughly 16k tokens: under every major client's own cap, so
30
30
  * the agent sees this explanation instead of a mid-JSON chop. */
31
31
  export const DEFAULT_MAX_RESULT_CHARS = 64_000;
32
+ /** Throw only from an application-owned credential resolver, before calling
33
+ * the API. The URL must show a sign-in/linking page, never a pre-authenticated
34
+ * resource, token or personal information. The page must verify the same user
35
+ * before linking. The runtime rechecks credentials on every subsequent call. */
36
+ export class McpAccountLinkRequired extends Error {
37
+ url;
38
+ constructor(url) {
39
+ super("Connect your API account in the browser to continue.");
40
+ this.name = "McpAccountLinkRequired";
41
+ try {
42
+ const parsed = new URL(url);
43
+ if (url.length > 2048 || url !== url.trim() || /[\u0000-\u0020\u007F"\\]/.test(url) || parsed.username || parsed.password || parsed.hash ||
44
+ !(parsed.protocol === "https:" || parsed.protocol === "http:" && ["127.0.0.1", "[::1]", "localhost"].includes(parsed.hostname)) ||
45
+ [...parsed.searchParams.keys()].some(key => /^(access_token|refresh_token|client_secret|api_key|password|authorization)$/i.test(key)))
46
+ throw new Error();
47
+ }
48
+ catch {
49
+ throw new Error("Provide a public HTTPS account-linking page without credentials or a fragment (HTTP loopback is allowed for development).");
50
+ }
51
+ this.url = url;
52
+ Object.freeze(this);
53
+ }
54
+ }
55
+ const API_LINK_INPUT = "typeship_api_account";
32
56
  /** Does the operation take a file (multipart form or raw binary body)? */
33
57
  export function isUploadOp(op) {
34
58
  return op.bodyKind === "multipart" || op.bodyKind === "binary";
@@ -180,11 +204,43 @@ export async function handleRpc(server, incoming) {
180
204
  if (args === null || typeof args !== "object" || Array.isArray(args)) {
181
205
  return rpcError(id, -32602, "tools/call arguments must be an object", 400);
182
206
  }
207
+ const responses = request.params?.inputResponses;
208
+ if (responses !== undefined && (responses === null || typeof responses !== "object" || Array.isArray(responses)))
209
+ return rpcError(id, -32602, "inputResponses must be an object", 400);
210
+ const response = responses && Object.hasOwn(responses, API_LINK_INPUT) ? responses[API_LINK_INPUT] : undefined;
211
+ if (response !== undefined) {
212
+ if (!response || typeof response !== "object" || Array.isArray(response) || !["accept", "decline", "cancel"].includes(response.action))
213
+ return rpcError(id, -32602, "Invalid API account-link response", 400);
214
+ if (response.action !== "accept") {
215
+ const cancelled = textError("API account linking was cancelled. No API request was made.", "ACCOUNT_LINK_CANCELLED");
216
+ return complete({ content: [{ type: "text", text: cancelled.text }], isError: true });
217
+ }
218
+ // A client acknowledgment is not authorization. Only a fresh lookup
219
+ // of the server's linked credentials can let the operation proceed.
220
+ }
183
221
  const denied = server.beforeToolCall ? await server.beforeToolCall(name) : null;
184
222
  if (denied)
185
223
  return denied;
186
224
  const started = Date.now();
187
- const outcome = await server.callTool(name, args);
225
+ let outcome;
226
+ try {
227
+ outcome = await server.callTool(name, args);
228
+ }
229
+ catch (error) {
230
+ if (!(error instanceof McpAccountLinkRequired))
231
+ throw error;
232
+ const caps = (request.params?._meta)[META_CLIENT_CAPS];
233
+ const urlMode = caps.elicitation?.url;
234
+ if (urlMode === null || typeof urlMode !== "object" || Array.isArray(urlMode)) {
235
+ 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");
236
+ return complete({ content: [{ type: "text", text: unsupported.text }], isError: true });
237
+ }
238
+ return { status: 200, message: { jsonrpc: "2.0", id, result: {
239
+ resultType: "input_required",
240
+ inputRequests: { [API_LINK_INPUT]: { method: "elicitation/create", params: { mode: "url", url: error.url, message: "Connect your API account in the browser to continue." } } },
241
+ _meta: { [META_SERVER_INFO]: server.serverInfo },
242
+ } } };
243
+ }
188
244
  if (outcome === undefined)
189
245
  return rpcError(id, -32602, server.unknownToolMessage?.(name) ?? "Unknown tool: " + name, 400);
190
246
  if (server.afterToolCall)
@@ -373,7 +429,7 @@ function exampleMatchingPattern(pattern, minLength, maxLength) {
373
429
  value = value.slice(0, maxLength);
374
430
  return value;
375
431
  };
376
- const candidates = [prefix + "123", prefix + "example", prefix, "example", "value"];
432
+ const candidates = [prefix + "123", prefix + "example", prefix, "resource.method", "example.value", "example_123", "example", "value"];
377
433
  for (const candidate of candidates) {
378
434
  const value = fit(candidate);
379
435
  regex.lastIndex = 0;
@@ -517,6 +573,11 @@ export function serverInstructions(input) {
517
573
  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.");
518
574
  }
519
575
  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).");
576
+ if (input.referenceResolution)
577
+ 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.");
578
+ if (input.identityTool && input.ops.some((op) => op.params.some((param) => param.type === "string" && param.resolve !== false && userShapedReference(param.name)))) {
579
+ parts.push("User-shaped reference arguments also accept \"me\", resolved through " + input.identityTool + ".");
580
+ }
520
581
  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.");
521
582
  parts.push("Errors carry error, code, message, status, the API's body and next_steps.");
522
583
  if (input.authHint)
@@ -750,6 +811,175 @@ export function prepareCall(op, rawArgs, options = {}) {
750
811
  return { ok: false, outcome: argumentsError(op, issues) };
751
812
  return { ok: true, call: { args, fields, maxChars: options.maxChars ?? DEFAULT_MAX_RESULT_CHARS } };
752
813
  }
814
+ function referenceError(code, message, argument, nextSteps, candidates) {
815
+ const structured = {
816
+ error: code === "REFERENCE_NOT_FOUND" ? "ReferenceNotFound" : code === "REFERENCE_AMBIGUOUS" ? "AmbiguousReference" : code === "REFERENCE_SCAN_LIMIT" ? "ReferenceScanLimit" : "ReferenceResolutionUnavailable",
817
+ code,
818
+ message,
819
+ argument,
820
+ ...(candidates ? { candidates } : {}),
821
+ next_steps: nextSteps,
822
+ };
823
+ return { text: JSON.stringify(structured), isError: true, structured };
824
+ }
825
+ function outcomeValue(outcome) {
826
+ if (outcome.structured !== undefined)
827
+ return outcome.structured;
828
+ try {
829
+ return JSON.parse(outcome.text);
830
+ }
831
+ catch {
832
+ return undefined;
833
+ }
834
+ }
835
+ function referencePage(value) {
836
+ if (Array.isArray(value)) {
837
+ return { items: value.filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item)), nextPage: null };
838
+ }
839
+ if (!value || typeof value !== "object")
840
+ return null;
841
+ const record = value;
842
+ const direct = Array.isArray(record.items) ? record.items : undefined;
843
+ const arrays = direct ? [direct] : Object.entries(record)
844
+ .filter(([name, entry]) => name !== "request_id" && name !== "requestId" && Array.isArray(entry))
845
+ .map(([, entry]) => entry);
846
+ if (arrays.length !== 1)
847
+ return null;
848
+ return {
849
+ items: arrays[0].filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item)),
850
+ nextPage: record.nextPage && typeof record.nextPage === "object" && !Array.isArray(record.nextPage)
851
+ ? record.nextPage
852
+ : null,
853
+ };
854
+ }
855
+ function userShapedReference(name) {
856
+ const normalized = name.toLowerCase().replace(/[^a-z0-9]/g, "").replace(/id$/, "");
857
+ return ["user", "assignee", "owner", "member", "actor", "creator", "account", "profile"].includes(normalized);
858
+ }
859
+ function referenceMatchLabel(fields) {
860
+ return fields.length === 1 ? fields[0] : fields.slice(0, -1).join(", ") + " or " + fields.at(-1);
861
+ }
862
+ function looksLikeIdentifier(value, resolver) {
863
+ if (/\s/.test(value))
864
+ return false;
865
+ if (resolver.idPattern !== undefined) {
866
+ try {
867
+ return new RegExp(resolver.idPattern).test(value);
868
+ }
869
+ catch {
870
+ return false;
871
+ }
872
+ }
873
+ return /^\d+$/.test(value) ||
874
+ /^[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) ||
875
+ /^[a-z][a-z0-9]*_[a-z0-9][a-z0-9_-]*$/i.test(value) ||
876
+ /^[A-Z][A-Z0-9]{1,9}-\d+$/.test(value) ||
877
+ /^[A-Za-z0-9_-]{20,}$/.test(value);
878
+ }
879
+ function putReferenceCache(cache, key, value) {
880
+ cache.set(key, value);
881
+ while (cache.size > 256)
882
+ cache.delete(cache.keys().next().value);
883
+ }
884
+ function candidateRecord(item, resolver) {
885
+ return Object.fromEntries([resolver.id, ...resolver.match]
886
+ .filter((name, index, all) => all.indexOf(name) === index && item[name] !== undefined)
887
+ .map((name) => [name, item[name]]));
888
+ }
889
+ /** Resolve every eligible reference after validation/coercion and before the
890
+ * requested API call. Matching is exact (case-insensitive for strings),
891
+ * never fuzzy. Zero matches fail before the requested API call. */
892
+ export async function resolveReferences(op, preparedArgs, options) {
893
+ const args = { ...preparedArgs };
894
+ const maxPages = Math.max(1, Math.floor(options.maxPages ?? 5));
895
+ for (const param of op.params) {
896
+ const raw = args[param.name];
897
+ if (typeof raw !== "string" || param.resolve === false)
898
+ continue;
899
+ const value = raw.trim();
900
+ if (value.toLowerCase() === "me" && userShapedReference(param.name) && options.identityTool) {
901
+ const identity = findOperation(options.ops, options.identityTool);
902
+ if (identity) {
903
+ const cacheKey = "me:" + identity.tool;
904
+ const cached = options.cache.get(cacheKey);
905
+ if (cached !== undefined) {
906
+ args[param.name] = cached;
907
+ continue;
908
+ }
909
+ const outcome = await options.runOperation(identity, {});
910
+ if (outcome.isError)
911
+ return { ok: false, outcome };
912
+ const body = outcomeValue(outcome);
913
+ const id = body && typeof body === "object" && !Array.isArray(body) ? body.id : undefined;
914
+ if (typeof id !== "string" && typeof id !== "number") {
915
+ 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."]) };
916
+ }
917
+ putReferenceCache(options.cache, cacheKey, id);
918
+ args[param.name] = id;
919
+ continue;
920
+ }
921
+ }
922
+ const resolver = param.resolve && typeof param.resolve === "object" ? param.resolve : undefined;
923
+ if (!resolver || looksLikeIdentifier(value, resolver))
924
+ continue;
925
+ const source = findOperation(options.ops, resolver.via);
926
+ if (!source)
927
+ continue;
928
+ const cacheKey = source.tool + ":" + resolver.id + ":" + resolver.match.join(",") + ":" + value.toLowerCase();
929
+ const cached = options.cache.get(cacheKey);
930
+ if (cached !== undefined) {
931
+ args[param.name] = cached;
932
+ continue;
933
+ }
934
+ const matches = new Map();
935
+ let pageArgs = {};
936
+ for (const sourceParam of source.params) {
937
+ if (args[sourceParam.name] !== undefined && sourceParam.name !== param.name)
938
+ pageArgs[sourceParam.name] = args[sourceParam.name];
939
+ }
940
+ if (resolver.filterParam)
941
+ pageArgs[resolver.filterParam] = value;
942
+ if (!hasOwnFieldsParam(source))
943
+ pageArgs.fields = [resolver.id, ...resolver.match];
944
+ let exhausted = false;
945
+ for (let pageNumber = 1; pageNumber <= maxPages; pageNumber++) {
946
+ const outcome = await options.runOperation(source, pageArgs);
947
+ if (outcome.isError)
948
+ return { ok: false, outcome };
949
+ const page = referencePage(outcomeValue(outcome));
950
+ if (!page) {
951
+ 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}.`]) };
952
+ }
953
+ for (const item of page.items) {
954
+ const id = item[resolver.id];
955
+ if (typeof id !== "string" && typeof id !== "number")
956
+ continue;
957
+ const hit = resolver.match.some((field) => typeof item[field] === "string" && item[field].toLowerCase() === value.toLowerCase());
958
+ if (hit)
959
+ matches.set(typeof id + ":" + String(id), { id, item });
960
+ }
961
+ if (matches.size > 1) {
962
+ const candidates = [...matches.values()].map(({ item }) => candidateRecord(item, resolver));
963
+ 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) };
964
+ }
965
+ if (!page.nextPage) {
966
+ exhausted = true;
967
+ break;
968
+ }
969
+ pageArgs = { ...pageArgs, ...page.nextPage };
970
+ }
971
+ if (!exhausted) {
972
+ 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.`]) };
973
+ }
974
+ const match = [...matches.values()][0];
975
+ if (!match) {
976
+ 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.`]) };
977
+ }
978
+ putReferenceCache(options.cache, cacheKey, match.id);
979
+ args[param.name] = match.id;
980
+ }
981
+ return { ok: true, args };
982
+ }
753
983
  /** The isError result for bad arguments: one stable code, one issue per
754
984
  * argument, and the way out. */
755
985
  export function argumentsError(op, issues) {
@@ -1168,8 +1398,6 @@ export function searchScore(op, query) {
1168
1398
  return score;
1169
1399
  }
1170
1400
  export async function docsSearch(source, query, page = 1) {
1171
- const term = query.trim().toLowerCase();
1172
- const terms = searchTerms(query);
1173
1401
  const sections = [];
1174
1402
  const ranked = source.ops
1175
1403
  .map((op) => ({ op, score: searchScore(op, query) }))
@@ -1193,34 +1421,20 @@ export async function docsSearch(source, query, page = 1) {
1193
1421
  else if (ranked.length > 0) {
1194
1422
  sections.push("No reference matches on page " + (pageIndex + 1) + "; there are " + Math.ceil(ranked.length / SEARCH_PAGE_SIZE) + " pages.");
1195
1423
  }
1196
- const prose = await fetchDocs(source, "llms-full.txt");
1197
- let proseMatchCount = 0;
1198
- if (prose !== null) {
1199
- let heading = "";
1200
- const proseMatches = [];
1201
- for (const line of prose.split("\n")) {
1202
- if (/^#{1,3} /.test(line))
1203
- heading = line.replace(/^#+ /, "").trim();
1204
- else {
1205
- const lowerHeading = heading.toLowerCase();
1206
- const lowerLine = line.toLowerCase();
1207
- const matched = terms.filter((word) => lowerHeading.includes(word) || lowerLine.includes(word));
1208
- if (matched.length > 0) {
1209
- const allTerms = matched.length === terms.length;
1210
- proseMatches.push({
1211
- heading,
1212
- excerpt: line.trim().slice(0, 160),
1213
- score: matched.length * 10 + (allTerms ? 50 : 0) + (lowerHeading.includes(term) || lowerLine.includes(term) ? 25 : 0),
1214
- });
1215
- }
1216
- }
1217
- }
1218
- proseMatches.sort((a, b) => b.score - a.score || a.heading.localeCompare(b.heading) || a.excerpt.localeCompare(b.excerpt));
1219
- proseMatchCount = proseMatches.length;
1220
- if (proseMatches.length > 0) {
1221
- sections.push("Guide matches (best first):\n" + proseMatches.slice(0, 15).map((match) => "- [" + match.heading + "] " + match.excerpt).join("\n"));
1222
- }
1424
+ const { guides, status } = await searchConnectedGuides(source.docsUrl(), source.docsIndexUrl?.() ?? null, (path) => fetchDocs(source, path), query);
1425
+ const guideSlice = guides.slice(pageIndex * SEARCH_PAGE_SIZE, (pageIndex + 1) * SEARCH_PAGE_SIZE);
1426
+ const proseMatchCount = guides.length;
1427
+ if (guideSlice.length > 0) {
1428
+ sections.push("Guide matches (best first, " + guides.length + " pages):\n" + guideSlice.map((match) => "- [" + match.title + (match.section ? " / " + match.section : "") + "](" + match.url + "): " + match.excerpt + "\n read_docs " + JSON.stringify({ page: match.url })).join("\n"));
1223
1429
  }
1430
+ if (status === "unavailable")
1431
+ sections.push("The docs site is unavailable; the API reference was still searched.");
1432
+ const structured = {
1433
+ schema_version: "1", query, page: pageIndex + 1,
1434
+ 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 } } })),
1435
+ guides: guideSlice.map((match) => ({ ...match, read_tool: { name: "read_docs", arguments: { page: match.url } } })),
1436
+ totals: { reference: ranked.length, guides: guides.length }, guides_status: status,
1437
+ };
1224
1438
  if (omittedRanked.length > 0 && ranked.length === 0 && proseMatchCount === 0) {
1225
1439
  return omittedPlanLimit(omittedRanked.map((result) => result.op));
1226
1440
  }
@@ -1232,9 +1446,10 @@ export async function docsSearch(source, query, page = 1) {
1232
1446
  return {
1233
1447
  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)" : ""),
1234
1448
  isError: false,
1449
+ structured,
1235
1450
  };
1236
1451
  }
1237
- return { text: [...(coverage ? [coverage] : []), ...sections].join("\n\n"), isError: false };
1452
+ return { text: [...(coverage ? [coverage] : []), ...sections].join("\n\n"), isError: false, structured };
1238
1453
  }
1239
1454
  export async function docsRead(source, page) {
1240
1455
  const opMatch = findOperation(source.ops, page);
@@ -1247,10 +1462,7 @@ export async function docsRead(source, page) {
1247
1462
  let target = page;
1248
1463
  if (!/^https?:\/\//.test(target)) {
1249
1464
  const index = await fetchDocs(source, "llms.txt");
1250
- const linked = index?.match(/\((https?:[^)]+)\)/g)?.map((m) => m.slice(1, -1)) ?? [];
1251
- const hit = linked.find((u) => u.toLowerCase().includes(target.toLowerCase()));
1252
- if (hit !== undefined)
1253
- target = hit;
1465
+ target = docsReadTarget(index, source.docsUrl(), source.docsIndexUrl?.() ?? null, target);
1254
1466
  }
1255
1467
  const text = await fetchDocs(source, target);
1256
1468
  if (text === null) {
package/dist/mcp.d.ts CHANGED
@@ -1,5 +1,23 @@
1
1
  #!/usr/bin/env node
2
- /** Fetch-style handler: mount in any runtime (workers, serverless, node).
3
- * Never rejects: every failure is an HTTP or JSON-RPC error response. */
4
- export declare function handleHttp(request: Request): Promise<Response>;
2
+ import { type ClientOptions } from "./index.js";
3
+ import { type McpAuthorizationConfiguration, type McpTokenIntrospectionConfiguration, type McpPrincipal } from "./mcp-authorization.js";
4
+ export type { McpAuthorizationConfiguration, McpTokenIntrospectionConfiguration, McpPrincipal } from "./mcp-authorization.js";
5
+ export { McpAccountLinkRequired } from "./mcp-protocol.js";
6
+ export interface McpHttpOptions {
7
+ authorization?: McpAuthorizationConfiguration;
8
+ /** Optional server-only introspection. When set, checks every token with the
9
+ * provider instead of local JWT verification. Never put secrets in config. */
10
+ introspection?: McpTokenIntrospectionConfiguration;
11
+ /** Application-owned lookup or token exchange for this verified identity.
12
+ * Keep it outside the generated directory so regeneration preserves it.
13
+ * Throw McpAccountLinkRequired with a public linking page when the user
14
+ * needs to connect an account. Always verify the link server-side on retry.
15
+ * The MCP access token and request headers are deliberately not provided. */
16
+ credentialsFor(principal: McpPrincipal): ClientOptions | Promise<ClientOptions>;
17
+ }
18
+ /** Create once at startup; metadata/key caching is isolated to this server. */
19
+ export declare function createMcpHandler(options: McpHttpOptions): (request: Request) => Promise<Response>;
20
+ /** A generated entry point cannot infer the host application's user-to-API
21
+ * credential mapping. It remains closed until the owner installs a handler. */
22
+ export declare function handleHttp(_request: Request): Promise<Response>;
5
23
  //# sourceMappingURL=mcp.d.ts.map
package/dist/mcp.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":";AA2SA;yEACyE;AACzE,wBAAsB,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAsDpE"}
1
+ {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":";AAuBA,OAAO,EAAoC,KAAK,aAAa,EAAmB,MAAM,YAAY,CAAC;AAcnG,OAAO,EAA8C,KAAK,6BAA6B,EAAE,KAAK,kCAAkC,EAAE,KAAK,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACpL,YAAY,EAAE,6BAA6B,EAAE,kCAAkC,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC9H,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AA4U3D,MAAM,WAAW,cAAc;IAC7B,aAAa,CAAC,EAAE,6BAA6B,CAAC;IAC9C;kFAC8E;IAC9E,aAAa,CAAC,EAAE,kCAAkC,CAAC;IACnD;;;;iFAI6E;IAC7E,cAAc,CAAC,SAAS,EAAE,YAAY,GAAG,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;CACjF;AAED,+EAA+E;AAC/E,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,cAAc,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAMjG;AAED;+EAC+E;AAC/E,wBAAsB,UAAU,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAErE"}