diff --git a/packages/sdk/generated/domain-map.json b/packages/sdk/generated/domain-map.json index 8b280bb..69f5085 100644 --- a/packages/sdk/generated/domain-map.json +++ b/packages/sdk/generated/domain-map.json @@ -156,7 +156,38 @@ "each": true } ] - } + }, + "errors": [ + { + "name": "ValidationError", + "match": "VALIDATION_ERROR", + "schema": { + "type": "object", + "properties": { + "details": { + "type": "array", + "items": { + "type": "object", + "properties": { + "field": { "type": "string" }, + "errorCode": { "type": "string" } + } + } + } + } + } + }, + { + "name": "RateLimitError", + "match": "RATE_LIMITED", + "schema": { + "type": "object", + "properties": { + "retry_after_ms": { "type": "number" } + } + } + } + ] }, { "tool": "edit_screens", diff --git a/packages/sdk/generated/src/designsystem.ts b/packages/sdk/generated/src/designsystem.ts index c53efb3..7473041 100644 --- a/packages/sdk/generated/src/designsystem.ts +++ b/packages/sdk/generated/src/designsystem.ts @@ -3,7 +3,7 @@ DO NOT EDIT — changes will be overwritten. Source: tools-manifest.json (sha256:88ec3dfa066c...) - domain-map.json (sha256:a6177cc7e2f4...) + domain-map.json (sha256:46e20fc68c7b...) */ import { type StitchToolClientSpec } from "../../src/spec/client.js"; import { StitchError } from "../../src/spec/errors.js"; diff --git a/packages/sdk/generated/src/index.ts b/packages/sdk/generated/src/index.ts index 24cfa1c..2eb3cb6 100644 --- a/packages/sdk/generated/src/index.ts +++ b/packages/sdk/generated/src/index.ts @@ -3,7 +3,7 @@ DO NOT EDIT — changes will be overwritten. Source: tools-manifest.json (sha256:88ec3dfa066c...) - domain-map.json (sha256:a6177cc7e2f4...) + domain-map.json (sha256:46e20fc68c7b...) */ export { Stitch } from "./stitch.js"; export { Project } from "./project.js"; @@ -16,4 +16,4 @@ export { type ToolPropertySchema, } from "./tool-definitions.js"; export type * from "./types.generated.js"; -export type * from "./responses.generated.js"; +export * from "./responses.generated.js"; diff --git a/packages/sdk/generated/src/project.ts b/packages/sdk/generated/src/project.ts index 04ef45a..d436824 100644 --- a/packages/sdk/generated/src/project.ts +++ b/packages/sdk/generated/src/project.ts @@ -3,7 +3,7 @@ DO NOT EDIT — changes will be overwritten. Source: tools-manifest.json (sha256:88ec3dfa066c...) - domain-map.json (sha256:a6177cc7e2f4...) + domain-map.json (sha256:46e20fc68c7b...) */ import { type StitchToolClientSpec } from "../../src/spec/client.js"; import { StitchError } from "../../src/spec/errors.js"; @@ -41,6 +41,10 @@ import { } from "./types.generated.js"; import { GenerateScreenFromTextResponse, + ValidationError, + ValidationErrorSchema, + RateLimitError, + RateLimitErrorSchema, ListScreensResponse, GetScreenResponse, CreateDesignSystemResponse, @@ -162,6 +166,30 @@ export class Project { deviceType: options?.deviceType ?? "DESKTOP", modelId: options?.modelId, }, + { + ValidationError: { + schema: ValidationErrorSchema, + match: "VALIDATION_ERROR", + create: (data: any, raw: any) => + new ValidationError( + "Tool Call Failed [generate_screen_from_text]: " + + (ValidationErrorSchema.description || "VALIDATION_ERROR"), + data, + raw, + ), + }, + RateLimitError: { + schema: RateLimitErrorSchema, + match: "RATE_LIMITED", + create: (data: any, raw: any) => + new RateLimitError( + "Tool Call Failed [generate_screen_from_text]: " + + (RateLimitErrorSchema.description || "RATE_LIMITED"), + data, + raw, + ), + }, + }, ); const _screens = ( (raw.outputComponents || []).flatMap( diff --git a/packages/sdk/generated/src/responses.generated.ts b/packages/sdk/generated/src/responses.generated.ts index 40bb79b..2682789 100644 --- a/packages/sdk/generated/src/responses.generated.ts +++ b/packages/sdk/generated/src/responses.generated.ts @@ -28,12 +28,50 @@ import { VariantOptions, SelectedScreenInstance, } from "./types.generated.js"; +import { z } from "zod"; +import { StitchError } from "../../src/spec/errors.js"; + +export const ValidationErrorSchema = z.object({ + details: z.array(z.object({ field: z.string(), errorCode: z.string() })), +}); +export type ValidationErrorPayload = z.infer; + +export interface ValidationError extends ValidationErrorPayload {} +export class ValidationError extends StitchError { + constructor(message: string, payload: ValidationErrorPayload, raw?: unknown) { + super({ + code: "VALIDATION_ERROR" as any, + message, + recoverable: false, + raw, + }); + this.name = "ValidationError"; + Object.assign(this, payload); + } +} + +export const RateLimitErrorSchema = z.object({ retry_after_ms: z.number() }); +export type RateLimitErrorPayload = z.infer; + +export interface RateLimitError extends RateLimitErrorPayload {} +export class RateLimitError extends StitchError { + constructor(message: string, payload: RateLimitErrorPayload, raw?: unknown) { + super({ + code: "RATE_LIMITED" as any, + message, + recoverable: true, + raw, + }); + this.name = "RateLimitError"; + Object.assign(this, payload); + } +} /** * AUTO-GENERATED by scripts/generate-sdk.ts DO NOT EDIT — changes will be overwritten. Source: tools-manifest.json (sha256:88ec3dfa066c...) - domain-map.json (sha256:a6177cc7e2f4...) + domain-map.json (sha256:46e20fc68c7b...) */ /** Response message for create_project. */ diff --git a/packages/sdk/generated/src/screen.ts b/packages/sdk/generated/src/screen.ts index 738f4ad..189ee1a 100644 --- a/packages/sdk/generated/src/screen.ts +++ b/packages/sdk/generated/src/screen.ts @@ -3,7 +3,7 @@ DO NOT EDIT — changes will be overwritten. Source: tools-manifest.json (sha256:88ec3dfa066c...) - domain-map.json (sha256:a6177cc7e2f4...) + domain-map.json (sha256:46e20fc68c7b...) */ import { type StitchToolClientSpec } from "../../src/spec/client.js"; import { StitchError } from "../../src/spec/errors.js"; diff --git a/packages/sdk/generated/src/stitch.ts b/packages/sdk/generated/src/stitch.ts index 65ddd51..7abc955 100644 --- a/packages/sdk/generated/src/stitch.ts +++ b/packages/sdk/generated/src/stitch.ts @@ -3,7 +3,7 @@ DO NOT EDIT — changes will be overwritten. Source: tools-manifest.json (sha256:88ec3dfa066c...) - domain-map.json (sha256:a6177cc7e2f4...) + domain-map.json (sha256:46e20fc68c7b...) */ import { type StitchToolClientSpec } from "../../src/spec/client.js"; import { StitchError } from "../../src/spec/errors.js"; diff --git a/packages/sdk/generated/src/tool-definitions.ts b/packages/sdk/generated/src/tool-definitions.ts index 1bdbc83..52b3436 100644 --- a/packages/sdk/generated/src/tool-definitions.ts +++ b/packages/sdk/generated/src/tool-definitions.ts @@ -3,7 +3,7 @@ DO NOT EDIT — changes will be overwritten. Source: tools-manifest.json (sha256:88ec3dfa066c...) - domain-map.json (sha256:a6177cc7e2f4...) + domain-map.json (sha256:46e20fc68c7b...) */ /** JSON Schema property descriptor for a tool parameter. */ export interface ToolPropertySchema { diff --git a/packages/sdk/generated/src/types.generated.ts b/packages/sdk/generated/src/types.generated.ts index 06a680a..a0e6bcf 100644 --- a/packages/sdk/generated/src/types.generated.ts +++ b/packages/sdk/generated/src/types.generated.ts @@ -3,7 +3,7 @@ DO NOT EDIT — changes will be overwritten. Source: tools-manifest.json (sha256:88ec3dfa066c...) - domain-map.json (sha256:a6177cc7e2f4...) + domain-map.json (sha256:46e20fc68c7b...) */ /** A component style token in a design system. */ diff --git a/packages/sdk/generated/stitch-sdk.lock b/packages/sdk/generated/stitch-sdk.lock index 685c2cb..0d668f9 100644 --- a/packages/sdk/generated/stitch-sdk.lock +++ b/packages/sdk/generated/stitch-sdk.lock @@ -1,10 +1,10 @@ { "schemaVersion": 1, "generated": { - "generatedAt": "2026-09-25T04:45:34.219Z", - "sourceHash": "sha256:aad79acad078a250f95d5e58767a9082c41be37d4f5565bf307c96f1cd654b38", + "generatedAt": "2026-09-25T04:57:37.343Z", + "sourceHash": "sha256:2cb353043fd99ef56818f90968eff1060ec95888bc7cf6a45d7b4a11e232eca2", "manifestHash": "sha256:88ec3dfa066ce5060be236b0a5c0b5fb51671a7dd60d05556c028e6c4c8f0e1c", - "domainMapHash": "sha256:a6177cc7e2f44d2d536928d4e0ae7bd51ed2dd3393235787216b976498c3b1b2", + "domainMapHash": "sha256:46e20fc68c7bc123f2545680389e4a70792fb1df48959f6d19aa8110a829bb1d", "fileCount": 8, "repairedTools": [ "generate_screen_from_text", @@ -13,8 +13,8 @@ ] }, "domainMap": { - "generatedAt": "2026-09-23T18:36:09.460Z", - "sourceHash": "sha256:a6177cc7e2f44d2d536928d4e0ae7bd51ed2dd3393235787216b976498c3b1b2", + "generatedAt": "2026-09-25T04:57:37.343Z", + "sourceHash": "sha256:46e20fc68c7bc123f2545680389e4a70792fb1df48959f6d19aa8110a829bb1d", "manifestHash": "sha256:88ec3dfa066ce5060be236b0a5c0b5fb51671a7dd60d05556c028e6c4c8f0e1c", "classCount": 4, "bindingCount": 15 diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts index 9df4c5d..86b22e1 100644 --- a/packages/sdk/src/client.ts +++ b/packages/sdk/src/client.ts @@ -312,8 +312,67 @@ export class StitchToolClient implements StitchToolClientSpec { }; } - private parseToolResponse(result: any, name: string): T { - return parseToolResult(result, name); + private parseToolResponse( + result: any, + name: string, + errorMap?: Record< + string, + { schema: any; match: string; create: (data: any, raw: any) => Error } + >, + ): T { + if (result.isError) { + const errorText = (result.content as any[]) + .map((c: any) => (c.type === "text" ? c.text : "")) + .join(""); + + let errorJson: any = null; + try { + errorJson = JSON.parse(errorText); + } catch (e) {} + + if (errorMap) { + for (const [errName, errDef] of Object.entries(errorMap)) { + if ( + errorText.includes(errDef.match) || + (errorJson && JSON.stringify(errorJson).includes(errDef.match)) + ) { + if (errorJson) { + const parsed = errDef.schema.safeParse(errorJson); + if (parsed.success) { + throw errDef.create(parsed.data, result); + } + } + } + } + } + + const code = classifyError({ text: errorText }); + + throw new StitchError({ + code, + message: `Tool Call Failed [${name}]: ${errorText}`, + recoverable: isRecoverable(code), + toolName: name, + raw: result, + }); + } + + // Stitch specific parsing: Check structuredContent first, then JSON in text + const anyResult = result as any; + if (anyResult.structuredContent) return anyResult.structuredContent as T; + + const textContent = (result.content as any[]).find( + (c: any) => c.type === "text", + ); + if (textContent && textContent.type === "text") { + try { + return JSON.parse(textContent.text) as T; + } catch { + return textContent.text as unknown as T; + } + } + + return anyResult as T; } async connect() { @@ -390,7 +449,14 @@ export class StitchToolClient implements StitchToolClientSpec { * with exponential backoff + full jitter, per `config.retry`. MCP text * errors carry no Retry-After header, so nothing else is honored. */ - async callTool(name: string, args: Record): Promise { + async callTool( + name: string, + args: Record, + errorMap?: Record< + string, + { schema: any; match: string; create: (data: any, raw: any) => Error } + >, + ): Promise { this.assertNotClosed(); if (!this.isConnected) await this.connect(); @@ -416,7 +482,7 @@ export class StitchToolClient implements StitchToolClientSpec { undefined, { timeout: this.config.timeout }, ); - return this.parseToolResponse(result, name); + return this.parseToolResponse(result, name, errorMap); } catch (rawErr) { // Normalize transport HTTP / network errors first, so a real 429 or // transient socket reset is classified and retry-eligible. @@ -500,6 +566,8 @@ export class StitchToolClient implements StitchToolClientSpec { code, message: `HTTP ${response.status}: ${text || response.statusText}`, recoverable: isRecoverable(code), + status: response.status, + raw: text, }); } diff --git a/packages/sdk/src/spec/client.ts b/packages/sdk/src/spec/client.ts index 546ec59..094d8a4 100644 --- a/packages/sdk/src/spec/client.ts +++ b/packages/sdk/src/spec/client.ts @@ -127,9 +127,17 @@ export interface StitchToolClientSpec { * Call a tool on the MCP server. * @param name - Tool name * @param args - Tool arguments + * @param errorMap - Optional mapping for typed error handling * @returns Parsed tool result */ - callTool: (name: string, args: Record) => Promise; + callTool: ( + name: string, + args: Record, + errorMap?: Record< + string, + { schema: any; match: string; create: (data: any, raw: any) => Error } + >, + ) => Promise; /** * Get available tools from the server. diff --git a/packages/sdk/src/spec/errors.ts b/packages/sdk/src/spec/errors.ts index 1d2413a..8e3296a 100644 --- a/packages/sdk/src/spec/errors.ts +++ b/packages/sdk/src/spec/errors.ts @@ -1,17 +1,3 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - import { z } from "zod"; /** @@ -46,6 +32,7 @@ export interface StitchErrorData { toolName?: string; /** Delay in milliseconds requested by the server before retrying. */ retryAfter?: number; + raw?: unknown; } /** @@ -62,6 +49,7 @@ export class StitchError extends Error { public readonly toolName?: string; /** Delay in milliseconds requested by the server before retrying. */ public readonly retryAfter?: number; + public readonly raw?: unknown; constructor(data: StitchErrorData) { super(data.message); @@ -72,6 +60,7 @@ export class StitchError extends Error { this.status = data.status; this.toolName = data.toolName; this.retryAfter = data.retryAfter; + this.raw = data.raw; } /** diff --git a/packages/sdk/test/unit/bridge-0.4.0-premortem.test.ts b/packages/sdk/test/unit/bridge-0.4.0-premortem.test.ts index 97b8129..b41cd6a 100644 --- a/packages/sdk/test/unit/bridge-0.4.0-premortem.test.ts +++ b/packages/sdk/test/unit/bridge-0.4.0-premortem.test.ts @@ -102,17 +102,15 @@ describe("0.4.0 Premortem Tigers & Reconciled PRs (#363, #368)", () => { it("accepts legacy 0.3.5 modelId values on project.generate and screen.edit", async () => { const client = new StitchToolClient({ apiKey: "k" }); - const callToolSpy = vi - .spyOn(client, "callTool") - .mockResolvedValue({ - outputComponents: [ - { - design: { - screens: [{ name: "projects/p1/screens/s1", title: "Home" }], - }, + const callToolSpy = vi.spyOn(client, "callTool").mockResolvedValue({ + outputComponents: [ + { + design: { + screens: [{ name: "projects/p1/screens/s1", title: "Home" }], }, - ], - } as any); + }, + ], + } as any); const sdk = new Stitch(client); const project = sdk.project("p1"); @@ -128,6 +126,7 @@ describe("0.4.0 Premortem Tigers & Reconciled PRs (#363, #368)", () => { expect.objectContaining({ modelId: "GEMINI_3_PRO", }), + expect.any(Object), ); const edited = await gen.screens[0].edit("Make dark", { diff --git a/packages/sdk/test/unit/schema-repair.test.ts b/packages/sdk/test/unit/schema-repair.test.ts index 2b7064d..02aad3b 100644 --- a/packages/sdk/test/unit/schema-repair.test.ts +++ b/packages/sdk/test/unit/schema-repair.test.ts @@ -268,7 +268,8 @@ describe("repairToolSchemas", () => { }, { name: "update_design_system", - description: "References DesignTheme without defining it or ColorPalette", + description: + "References DesignTheme without defining it or ColorPalette", inputSchema: { type: "object", properties: { diff --git a/packages/sdk/test/unit/sdk.test.ts b/packages/sdk/test/unit/sdk.test.ts index 27f094a..3a878c0 100644 --- a/packages/sdk/test/unit/sdk.test.ts +++ b/packages/sdk/test/unit/sdk.test.ts @@ -354,6 +354,7 @@ describe("SDK Unit Tests", () => { deviceType: "DESKTOP", modelId: undefined, }, + expect.any(Object), ); expect(result.first).toBeInstanceOf(Screen); diff --git a/scripts/generate-sdk.ts b/scripts/generate-sdk.ts index a727675..386e75a 100644 --- a/scripts/generate-sdk.ts +++ b/scripts/generate-sdk.ts @@ -817,8 +817,17 @@ function buildMethodBody( statements.push(`try {`); const responseName = toResponseName(binding.tool); + + let errorMapStr = ""; + if (binding.errors && binding.errors.length > 0) { + const mapEntries = binding.errors.map((err) => { + return `"${err.name}": { schema: ${err.name}Schema, match: "${err.match}", create: (data: any, raw: any) => new ${err.name}("Tool Call Failed [${binding.tool}]: " + (${err.name}Schema.description || "${err.match}"), data, raw) }`; + }); + errorMapStr = `, { ${mapEntries.join(", ")} }`; + } + statements.push( - ` const raw = await this.client.callTool<${responseName}>("${binding.tool}", ${generateArgsObject(binding.args)});`, + ` const raw = await this.client.callTool<${responseName}>("${binding.tool}", ${generateArgsObject(binding.args)}${errorMapStr});`, ); if (binding.cache?.writeBack) { statements.push( @@ -842,6 +851,66 @@ function buildMethodBody( return statements; } +// ── Error Code Emission ─────────────────────────────────────── + +export function jsonSchemaToZod(schema: ToolSchema | undefined | any): string { + if (!schema) return "z.any()"; + switch (schema.type) { + case "string": + return "z.string()"; + case "number": + return "z.number()"; + case "integer": + return "z.number().int()"; + case "boolean": + return "z.boolean()"; + case "array": + return `z.array(${jsonSchemaToZod(schema.items || {})})`; + case "object": + if (schema.properties) { + const props = Object.entries(schema.properties).map(([k, v]) => { + return `${k}: ${jsonSchemaToZod(v)}`; + }); + return `z.object({ ${props.join(", ")} })`; + } + return "z.record(z.string(), z.any())"; + default: + return "z.any()"; + } +} + +export function emitExceptions(domainMap: any): string[] { + const exceptions = new Map(); + for (const binding of domainMap.bindings) { + if (binding.errors) { + for (const err of binding.errors) { + if (!exceptions.has(err.name)) { + const zodSchemaStr = jsonSchemaToZod(err.schema); + const code = ` +export const ${err.name}Schema = ${zodSchemaStr}; +export type ${err.name}Payload = z.infer; + +export interface ${err.name} extends ${err.name}Payload {} +export class ${err.name} extends StitchError { + constructor(message: string, payload: ${err.name}Payload, raw?: unknown) { + super({ + code: "${err.match}" as any, + message, + recoverable: ${err.name === "RateLimitError" ? "true" : "false"}, + raw + }); + this.name = "${err.name}"; + Object.assign(this, payload); + } +}`; + exceptions.set(err.name, code); + } + } + } + } + return Array.from(exceptions.values()); +} + // ── Main ────────────────────────────────────────────────────── async function main() { @@ -1067,6 +1136,20 @@ async function main() { for (const tool of manifest) { responseTypes.push(emitResponseType(tool, namedTypes)); } + + const exceptionDefs = emitExceptions(domainMap); + if (exceptionDefs.length > 0) { + responsesFile.addImportDeclaration({ + moduleSpecifier: "zod", + namedImports: ["z"], + }); + responsesFile.addImportDeclaration({ + moduleSpecifier: "../../src/spec/errors.js", + namedImports: ["StitchError"], + }); + responsesFile.addStatements(exceptionDefs.join("\n\n")); + } + responsesFile.addStatements( `/**\n * ${headerComment}\n */\n\n${responseTypes.join("\n\n")}`, ); @@ -1141,6 +1224,12 @@ async function main() { const requiredResponses = new Set(); for (const b of classBindings) { requiredResponses.add(toResponseName(b.tool)); + if (b.errors) { + for (const err of b.errors) { + requiredResponses.add(err.name); + requiredResponses.add(err.name + "Schema"); + } + } } if (requiredResponses.size > 0) { sourceFile.addImportDeclaration({ @@ -1511,7 +1600,7 @@ async function main() { } indexFile.addExportDeclaration({ moduleSpecifier: "./responses.generated.js", - isTypeOnly: true, + isTypeOnly: exceptionDefs.length === 0, }); await Bun.write(resolve(GENERATED_DIR, "index.ts"), indexFile.getFullText()); fileCount++; diff --git a/scripts/ir-schema.ts b/scripts/ir-schema.ts index 937a8ca..1c1b9e3 100644 --- a/scripts/ir-schema.ts +++ b/scripts/ir-schema.ts @@ -255,6 +255,15 @@ export const DomainClassConfig = z .strict(); export type DomainClassConfig = z.infer; +// ── Error Spec ──────────────────────────────────────────────── + +export const ErrorSpec = z.object({ + name: z.string(), + match: z.string(), + schema: z.any(), // JSON Schema object for the error payload +}); +export type ErrorSpec = z.infer; + // ── Binding ─────────────────────────────────────────────────── export const Binding = z @@ -277,6 +286,8 @@ export const Binding = z returns: ReturnSpec, /** Optional cache spec for methods that check this.data first */ cache: CacheSpec.optional(), + /** Custom typed errors to throw when a specific match condition is met */ + errors: z.array(ErrorSpec).optional(), }) .strict(); export type Binding = z.infer;