--- name: stitch-sdk-development description: Develop the Stitch SDK. Covers the generation pipeline, dual modality (agent vs SDK), error handling, and Traffic Light (Red-Green-Yellow) implementation workflow. Use when adding features, fixing bugs, or understanding the architecture. --- # Stitch SDK Development This skill encodes the expertise needed to develop `@google/stitch-sdk` — the core systems, patterns, and philosophies. It does not enumerate every method (the codebase is the source of truth for that). It teaches you how to think about the system. --- ## The Generation Pipeline The domain layer is **fully generated**. No handwritten domain classes. The pipeline has 3 stages: ``` Stage 1: Capture Stage 2: Domain Design Stage 3: Generate ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────────────┐ │ capture-tools.ts │──────▶│ domain-map.json │───────▶│ generate-sdk.ts │ │ │ │ (the IR) │ │ │ │ Connects to MCP │ │ Classes, bindings│ │ Deterministic │ │ server, calls │ │ arg routing, │ │ codegen into │ │ tools/list │ │ cache, extraction│ │ packages/sdk/generated/ │ └──────────────────┘ └──────────────────┘ └──────────────────────────┘ │ │ │ ▼ ▼ ▼ tools-manifest.json domain-map.json packages/sdk/generated/src/*.ts (raw MCP tool schemas) (tool→class mapping) (Stitch, Project, Screen) ``` **Stage 1** (`bun scripts/capture-tools.ts`): Connects to the live Stitch MCP server, calls `tools/list`, writes `tools-manifest.json`. Source of truth for what tools exist. **Stage 2** (agent/human): Reads the manifest and produces `domain-map.json` — the intermediate representation. This is where judgment lives: which tool maps to which class, what args come from `self` vs `param` vs `computed`, how to extract the return value, and what data to cache. **Stage 3** (`bun scripts/generate-sdk.ts`): Deterministic codegen. Reads manifest + domain-map, emits TypeScript classes in `packages/sdk/generated/src/`. No LLM involved — pure template expansion. **Integrity**: `stitch-sdk.lock` records SHA-256 hashes of all inputs and outputs. `bun scripts/validate-generated.ts` verifies consistency. Run in CI to prevent publishing stale code. ### Supporting a New Tool When the Stitch MCP server adds a new tool: 1. Run Stage 1 to capture the updated manifest 2. Run Stage 2: add a binding in `domain-map.json` for the new tool 3. Run Stage 3 to regenerate the SDK classes 4. Run `validate-generated.ts` to confirm consistency 5. Update tests as needed ### The Domain Map IR `domain-map.json` expresses two things: **Classes**: What domain objects exist and how they're constructed. ```json { "Screen": { "constructorParams": ["projectId", "screenId"], "parentField": "projectId", "reference": { "keys": ["projectId", "screenId"] } } } ``` **Bindings**: How MCP tools map to class methods. ```json { "tool": "generate_screen_from_text", "class": "Project", "method": "generate", "args": { "projectId": { "from": "self" }, "prompt": { "from": "param" }, "name": { "from": "computed", "template": "projects/{projectId}/screens/{screenId}" } }, "returns": { "class": "Screen", "projection": [ { "prop": "outputComponents", "index": 0 }, { "prop": "design" }, { "prop": "screens", "index": 0 } ] } } ``` **Arg routing**: `self` = injected from `this`, `param` = passed by the caller, `computed` = built from a template at call time, `selfArray` = `[this.field]` wrapped as array. **Response projections**: Structured `ProjectionStep[]` arrays validated against `outputSchema`. Use `index` for single items, `each` for arrays. Empty `[]` = direct return. **Cache**: Methods can specify a `cache` with a structured `projection` to check `this.data` before calling the API: ```json { "cache": { "projection": [{ "prop": "htmlCode" }, { "prop": "downloadUrl" }], "description": "Use cached download URL from generation response" } } ``` --- ## Dual Modality The SDK serves two distinct consumers with different needs: ### Agent Modality — `StitchToolClient` For AI agents and orchestration scripts. Raw tool pipe. The agent receives tool schemas, constructs JSON, sends it, gets JSON back. No domain knowledge required. #### Quick Start (Singleton) The `stitch` singleton exposes both domain methods and tool methods via a `Proxy`. No instantiation needed — it lazily creates a `StitchToolClient` from env vars on first access. ```typescript import { stitch } from "@google/stitch-sdk"; // Discover available tools const { tools } = await stitch.listTools(); // Call any tool by name with a JSON payload const result = await stitch.callTool("generate_screen_from_text", { projectId: "123", prompt: "A login page", }); // Clean up when done await stitch.close(); ``` The singleton reads `STITCH_API_KEY` (or `STITCH_ACCESS_TOKEN` + `GOOGLE_CLOUD_PROJECT`) from the environment. Set `STITCH_BASE_URL` to override the server URL. (`STITCH_HOST` is a deprecated alias, removed in 2.0.) #### Direct Instantiation For explicit control (multiple clients, custom config, testing), instantiate `StitchToolClient` directly: ```typescript import { StitchToolClient } from "@google/stitch-sdk"; const client = new StitchToolClient({ apiKey: "my-key" }); const tools = await client.listTools(); const result = await client.callTool("create_project", { title: "My App" }); await client.close(); ``` **Config resolution**: Constructor params → env vars → defaults. Auth requires either `apiKey` or `accessToken` + `projectId` (validated via Zod at construction time). **Connection**: `callTool` and `listTools` auto-connect on first call. Concurrent calls safely share the connection via a promise-based lock. #### AI SDK Adapter — `stitchTools()` For agents built on the [Vercel AI SDK](https://sdk.vercel.ai/). Transforms MCP tool schemas into AI SDK-compatible tool definitions, enabling plug-and-play with `generateText()`. ```typescript import { stitchTools } from "@google/stitch-sdk/ai"; import { generateText } from "ai"; import { google } from "@ai-sdk/google"; const result = await generateText({ model: google("gemini-2.0-flash"), tools: stitchTools(), // all tools // or: stitchTools({ include: ["create_project"] }) // filtered prompt: "Create a project called My App", }); ``` `stitchTools()` is exported from the `/ai` subpath to keep the `ai` dependency optional. It uses the same shared `StitchToolClient` singleton internally. The `/tools` subpath provides O(1) tool lookup with pre-parsed params — static, auth-free, no network call: ```typescript import { toolMap } from "@google/stitch-sdk/tools"; const tool = toolMap.get("create_project"); tool.params; // ToolParam[] — flat, pre-parsed tool.params.filter((p) => p.required); // required params only tool.inputSchema; // raw ToolInputSchema still available ``` Both `toolDefinitions` and `toolMap` are exported from the `@google/stitch-sdk/tools` subpath (kept off the root entry so it stays lean). ### SDK Modality — Generated Domain Classes For humans writing precise, programmatic scripts. Generated domain facade over `callTool`. Typed parameters, domain objects returned, `StitchError` thrown on failure. ```typescript const project = await stitch.createProject({ title: "My App" }); const generation = await project.generate("A login page"); const html = await generation.first.getHtml(); ``` Both modalities share `StitchToolClient` underneath. The domain classes are a typed layer over `callTool`. ### Error Handling — Throws at the Boundary All generated methods use `throw StitchError` for error handling. No `Result` pattern. ```typescript // Generated method pattern (inside each method): try { const raw = await this.client.callTool("tool_name", args); return /* extracted result */; } catch (error) { throw StitchError.fromUnknown(error); } ``` `StitchError.fromUnknown()` ensures all errors are normalized to `StitchError` with a code, message, and recoverability hint. ### Infrastructure (Handwritten) These components remain handwritten as they provide foundational plumbing: - `StitchToolClient` — MCP transport, auth, tool invocation - `StitchError` — typed error class with codes, messages, recovery hints - `StitchProxy` — MCP proxy server for re-exposing Stitch to other agents - `singleton.ts` — lazy proxy for `stitch` export with env var config ### The Side-Effect Membrane The SDK has two hemispheres separated by a formal boundary: - **Generated Hemisphere** — Every MCP tool call, response projection, arg routing, and class constructor. If it sends JSON and gets JSON back, it MUST be generated. - **Handwritten Hemisphere** — Operations that touch the real world: filesystem I/O, binary streams, non-MCP REST endpoints. If it touches disk or uses a private API, it MUST be handwritten. The membrane is declared in `domain-map.json` via `sideEffects` on any class with an `extensionPath`: ```json { "Project": { "extensionPath": "../../src/project-ext.js", "sideEffects": [ { "method": "upload", "reason": "private_rest", "specPath": "src/spec/upload.ts" }, { "method": "downloadAssets", "reason": "filesystem_io", "specPath": "src/spec/download.ts" } ] } } ``` Valid `reason` values: `filesystem_io`, `binary_data`, `private_rest`, `complex_orchestration`. The generator validates at Stage 3: 1. No `sideEffect.method` collides with a generated binding method name 2. Each `specPath` points to an existing file #### Adding a New Side-Effect Method Follow the Spec → Handler → Extension pattern: 1. **Create the Spec** (`src/spec/my-operation.ts`): Define input schema (Zod), error codes, Result type, and interface. The Handler must implement this interface and never throw. 2. **Create the Handler** (`src/my-operation-handler.ts`): Implement the Spec interface. All failures return `Result`, never `throw`. This is where side-effect logic lives (disk, network, binary). 3. **Add to the Extension** (`src/project-ext.ts`): A thin adapter (< 15 lines per method body) that parses input, calls the Handler, and maps the Result to `throw StitchError` on failure. 4. **Declare in domain-map.json**: Add a `sideEffect` entry with `method`, `reason`, and `specPath`. 5. **Regenerate**: Run `bun scripts/generate-sdk.ts` — the generator validates the declaration. #### Rules | Rule | Rationale | | ----------------------------------------------------- | ------------------------------ | | Extension methods must NOT override generated methods | Prevents silent shadowing | | Extension methods must delegate to a Handler | Prevents inline business logic | | Handlers must implement a Spec interface | Typed service contract | | Handlers must return Result, never throw | Consistent error surface | | Extensions must NOT import from `singleton.ts` | Prevents circular dependencies | --- ## Traffic Light Implementation (Red → Green → Yellow) When implementing a new feature or fixing a bug, follow the Traffic Light pattern: ### 🔴 Red — Write Breaking Tests Write the test first. It must fail. This defines the contract before any implementation exists. ```bash # Unit tests for generated classes npx vitest run test/unit/sdk.test.ts # → FAIL (new method doesn't exist yet) # E2E test for the public API bun scripts/e2e-test.ts # → FAIL (method doesn't exist yet) ``` ### 🟢 Green — Implement 1. Add a binding to `domain-map.json` (Stage 2) 2. Run `bun scripts/generate-sdk.ts` (Stage 3) 3. Update tests to verify correct behavior ```bash npx vitest run # All tests pass bun scripts/e2e-test.ts # E2E passes ``` ### 🟡 Yellow — Refactor / Refine / Revisit With passing tests as your safety net: - Refactor for clarity (extract helpers, simplify args) - Add edge case tests - Run `npx tsc` to verify type safety - Run `bun scripts/validate-generated.ts` to verify pipeline integrity --- ## Orienting in the Codebase Discover the current state by reading the codebase directly. The key entry points: - **Public surface**: Start at `packages/sdk/src/index.ts` — every public export is listed here - **Generated classes**: `packages/sdk/generated/src/` — Stitch, Project, Screen, DesignSystem - **Pipeline artifacts**: `packages/sdk/generated/domain-map.json`, `packages/sdk/generated/tools-manifest.json` - **Infrastructure**: `packages/sdk/src/client.ts`, `packages/sdk/src/spec/errors.ts`, `packages/sdk/src/singleton.ts` - **Test structure**: `packages/sdk/test/unit/` for unit tests, `packages/sdk/test/integration/` for live tests - **Available commands**: Read the `scripts` field in `package.json` Do not rely on cached descriptions of files or directory trees. Read the source. ## Import Convention Use `.js` extensions for ESM compatibility: ```typescript import { StitchError } from "../../src/spec/errors.js"; // ✓ import { StitchError } from "../../src/spec/errors"; // ✗ ```