stitch-sdk-development

$npx mdskill add google-labs-code/stitch-sdk/stitch-sdk-development

Build 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"; // ✗
```
More from google-labs-code/stitch-sdk