stitch-sdk-development
$
npx mdskill add google-labs-code/stitch-sdk/stitch-sdk-developmentBuild deterministic Stitch SDK code via generated pipelines.
- Develops core systems without handwritten domain classes.
- Integrates MCP servers for tool capture and generation.
- Executes three-stage workflow from capture to code output.
- Delivers deterministic packages into the generated directory.
SKILL.md
.github/skills/stitch-sdk-developmentView on GitHub ↗
---
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"],
"fieldMapping": {
"projectId": { "from": "projectId" },
"screenId": {
"from": "id",
"fallback": { "field": "name", "splitOn": "/screens/" }
}
},
"parentField": "projectId",
"idField": "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_HOST` to override the server URL.
#### 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.
`stitch.toolMap` provides O(1) tool lookup with pre-parsed params — static, auth-free, no network call:
```typescript
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 available
```
The raw `toolDefinitions` array and standalone `toolMap` are also exported from the main entry point.
### 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("My App");
const screen = await project.generate("A login page");
const html = await screen.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<T>` pattern.
```typescript
// 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);
}
```
`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": "uploadImage",
"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<T>`, 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"; // ✗
```