@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.
- package/AGENTS.md +31 -0
- package/README.md +66 -9
- package/api.json +3153 -761
- package/api.md +9796 -382
- package/dist/api-identity.d.ts +40 -0
- package/dist/api-identity.d.ts.map +1 -0
- package/dist/api-identity.js +128 -0
- package/dist/auth-profiles.d.ts +30 -0
- package/dist/auth-profiles.d.ts.map +1 -0
- package/dist/auth-profiles.js +138 -0
- package/dist/core/http.d.ts +17 -2
- package/dist/core/http.d.ts.map +1 -1
- package/dist/core/http.js +78 -17
- package/dist/credential-storage.d.ts +24 -0
- package/dist/credential-storage.d.ts.map +1 -0
- package/dist/credential-storage.js +207 -0
- package/dist/docs.d.ts +25 -0
- package/dist/docs.d.ts.map +1 -1
- package/dist/docs.js +144 -0
- package/dist/errors.d.ts +18 -10
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +24 -14
- package/dist/index.d.ts +10 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -4
- package/dist/mcp-authorization.d.ts +52 -0
- package/dist/mcp-authorization.d.ts.map +1 -0
- package/dist/mcp-authorization.js +232 -0
- package/dist/mcp-protocol.d.ts +51 -2
- package/dist/mcp-protocol.d.ts.map +1 -1
- package/dist/mcp-protocol.js +249 -37
- package/dist/mcp.d.ts +21 -3
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +185 -68
- package/dist/named-credentials.d.ts +21 -0
- package/dist/named-credentials.d.ts.map +1 -0
- package/dist/named-credentials.js +86 -0
- package/dist/oauth-request.d.ts +21 -0
- package/dist/oauth-request.d.ts.map +1 -0
- package/dist/oauth-request.js +119 -0
- package/dist/oauth-session.d.ts +106 -0
- package/dist/oauth-session.d.ts.map +1 -0
- package/dist/oauth-session.js +244 -0
- package/dist/ops.d.ts +14 -1
- package/dist/ops.d.ts.map +1 -1
- package/dist/ops.js +34 -30
- package/dist/resources/account.d.ts +2 -2
- package/dist/resources/account.d.ts.map +1 -1
- package/dist/resources/account.js +1 -0
- package/dist/resources/api-keys.d.ts +3 -3
- package/dist/resources/api-keys.d.ts.map +1 -1
- package/dist/resources/api-keys.js +2 -0
- package/dist/resources/definition-revisions.d.ts +5 -5
- package/dist/resources/definition-revisions.d.ts.map +1 -1
- package/dist/resources/definition-revisions.js +4 -0
- package/dist/resources/definitions.d.ts +15 -4
- package/dist/resources/definitions.d.ts.map +1 -1
- package/dist/resources/definitions.js +11 -2
- package/dist/resources/generate.d.ts +14 -3
- package/dist/resources/generate.d.ts.map +1 -1
- package/dist/resources/generate.js +10 -2
- package/dist/resources/generations.d.ts +3 -3
- package/dist/resources/generations.d.ts.map +1 -1
- package/dist/resources/generations.js +2 -0
- package/dist/resources/projects.d.ts +56 -20
- package/dist/resources/projects.d.ts.map +1 -1
- package/dist/resources/projects.js +40 -4
- package/dist/resources/targets.d.ts +84 -10
- package/dist/resources/targets.d.ts.map +1 -1
- package/dist/resources/targets.js +127 -2
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +55 -26
- package/dist/types.d.ts +602 -123
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +24 -0
- package/dist/worker.js +4 -4
- package/package.json +11 -1
- package/server.json +42 -0
- package/src/api-identity.ts +98 -0
- package/src/auth-profiles.ts +114 -0
- package/src/core/http.ts +88 -19
- package/src/credential-storage.ts +183 -0
- package/src/docs.ts +138 -0
- package/src/errors.ts +26 -15
- package/src/index.ts +29 -4
- package/src/mcp-authorization.ts +211 -0
- package/src/mcp-protocol.ts +287 -38
- package/src/mcp.ts +186 -72
- package/src/named-credentials.ts +74 -0
- package/src/oauth-request.ts +90 -0
- package/src/oauth-session.ts +258 -0
- package/src/ops.ts +48 -31
- package/src/resources/account.ts +3 -0
- package/src/resources/api-keys.ts +5 -0
- package/src/resources/definition-revisions.ts +9 -0
- package/src/resources/definitions.ts +25 -0
- package/src/resources/generate.ts +23 -0
- package/src/resources/generations.ts +5 -0
- package/src/resources/projects.ts +95 -7
- package/src/resources/targets.ts +241 -0
- package/src/schemas.ts +55 -26
- package/src/types.ts +640 -123
- package/src/worker.ts +4 -4
package/src/mcp-protocol.ts
CHANGED
|
@@ -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: {
|
|
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.
|
|
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
|
-
|
|
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"
|
|
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
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
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
|
-
|
|
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) {
|