@typeship-ax/mcp 0.22.0 → 0.23.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 +1 -1
- package/README.md +1 -1
- package/api.md +1 -1
- package/dist/arguments.d.ts +9 -2
- package/dist/arguments.d.ts.map +1 -1
- package/dist/arguments.js +17 -6
- package/dist/fields.d.ts +8 -1
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +91 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/mcp-protocol.d.ts +21 -0
- package/dist/mcp-protocol.d.ts.map +1 -1
- package/dist/mcp-protocol.js +280 -58
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +4 -3
- package/dist/ops.d.ts +5 -0
- package/dist/ops.d.ts.map +1 -1
- package/dist/ops.js +66 -6
- package/dist/resources/deliveries.d.ts +1 -1
- package/dist/resources/deliveries.d.ts.map +1 -1
- package/dist/resources/deliveries.js +1 -1
- package/dist/resources/drafts.d.ts +1 -1
- package/dist/resources/drafts.d.ts.map +1 -1
- package/dist/resources/drafts.js +1 -1
- package/dist/resources/generations.d.ts +2 -2
- package/dist/resources/generations.d.ts.map +1 -1
- package/dist/resources/generations.js +2 -2
- package/dist/resources/releases.d.ts +1 -1
- package/dist/resources/releases.d.ts.map +1 -1
- package/dist/resources/releases.js +1 -1
- package/dist/resources/spec-revisions.d.ts +1 -1
- package/dist/resources/spec-revisions.d.ts.map +1 -1
- package/dist/resources/spec-revisions.js +1 -1
- package/dist/resources/targets.d.ts +1 -1
- package/dist/resources/targets.d.ts.map +1 -1
- package/dist/resources/targets.js +1 -1
- package/dist/type-docs.d.ts +61 -0
- package/dist/type-docs.d.ts.map +1 -0
- package/dist/type-docs.js +174 -0
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/arguments.ts +19 -7
- package/src/fields.ts +81 -5
- package/src/index.ts +3 -3
- package/src/mcp-protocol.ts +279 -58
- package/src/mcp.ts +4 -3
- package/src/ops.ts +69 -6
- package/src/resources/deliveries.ts +2 -2
- package/src/resources/drafts.ts +2 -2
- package/src/resources/generations.ts +4 -4
- package/src/resources/releases.ts +2 -2
- package/src/resources/spec-revisions.ts +2 -2
- package/src/resources/targets.ts +2 -2
- package/src/type-docs.ts +205 -0
package/src/mcp-protocol.ts
CHANGED
|
@@ -16,10 +16,11 @@
|
|
|
16
16
|
* Spec: https://modelcontextprotocol.io/specification/2026-07-28
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { checkValue, closestName, isMeReference, normalizeName, userShapedReference, type ArgumentIssue } from "./arguments.js";
|
|
19
|
+
import { checkValue, closestName, isMeReference, meIdentityField, normalizeName, userShapedReference, type ArgumentIssue } from "./arguments.js";
|
|
20
20
|
import { docsReadTarget, resolveDocsContentUrl, searchConnectedGuides } from "./docs.js";
|
|
21
21
|
import { projectFields, unmatchedFields, unmatchedFieldsMessage } from "./fields.js";
|
|
22
22
|
import { SEARCH_PAGE_SIZE, rankOperations } from "./search.js";
|
|
23
|
+
import { argumentPathText, findInputType, inputTypeText, namedTypesIn, type InputTypes } from "./type-docs.js";
|
|
23
24
|
|
|
24
25
|
export { projectFields } from "./fields.js";
|
|
25
26
|
|
|
@@ -589,8 +590,8 @@ export const SEARCH_DOCS_TOOL: ToolDefinition = {
|
|
|
589
590
|
|
|
590
591
|
export const READ_DOCS_TOOL: ToolDefinition = {
|
|
591
592
|
name: "read_docs",
|
|
592
|
-
description: 'Read a documentation page: an operation reference (a tool name, or dotted "resource.method") or a docs-site guide page by name or URL. Long pages come in parts; pass the offset a part ends with to continue.',
|
|
593
|
-
inputSchema: { type: "object", properties: { page: { type: "string" }, schema: { type: "boolean", description: "Also return the operation's complete input and output JSON Schemas, default false" }, offset: { type: "integer", minimum: 0, description: "Character offset to continue a long page from, default 0" } }, required: ["page"] },
|
|
593
|
+
description: 'Read a documentation page: an operation reference (a tool name, or dotted "resource.method"), a named input type such as a filter, or a docs-site guide page by name or URL. Long pages come in parts; pass the offset a part ends with to continue.',
|
|
594
|
+
inputSchema: { type: "object", properties: { page: { type: "string" }, path: { type: "string", description: "A dotted argument path within the operation or type, e.g. \"filter.team.key\": that field's type, with its named type's fields or values" }, schema: { type: "boolean", description: "Also return the operation's complete input and output JSON Schemas, default false" }, offset: { type: "integer", minimum: 0, description: "Character offset to continue a long page from, default 0" } }, required: ["page"] },
|
|
594
595
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
595
596
|
};
|
|
596
597
|
|
|
@@ -709,7 +710,7 @@ export function toolDefinitions(ops: OpLike[], mode: "operations" | "meta", omit
|
|
|
709
710
|
: "";
|
|
710
711
|
return [
|
|
711
712
|
{ ...SEARCH_DOCS_TOOL, description: "Search this API's " + ops.length + " generated operations and, when a docs site is configured, its guides. Start here to find the operation you need." + coverage },
|
|
712
|
-
{ ...READ_DOCS_TOOL, description: "Read an operation's reference (arguments, authentication, safety, an example and the result's shape) by tool name, or a docs-site guide page. schema: true adds the complete JSON Schemas." },
|
|
713
|
+
{ ...READ_DOCS_TOOL, description: "Read an operation's reference (arguments, authentication, safety, an example and the result's shape) by tool name, a named input type, or a docs-site guide page. path narrows to one nested argument, e.g. \"filter.team.key\"; schema: true adds the complete JSON Schemas." },
|
|
713
714
|
execute,
|
|
714
715
|
];
|
|
715
716
|
}
|
|
@@ -730,6 +731,55 @@ export function missingArguments(op: OpLike, args: Record<string, unknown>): str
|
|
|
730
731
|
|
|
731
732
|
// ---- server instructions ----------------------------------------------------------
|
|
732
733
|
|
|
734
|
+
/** Identity fields that name the caller for a login-shaped "me" (GitHub's
|
|
735
|
+
* users_get_authenticated returns login). */
|
|
736
|
+
const IDENTITY_LOGIN_FIELDS = ["login", "username", "handle"];
|
|
737
|
+
|
|
738
|
+
const MAX_ME_SCHEMA_DEPTH = 6;
|
|
739
|
+
|
|
740
|
+
function acceptsString(schema: unknown): boolean {
|
|
741
|
+
if (!schema || typeof schema !== "object") return false;
|
|
742
|
+
const record = schema as Record<string, unknown>;
|
|
743
|
+
const type = record.type;
|
|
744
|
+
if (type === "string" || (Array.isArray(type) && type.includes("string"))) return true;
|
|
745
|
+
const variants = record.anyOf ?? record.oneOf;
|
|
746
|
+
return Array.isArray(variants) && variants.some(acceptsString);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function objectVariants(schema: unknown): Record<string, unknown>[] {
|
|
750
|
+
if (!schema || typeof schema !== "object") return [];
|
|
751
|
+
const record = schema as Record<string, unknown>;
|
|
752
|
+
const variants = record.anyOf ?? record.oneOf;
|
|
753
|
+
return [record, ...(Array.isArray(variants) ? variants.flatMap(objectVariants) : [])]
|
|
754
|
+
.filter((candidate) => candidate.properties && typeof candidate.properties === "object");
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/** Every argument path where the server resolves "me": a user-shaped string
|
|
758
|
+
* (or a list or union that takes one) at the top level or inside an object
|
|
759
|
+
* argument. The instructions name exactly these. */
|
|
760
|
+
export function meReferenceArguments(ops: OpLike[]): string[] {
|
|
761
|
+
const found = new Set<string>();
|
|
762
|
+
const visit = (name: string, schema: unknown, path: string, depth: number): void => {
|
|
763
|
+
if (!schema || typeof schema !== "object" || depth > MAX_ME_SCHEMA_DEPTH) return;
|
|
764
|
+
const record = schema as Record<string, unknown>;
|
|
765
|
+
const items = record.items ?? (Array.isArray(record.anyOf ?? record.oneOf) ? ((record.anyOf ?? record.oneOf) as Record<string, unknown>[]).find((variant) => variant && variant.items)?.items : undefined);
|
|
766
|
+
if (userShapedReference(name) && (acceptsString(record) || acceptsString(items))) found.add(path);
|
|
767
|
+
const nested: [Record<string, unknown>[], string][] = [[objectVariants(record), path], [objectVariants(items), path + "[]"]];
|
|
768
|
+
for (const [variants, prefix] of nested) {
|
|
769
|
+
for (const variant of variants) {
|
|
770
|
+
for (const [key, child] of Object.entries(variant.properties as Record<string, unknown>)) visit(key, child, prefix + "." + key, depth + 1);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
for (const op of ops) {
|
|
775
|
+
const properties = (op.inputSchema.properties ?? {}) as Record<string, unknown>;
|
|
776
|
+
for (const param of op.params) {
|
|
777
|
+
if (param.resolve !== false) visit(param.name, properties[param.name], param.name, 0);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return [...found].sort((a, b) => a.split(".").length - b.split(".").length || a.localeCompare(b));
|
|
781
|
+
}
|
|
782
|
+
|
|
733
783
|
export interface InstructionsInput {
|
|
734
784
|
title: string;
|
|
735
785
|
/** Callable operations, used to advertise only capabilities the surface has. */
|
|
@@ -770,8 +820,9 @@ export function serverInstructions(input: InstructionsInput): string {
|
|
|
770
820
|
}
|
|
771
821
|
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).");
|
|
772
822
|
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.");
|
|
773
|
-
|
|
774
|
-
|
|
823
|
+
const meArguments = input.identityTool ? meReferenceArguments(input.ops) : [];
|
|
824
|
+
if (meArguments.length > 0) {
|
|
825
|
+
parts.push("These arguments also accept \"me\" for the caller, resolved through " + input.identityTool + " (ID arguments take its id, others its login when it has one): " + meArguments.join(", ") + ".");
|
|
775
826
|
}
|
|
776
827
|
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.");
|
|
777
828
|
parts.push("Errors carry error, code, message, status, the API's body and next_steps.");
|
|
@@ -876,7 +927,7 @@ export function prepareCall(
|
|
|
876
927
|
// Patterns apply inside objects and arrays. A top-level argument's own
|
|
877
928
|
// pattern is left to the API: its documented example values do not all
|
|
878
929
|
// satisfy it yet, and a name or "me" is only resolved to an ID later.
|
|
879
|
-
args[name] = checkValue(value, propSchema, name, issues, { skipPattern: true });
|
|
930
|
+
args[name] = checkValue(value, propSchema, name, issues, { skipPattern: true, meName: name });
|
|
880
931
|
}
|
|
881
932
|
|
|
882
933
|
// A path argument that is the configured credential's username (Twilio's
|
|
@@ -1008,27 +1059,36 @@ export async function resolveReferences(
|
|
|
1008
1059
|
const args = { ...preparedArgs };
|
|
1009
1060
|
const maxPages = Math.max(1, Math.floor(options.maxPages ?? 5));
|
|
1010
1061
|
const identity = options.identityTool ? findOperation(options.ops, options.identityTool) : undefined;
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1062
|
+
let identityBody: Record<string, unknown> | undefined;
|
|
1063
|
+
/** The caller's ID, or login for a login-shaped name (GitHub's owner and
|
|
1064
|
+
* assignees), for "me": one identity call per cache. */
|
|
1065
|
+
const callerId = async (argument: string, key: string): Promise<{ id: string | number } | { outcome: ToolOutcome }> => {
|
|
1066
|
+
const field = meIdentityField(key);
|
|
1067
|
+
const cacheKey = "me:" + identity!.tool + (field === "id" ? "" : ":" + field);
|
|
1014
1068
|
const cached = options.cache.get(cacheKey);
|
|
1015
1069
|
if (cached !== undefined) return { id: cached };
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1070
|
+
if (!identityBody) {
|
|
1071
|
+
const outcome = await options.runOperation(identity!, {});
|
|
1072
|
+
if (outcome.isError) return { outcome };
|
|
1073
|
+
const body = outcomeValue(outcome);
|
|
1074
|
+
identityBody = body && typeof body === "object" && !Array.isArray(body) ? body as Record<string, unknown> : {};
|
|
1075
|
+
}
|
|
1076
|
+
const login = IDENTITY_LOGIN_FIELDS.map((name) => identityBody![name]).find((value) => typeof value === "string" && value !== "");
|
|
1077
|
+
const id = field === "login" && login !== undefined ? login : identityBody.id;
|
|
1020
1078
|
if (typeof id !== "string" && typeof id !== "number") {
|
|
1021
|
-
|
|
1079
|
+
const expected = field === "id" ? "a top-level id" : "a top-level " + IDENTITY_LOGIN_FIELDS.join(", ") + " or id";
|
|
1080
|
+
return { outcome: referenceError("NOT_AVAILABLE", `${identity!.tool} did not return ${expected}, so "me" cannot be resolved for ${argument}.`, argument, [field === "id" ? "Pass the caller's exact ID instead." : "Pass the caller's exact login instead."]) };
|
|
1022
1081
|
}
|
|
1023
1082
|
putReferenceCache(options.cache, cacheKey, id);
|
|
1024
1083
|
return { id };
|
|
1025
1084
|
};
|
|
1026
|
-
/** "me" inside an object argument (a GraphQL input's assigneeId,
|
|
1027
|
-
* of its subscriberIds): the same
|
|
1085
|
+
/** "me" inside an object or array argument (a GraphQL input's assigneeId,
|
|
1086
|
+
* each of its subscriberIds, each of GitHub's assignees): the same
|
|
1087
|
+
* resolution, keyed by property name. */
|
|
1028
1088
|
const resolveNestedMe = async (value: unknown, key: string, path: string): Promise<{ value: unknown } | { outcome: ToolOutcome }> => {
|
|
1029
1089
|
if (typeof value === "string") {
|
|
1030
1090
|
if (!isMeReference(key, value)) return { value };
|
|
1031
|
-
const caller = await callerId(path);
|
|
1091
|
+
const caller = await callerId(path, key);
|
|
1032
1092
|
return "outcome" in caller ? caller : { value: caller.id };
|
|
1033
1093
|
}
|
|
1034
1094
|
if (Array.isArray(value)) {
|
|
@@ -1063,7 +1123,7 @@ export async function resolveReferences(
|
|
|
1063
1123
|
const value = raw.trim();
|
|
1064
1124
|
|
|
1065
1125
|
if (identity && isMeReference(param.name, value)) {
|
|
1066
|
-
const caller = await callerId(param.name);
|
|
1126
|
+
const caller = await callerId(param.name, param.name);
|
|
1067
1127
|
if ("outcome" in caller) return { ok: false, outcome: caller.outcome };
|
|
1068
1128
|
args[param.name] = caller.id;
|
|
1069
1129
|
continue;
|
|
@@ -1161,6 +1221,10 @@ export interface ResultOptions {
|
|
|
1161
1221
|
/** Paths the result may lack, such as a server's default projection:
|
|
1162
1222
|
* they are left out rather than reported. */
|
|
1163
1223
|
optionalFields?: string[];
|
|
1224
|
+
/** The tool's outputSchema. A fields path it declares may be absent from
|
|
1225
|
+
* the result (an optional key) without being reported; a page checks its
|
|
1226
|
+
* items against the schema's items. */
|
|
1227
|
+
outputSchema?: Record<string, unknown>;
|
|
1164
1228
|
/** Arguments nextPage must repeat from args (required path parameters
|
|
1165
1229
|
* such as owner and repo; fields is repeated too): the next-page
|
|
1166
1230
|
* parameters alone, a page_url for instance, would fail validation when
|
|
@@ -1169,31 +1233,52 @@ export interface ResultOptions {
|
|
|
1169
1233
|
}
|
|
1170
1234
|
|
|
1171
1235
|
/** A fields path that selects nothing is an error that names the keys
|
|
1172
|
-
* that exist, not a silent {}.
|
|
1173
|
-
*
|
|
1174
|
-
|
|
1236
|
+
* that exist, not a silent {}. A path the response schema declares is not
|
|
1237
|
+
* one: an optional key no item has is simply absent. The API call already
|
|
1238
|
+
* happened, so the error says so and carries the unprojected result (as
|
|
1239
|
+
* much as fits under half the cap), so neither a read nor a write has to
|
|
1240
|
+
* run again to see it. */
|
|
1241
|
+
function unmatchedFieldsOutcome(value: unknown, fields: string[][] | null, perItem: boolean, options: ResultOptions, schema: unknown, page?: { nextPage: Record<string, unknown> | null }): ToolOutcome | null {
|
|
1175
1242
|
if (fields === null) return null;
|
|
1176
1243
|
const optional = new Set(options.optionalFields ?? []);
|
|
1177
|
-
const unmatched = unmatchedFields(value, fields).filter((u) => !optional.has(u.path));
|
|
1244
|
+
const unmatched = unmatchedFields(value, fields, schema).filter((u) => !optional.has(u.path));
|
|
1178
1245
|
if (unmatched.length === 0) return null;
|
|
1179
1246
|
const write = options.safety !== "read";
|
|
1180
1247
|
const maxChars = options.maxChars ?? DEFAULT_MAX_RESULT_CHARS;
|
|
1181
|
-
const
|
|
1248
|
+
const retained = retainedResult(value, Math.floor(maxChars / 2));
|
|
1249
|
+
const steps: string[] = [];
|
|
1250
|
+
if (write) steps.push("This operation has already run; do not call it again to change fields." + (retained.whole ? " Its full result is in result." : retained.result !== undefined ? " The first items of its result are in result." : ""));
|
|
1251
|
+
else if (retained.whole) steps.push("The full result is in result; use it rather than calling again.");
|
|
1252
|
+
else if (retained.result !== undefined) steps.push("result holds the first " + (retained.result as unknown[]).length + " of " + (value as unknown[]).length + " items; the rest are over the size cap.");
|
|
1253
|
+
steps.push((write ? "Next time, use" : "To project a later call, use") + " fields from the available keys" + (perItem ? " (fields apply to each item)" : "") + ", or omit fields for the whole result.");
|
|
1182
1254
|
const structured = {
|
|
1183
1255
|
error: "UnmatchedFields",
|
|
1184
1256
|
code: "FIELDS_UNMATCHED",
|
|
1185
1257
|
message: unmatchedFieldsMessage(unmatched, perItem),
|
|
1186
1258
|
unmatched,
|
|
1187
|
-
...(
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
],
|
|
1259
|
+
...(retained.result !== undefined ? { result: retained.result } : {}),
|
|
1260
|
+
...(retained.omitted ? { result_omitted: retained.omitted } : {}),
|
|
1261
|
+
...(page ? { hasMore: page.nextPage !== null, ...(page.nextPage !== null ? { nextPage: page.nextPage } : {}) } : {}),
|
|
1262
|
+
...(options.requestId ? { request_id: options.requestId } : {}),
|
|
1263
|
+
next_steps: steps,
|
|
1193
1264
|
};
|
|
1194
1265
|
return { text: JSON.stringify(structured), isError: true, structured };
|
|
1195
1266
|
}
|
|
1196
1267
|
|
|
1268
|
+
/** The unprojected result an unmatched-fields error carries: whole when it
|
|
1269
|
+
* fits the budget, the leading items of a list when only some do, and
|
|
1270
|
+
* nothing for an object over the budget. */
|
|
1271
|
+
function retainedResult(value: unknown, budget: number): { result?: unknown; whole: boolean; omitted?: number } {
|
|
1272
|
+
const text = JSON.stringify(value);
|
|
1273
|
+
if (text === undefined) return { whole: false };
|
|
1274
|
+
if (text.length <= budget) return { result: value, whole: true };
|
|
1275
|
+
if (!Array.isArray(value)) return { whole: false };
|
|
1276
|
+
const k = itemsThatFit(value, budget);
|
|
1277
|
+
const head = value.slice(0, k);
|
|
1278
|
+
if (JSON.stringify(head).length > budget) return { whole: false };
|
|
1279
|
+
return { result: head, whole: false, omitted: value.length - k };
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1197
1282
|
const fieldsHint = (perItem: boolean) => "Pass fields (dotted paths" + (perItem ? ", applied per item" : "") + ") to keep only the keys you need.";
|
|
1198
1283
|
|
|
1199
1284
|
/** How many leading items fit under `budget` characters once serialized
|
|
@@ -1228,7 +1313,8 @@ export function pageOutcome(items: unknown[], nextPage: Record<string, unknown>
|
|
|
1228
1313
|
}
|
|
1229
1314
|
const fields = options.fields ?? null;
|
|
1230
1315
|
const maxChars = options.maxChars ?? DEFAULT_MAX_RESULT_CHARS;
|
|
1231
|
-
const
|
|
1316
|
+
const pageSchema = options.outputSchema?.properties as Record<string, { items?: unknown }> | undefined;
|
|
1317
|
+
const unmatched = unmatchedFieldsOutcome(items, fields, true, options, pageSchema?.items, { nextPage });
|
|
1232
1318
|
if (unmatched) return unmatched;
|
|
1233
1319
|
const shown = projectFields(items, fields) as unknown[];
|
|
1234
1320
|
const full = {
|
|
@@ -1287,7 +1373,7 @@ const omittedMarker = (key: string, chars: number, maxChars: number) => chars >
|
|
|
1287
1373
|
export function dataOutcome(data: unknown, options: ResultOptions = {}): ToolOutcome {
|
|
1288
1374
|
const fields = options.fields ?? null;
|
|
1289
1375
|
const maxChars = options.maxChars ?? DEFAULT_MAX_RESULT_CHARS;
|
|
1290
|
-
const unmatched = data !== undefined && data !== null ? unmatchedFieldsOutcome(data, fields, Array.isArray(data), options) : null;
|
|
1376
|
+
const unmatched = data !== undefined && data !== null ? unmatchedFieldsOutcome(data, fields, Array.isArray(data), options, options.outputSchema) : null;
|
|
1291
1377
|
if (unmatched) return unmatched;
|
|
1292
1378
|
const value = data !== undefined && data !== null ? projectFields(data, fields) : { ok: true };
|
|
1293
1379
|
if (typeof value === "string") {
|
|
@@ -1444,6 +1530,28 @@ function payloadFailureCode(code: unknown): ErrorCode | undefined {
|
|
|
1444
1530
|
return undefined;
|
|
1445
1531
|
}
|
|
1446
1532
|
|
|
1533
|
+
/** The vendor's own code on an in-band GraphQL error: the first error's
|
|
1534
|
+
* extensions.code, or its type (GitHub). */
|
|
1535
|
+
function graphqlVendorCode(error: unknown): string | undefined {
|
|
1536
|
+
const first = (error as { errors?: { extensions?: { code?: unknown }; type?: unknown }[] } | null)?.errors?.[0];
|
|
1537
|
+
const code = first?.extensions?.code ?? first?.type;
|
|
1538
|
+
return typeof code === "string" && code !== "" ? code : undefined;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
/** GraphQL error codes whose meaning is settled (Apollo's standard codes,
|
|
1542
|
+
* GitHub's types, Linear's codes), by recovery class. Any other code is
|
|
1543
|
+
* passed through as is, with no guessed next step. The CLI's
|
|
1544
|
+
* classification (cli-agent.ts) uses the same table. */
|
|
1545
|
+
function graphqlErrorClass(code: string): "not_found" | "unauthenticated" | "forbidden" | "bad_input" | "rate_limited" | undefined {
|
|
1546
|
+
const c = code.toUpperCase();
|
|
1547
|
+
if (c === "NOT_FOUND") return "not_found";
|
|
1548
|
+
if (c === "UNAUTHENTICATED" || c === "AUTHENTICATION_ERROR") return "unauthenticated";
|
|
1549
|
+
if (c === "FORBIDDEN") return "forbidden";
|
|
1550
|
+
if (c === "BAD_USER_INPUT" || c === "GRAPHQL_VALIDATION_FAILED" || c === "GRAPHQL_PARSE_FAILED" || c === "INPUT_ERROR") return "bad_input";
|
|
1551
|
+
if (c === "RATE_LIMITED" || c === "RATELIMITED") return "rate_limited";
|
|
1552
|
+
return undefined;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1447
1555
|
/** Classify an SDK error result by status: the code and what to do next. */
|
|
1448
1556
|
export function classifyError(error: unknown, context: ErrorContext = {}): { code: ErrorCode; nextSteps: string[] } {
|
|
1449
1557
|
const e = (error ?? {}) as { name?: string; message?: string; status?: number; code?: unknown; body?: unknown; violations?: unknown; rateLimit?: { retryAt?: Date } };
|
|
@@ -1467,13 +1575,24 @@ export function classifyError(error: unknown, context: ErrorContext = {}): { cod
|
|
|
1467
1575
|
if (reported === "RATE_LIMITED") return { code: "RATE_LIMITED", nextSteps: [rateLimitNextStep(undefined)] };
|
|
1468
1576
|
return { code: "CALL_FAILED", nextSteps: ["The API reported a failure in a successful response; body says why. Do not treat the call as done."] };
|
|
1469
1577
|
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1578
|
+
const unauthenticated = (): { code: ErrorCode; nextSteps: string[] } => context.hadCredential
|
|
1579
|
+
? { code: "AUTH_INVALID", nextSteps: ["The credential was rejected; it may be expired or for another environment." + (auth ? " " + auth : "")] }
|
|
1580
|
+
: { code: "NO_AUTH", nextSteps: ["No credential was sent." + (auth ? " " + auth : "")] };
|
|
1581
|
+
const forbidden = (): { code: ErrorCode; nextSteps: string[] } => scopes.length ? scopeFailure : { code: "AUTH_INVALID", nextSteps: ["The credential lacks access to this operation; it is not a retryable error."] };
|
|
1582
|
+
// An in-band GraphQL error (HTTP 200): classified by the vendor's code.
|
|
1583
|
+
if (e.name === "GraphQLRequestError") {
|
|
1584
|
+
const vendor = graphqlVendorCode(error);
|
|
1585
|
+
switch (vendor === undefined ? undefined : graphqlErrorClass(vendor)) {
|
|
1586
|
+
case "not_found": return { code: "NOT_FOUND", nextSteps: ["Check the id in the arguments; list the resource first to find the right one."] };
|
|
1587
|
+
case "unauthenticated": return unauthenticated();
|
|
1588
|
+
case "forbidden": return forbidden();
|
|
1589
|
+
case "bad_input": return { code: "INVALID_REQUEST", nextSteps: ["The API rejected an argument or the selection; the errors in body name it (message, path). Fix that and call again."] };
|
|
1590
|
+
case "rate_limited": return { code: "RATE_LIMITED", nextSteps: [rateLimitNextStep(undefined)] };
|
|
1591
|
+
default: return { code: "CALL_FAILED", nextSteps: [] };
|
|
1592
|
+
}
|
|
1474
1593
|
}
|
|
1475
|
-
if (status ===
|
|
1476
|
-
if (status === 403) return
|
|
1594
|
+
if (status === 401) return unauthenticated();
|
|
1595
|
+
if (status === 403) return forbidden();
|
|
1477
1596
|
if (status === 402) return { code: "PLAN_LIMIT", nextSteps: ["The account's plan stops here; the body may name where to lift the limit. Do not retry the same call as is."] };
|
|
1478
1597
|
if (status === 404) return { code: "NOT_FOUND", nextSteps: notFoundNextSteps(message, e.body) };
|
|
1479
1598
|
if (status === 422 && body?.errors?.[0]?.code === "spec_error") {
|
|
@@ -1531,9 +1650,12 @@ export function errorOutcome(error: unknown, context: ErrorContext = {}): ToolOu
|
|
|
1531
1650
|
? (e.body as Record<string, unknown>).request_id ?? (e.body as Record<string, unknown>).requestId
|
|
1532
1651
|
: undefined;
|
|
1533
1652
|
const requestId = e?.response?.requestId ?? (typeof bodyRequestId === "string" ? bodyRequestId : undefined);
|
|
1653
|
+
// The vendor's identity for the failure, next to the normalized code.
|
|
1654
|
+
const vendorCode = e?.name === "GraphQLRequestError" ? graphqlVendorCode(error) : undefined;
|
|
1534
1655
|
const structured = {
|
|
1535
1656
|
error: e?.name ?? "Error",
|
|
1536
1657
|
code,
|
|
1658
|
+
...(vendorCode ? { vendor_code: vendorCode } : {}),
|
|
1537
1659
|
message: e?.message,
|
|
1538
1660
|
...(typeof e?.status === "number" ? { status: e.status } : {}),
|
|
1539
1661
|
...(requestId ? { request_id: requestId } : {}),
|
|
@@ -1570,6 +1692,8 @@ export interface DocsSource {
|
|
|
1570
1692
|
hiddenOps?: HiddenOperation[];
|
|
1571
1693
|
/** Count generated before runtime surface filters. */
|
|
1572
1694
|
generatedOperationCount?: number;
|
|
1695
|
+
/** Named input types for type and argument-path lookups. */
|
|
1696
|
+
inputTypes?: InputTypes;
|
|
1573
1697
|
/** Base URL of the docs site, or null when none is configured. */
|
|
1574
1698
|
docsUrl(): string | null;
|
|
1575
1699
|
/** Exact llms.txt URL when it is not at <docsUrl>/llms.txt. */
|
|
@@ -1617,10 +1741,56 @@ const REFERENCE_ARGUMENT_BUDGET = 6_000;
|
|
|
1617
1741
|
/** Enum values listed inline; longer enums are cut with a count. */
|
|
1618
1742
|
const REFERENCE_ENUM_VALUES = 30;
|
|
1619
1743
|
|
|
1620
|
-
/**
|
|
1621
|
-
|
|
1744
|
+
/** HTML that API descriptions carry for formatting only. Anything else
|
|
1745
|
+
* in angle brackets (a <placeholder>) is text and stays. */
|
|
1746
|
+
const FORMATTING_TAGS = /<\/?(?:a|abbr|b|br|code|div|em|i|li|ol|p|pre|small|span|strong|sub|sup|u|ul)(?:\s[^<>]*)?\/?>/gi;
|
|
1747
|
+
/** A Markdown link or image: [text](url "title"), where the URL may hold
|
|
1748
|
+
* one level of parentheses (Wikipedia's Foo_(bar)). */
|
|
1749
|
+
const MARKDOWN_LINK = /!?\[([^\[\]]*(?:\[[^\[\]]*\][^\[\]]*)*)\]\((?:[^()\s]|\([^()\s]*\))*(?:\s+"[^"]*")?\)/g;
|
|
1750
|
+
|
|
1751
|
+
/** Prose for one description, as one line: Markdown and HTML links reduced
|
|
1752
|
+
* to their visible text, formatting tags and emphasis dropped, <code> as
|
|
1753
|
+
* inline code (kept, because agents copy it). */
|
|
1754
|
+
export function referenceProse(text: unknown): string {
|
|
1622
1755
|
if (typeof text !== "string") return "";
|
|
1623
|
-
return text
|
|
1756
|
+
return text
|
|
1757
|
+
.replace(/<code>([\s\S]*?)<\/code>/gi, "`$1`")
|
|
1758
|
+
.replace(/<br\s*\/?>/gi, " ")
|
|
1759
|
+
.replace(FORMATTING_TAGS, "")
|
|
1760
|
+
.replace(MARKDOWN_LINK, "$1")
|
|
1761
|
+
.replace(/\[([^\[\]]+)\]\[[^\[\]]*\]/g, "$1")
|
|
1762
|
+
.replace(/(\*\*|__)(?=\S)([^*_]*?\S)\1/g, "$2")
|
|
1763
|
+
.replace(/ /g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, "\"").replace(/'/g, "'").replace(/&/g, "&")
|
|
1764
|
+
.replace(/\s+/g, " ")
|
|
1765
|
+
.trim();
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
/** Abbreviations whose period does not end a sentence. */
|
|
1769
|
+
const ABBREVIATION = /(?:^|[\s(])(?:e\.g|i\.e|etc|vs|approx|incl|cf|no|min|max)\.$/i;
|
|
1770
|
+
|
|
1771
|
+
/** A description's sentences, every character kept: a sentence ends at
|
|
1772
|
+
* . ! or ? (closing quotes and brackets included) before whitespace, not
|
|
1773
|
+
* after an abbreviation, and never inside inline code. */
|
|
1774
|
+
function sentencesOf(prose: string): string[] {
|
|
1775
|
+
const sentences: string[] = [];
|
|
1776
|
+
let current = "";
|
|
1777
|
+
for (const part of prose.split(/(?<=[.!?]["')\]]?)\s+/)) {
|
|
1778
|
+
current = current ? current + " " + part : part;
|
|
1779
|
+
const openCode = (current.match(/`/g) ?? []).length % 2 === 1;
|
|
1780
|
+
if (!openCode && !ABBREVIATION.test(current)) {
|
|
1781
|
+
sentences.push(current);
|
|
1782
|
+
current = "";
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
if (current) sentences.push(current);
|
|
1786
|
+
return sentences;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
/** Cut one long sentence at a word boundary, never inside inline code. */
|
|
1790
|
+
function cutSentence(sentence: string, limit: number): string {
|
|
1791
|
+
let cut = sentence.slice(0, limit).replace(/\s+\S*$/, "");
|
|
1792
|
+
if ((cut.match(/`/g) ?? []).length % 2 === 1) cut = cut.slice(0, cut.lastIndexOf("`")).trimEnd();
|
|
1793
|
+
return cut.replace(/[\s,;:(]+$/, "") + "…";
|
|
1624
1794
|
}
|
|
1625
1795
|
|
|
1626
1796
|
/** Sentences the generator appends to an argument's description because a
|
|
@@ -1633,18 +1803,16 @@ const REFERENCE_ARGUMENT_PROSE = 160;
|
|
|
1633
1803
|
/** An argument's description, cut to its leading sentences within the
|
|
1634
1804
|
* budget plus every note the generator added. The full text is in the
|
|
1635
1805
|
* schema (schema: true). */
|
|
1636
|
-
function argumentProse(text: unknown): string {
|
|
1806
|
+
export function argumentProse(text: unknown): string {
|
|
1637
1807
|
const prose = referenceProse(text);
|
|
1638
1808
|
if (prose.length <= REFERENCE_ARGUMENT_PROSE) return prose;
|
|
1639
|
-
const sentences = prose.match(/[^.!?]+(?:[.!?]+(?=\s|$)|$)\s*/g) ?? [prose];
|
|
1640
1809
|
const kept: string[] = [];
|
|
1641
1810
|
let used = 0;
|
|
1642
1811
|
let cut = false;
|
|
1643
|
-
for (const
|
|
1644
|
-
const sentence = raw.trim();
|
|
1812
|
+
for (const sentence of sentencesOf(prose)) {
|
|
1645
1813
|
if (REFERENCE_NOTE.test(sentence)) { kept.push(sentence); continue; }
|
|
1646
1814
|
if (kept.length === 0 || used + sentence.length <= REFERENCE_ARGUMENT_PROSE) {
|
|
1647
|
-
kept.push(sentence.length > REFERENCE_ARGUMENT_PROSE * 2 ? sentence
|
|
1815
|
+
kept.push(sentence.length > REFERENCE_ARGUMENT_PROSE * 2 ? cutSentence(sentence, REFERENCE_ARGUMENT_PROSE * 2) : sentence);
|
|
1648
1816
|
used += sentence.length;
|
|
1649
1817
|
} else {
|
|
1650
1818
|
cut = true;
|
|
@@ -1688,9 +1856,13 @@ function referenceObject(schema: Record<string, unknown>): Record<string, unknow
|
|
|
1688
1856
|
return undefined;
|
|
1689
1857
|
}
|
|
1690
1858
|
|
|
1859
|
+
/** Where an argument line sits: the operation, the dotted path to it,
|
|
1860
|
+
* and the named input type of the object that holds it, when known. */
|
|
1861
|
+
interface ArgumentContext { tool: string; types?: InputTypes; path: string[]; typeName?: string }
|
|
1862
|
+
|
|
1691
1863
|
/** One line per argument, nested fields indented beneath their object.
|
|
1692
1864
|
* Each top-level argument is spelled out as deep as fits its budget. */
|
|
1693
|
-
function referenceArguments(schema: Record<string, unknown>, lines: string[]): void {
|
|
1865
|
+
function referenceArguments(schema: Record<string, unknown>, lines: string[], context: ArgumentContext): void {
|
|
1694
1866
|
const properties = (schema.properties ?? {}) as Record<string, Record<string, unknown>>;
|
|
1695
1867
|
for (const name of Object.keys(properties)) {
|
|
1696
1868
|
const only = { ...schema, properties: { [name]: properties[name] } };
|
|
@@ -1699,14 +1871,14 @@ function referenceArguments(schema: Record<string, unknown>, lines: string[]): v
|
|
|
1699
1871
|
// below the cut; then one level with names only.
|
|
1700
1872
|
for (const [depth, names] of [[REFERENCE_DEPTH, true], [2, true], [2, false], [1, true]] as const) {
|
|
1701
1873
|
block = [];
|
|
1702
|
-
referenceArgumentLines(only, 0, depth, names, " ", block);
|
|
1874
|
+
referenceArgumentLines(only, 0, depth, names, " ", block, context);
|
|
1703
1875
|
if (block.join("\n").length <= REFERENCE_ARGUMENT_BUDGET) break;
|
|
1704
1876
|
}
|
|
1705
1877
|
lines.push(...block);
|
|
1706
1878
|
}
|
|
1707
1879
|
}
|
|
1708
1880
|
|
|
1709
|
-
function referenceArgumentLines(schema: Record<string, unknown>, depth: number, maxDepth: number, names: boolean, indent: string, lines: string[]): void {
|
|
1881
|
+
function referenceArgumentLines(schema: Record<string, unknown>, depth: number, maxDepth: number, names: boolean, indent: string, lines: string[], context: ArgumentContext): void {
|
|
1710
1882
|
const properties = (schema.properties ?? {}) as Record<string, Record<string, unknown>>;
|
|
1711
1883
|
const required = new Set(Array.isArray(schema.required) ? schema.required as string[] : []);
|
|
1712
1884
|
for (const [name, child] of Object.entries(properties)) {
|
|
@@ -1716,16 +1888,27 @@ function referenceArgumentLines(schema: Record<string, unknown>, depth: number,
|
|
|
1716
1888
|
...(child.default !== undefined && !/\bdefault\b/i.test(description) ? ["default " + JSON.stringify(child.default)] : []),
|
|
1717
1889
|
...(child.deprecated === true && !/deprecated/i.test(description) ? ["deprecated"] : []),
|
|
1718
1890
|
];
|
|
1719
|
-
lines.push(indent + name + " (" + referenceType(child) + (required.has(name) ? ", required" : "") + (extras.length ? ", " + extras.join(", ") : "") + ")" + (description ? ": " + description : ""));
|
|
1720
1891
|
const nested = referenceObject(child);
|
|
1892
|
+
// An object argument reads as its named type (IssueFilter), which
|
|
1893
|
+
// read_docs can look up; everything else keeps its inline type.
|
|
1894
|
+
const expression = context.path.length === 0
|
|
1895
|
+
? context.types?.args[context.tool]?.[name]
|
|
1896
|
+
: context.typeName ? context.types?.types[context.typeName]?.fields?.[name]?.type : undefined;
|
|
1897
|
+
const named = nested ? namedTypesIn(context.types, expression) : [];
|
|
1898
|
+
const type = named.length > 0 ? expression! : referenceType(child);
|
|
1899
|
+
lines.push(indent + name + " (" + type + (required.has(name) ? ", required" : "") + (extras.length ? ", " + extras.join(", ") : "") + ")" + (description ? ": " + description : ""));
|
|
1721
1900
|
if (!nested) continue;
|
|
1901
|
+
const path = [...context.path, name];
|
|
1902
|
+
const objects = named.filter((typeName) => context.types!.types[typeName]!.fields);
|
|
1903
|
+
const inner: ArgumentContext = { ...context, path, typeName: objects.length === 1 ? objects[0] : undefined };
|
|
1722
1904
|
if (depth + 1 < maxDepth) {
|
|
1723
|
-
referenceArgumentLines(nested, depth + 1, maxDepth, names, indent + " ", lines);
|
|
1905
|
+
referenceArgumentLines(nested, depth + 1, maxDepth, names, indent + " ", lines, inner);
|
|
1724
1906
|
continue;
|
|
1725
1907
|
}
|
|
1726
1908
|
if (!names) continue;
|
|
1727
1909
|
const keys = Object.keys(nested.properties as object);
|
|
1728
|
-
|
|
1910
|
+
// Field types (and any cut names) are one path lookup away.
|
|
1911
|
+
lines.push(indent + " fields: " + keys.slice(0, 40).join(", ") + (keys.length > 40 ? ", … " + (keys.length - 40) + " more: read_docs " + JSON.stringify({ page: context.tool, path: path.join(".") }) + " lists all" : ""));
|
|
1729
1912
|
}
|
|
1730
1913
|
}
|
|
1731
1914
|
|
|
@@ -1758,7 +1941,7 @@ function referenceShape(schema: Record<string, unknown> | undefined, depth = 0):
|
|
|
1758
1941
|
* Schemas come with `schema: true`, as the CLI's `docs --schema` does; they
|
|
1759
1942
|
* cost several times the rest and an agent rarely needs them to call.
|
|
1760
1943
|
*/
|
|
1761
|
-
export function referenceText(op: OpLike, options: { schema?: boolean } = {}): string {
|
|
1944
|
+
export function referenceText(op: OpLike, options: { schema?: boolean; types?: InputTypes } = {}): string {
|
|
1762
1945
|
const safety = operationSafety(op);
|
|
1763
1946
|
// A credential-defaulted argument (Twilio's AccountSid) is left out, so the
|
|
1764
1947
|
// example shows the call an agent should make.
|
|
@@ -1776,7 +1959,7 @@ export function referenceText(op: OpLike, options: { schema?: boolean } = {}): s
|
|
|
1776
1959
|
const input = toolInputSchema(op);
|
|
1777
1960
|
if (Object.keys((input.properties ?? {}) as object).length > 0) {
|
|
1778
1961
|
lines.push("", "Arguments:");
|
|
1779
|
-
referenceArguments(input, lines);
|
|
1962
|
+
referenceArguments(input, lines, { tool: op.tool, types: options.types, path: [] });
|
|
1780
1963
|
}
|
|
1781
1964
|
lines.push("", "Example arguments: " + JSON.stringify(example));
|
|
1782
1965
|
if (op.outputSchema) {
|
|
@@ -1786,6 +1969,15 @@ export function referenceText(op: OpLike, options: { schema?: boolean } = {}): s
|
|
|
1786
1969
|
lines.push("", "Input schema: " + JSON.stringify(input));
|
|
1787
1970
|
if (op.outputSchema) lines.push("", "Output schema: " + JSON.stringify(op.outputSchema));
|
|
1788
1971
|
} else {
|
|
1972
|
+
// Say how to drill into an object argument: the page above stops at a
|
|
1973
|
+
// depth and a size, and the schema is the whole graph at once.
|
|
1974
|
+
const nested = Object.entries((input.properties ?? {}) as Record<string, Record<string, unknown>>)
|
|
1975
|
+
.find(([, child]) => child && typeof child === "object" && referenceObject(child))?.[0];
|
|
1976
|
+
if (nested) {
|
|
1977
|
+
const named = namedTypesIn(options.types, options.types?.args[op.tool]?.[nested])[0];
|
|
1978
|
+
lines.push("", "Nested arguments: read_docs " + JSON.stringify({ page: op.tool, path: nested }) + " gives an argument's type and all its fields; extend the path (\"" + nested + ".<field>\") to go deeper."
|
|
1979
|
+
+ (named ? " Named types read the same way: read_docs " + JSON.stringify({ page: named }) + "." : ""));
|
|
1980
|
+
}
|
|
1789
1981
|
lines.push("", "Full input and output JSON Schemas: read_docs " + JSON.stringify({ page: op.tool, schema: true }) + ".");
|
|
1790
1982
|
}
|
|
1791
1983
|
return lines.join("\n");
|
|
@@ -1857,20 +2049,48 @@ export const READ_DOCS_LIMIT = 20_000;
|
|
|
1857
2049
|
|
|
1858
2050
|
/** One part of a long page, ending with how to read the next part. */
|
|
1859
2051
|
function docsPart(page: string, text: string, offset: number, schema = false): string {
|
|
2052
|
+
return docsPartFor({ page, ...(schema ? { schema: true } : {}) }, text, offset);
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
/** docsPart for any read_docs arguments (a page, a path, schema). */
|
|
2056
|
+
function docsPartFor(request: Record<string, unknown>, text: string, offset: number): string {
|
|
1860
2057
|
if (offset <= 0 && text.length <= READ_DOCS_LIMIT) return text;
|
|
1861
2058
|
const start = Math.min(Math.max(0, offset), text.length);
|
|
1862
2059
|
const end = Math.min(text.length, start + READ_DOCS_LIMIT);
|
|
1863
2060
|
const more = end < text.length
|
|
1864
|
-
? "\n\n[Characters " + start + "-" + end + " of " + text.length + ". Continue with read_docs " + JSON.stringify({
|
|
2061
|
+
? "\n\n[Characters " + start + "-" + end + " of " + text.length + ". Continue with read_docs " + JSON.stringify({ ...request, offset: end }) + ".]"
|
|
1865
2062
|
: "\n\n[Characters " + start + "-" + end + " of " + text.length + "; end of page.]";
|
|
1866
2063
|
return text.slice(start, end) + more;
|
|
1867
2064
|
}
|
|
1868
2065
|
|
|
1869
|
-
|
|
2066
|
+
/** How read_docs names a type lookup, for the pages that mention types. */
|
|
2067
|
+
function readDocsTypeHint(name: string): string {
|
|
2068
|
+
return "read_docs " + JSON.stringify({ page: name });
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
/** One argument path within an operation or a named type. */
|
|
2072
|
+
function docsPathOutcome(source: DocsSource, root: { tool: string; inputSchema: Record<string, unknown> } | { type: string }, page: string, path: string, offset: number): ToolOutcome {
|
|
2073
|
+
const found = argumentPathText(source.inputTypes, root, path, readDocsTypeHint);
|
|
2074
|
+
if (found.ok) return { text: docsPartFor({ page, path }, found.text, offset), isError: false };
|
|
2075
|
+
const structured = {
|
|
2076
|
+
error: "NotFoundError", code: "NOT_FOUND", message: found.message,
|
|
2077
|
+
...(found.available.length > 0 ? { available: found.available } : {}),
|
|
2078
|
+
next_steps: [found.available.length > 0 ? "Pass one of the available fields as the next path segment." : "read_docs " + JSON.stringify({ page }) + " lists the arguments."],
|
|
2079
|
+
};
|
|
2080
|
+
return { text: JSON.stringify(structured), isError: true, structured };
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
export async function docsRead(source: DocsSource, page: string, offset = 0, options: { schema?: boolean; path?: string } = {}): Promise<ToolOutcome> {
|
|
1870
2084
|
const opMatch = findOperation(source.ops, page);
|
|
1871
|
-
|
|
2085
|
+
const typeMatch = opMatch ? undefined : findInputType(source.inputTypes, page);
|
|
2086
|
+
if (options.path !== undefined && options.path.trim() !== "") {
|
|
2087
|
+
if (opMatch) return docsPathOutcome(source, opMatch, page, options.path, offset);
|
|
2088
|
+
if (typeMatch) return docsPathOutcome(source, { type: typeMatch }, page, options.path, offset);
|
|
2089
|
+
}
|
|
2090
|
+
if (opMatch) return { text: docsPart(page, referenceText(opMatch, { schema: options.schema, types: source.inputTypes }), offset, options.schema === true), isError: false };
|
|
1872
2091
|
const omittedMatch = findOperation(source.omittedOps ?? [], page);
|
|
1873
2092
|
if (omittedMatch) return omittedPlanLimit(source, [omittedMatch], omittedMatch.tool);
|
|
2093
|
+
if (typeMatch) return { text: docsPartFor({ page }, inputTypeText(source.inputTypes!, typeMatch, readDocsTypeHint), offset), isError: false };
|
|
1874
2094
|
let target = page;
|
|
1875
2095
|
if (!/^https?:\/\//.test(target)) {
|
|
1876
2096
|
const index = await fetchDocs(source, "llms.txt");
|
|
@@ -1909,7 +2129,8 @@ export async function callSharedTool(
|
|
|
1909
2129
|
const hidden = findHidden(source, args.page);
|
|
1910
2130
|
if (hidden) return hiddenOutcome(hidden);
|
|
1911
2131
|
}
|
|
1912
|
-
|
|
2132
|
+
const path = typeof args.path === "string" ? args.path : undefined;
|
|
2133
|
+
return typeof args.page === "string" ? docsRead(source, args.page, offset, { schema, path }) : argumentsError({ tool: name }, [{ code: "MISSING_ARGUMENT", argument: "page", message: "read_docs requires a page string." }]);
|
|
1913
2134
|
}
|
|
1914
2135
|
if (name === "execute") {
|
|
1915
2136
|
if (typeof args.operation !== "string") return argumentsError({ tool: name }, [{ code: "MISSING_ARGUMENT", argument: "operation", message: "execute requires an operation name." }]);
|
package/src/mcp.ts
CHANGED
|
@@ -23,7 +23,7 @@ import { basename, join } from "node:path";
|
|
|
23
23
|
import { fileURLToPath } from "node:url";
|
|
24
24
|
import { TypeshipClient, formatDebugEvent, type ClientOptions, type DebugEvent } from "./index.js";
|
|
25
25
|
import { asApiResult, mediaTypeForPath } from "./core/http.js";
|
|
26
|
-
import { GLOBALS, OMITTED_OPS, OPS, buildArgs, type OpSpec } from "./ops.js";
|
|
26
|
+
import { GLOBALS, INPUT_TYPES, OMITTED_OPS, OPS, buildArgs, type OpSpec } from "./ops.js";
|
|
27
27
|
import {
|
|
28
28
|
DEFAULT_MAX_RESULT_CHARS, SUPPORTED_PROTOCOL_VERSIONS, McpAccountLinkRequired, argumentsError, asJsonRpc, binaryOutcome, callSharedTool, checkRequestHeaders,
|
|
29
29
|
createStdioRpcHandler, dataOutcome, errorOutcome, handleRpc, hiddenOperations, isRpcOutcome, pageOutcome, parseIncludeList, prepareCall, resolveReferences, serverInstructions,
|
|
@@ -43,7 +43,7 @@ export { McpAccountLinkRequired } from "./mcp-protocol.js";
|
|
|
43
43
|
const BIN = "typeship";
|
|
44
44
|
const PKG_NAME = "@typeship-ax/mcp";
|
|
45
45
|
const SERVER_NAME = "typeship-mcp";
|
|
46
|
-
const SERVER_VERSION = "0.
|
|
46
|
+
const SERVER_VERSION = "0.23.0";
|
|
47
47
|
/** The MCP client's announced name (clientInfo in request _meta), for the User-Agent. */
|
|
48
48
|
let MCP_CLIENT_NAME: string | null = null;
|
|
49
49
|
function noteClientInfo(message: unknown): void {
|
|
@@ -264,7 +264,7 @@ async function callOperationRaw(op: OpSpec, rawArgs: Record<string, unknown>, re
|
|
|
264
264
|
hadCredential: !!remote || CLIENT_CREDENTIALS.get(client) === true,
|
|
265
265
|
requiredScopes: requiredScopes(op.security),
|
|
266
266
|
};
|
|
267
|
-
const shape = { fields, optionalFields, maxChars, pagination: op.pagination, args, safety: op.safety };
|
|
267
|
+
const shape = { fields, optionalFields, maxChars, pagination: op.pagination, args, safety: op.safety, outputSchema: op.outputSchema };
|
|
268
268
|
try {
|
|
269
269
|
const target = (client as unknown as Record<string, Record<string, (...a: unknown[]) => unknown>>)[op.resource]!;
|
|
270
270
|
let result = await asApiResult(target[op.method]!(...callArgs) as Promise<unknown>);
|
|
@@ -377,6 +377,7 @@ function saveBinary(bytes: Uint8Array, _mediaType: string, suggestedName: string
|
|
|
377
377
|
const docsSource: DocsSource = {
|
|
378
378
|
ops: MCP_OPS as unknown as OpLike[],
|
|
379
379
|
omittedOps: OMITTED_OPS as unknown as OpLike[],
|
|
380
|
+
inputTypes: INPUT_TYPES,
|
|
380
381
|
hiddenOps: hiddenOperations(OPS as unknown as OpLike[], { readOnly: READ_ONLY, include: INCLUDE, uploads: LOCAL_PROCESS }, { readOnly: READ_ONLY_SWITCH, include: TOOLS_SWITCH }),
|
|
381
382
|
generatedOperationCount: OPS.length,
|
|
382
383
|
docsUrl: () => readJson<{ docsUrl?: string }>("config.json")?.docsUrl ?? DOCS_URL_DEFAULT,
|