@typeship-ax/mcp 0.6.0 → 0.8.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/README.md +1 -1
- package/api.json +5597 -3010
- package/api.md +433 -54
- package/dist/core/http.d.ts +6 -92
- package/dist/core/http.d.ts.map +1 -1
- package/dist/core/http.js +70 -209
- package/dist/core/pagination.d.ts.map +1 -1
- package/dist/core/pagination.js +6 -34
- package/dist/dates.d.ts +0 -2
- package/dist/dates.d.ts.map +1 -1
- package/dist/dates.js +0 -1
- package/dist/docs.d.ts +11 -0
- package/dist/docs.d.ts.map +1 -0
- package/dist/docs.js +114 -0
- package/dist/errors.d.ts +27 -27
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +7 -7
- package/dist/index.d.ts +19 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -13
- package/dist/mcp-protocol.d.ts +19 -24
- package/dist/mcp-protocol.d.ts.map +1 -1
- package/dist/mcp-protocol.js +160 -124
- package/dist/mcp.js +16 -19
- package/dist/ops.d.ts +5 -0
- package/dist/ops.d.ts.map +1 -1
- package/dist/ops.js +31 -17
- package/dist/resources/account.d.ts +2 -2
- package/dist/resources/account.d.ts.map +1 -1
- package/dist/resources/api-keys.d.ts +10 -5
- package/dist/resources/api-keys.d.ts.map +1 -1
- package/dist/resources/api-keys.js +3 -1
- package/dist/resources/definition-revisions.d.ts +58 -0
- package/dist/resources/definition-revisions.d.ts.map +1 -0
- package/dist/resources/definition-revisions.js +110 -0
- package/dist/resources/definitions.d.ts +24 -0
- package/dist/resources/definitions.d.ts.map +1 -0
- package/dist/resources/definitions.js +51 -0
- package/dist/resources/generate.d.ts +5 -5
- package/dist/resources/generate.d.ts.map +1 -1
- package/dist/resources/generate.js +3 -3
- package/dist/resources/generations.d.ts +3 -3
- package/dist/resources/generations.d.ts.map +1 -1
- package/dist/resources/generations.js +1 -1
- package/dist/resources/projects.d.ts +66 -26
- package/dist/resources/projects.d.ts.map +1 -1
- package/dist/resources/projects.js +87 -13
- package/dist/resources/targets.d.ts +86 -0
- package/dist/resources/targets.d.ts.map +1 -0
- package/dist/resources/targets.js +184 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +119 -62
- package/dist/types.d.ts +1761 -222
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -3
- package/package.json +1 -1
- package/src/core/http.ts +75 -293
- package/src/core/pagination.ts +6 -30
- package/src/dates.ts +0 -1
- package/src/docs.ts +101 -0
- package/src/errors.ts +30 -30
- package/src/index.ts +23 -13
- package/src/mcp-protocol.ts +170 -120
- package/src/mcp.ts +22 -22
- package/src/ops.ts +43 -17
- package/src/resources/account.ts +3 -3
- package/src/resources/api-keys.ts +22 -7
- package/src/resources/definition-revisions.ts +198 -0
- package/src/resources/definitions.ts +97 -0
- package/src/resources/generate.ts +6 -6
- package/src/resources/generations.ts +4 -4
- package/src/resources/projects.ts +182 -37
- package/src/resources/targets.ts +346 -0
- package/src/schemas.ts +119 -62
- package/src/types.ts +1947 -281
- package/dist/resources/spec-revisions.d.ts +0 -47
- package/dist/resources/spec-revisions.d.ts.map +0 -1
- package/dist/resources/spec-revisions.js +0 -90
- package/src/resources/spec-revisions.ts +0 -150
package/src/mcp-protocol.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { dateKindOf, relativeDate } from "./dates.js";
|
|
19
|
+
import { resolveDocsContentUrl } from "./docs.js";
|
|
19
20
|
|
|
20
21
|
export const MCP_PROTOCOL_VERSION = "2026-07-28";
|
|
21
22
|
/** Revisions served. Legacy (initialize-handshake) revisions are not; an
|
|
@@ -154,6 +155,10 @@ export interface McpServer {
|
|
|
154
155
|
* -32602), a ToolOutcome otherwise. Throwing yields -32603.
|
|
155
156
|
*/
|
|
156
157
|
callTool(name: string, args: Record<string, unknown>): Promise<ToolOutcome | undefined>;
|
|
158
|
+
/** Optional actionable wording for an unknown tool name. The protocol
|
|
159
|
+
* code remains -32602; compact tool surfaces can explain how to call an
|
|
160
|
+
* operation without pretending that operation is absent. */
|
|
161
|
+
unknownToolMessage?(name: string): string;
|
|
157
162
|
/** Called before a tool runs; a non-null outcome is sent instead
|
|
158
163
|
* (rate limiting, entitlement). */
|
|
159
164
|
beforeToolCall?(name: string): Promise<RpcOutcome | null> | RpcOutcome | null;
|
|
@@ -311,7 +316,7 @@ export async function handleRpc(server: McpServer, incoming: unknown): Promise<R
|
|
|
311
316
|
if (denied) return denied;
|
|
312
317
|
const started = Date.now();
|
|
313
318
|
const outcome = await server.callTool(name, args);
|
|
314
|
-
if (outcome === undefined) return rpcError(id, -32602, "Unknown tool: " + name, 400);
|
|
319
|
+
if (outcome === undefined) return rpcError(id, -32602, server.unknownToolMessage?.(name) ?? "Unknown tool: " + name, 400);
|
|
315
320
|
if (server.afterToolCall) await server.afterToolCall(name, outcome, Date.now() - started);
|
|
316
321
|
// A tool that declares an outputSchema MUST return structuredContent,
|
|
317
322
|
// and clients enforce it: a successful result without it fails the
|
|
@@ -396,7 +401,16 @@ export function exampleFromSchema(schema: unknown, field = "value", depth = 0):
|
|
|
396
401
|
if (node.example !== undefined) return node.example;
|
|
397
402
|
if (Array.isArray(node.examples) && node.examples.length > 0) return node.examples[0];
|
|
398
403
|
if (node.default !== undefined) return node.default;
|
|
399
|
-
if (Array.isArray(node.enum) && node.enum.length > 0)
|
|
404
|
+
if (Array.isArray(node.enum) && node.enum.length > 0) {
|
|
405
|
+
if (typeof node.pattern === "string") {
|
|
406
|
+
try {
|
|
407
|
+
const pattern = new RegExp(node.pattern);
|
|
408
|
+
const matching = node.enum.find((value) => typeof value === "string" && pattern.test(value));
|
|
409
|
+
if (matching !== undefined) return matching;
|
|
410
|
+
} catch { /* malformed patterns are ignored for examples */ }
|
|
411
|
+
}
|
|
412
|
+
return node.enum.find((v) => v !== null) ?? node.enum[0];
|
|
413
|
+
}
|
|
400
414
|
const variants = (Array.isArray(node.oneOf) ? node.oneOf : Array.isArray(node.anyOf) ? node.anyOf : null) as unknown[] | null;
|
|
401
415
|
if (variants) {
|
|
402
416
|
const useful = variants.find((v) => v && typeof v === "object" && (v as Record<string, unknown>).type !== "null") ?? variants[0];
|
|
@@ -432,7 +446,10 @@ export function exampleFromSchema(schema: unknown, field = "value", depth = 0):
|
|
|
432
446
|
if (type === "string" || type === undefined) {
|
|
433
447
|
const format = typeof node.format === "string" ? node.format : "";
|
|
434
448
|
const lower = field.toLowerCase();
|
|
435
|
-
|
|
449
|
+
const min = typeof node.minLength === "number" ? node.minLength : 0;
|
|
450
|
+
const max = typeof node.maxLength === "number" ? node.maxLength : undefined;
|
|
451
|
+
const patterned = typeof node.pattern === "string" ? exampleMatchingPattern(node.pattern, min, max) : null;
|
|
452
|
+
let value = patterned ?? (format === "date-time" ? "2026-01-15T12:00:00Z"
|
|
436
453
|
: format === "date" ? "2026-01-15"
|
|
437
454
|
: format === "email" || lower.includes("email") ? "person@example.com"
|
|
438
455
|
: (format === "uri" || format === "url" || lower.endsWith("url")) && (lower.includes("webhook") || lower.includes("callback")) ? "https://example.com/webhook"
|
|
@@ -443,11 +460,35 @@ export function exampleFromSchema(schema: unknown, field = "value", depth = 0):
|
|
|
443
460
|
: lower.includes("version") ? "1.0.0"
|
|
444
461
|
: /(^|_)id$|Id$/.test(field) ? (lower === "id" ? "id" : field.replace(/[_-]?id$/i, "")) + "_123"
|
|
445
462
|
: lower.includes("name") ? "example"
|
|
446
|
-
: "value";
|
|
447
|
-
const min = typeof node.minLength === "number" ? node.minLength : 0;
|
|
463
|
+
: "value");
|
|
448
464
|
while (value.length < min) value += "x";
|
|
449
|
-
if (
|
|
465
|
+
if (max !== undefined) value = value.slice(0, max);
|
|
466
|
+
return value;
|
|
467
|
+
}
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** A useful value for the common API-id pattern (`^agt_`, `^src_[a-z0-9]+$`).
|
|
472
|
+
* Full regex generation would be surprising and heavyweight; an anchored
|
|
473
|
+
* literal prefix plus ordinary id suffix covers the schemas that use a
|
|
474
|
+
* pattern to communicate a typed identifier. Every candidate is checked by
|
|
475
|
+
* the actual RegExp before it is returned. */
|
|
476
|
+
function exampleMatchingPattern(pattern: string, minLength: number, maxLength?: number): string | null {
|
|
477
|
+
let regex: RegExp;
|
|
478
|
+
try { regex = new RegExp(pattern); } catch { return null; }
|
|
479
|
+
const match = /^\^((?:\\.|[A-Za-z0-9_-])+)/.exec(pattern);
|
|
480
|
+
const prefix = match?.[1]?.replace(/\\(.)/g, "$1") ?? "";
|
|
481
|
+
const fit = (candidate: string): string => {
|
|
482
|
+
let value = candidate;
|
|
483
|
+
while (value.length < minLength) value += "x";
|
|
484
|
+
if (maxLength !== undefined) value = value.slice(0, maxLength);
|
|
450
485
|
return value;
|
|
486
|
+
};
|
|
487
|
+
const candidates = [prefix + "123", prefix + "example", prefix, "example", "value"];
|
|
488
|
+
for (const candidate of candidates) {
|
|
489
|
+
const value = fit(candidate);
|
|
490
|
+
regex.lastIndex = 0;
|
|
491
|
+
if (regex.test(value)) return value;
|
|
451
492
|
}
|
|
452
493
|
return null;
|
|
453
494
|
}
|
|
@@ -523,7 +564,7 @@ export const EXECUTE_TOOL: ToolDefinition = {
|
|
|
523
564
|
inputSchema: {
|
|
524
565
|
type: "object",
|
|
525
566
|
properties: {
|
|
526
|
-
operation: { type: "string", description: "Operation tool name
|
|
567
|
+
operation: { type: "string", description: "Operation tool name returned by search_docs" },
|
|
527
568
|
arguments: { type: "object", description: "Operation arguments keyed by parameter name" },
|
|
528
569
|
confirm: { type: "boolean", description: "Required and must be true for destructive operations. Omit for reads and ordinary writes." },
|
|
529
570
|
},
|
|
@@ -569,42 +610,23 @@ export function parseIncludeList(value: string | undefined | null): string[] | u
|
|
|
569
610
|
* three-tool "meta" shape (search_docs, read_docs, execute) that keeps huge
|
|
570
611
|
* APIs from flooding an agent's context. Deterministic order.
|
|
571
612
|
*/
|
|
572
|
-
export function toolDefinitions(ops: OpLike[], mode: "operations" | "meta"): ToolDefinition[] {
|
|
613
|
+
export function toolDefinitions(ops: OpLike[], mode: "operations" | "meta", omittedOps: OpLike[] = []): ToolDefinition[] {
|
|
573
614
|
if (mode === "meta") {
|
|
615
|
+
const execute = structuredClone(EXECUTE_TOOL);
|
|
616
|
+
const operation = (execute.inputSchema.properties as Record<string, Record<string, unknown>>).operation!;
|
|
617
|
+
if (ops[0]) operation.examples = [ops[0].tool];
|
|
618
|
+
const coverage = omittedOps.length > 0
|
|
619
|
+
? " Generated " + ops.length + " of " + (ops.length + omittedOps.length) + " operations; search_docs names operations omitted by the plan limit."
|
|
620
|
+
: "";
|
|
574
621
|
return [
|
|
575
|
-
{ ...SEARCH_DOCS_TOOL, description: "Search this API's " + ops.length + " operations and, when a docs site is configured, its guides. Start here to find the operation you need." },
|
|
622
|
+
{ ...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 },
|
|
576
623
|
{ ...READ_DOCS_TOOL, description: "Read an operation's full reference (arguments, schemas, authentication, safety and example) by tool name, or a docs-site guide page." },
|
|
577
|
-
|
|
624
|
+
execute,
|
|
578
625
|
];
|
|
579
626
|
}
|
|
580
627
|
return [...ops.map(operationTool), SEARCH_DOCS_TOOL, READ_DOCS_TOOL];
|
|
581
628
|
}
|
|
582
629
|
|
|
583
|
-
/** How big a tools/list is, as agents pay for it. Characters of JSON and a
|
|
584
|
-
* rough token count (one token per four characters, the usual estimate for
|
|
585
|
-
* JSON). Generation and `<bin> mcp` report it. */
|
|
586
|
-
export function toolsListSize(tools: ToolDefinition[]): { tools: number; chars: number; approxTokens: number } {
|
|
587
|
-
const chars = JSON.stringify(tools).length;
|
|
588
|
-
return { tools: tools.length, chars, approxTokens: Math.round(chars / 4) };
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
/** Keep automatic per-operation discovery under roughly 10k tokens. The
|
|
592
|
-
* schema, not merely the operation count, determines what an agent pays. */
|
|
593
|
-
export const MCP_AUTO_MAX_TOOLS_LIST_CHARS = 40_000;
|
|
594
|
-
|
|
595
|
-
export function resolveToolMode(
|
|
596
|
-
ops: OpLike[],
|
|
597
|
-
requested: "operations" | "meta" | "auto" = "auto",
|
|
598
|
-
): { mode: "operations" | "meta"; tools: number; chars: number; approxTokens: number } {
|
|
599
|
-
if (requested === "operations" || requested === "meta") {
|
|
600
|
-
return { mode: requested, ...toolsListSize(toolDefinitions(ops, requested)) };
|
|
601
|
-
}
|
|
602
|
-
const operations = toolsListSize(toolDefinitions(ops, "operations"));
|
|
603
|
-
const mode = ops.length > 100 || operations.chars > MCP_AUTO_MAX_TOOLS_LIST_CHARS ? "meta" : "operations";
|
|
604
|
-
return mode === "operations"
|
|
605
|
-
? { mode, ...operations }
|
|
606
|
-
: { mode, ...toolsListSize(toolDefinitions(ops, mode)) };
|
|
607
|
-
}
|
|
608
630
|
|
|
609
631
|
/** Resolve an operation by tool name or dotted resource.method. */
|
|
610
632
|
export function findOperation(ops: OpLike[], wanted: string): OpLike | undefined {
|
|
@@ -623,6 +645,10 @@ export interface InstructionsInput {
|
|
|
623
645
|
title: string;
|
|
624
646
|
/** Operations the server serves (after read-only / include filtering). */
|
|
625
647
|
toolCount: number;
|
|
648
|
+
/** Operations present in the Definition but absent from this capped generation. */
|
|
649
|
+
omittedOps?: OpLike[];
|
|
650
|
+
/** Count generated before read-only or include filters narrow this server. */
|
|
651
|
+
generatedOperationCount?: number;
|
|
626
652
|
mode: "operations" | "meta";
|
|
627
653
|
readOnly?: boolean;
|
|
628
654
|
/** Write operations hidden by read-only mode. */
|
|
@@ -645,6 +671,10 @@ export function serverInstructions(input: InstructionsInput): string {
|
|
|
645
671
|
parts.push(input.mode === "meta"
|
|
646
672
|
? input.title + " as MCP tools: search_docs, read_docs and execute over " + input.toolCount + " operations. Start with search_docs to find an operation, read_docs <tool> for its full argument reference, then execute it by name. Destructive operations require confirm: true on execute."
|
|
647
673
|
: input.title + " as MCP tools: one tool per operation (" + input.toolCount + "), plus search_docs to find operations and read_docs <tool> for an operation's full argument reference.");
|
|
674
|
+
if (input.omittedOps?.length) {
|
|
675
|
+
const generated = input.generatedOperationCount ?? input.toolCount;
|
|
676
|
+
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
|
+
}
|
|
648
678
|
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).");
|
|
649
679
|
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.");
|
|
650
680
|
parts.push("Errors carry error, code, message, status, the API's body and next_steps.");
|
|
@@ -920,6 +950,9 @@ export function projectFields(value: unknown, paths: string[][] | null): unknown
|
|
|
920
950
|
export interface ResultOptions {
|
|
921
951
|
fields?: string[][] | null;
|
|
922
952
|
maxChars?: number;
|
|
953
|
+
/** Request identifier for a page that is reshaped into the MCP paging
|
|
954
|
+
* envelope. Ordinary object results already retain their body field. */
|
|
955
|
+
requestId?: string;
|
|
923
956
|
/** For paginated results that must be cut: the op's pagination config and
|
|
924
957
|
* the call's arguments let some styles resume exactly where the cut fell. */
|
|
925
958
|
pagination?: OpLike["pagination"];
|
|
@@ -951,13 +984,18 @@ export function pageOutcome(items: unknown[], nextPage: Record<string, unknown>
|
|
|
951
984
|
const fields = options.fields ?? null;
|
|
952
985
|
const maxChars = options.maxChars ?? DEFAULT_MAX_RESULT_CHARS;
|
|
953
986
|
const shown = projectFields(items, fields) as unknown[];
|
|
954
|
-
const full = {
|
|
987
|
+
const full = {
|
|
988
|
+
items: shown,
|
|
989
|
+
hasMore: nextPage !== null,
|
|
990
|
+
...(nextPage !== null ? { nextPage } : {}),
|
|
991
|
+
...(options.requestId ? { request_id: options.requestId } : {}),
|
|
992
|
+
};
|
|
955
993
|
const text = JSON.stringify(full);
|
|
956
994
|
if (text.length <= maxChars) {
|
|
957
995
|
return { text, isError: false, structured: full };
|
|
958
996
|
}
|
|
959
997
|
|
|
960
|
-
const overhead = JSON.stringify({ items: [], hasMore: true, nextPage: nextPage ?? {}, truncated: { omitted: 0, of: 0, reason: "x".repeat(160), next_steps: ["x".repeat(220), "x".repeat(120)] } }).length;
|
|
998
|
+
const overhead = JSON.stringify({ items: [], hasMore: true, nextPage: nextPage ?? {}, ...(options.requestId ? { request_id: options.requestId } : {}), truncated: { omitted: 0, of: 0, reason: "x".repeat(160), next_steps: ["x".repeat(220), "x".repeat(120)] } }).length;
|
|
961
999
|
const k = itemsThatFit(shown, Math.max(0, maxChars - overhead));
|
|
962
1000
|
const omitted = shown.length - k;
|
|
963
1001
|
const pg = options.pagination;
|
|
@@ -981,6 +1019,7 @@ export function pageOutcome(items: unknown[], nextPage: Record<string, unknown>
|
|
|
981
1019
|
items: shown.slice(0, k),
|
|
982
1020
|
hasMore: resume !== null ? true : nextPage !== null,
|
|
983
1021
|
...(resume !== null ? { nextPage: resume } : nextPage !== null ? { nextPage } : {}),
|
|
1022
|
+
...(options.requestId ? { request_id: options.requestId } : {}),
|
|
984
1023
|
truncated: {
|
|
985
1024
|
omitted,
|
|
986
1025
|
of: shown.length,
|
|
@@ -1123,7 +1162,7 @@ export async function binaryOutcome(blob: Blob, options: BinaryOptions = {}): Pr
|
|
|
1123
1162
|
* generated CLI's error envelope). Additive only. */
|
|
1124
1163
|
export type ErrorCode =
|
|
1125
1164
|
| "NO_AUTH" | "AUTH_INVALID" | "PLAN_LIMIT" | "NOT_FOUND" | "INVALID_REQUEST" | "RATE_LIMITED"
|
|
1126
|
-
| "SERVER_ERROR" | "NETWORK_ERROR" | "VALIDATION_FAILED" | "INVALID_ARGUMENTS" | "CONFIRMATION_REQUIRED" | "NOT_AVAILABLE" | "CALL_FAILED";
|
|
1165
|
+
| "SPEC_INVALID" | "SERVER_ERROR" | "NETWORK_ERROR" | "VALIDATION_FAILED" | "INVALID_ARGUMENTS" | "CONFIRMATION_REQUIRED" | "NOT_AVAILABLE" | "CALL_FAILED";
|
|
1127
1166
|
|
|
1128
1167
|
export interface ErrorContext {
|
|
1129
1168
|
/** One sentence on how to supply a credential on this transport. */
|
|
@@ -1151,6 +1190,7 @@ export function classifyError(error: unknown, context: ErrorContext = {}): { cod
|
|
|
1151
1190
|
return { code: "NETWORK_ERROR", nextSteps: ["The API could not be reached (network, DNS, TLS or timeout). Retry once with backoff; do not loop."] };
|
|
1152
1191
|
}
|
|
1153
1192
|
const status = typeof e.status === "number" ? e.status : 0;
|
|
1193
|
+
const body = e.body as { errors?: { code?: string; message?: string }[] } | undefined;
|
|
1154
1194
|
const auth = context.authHint ? context.authHint.trim().replace(/[.]?$/, ".") : null;
|
|
1155
1195
|
if (status === 401) {
|
|
1156
1196
|
return context.hadCredential
|
|
@@ -1164,6 +1204,15 @@ export function classifyError(error: unknown, context: ErrorContext = {}): { cod
|
|
|
1164
1204
|
const retryAfter = extractRetryAfter(e);
|
|
1165
1205
|
return { code: "RATE_LIMITED", nextSteps: [retryAfter ? "Wait " + retryAfter + " seconds, then call again." : "Back off and retry once; the request was already retried with the server's Retry-After."] };
|
|
1166
1206
|
}
|
|
1207
|
+
if (status === 422 && body?.errors?.[0]?.code === "spec_error") {
|
|
1208
|
+
return {
|
|
1209
|
+
code: "SPEC_INVALID",
|
|
1210
|
+
nextSteps: [
|
|
1211
|
+
"The API rejected the spec it was given; its error message names the invalid part.",
|
|
1212
|
+
context.docsUrl ? "Use search_docs with the error message, fix the spec, then call again." : "Fix the spec, then call again.",
|
|
1213
|
+
],
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1167
1216
|
if (status === 400 || status === 409 || status === 413 || status === 422) {
|
|
1168
1217
|
return { code: "INVALID_REQUEST", nextSteps: ["Read body for the field the API named, fix that argument and call again."] };
|
|
1169
1218
|
}
|
|
@@ -1189,13 +1238,18 @@ function notFoundNextSteps(message: string, body: unknown): string[] {
|
|
|
1189
1238
|
/** A typed API error as the agent should see it: name, a stable code, the
|
|
1190
1239
|
* message, status, the API's body, where to read more, and what to do. */
|
|
1191
1240
|
export function errorOutcome(error: unknown, context: ErrorContext = {}): ToolOutcome {
|
|
1192
|
-
const e = error as { name?: string; message?: string; status?: number; body?: unknown };
|
|
1241
|
+
const e = error as { name?: string; message?: string; status?: number; body?: unknown; response?: { requestId?: string } };
|
|
1193
1242
|
const { code, nextSteps } = classifyError(error, context);
|
|
1243
|
+
const bodyRequestId = e?.body && typeof e.body === "object" && !Array.isArray(e.body)
|
|
1244
|
+
? (e.body as Record<string, unknown>).request_id ?? (e.body as Record<string, unknown>).requestId
|
|
1245
|
+
: undefined;
|
|
1246
|
+
const requestId = e?.response?.requestId ?? (typeof bodyRequestId === "string" ? bodyRequestId : undefined);
|
|
1194
1247
|
const structured = {
|
|
1195
1248
|
error: e?.name ?? "Error",
|
|
1196
1249
|
code,
|
|
1197
1250
|
message: e?.message,
|
|
1198
1251
|
...(typeof e?.status === "number" ? { status: e.status } : {}),
|
|
1252
|
+
...(requestId ? { request_id: requestId } : {}),
|
|
1199
1253
|
...(e?.body !== undefined ? { body: e.body } : {}),
|
|
1200
1254
|
...(context.docsUrl ? { docs_url: context.docsUrl } : {}),
|
|
1201
1255
|
next_steps: nextSteps,
|
|
@@ -1212,36 +1266,44 @@ export function textError(text: string, code: ErrorCode = "CALL_FAILED", nextSte
|
|
|
1212
1266
|
|
|
1213
1267
|
export interface DocsSource {
|
|
1214
1268
|
ops: OpLike[];
|
|
1269
|
+
/** Operations omitted from a capped generation. */
|
|
1270
|
+
omittedOps?: OpLike[];
|
|
1271
|
+
/** Count generated before runtime surface filters. */
|
|
1272
|
+
generatedOperationCount?: number;
|
|
1215
1273
|
/** Base URL of the docs site, or null when none is configured. */
|
|
1216
1274
|
docsUrl(): string | null;
|
|
1275
|
+
/** Exact llms.txt URL when it is not at <docsUrl>/llms.txt. */
|
|
1276
|
+
docsIndexUrl?(): string | null;
|
|
1217
1277
|
/** Fetch a URL's text, or null on any failure. */
|
|
1218
1278
|
fetchText(url: string): Promise<string | null>;
|
|
1219
1279
|
}
|
|
1220
1280
|
|
|
1221
|
-
function docsPageUrl(source: DocsSource, pathOrFile: string): string | null {
|
|
1222
|
-
const base = source.docsUrl();
|
|
1223
|
-
if (base === null) return null;
|
|
1224
|
-
try {
|
|
1225
|
-
const baseUrl = new URL(base);
|
|
1226
|
-
if (baseUrl.protocol !== "https:" && baseUrl.protocol !== "http:") return null;
|
|
1227
|
-
const target = /^https?:\/\//.test(pathOrFile)
|
|
1228
|
-
? new URL(pathOrFile)
|
|
1229
|
-
: new URL(pathOrFile.replace(/^\/+/, ""), baseUrl.toString().replace(/\/+$/, "") + "/");
|
|
1230
|
-
// llms.txt commonly contains absolute links, but a docs tool is not a
|
|
1231
|
-
// general-purpose URL fetcher. Keeping every page on the configured
|
|
1232
|
-
// origin prevents an agent from turning hosted MCP into an SSRF proxy.
|
|
1233
|
-
if (target.origin !== baseUrl.origin || target.username || target.password) return null;
|
|
1234
|
-
return target.toString();
|
|
1235
|
-
} catch {
|
|
1236
|
-
return null;
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
1281
|
async function fetchDocs(source: DocsSource, pathOrFile: string): Promise<string | null> {
|
|
1241
|
-
const url =
|
|
1282
|
+
const url = resolveDocsContentUrl(source.docsUrl(), source.docsIndexUrl?.() ?? null, pathOrFile);
|
|
1242
1283
|
return url === null ? null : source.fetchText(url);
|
|
1243
1284
|
}
|
|
1244
1285
|
|
|
1286
|
+
function coverageText(source: DocsSource): string | null {
|
|
1287
|
+
const omitted = source.omittedOps ?? [];
|
|
1288
|
+
const generated = source.generatedOperationCount ?? source.ops.length;
|
|
1289
|
+
return omitted.length > 0
|
|
1290
|
+
? "Coverage: generated " + generated + " of " + (generated + omitted.length) + " operations. Omitted by the plan limit: " + omitted.map((op) => op.tool + " (" + op.httpMethod + " " + op.path + ")").join(", ") + "."
|
|
1291
|
+
: null;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function omittedPlanLimit(ops: OpLike[], requested?: string): ToolOutcome {
|
|
1295
|
+
const structured = {
|
|
1296
|
+
error: "PlanLimitError",
|
|
1297
|
+
code: "PLAN_LIMIT",
|
|
1298
|
+
message: requested
|
|
1299
|
+
? "The operation " + requested + " exists in the API Definition but was omitted from this generated package by its plan limit."
|
|
1300
|
+
: "Matching operations exist in the API Definition but were omitted from this generated package by its plan limit.",
|
|
1301
|
+
omitted_operations: ops.map((op) => ({ tool: op.tool, method: op.httpMethod, path: op.path })),
|
|
1302
|
+
next_steps: ["Upgrade at https://typeship.dev/pricing and regenerate the package without the operation cap.", "Do not invent or retry an omitted operation against this generated package."],
|
|
1303
|
+
};
|
|
1304
|
+
return { text: JSON.stringify(structured), isError: true, structured };
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1245
1307
|
export function referenceText(op: OpLike): string {
|
|
1246
1308
|
const safety = operationSafety(op);
|
|
1247
1309
|
const example = op.exampleArguments ?? exampleArgumentsFromSchema(op.inputSchema);
|
|
@@ -1306,12 +1368,19 @@ export function searchScore(op: OpLike, query: string): number {
|
|
|
1306
1368
|
}
|
|
1307
1369
|
|
|
1308
1370
|
export async function docsSearch(source: DocsSource, query: string, page = 1): Promise<ToolOutcome> {
|
|
1309
|
-
const term = query.toLowerCase();
|
|
1371
|
+
const term = query.trim().toLowerCase();
|
|
1372
|
+
const terms = searchTerms(query);
|
|
1310
1373
|
const sections: string[] = [];
|
|
1311
1374
|
const ranked = source.ops
|
|
1312
1375
|
.map((op) => ({ op, score: searchScore(op, query) }))
|
|
1313
1376
|
.filter((r) => r.score > 0)
|
|
1314
1377
|
.sort((a, b) => b.score - a.score || a.op.tool.localeCompare(b.op.tool));
|
|
1378
|
+
const omittedRanked = (source.omittedOps ?? [])
|
|
1379
|
+
.map((op) => ({ op, score: searchScore(op, query) }))
|
|
1380
|
+
.filter((result) => result.score > 0)
|
|
1381
|
+
.sort((a, b) => b.score - a.score || a.op.tool.localeCompare(b.op.tool));
|
|
1382
|
+
const exactOmitted = findOperation(source.omittedOps ?? [], query);
|
|
1383
|
+
if (exactOmitted) return omittedPlanLimit([exactOmitted], exactOmitted.tool);
|
|
1315
1384
|
const pageIndex = Math.max(1, Math.floor(page)) - 1;
|
|
1316
1385
|
const slice = ranked.slice(pageIndex * SEARCH_PAGE_SIZE, (pageIndex + 1) * SEARCH_PAGE_SIZE);
|
|
1317
1386
|
if (slice.length > 0) {
|
|
@@ -1323,29 +1392,54 @@ export async function docsSearch(source: DocsSource, query: string, page = 1): P
|
|
|
1323
1392
|
sections.push("No reference matches on page " + (pageIndex + 1) + "; there are " + Math.ceil(ranked.length / SEARCH_PAGE_SIZE) + " pages.");
|
|
1324
1393
|
}
|
|
1325
1394
|
const prose = await fetchDocs(source, "llms-full.txt");
|
|
1395
|
+
let proseMatchCount = 0;
|
|
1326
1396
|
if (prose !== null) {
|
|
1327
1397
|
let heading = "";
|
|
1328
|
-
const proseMatches: string[] = [];
|
|
1398
|
+
const proseMatches: { heading: string; excerpt: string; score: number }[] = [];
|
|
1329
1399
|
for (const line of prose.split("\n")) {
|
|
1330
1400
|
if (/^#{1,3} /.test(line)) heading = line.replace(/^#+ /, "").trim();
|
|
1331
|
-
else
|
|
1332
|
-
|
|
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
|
+
}
|
|
1333
1413
|
}
|
|
1334
1414
|
}
|
|
1335
|
-
|
|
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
|
+
}
|
|
1336
1420
|
}
|
|
1421
|
+
if (omittedRanked.length > 0 && ranked.length === 0 && proseMatchCount === 0) {
|
|
1422
|
+
return omittedPlanLimit(omittedRanked.map((result) => result.op));
|
|
1423
|
+
}
|
|
1424
|
+
if (omittedRanked.length > 0) {
|
|
1425
|
+
sections.push("Operations omitted by the plan limit:\n" + omittedRanked.slice(0, SEARCH_PAGE_SIZE).map((result) => "- " + result.op.tool + ": " + (result.op.summary ?? result.op.httpMethod + " " + result.op.path)).join("\n"));
|
|
1426
|
+
}
|
|
1427
|
+
const coverage = coverageText(source);
|
|
1337
1428
|
if (sections.length === 0) {
|
|
1338
1429
|
return {
|
|
1339
|
-
text: "No matches for: " + query + (source.docsUrl() === null ? " (
|
|
1430
|
+
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)" : ""),
|
|
1340
1431
|
isError: false,
|
|
1341
1432
|
};
|
|
1342
1433
|
}
|
|
1343
|
-
return { text: sections.join("\n\n"), isError: false };
|
|
1434
|
+
return { text: [...(coverage ? [coverage] : []), ...sections].join("\n\n"), isError: false };
|
|
1344
1435
|
}
|
|
1345
1436
|
|
|
1346
1437
|
export async function docsRead(source: DocsSource, page: string): Promise<ToolOutcome> {
|
|
1347
1438
|
const opMatch = findOperation(source.ops, page);
|
|
1348
|
-
|
|
1439
|
+
const coverage = coverageText(source);
|
|
1440
|
+
if (opMatch) return { text: [...(coverage ? [coverage] : []), referenceText(opMatch)].join("\n\n"), isError: false };
|
|
1441
|
+
const omittedMatch = findOperation(source.omittedOps ?? [], page);
|
|
1442
|
+
if (omittedMatch) return omittedPlanLimit([omittedMatch], omittedMatch.tool);
|
|
1349
1443
|
let target = page;
|
|
1350
1444
|
if (!/^https?:\/\//.test(target)) {
|
|
1351
1445
|
const index = await fetchDocs(source, "llms.txt");
|
|
@@ -1355,11 +1449,11 @@ export async function docsRead(source: DocsSource, page: string): Promise<ToolOu
|
|
|
1355
1449
|
}
|
|
1356
1450
|
const text = await fetchDocs(source, target);
|
|
1357
1451
|
if (text === null) {
|
|
1358
|
-
return textError(source.docsUrl() === null
|
|
1359
|
-
? "
|
|
1452
|
+
return textError(source.docsUrl() === null && (source.docsIndexUrl?.() ?? null) === null
|
|
1453
|
+
? "A docs URL was not provided at generate time, and no generated operation matches \"" + page + "\"."
|
|
1360
1454
|
: "Couldn't fetch \"" + page + "\". Use search_docs to find pages.", "NOT_FOUND", ["search_docs finds operations and guide pages."]);
|
|
1361
1455
|
}
|
|
1362
|
-
return { text, isError: false };
|
|
1456
|
+
return { text: [...(coverage ? [coverage] : []), text].join("\n\n"), isError: false };
|
|
1363
1457
|
}
|
|
1364
1458
|
|
|
1365
1459
|
/**
|
|
@@ -1385,7 +1479,12 @@ export async function callSharedTool(
|
|
|
1385
1479
|
if (name === "execute") {
|
|
1386
1480
|
if (typeof args.operation !== "string") return argumentsError({ tool: name }, [{ code: "MISSING_ARGUMENT", argument: "operation", message: "execute requires an operation name." }]);
|
|
1387
1481
|
const target = findOperation(source.ops, args.operation);
|
|
1388
|
-
if (!target)
|
|
1482
|
+
if (!target) {
|
|
1483
|
+
const omitted = findOperation(source.omittedOps ?? [], args.operation);
|
|
1484
|
+
return omitted
|
|
1485
|
+
? omittedPlanLimit([omitted], omitted.tool)
|
|
1486
|
+
: textError("Unknown operation: " + args.operation + ".", "NOT_FOUND", ["search_docs finds operations by name, path or description."]);
|
|
1487
|
+
}
|
|
1389
1488
|
if (operationSafety(target) === "destructive" && args.confirm !== true) {
|
|
1390
1489
|
return textError(
|
|
1391
1490
|
"The destructive operation " + target.tool + " requires explicit confirmation.",
|
|
@@ -1400,52 +1499,3 @@ export async function callSharedTool(
|
|
|
1400
1499
|
}
|
|
1401
1500
|
return undefined;
|
|
1402
1501
|
}
|
|
1403
|
-
|
|
1404
|
-
/** A fetch for docs pages: markdown preferred, same-origin redirects only,
|
|
1405
|
-
* a 10s deadline, and a 2 MB streaming cap. The caller already constrained
|
|
1406
|
-
* the first URL to its configured docs origin; redirects must not escape it. */
|
|
1407
|
-
export const MAX_DOCS_TEXT_BYTES = 2_000_000;
|
|
1408
|
-
export async function fetchDocsText(url: string): Promise<string | null> {
|
|
1409
|
-
try {
|
|
1410
|
-
const allowedOrigin = new URL(url).origin;
|
|
1411
|
-
let current = url;
|
|
1412
|
-
const signal = AbortSignal.timeout(10_000);
|
|
1413
|
-
for (let redirects = 0; redirects <= 3; redirects += 1) {
|
|
1414
|
-
const response = await fetch(current, {
|
|
1415
|
-
headers: { Accept: "text/markdown, text/plain, */*" },
|
|
1416
|
-
redirect: "manual",
|
|
1417
|
-
signal,
|
|
1418
|
-
});
|
|
1419
|
-
if (response.status >= 300 && response.status < 400) {
|
|
1420
|
-
const location = response.headers.get("location");
|
|
1421
|
-
if (!location || redirects === 3) return null;
|
|
1422
|
-
const next = new URL(location, current);
|
|
1423
|
-
if (next.origin !== allowedOrigin || next.username || next.password) return null;
|
|
1424
|
-
current = next.toString();
|
|
1425
|
-
continue;
|
|
1426
|
-
}
|
|
1427
|
-
if (!response.ok) return null;
|
|
1428
|
-
const declared = Number(response.headers.get("content-length"));
|
|
1429
|
-
if (Number.isFinite(declared) && declared > MAX_DOCS_TEXT_BYTES) return null;
|
|
1430
|
-
if (!response.body) return "";
|
|
1431
|
-
const reader = response.body.getReader();
|
|
1432
|
-
const decoder = new TextDecoder();
|
|
1433
|
-
let bytes = 0;
|
|
1434
|
-
let text = "";
|
|
1435
|
-
for (;;) {
|
|
1436
|
-
const chunk = await reader.read();
|
|
1437
|
-
if (chunk.done) break;
|
|
1438
|
-
bytes += chunk.value.byteLength;
|
|
1439
|
-
if (bytes > MAX_DOCS_TEXT_BYTES) {
|
|
1440
|
-
await reader.cancel();
|
|
1441
|
-
return null;
|
|
1442
|
-
}
|
|
1443
|
-
text += decoder.decode(chunk.value, { stream: true });
|
|
1444
|
-
}
|
|
1445
|
-
return text + decoder.decode();
|
|
1446
|
-
}
|
|
1447
|
-
return null;
|
|
1448
|
-
} catch {
|
|
1449
|
-
return null;
|
|
1450
|
-
}
|
|
1451
|
-
}
|
package/src/mcp.ts
CHANGED
|
@@ -20,19 +20,20 @@ import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
|
20
20
|
import { homedir, tmpdir } from "node:os";
|
|
21
21
|
import { basename, join } from "node:path";
|
|
22
22
|
import { fileURLToPath } from "node:url";
|
|
23
|
-
import { TypeshipClient, formatDebugEvent, type DebugEvent } from "./index.js";
|
|
24
|
-
import { GLOBALS, OPS, buildArgs, type OpSpec } from "./ops.js";
|
|
23
|
+
import { TypeshipClient, formatDebugEvent, type ClientOptions, type DebugEvent } from "./index.js";
|
|
24
|
+
import { GLOBALS, OMITTED_OPS, OPS, buildArgs, type OpSpec } from "./ops.js";
|
|
25
25
|
import {
|
|
26
26
|
DEFAULT_MAX_RESULT_CHARS, SUPPORTED_PROTOCOL_VERSIONS, argumentsError, asJsonRpc, binaryOutcome, callSharedTool, checkRequestHeaders,
|
|
27
|
-
dataOutcome, errorOutcome,
|
|
27
|
+
dataOutcome, errorOutcome, handleRpc, isRpcOutcome, pageOutcome, parseIncludeList, prepareCall, serverInstructions,
|
|
28
28
|
takeCancelled, textError, toolDefinitions, visibleOps,
|
|
29
29
|
type ArgumentIssue, type DocsSource, type McpServer, type OpLike, type RpcOutcome, type ToolOutcome,
|
|
30
30
|
} from "./mcp-protocol.js";
|
|
31
|
+
import { fetchDocsText } from "./docs.js";
|
|
31
32
|
|
|
32
33
|
const BIN = "typeship";
|
|
33
34
|
const PKG_NAME = "@typeship-ax/mcp";
|
|
34
35
|
const SERVER_NAME = "typeship-mcp";
|
|
35
|
-
const SERVER_VERSION = "0.
|
|
36
|
+
const SERVER_VERSION = "0.8.0";
|
|
36
37
|
/** The MCP client's announced name (clientInfo in request _meta), for the User-Agent. */
|
|
37
38
|
let MCP_CLIENT_NAME: string | null = null;
|
|
38
39
|
function noteClientInfo(message: unknown): void {
|
|
@@ -43,9 +44,10 @@ function noteClientInfo(message: unknown): void {
|
|
|
43
44
|
const DEFAULT_BASE_URL = "https://typeship.dev/api/v1";
|
|
44
45
|
const AUTH_SCALARS: { option: string; flag: string; env: string }[] = [{"option":"bearerToken","flag":"token","env":"TYPESHIP_TOKEN"}];
|
|
45
46
|
const BASIC: { envUser: string; envPass: string } | null = null;
|
|
46
|
-
|
|
47
|
+
|
|
47
48
|
const ENVIRONMENTS: Record<string, string> = {};
|
|
48
49
|
const DOCS_URL_DEFAULT: string | null = "https://typeship.dev";
|
|
50
|
+
const DOCS_INDEX_URL_DEFAULT: string | null = null;
|
|
49
51
|
/** "meta" collapses per-operation tools into search/read/execute so huge
|
|
50
52
|
* APIs don't flood agent context with hundreds of tools. */
|
|
51
53
|
const TOOL_MODE: "operations" | "meta" = "meta";
|
|
@@ -88,7 +90,6 @@ function makeClient(): TypeshipClient {
|
|
|
88
90
|
oauth?: { accessToken: string };
|
|
89
91
|
}>("credentials.json");
|
|
90
92
|
const config = readJson<{ baseUrl?: string; environment?: string }>("config.json");
|
|
91
|
-
const options: Record<string, unknown> = {};
|
|
92
93
|
const baseUrl = process.env["TYPESHIP_BASE_URL"]
|
|
93
94
|
?? config?.baseUrl
|
|
94
95
|
?? (config?.environment !== undefined ? ENVIRONMENTS[config.environment] : undefined)
|
|
@@ -98,7 +99,7 @@ function makeClient(): TypeshipClient {
|
|
|
98
99
|
// instead of a server that vanished mid-conversation.
|
|
99
100
|
throw new Error("No base URL configured: set TYPESHIP_BASE_URL in the MCP server's environment, or run '" + BIN + " config set base-url <url>'.");
|
|
100
101
|
}
|
|
101
|
-
options
|
|
102
|
+
const options: ClientOptions & Record<string, unknown> = { baseUrl };
|
|
102
103
|
for (const a of AUTH_SCALARS) {
|
|
103
104
|
const v = process.env[a.env] ?? stored?.scalars?.[a.option];
|
|
104
105
|
if (v !== undefined) options[a.option] = v;
|
|
@@ -108,16 +109,7 @@ function makeClient(): TypeshipClient {
|
|
|
108
109
|
const password = process.env[BASIC.envPass] ?? stored?.basic?.password;
|
|
109
110
|
if (username !== undefined && password !== undefined) options.basicAuth = { username, password };
|
|
110
111
|
}
|
|
111
|
-
|
|
112
|
-
const oauthClientSecret = process.env["TYPESHIP_CLIENT_SECRET"];
|
|
113
|
-
if ((oauthClientId === undefined) !== (oauthClientSecret === undefined)) {
|
|
114
|
-
throw new Error("OAuth client credentials are incomplete: set both TYPESHIP_CLIENT_ID and TYPESHIP_CLIENT_SECRET in the MCP server's environment.");
|
|
115
|
-
}
|
|
116
|
-
if (oauthClientId !== undefined && oauthClientSecret !== undefined) {
|
|
117
|
-
const tokenUrl = process.env["TYPESHIP_TOKEN_URL"] ?? OAUTH_TOKEN_URL ?? undefined;
|
|
118
|
-
if (!tokenUrl) throw new Error("OAuth client credentials need a token URL: set TYPESHIP_TOKEN_URL in the MCP server's environment.");
|
|
119
|
-
options.clientCredentials = { clientId: oauthClientId, clientSecret: oauthClientSecret, tokenUrl };
|
|
120
|
-
}
|
|
112
|
+
|
|
121
113
|
if (options.bearerToken === undefined && stored?.oauth?.accessToken !== undefined) {
|
|
122
114
|
options.bearerToken = stored.oauth.accessToken;
|
|
123
115
|
}
|
|
@@ -130,7 +122,7 @@ function makeClient(): TypeshipClient {
|
|
|
130
122
|
}
|
|
131
123
|
// The local MCP server identifies itself (surface + the client it serves, when announced).
|
|
132
124
|
options.defaultHeaders = { "User-Agent": PKG_NAME + "-mcp/" + SERVER_VERSION + " (typeship" + (MCP_CLIENT_NAME ? "; client=" + MCP_CLIENT_NAME : "") + ")" };
|
|
133
|
-
return new TypeshipClient(options
|
|
125
|
+
return new TypeshipClient(options);
|
|
134
126
|
}
|
|
135
127
|
|
|
136
128
|
let clientInstance: TypeshipClient | undefined;
|
|
@@ -180,11 +172,11 @@ async function callOperation(op: OpSpec, rawArgs: Record<string, unknown>, authH
|
|
|
180
172
|
const shape = { fields, maxChars, pagination: op.pagination, args };
|
|
181
173
|
try {
|
|
182
174
|
const target = (getClient() as unknown as Record<string, Record<string, (...a: unknown[]) => unknown>>)[op.resource]!;
|
|
183
|
-
const result = await (target[op.method]!(...callArgs) as Promise<{ ok: boolean; data?: unknown; error?: unknown }>);
|
|
175
|
+
const result = await (target[op.method]!(...callArgs) as Promise<{ ok: boolean; data?: unknown; error?: unknown; response?: { requestId?: string } }>);
|
|
184
176
|
if (!result.ok) return errorOutcome(result.error, errorContext);
|
|
185
177
|
if (op.paginated) {
|
|
186
|
-
const page = result.data as { items: unknown[]; nextPageParams(): Record<string, unknown> | null };
|
|
187
|
-
return pageOutcome(page.items, page.nextPageParams(), shape);
|
|
178
|
+
const page = result.data as { items: unknown[]; nextPageParams(): Record<string, unknown> | null; response: { requestId?: string } };
|
|
179
|
+
return pageOutcome(page.items, page.nextPageParams(), { ...shape, requestId: page.response.requestId });
|
|
188
180
|
}
|
|
189
181
|
// A binary body (the SDK hands back a Blob): an image block, or a file
|
|
190
182
|
// on disk, never "{}".
|
|
@@ -223,7 +215,10 @@ function saveBinary(bytes: Uint8Array, _mediaType: string, suggestedName: string
|
|
|
223
215
|
|
|
224
216
|
const docsSource: DocsSource = {
|
|
225
217
|
ops: MCP_OPS as unknown as OpLike[],
|
|
218
|
+
omittedOps: OMITTED_OPS as unknown as OpLike[],
|
|
219
|
+
generatedOperationCount: OPS.length,
|
|
226
220
|
docsUrl: () => readJson<{ docsUrl?: string }>("config.json")?.docsUrl ?? DOCS_URL_DEFAULT,
|
|
221
|
+
docsIndexUrl: () => readJson<{ docsUrl?: string }>("config.json")?.docsUrl ? null : DOCS_INDEX_URL_DEFAULT,
|
|
227
222
|
fetchText: fetchDocsText,
|
|
228
223
|
};
|
|
229
224
|
|
|
@@ -236,6 +231,8 @@ function serverFor(authHeader?: string): McpServer {
|
|
|
236
231
|
instructions: serverInstructions({
|
|
237
232
|
title: "typeship",
|
|
238
233
|
toolCount: MCP_OPS.length,
|
|
234
|
+
generatedOperationCount: OPS.length,
|
|
235
|
+
omittedOps: docsSource.omittedOps,
|
|
239
236
|
mode: TOOL_MODE,
|
|
240
237
|
readOnly: READ_ONLY,
|
|
241
238
|
hiddenWrites: HIDDEN_WRITES,
|
|
@@ -245,7 +242,10 @@ function serverFor(authHeader?: string): McpServer {
|
|
|
245
242
|
custom: CUSTOM_INSTRUCTIONS,
|
|
246
243
|
}),
|
|
247
244
|
toolsTtlMs: TOOLS_TTL_MS,
|
|
248
|
-
listTools: () => toolDefinitions(docsSource.ops, TOOL_MODE),
|
|
245
|
+
listTools: () => toolDefinitions(docsSource.ops, TOOL_MODE, docsSource.omittedOps),
|
|
246
|
+
unknownToolMessage: TOOL_MODE === "meta"
|
|
247
|
+
? (name) => "Unknown tool: " + name + ". This server uses compact mode; call search_docs to discover an operation, then call execute with operation: \"" + name + "\"."
|
|
248
|
+
: undefined,
|
|
249
249
|
callTool: async (name, args) => {
|
|
250
250
|
const shared = await callSharedTool(name, args, docsSource, (op, opArgs) => callOperation(op as OpSpec, opArgs, authHeader));
|
|
251
251
|
if (shared !== undefined) return shared;
|