Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/sdk/generated/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ DO NOT EDIT — changes will be overwritten.

Source: tools-manifest.json (sha256:1f84b31604f9...)
domain-map.json (sha256:99b823ad9306...)
Generated: 2026-03-19T18:56:19.253Z
Generated: 2026-03-21T00:17:24.039Z
*/
export { Stitch } from "./stitch.js";
export { Project } from "./project.js";
Expand Down
8 changes: 5 additions & 3 deletions packages/sdk/generated/src/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ DO NOT EDIT — changes will be overwritten.

Source: tools-manifest.json (sha256:1f84b31604f9...)
domain-map.json (sha256:99b823ad9306...)
Generated: 2026-03-19T18:56:19.253Z
Generated: 2026-03-21T00:17:24.039Z
*/
import { type StitchToolClient } from "../../src/client.js";
import { StitchError } from "../../src/spec/errors.js";
Expand Down Expand Up @@ -37,7 +37,9 @@ export class Project {
async generate(prompt: string, deviceType?: "DEVICE_TYPE_UNSPECIFIED" | "MOBILE" | "DESKTOP" | "TABLET" | "AGNOSTIC", modelId?: "MODEL_ID_UNSPECIFIED" | "GEMINI_3_PRO" | "GEMINI_3_FLASH"): Promise<Screen> {
try {
const raw = await this.client.callTool<any>("generate_screen_from_text", { projectId: this.projectId, prompt, deviceType, modelId });
return new Screen(this.client, { ...raw.outputComponents[0].design.screens[0], projectId: this.projectId });
const _projected = raw?.outputComponents?.[0]?.design?.screens?.[0];
if (!_projected) throw new StitchError({ code: "UNKNOWN_ERROR", message: "Incomplete API response from generate_screen_from_text: expected object at projection path", recoverable: false });
return new Screen(this.client, { ..._projected, projectId: this.projectId })
} catch (error) {
throw StitchError.fromUnknown(error);
}
Expand All @@ -50,7 +52,7 @@ export class Project {
async screens(): Promise<Screen[]> {
try {
const raw = await this.client.callTool<any>("list_screens", { projectId: this.projectId });
return (raw.screens || []).map((item: any) => new Screen(this.client, { ...item, projectId: this.projectId }));
return (raw?.screens || []).map((item: any) => new Screen(this.client, { ...item, projectId: this.projectId }));
} catch (error) {
throw StitchError.fromUnknown(error);
}
Expand Down
10 changes: 6 additions & 4 deletions packages/sdk/generated/src/screen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ DO NOT EDIT — changes will be overwritten.

Source: tools-manifest.json (sha256:1f84b31604f9...)
domain-map.json (sha256:99b823ad9306...)
Generated: 2026-03-19T18:56:19.253Z
Generated: 2026-03-21T00:17:24.039Z
*/
import { type StitchToolClient } from "../../src/client.js";
import { StitchError } from "../../src/spec/errors.js";
Expand Down Expand Up @@ -38,7 +38,9 @@ export class Screen {
async edit(prompt: string, deviceType?: "DEVICE_TYPE_UNSPECIFIED" | "MOBILE" | "DESKTOP" | "TABLET" | "AGNOSTIC", modelId?: "MODEL_ID_UNSPECIFIED" | "GEMINI_3_PRO" | "GEMINI_3_FLASH"): Promise<Screen> {
try {
const raw = await this.client.callTool<any>("edit_screens", { projectId: this.projectId, selectedScreenIds: [this.screenId], prompt, deviceType, modelId });
return new Screen(this.client, { ...raw.outputComponents[0].design.screens[0], projectId: this.projectId });
const _projected = raw?.outputComponents?.[0]?.design?.screens?.[0];
if (!_projected) throw new StitchError({ code: "UNKNOWN_ERROR", message: "Incomplete API response from edit_screens: expected object at projection path", recoverable: false });
return new Screen(this.client, { ..._projected, projectId: this.projectId })
} catch (error) {
throw StitchError.fromUnknown(error);
}
Expand Down Expand Up @@ -67,7 +69,7 @@ export class Screen {

try {
const raw = await this.client.callTool<any>("get_screen", { projectId: this.projectId, screenId: this.screenId, name: `projects/${this.projectId}/screens/${this.screenId}` });
return raw.htmlCode.downloadUrl || "";
return raw?.htmlCode?.downloadUrl || "";
} catch (error) {
throw StitchError.fromUnknown(error);
}
Expand All @@ -83,7 +85,7 @@ export class Screen {

try {
const raw = await this.client.callTool<any>("get_screen", { projectId: this.projectId, screenId: this.screenId, name: `projects/${this.projectId}/screens/${this.screenId}` });
return raw.screenshot.downloadUrl || "";
return raw?.screenshot?.downloadUrl || "";
} catch (error) {
throw StitchError.fromUnknown(error);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/sdk/generated/src/stitch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ DO NOT EDIT — changes will be overwritten.

Source: tools-manifest.json (sha256:1f84b31604f9...)
domain-map.json (sha256:99b823ad9306...)
Generated: 2026-03-19T18:56:19.253Z
Generated: 2026-03-21T00:17:24.039Z
*/
import { type StitchToolClient } from "../../src/client.js";
import { StitchError } from "../../src/spec/errors.js";
Expand All @@ -22,7 +22,7 @@ export class Stitch {
async projects(): Promise<Project[]> {
try {
const raw = await this.client.callTool<any>("list_projects", { });
return (raw.projects || []).map((item: any) => new Project(this.client, item));
return (raw?.projects || []).map((item: any) => new Project(this.client, item));
} catch (error) {
throw StitchError.fromUnknown(error);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/generated/src/tool-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ DO NOT EDIT — changes will be overwritten.

Source: tools-manifest.json (sha256:1f84b31604f9...)
domain-map.json (sha256:99b823ad9306...)
Generated: 2026-03-19T18:56:19.253Z
Generated: 2026-03-21T00:17:24.039Z
*/
/** JSON Schema property descriptor for a tool parameter. */
export interface ToolPropertySchema {
Expand Down
6 changes: 3 additions & 3 deletions packages/sdk/generated/stitch-sdk.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
"serverUrl": "https://stitch.googleapis.com/mcp"
},
"generated": {
"generatedAt": "2026-03-19T18:56:19.351Z",
"sourceHash": "sha256:06c97f633a04942efd348aa1634356c9f5f0fd3e16d33802bffe7e8e9a650905",
"generatedAt": "2026-03-21T00:17:24.086Z",
"sourceHash": "sha256:8bae6b2e2940443b45a7dd8b5ee5ba73464cb213ccc22aa3bea98b1db3b66468",
"manifestHash": "sha256:1f84b31604f95580325952f0c150a3e045543fad2596e9a4d8ed15c07e0cbf9b",
"domainMapHash": "sha256:99b823ad930620c571a9443c7b956f90b092d626be9ce2319d7a7bfe3a2e3db4",
"fileCount": 5
},
"domainMap": {
"generatedAt": "2026-03-19T18:56:19.351Z",
"generatedAt": "2026-03-21T00:17:24.086Z",
"sourceHash": "sha256:99b823ad930620c571a9443c7b956f90b092d626be9ce2319d7a7bfe3a2e3db4",
"manifestHash": "sha256:1f84b31604f95580325952f0c150a3e045543fad2596e9a4d8ed15c07e0cbf9b",
"classCount": 3,
Expand Down
68 changes: 54 additions & 14 deletions packages/sdk/test/unit/sdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { Screen } from "../../generated/src/screen.js";
import { Project } from "../../generated/src/project.js";
import { Stitch } from "../../generated/src/stitch.js";
import { StitchToolClient } from "../../src/client.js";
import { StitchError } from "../../src/spec/errors.js";

// Mock the StitchToolClient class
vi.mock("../../src/client");
Expand Down Expand Up @@ -135,6 +136,30 @@ describe("SDK Unit Tests", () => {
expect(edited.id).toBe("edited-screen");
});

it("edit should throw StitchError (not TypeError) when response has no screens", async () => {
const screen = new Screen(mockClient, screenData);

(mockClient.callTool as Mock).mockResolvedValue({
outputComponents: [{ design: {} }],
projectId,
});

const err = await screen.edit("Make it dark").catch((e: unknown) => e);
expect(err).toBeInstanceOf(StitchError);
expect((err as StitchError).code).toBe("UNKNOWN_ERROR");
expect((err as StitchError).message).toContain("edit_screens");
});

it("edit should throw StitchError when outputComponents is empty", async () => {
const screen = new Screen(mockClient, screenData);

(mockClient.callTool as Mock).mockResolvedValue({ outputComponents: [] });

const err = await screen.edit("Make it dark").catch((e: unknown) => e);
expect(err).toBeInstanceOf(StitchError);
expect((err as StitchError).message).toContain("edit_screens");
});

it("variants should call generate_variants and return Screen[]", async () => {
const screen = new Screen(mockClient, screenData);

Expand Down Expand Up @@ -230,26 +255,41 @@ describe("SDK Unit Tests", () => {
});


it("generate should handle missing design.screens in outputComponents gracefully", async () => {
it("generate should throw StitchError (not TypeError) when response has no screens", async () => {
const project = new Project(mockClient, projectId);

// Mock with missing screens array
(mockClient.callTool as Mock).mockResolvedValue({
outputComponents: [
{
design: {
// screens is missing
},
},
],
outputComponents: [{ design: { /* screens missing */ } }],
projectId: projectId,
sessionId: "session-1",
});

// Based on the Screen class constructor, if it's passed undefined, it might throw
// or if it tries to access missing screens it might throw TypeError.
// The generated code in project.ts wraps the try block and throws StitchError.
await expect(project.generate("test")).rejects.toThrow();
const err = await project.generate("test").catch((e: unknown) => e);
expect(err).toBeInstanceOf(StitchError);
expect((err as StitchError).code).toBe("UNKNOWN_ERROR");
expect((err as StitchError).message).toContain("generate_screen_from_text");
});

it("generate should throw StitchError when outputComponents is empty", async () => {
const project = new Project(mockClient, projectId);

(mockClient.callTool as Mock).mockResolvedValue({
outputComponents: [],
projectId: projectId,
});

const err = await project.generate("test").catch((e: unknown) => e);
expect(err).toBeInstanceOf(StitchError);
expect((err as StitchError).message).toContain("generate_screen_from_text");
});

it("generate should throw StitchError when outputComponents is missing", async () => {
const project = new Project(mockClient, projectId);

(mockClient.callTool as Mock).mockResolvedValue({ projectId: projectId });

const err = await project.generate("test").catch((e: unknown) => e);
expect(err).toBeInstanceOf(StitchError);
expect((err as StitchError).message).toContain("generate_screen_from_text");
});

it("screens should list screens and return Screen instances", async () => {
Expand Down
27 changes: 23 additions & 4 deletions scripts/generate-sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,12 +166,12 @@ export function emitProjection(steps: ProjectionStep[], rawVar: string = "raw"):
return emitFlatMapProjection(steps, rawVar);
}

// Simple chain: raw.prop1[0].prop2.prop3
// Simple chain with optional chaining: raw.outputComponents?.[0]?.design?.screens?.[0]
let code = rawVar;
for (const step of steps) {
code += `.${step.prop}`;
code += `?.${step.prop}`;
if (step.index !== undefined) {
code += `[${step.index}]`;
code += `?.[${step.index}]`;
}
}
return code;
Expand Down Expand Up @@ -330,6 +330,19 @@ function generateReturnExpression(
return `(${projectionExpr} || []).map((item: any) => new ${binding.returns.class}(this.client, ${itemExpr}))`;
}

// Only emit guard when projection has actual steps (not just `raw`)
if (projection.length > 0) {
const guardVar = "_projected";
const dataExpr = parentField
? `{ ...${guardVar}, ${parentField}: this.${parentField} }`
: guardVar;
const toolName = binding.tool;
return `const ${guardVar} = ${projectionExpr};\n` +
` if (!${guardVar}) throw new StitchError({ code: "UNKNOWN_ERROR", message: "Incomplete API response from ${toolName}: expected object at projection path", recoverable: false });\n` +
` return new ${binding.returns.class}(this.client, ${dataExpr})`;
}

// Direct return — projection is empty, raw is the result itself
const dataExpr = parentField
? `{ ...${projectionExpr}, ${parentField}: this.${parentField} }`
: projectionExpr;
Expand Down Expand Up @@ -396,7 +409,13 @@ function buildMethodBody(

statements.push(`try {`);
statements.push(` const raw = await this.client.callTool<any>("${binding.tool}", ${generateArgsObject(binding.args)});`);
statements.push(` return ${generateReturnExpression(binding, className, domainMap)};`);
const retExpr = generateReturnExpression(binding, className, domainMap);
// If retExpr contains newlines, it has guard statements — don't wrap in return
if (retExpr.includes("\n")) {
statements.push(` ${retExpr}`);
} else {
statements.push(` return ${retExpr};`);
}
statements.push(`} catch (error) {`);
statements.push(` throw StitchError.fromUnknown(error);`);
statements.push(`}`);
Expand Down