npx skills add ...
npx skills add google-labs-code/stitch-sdk --skill stitch-sdk-development
npx skills add google-labs-code/stitch-sdk --skill stitch-sdk-development
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.
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 domain layer is fully generated. No handwritten domain classes. The pipeline has 3 stages:
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.
When the Stitch MCP server adds a new tool:
domain-map.json for the new toolvalidate-generated.ts to confirm consistencydomain-map.json expresses two things:
Classes: What domain objects exist and how they're constructed.
Bindings: How MCP tools map to class methods.
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:
The SDK serves two distinct consumers with different needs:
StitchToolClientFor AI agents and orchestration scripts. Raw tool pipe. The agent receives tool schemas, constructs JSON, sends it, gets JSON back. No domain knowledge required.
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.
The singleton reads STITCH_API_KEY (or STITCH_ACCESS_TOKEN + GOOGLE_CLOUD_PROJECT) from the environment. Set STITCH_HOST to override the server URL.
For explicit control (multiple clients, custom config, testing), instantiate StitchToolClient directly:
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.
stitchTools()For agents built on the Vercel AI SDK. Transforms MCP tool schemas into AI SDK-compatible tool definitions, enabling plug-and-play with generateText().
stitchTools() is exported from the /ai subpath to keep the ai dependency optional. It uses the same shared StitchToolClient singleton internally.
stitch.toolMap provides O(1) tool lookup with pre-parsed params — static, auth-free, no network call:
The raw toolDefinitions array and standalone toolMap are also exported from the main entry point.
For humans writing precise, programmatic scripts. Generated domain facade over callTool. Typed parameters, domain objects returned, StitchError thrown on failure.
Both modalities share StitchToolClient underneath. The domain classes are a typed layer over callTool.
All generated methods use throw StitchError for error handling. No Result<T> pattern.
StitchError.fromUnknown() ensures all errors are normalized to StitchError with a code, message, and recoverability hint.
These components remain handwritten as they provide foundational plumbing:
StitchToolClient — MCP transport, auth, tool invocationStitchError — typed error class with codes, messages, recovery hintsStitchProxy — MCP proxy server for re-exposing Stitch to other agentssingleton.ts — lazy proxy for stitch export with env var configThe SDK has two hemispheres separated by a formal boundary:
The membrane is declared in domain-map.json via sideEffects on any class with an extensionPath:
Valid reason values: filesystem_io, binary_data, private_rest, complex_orchestration.
The generator validates at Stage 3:
sideEffect.method collides with a generated binding method namespecPath points to an existing fileFollow the Spec → Handler → Extension pattern:
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.
Create the Handler (src/my-operation-handler.ts): Implement the Spec interface. All failures return Result<T>, never throw. This is where side-effect logic lives (disk, network, binary).
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.
Declare in domain-map.json: Add a sideEffect entry with method, reason, and specPath.
Regenerate: Run bun scripts/generate-sdk.ts — the generator validates the declaration.
| 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 |
When implementing a new feature or fixing a bug, follow the Traffic Light pattern:
Write the test first. It must fail. This defines the contract before any implementation exists.
domain-map.json (Stage 2)bun scripts/generate-sdk.ts (Stage 3)With passing tests as your safety net:
npx tsc to verify type safetybun scripts/validate-generated.ts to verify pipeline integrityDiscover the current state by reading the codebase directly. The key entry points:
packages/sdk/src/index.ts — every public export is listed herepackages/sdk/generated/src/ — Stitch, Project, Screen, DesignSystempackages/sdk/generated/domain-map.json, packages/sdk/generated/tools-manifest.jsonpackages/sdk/src/client.ts, packages/sdk/src/spec/errors.ts, packages/sdk/src/singleton.tspackages/sdk/test/unit/ for unit tests, packages/sdk/test/integration/ for live testsscripts field in package.jsonDo not rely on cached descriptions of files or directory trees. Read the source.
Use .js extensions for ESM compatibility:
{
"Screen": {
"constructorParams": ["projectId", "screenId"],
"fieldMapping": {
"projectId": { "from": "projectId" },
"screenId": {
"from": "id",
"fallback": { "field": "name", "splitOn": "/screens/" }
}
},
"parentField": "projectId",
"idField": "screenId"
}
}{
"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 }
]
}
}{
"cache": {
"projection": [{ "prop": "htmlCode" }, { "prop": "downloadUrl" }],
"description": "Use cached download URL from generation response"
}
}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();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();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",
});const tool = stitch.toolMap.get("create_project");
tool.params; // ToolParam[] — flat, pre-parsed
tool.params.filter((p) => p.required); // required params only
tool.inputSchema; // raw ToolInputSchema still availableconst project = await stitch.createProject("My App");
const screen = await project.generate("A login page");
const html = await screen.getHtml();// Generated method pattern (inside each method):
try {
const raw = await this.client.callTool<any>("tool_name", args);
return /* extracted result */;
} catch (error) {
throw StitchError.fromUnknown(error);
}{
"Project": {
"extensionPath": "../../src/project-ext.js",
"sideEffects": [
{
"method": "uploadImage",
"reason": "private_rest",
"specPath": "src/spec/upload.ts"
},
{
"method": "downloadAssets",
"reason": "filesystem_io",
"specPath": "src/spec/download.ts"
}
]
}
}# 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)npx vitest run # All tests pass
bun scripts/e2e-test.ts # E2E passesimport { StitchError } from "../../src/spec/errors.js"; // ✓
import { StitchError } from "../../src/spec/errors"; // ✗