Skip to content
Closed
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
4 changes: 3 additions & 1 deletion packages/sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ await client.close();

### `StitchProxy`

For a more advanced example of building a custom MCP server with compound tools, see the [MCP Server Example](./examples/mcp-server/README.md).

An MCP proxy server that forwards requests to Stitch. Use this to expose Stitch tools through your own MCP server.

```ts
Expand Down Expand Up @@ -297,4 +299,4 @@ Program](https://bughunters.google.com/open-source-security).

## License

Apache 2.0 — see [LICENSE](LICENSE) for details.
Apache 2.0 — see [LICENSE](LICENSE) for details.
44 changes: 44 additions & 0 deletions packages/sdk/examples/mcp-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# MCP Server Example

This example demonstrates how to wrap the Stitch SDK (`@google/stitch-sdk`) into an MCP server that exposes compound tools.

## Why Compound Tools?

The raw Stitch MCP tools are granular (e.g., generate a screen, fetch HTML). Agents often need to perform multiple steps to get a usable result. Compound tools reduce round-trips by combining these steps on the server side.

For example, `generate_and_extract` generates a screen, fetches the HTML, extracts the Tailwind theme and body, and returns them in one call.

## Prerequisites

- `STITCH_API_KEY` environment variable must be set.
- Install dependencies: `bun install`

## Running the Server

Since this is an MCP server, it communicates over stdio. You can run it directly:

```bash
bun index.ts
```

Then type a JSON-RPC request like:

```json
{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}
```

Or configure it in an MCP client (like Claude Desktop) with the command:

```json
{
"mcpServers": {
"stitch-design": {
"command": "bun",
"args": ["run", "/path/to/packages/sdk/examples/mcp-server/index.ts"],
"env": {
"STITCH_API_KEY": "your-api-key"
}
}
}
}
```
30 changes: 30 additions & 0 deletions packages/sdk/examples/mcp-server/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Skill: Using the Stitch Design MCP Server

This skill demonstrates how to expose the Stitch SDK as an MCP server with compound tools that combine multiple Stitch operations into single calls.

## Overview

The Stitch SDK provides granular tools (e.g., generate a screen, fetch HTML, edit a screen). While powerful, agents often need compound tools to reduce round-trips.

This MCP server exposes compound tools like:
- `generate_and_extract`: Generates a screen and returns the extracted Tailwind theme, HTML body, and screenshot URL in a single call.
- `compare_themes`: Generates variants and diffs their configs.
- `scaffold_project`: Generates multiple screens and returns them as a page manifest.

## Usage

This is a standard MCP server running over stdio.

1. Ensure `STITCH_API_KEY` is set in your environment.
2. Run the server using an MCP client (e.g., Claude Desktop, Cursor, or another agent):
```bash
bun run packages/sdk/examples/mcp-server/index.ts
```

## Example: `generate_and_extract`

When you call `generate_and_extract` with `{ "projectId": "...", "prompt": "..." }`, the server will:
1. Call `project.generate(prompt)` to create the screen.
2. Fetch the HTML content.
3. Parse the HTML to extract the Tailwind `<script>` block and the `<body>` content.
4. Return a JSON object with `theme`, `body`, `imageUrl`, and `screenId`.
257 changes: 257 additions & 0 deletions packages/sdk/examples/mcp-server/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { stitch } from "@google/stitch-sdk";

const server = new Server(
{
name: "stitch-design-mcp",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);

server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "generate_and_extract",
description:
"Generate a screen and return extracted theme + HTML body + screenshot URL in one call",
inputSchema: {
type: "object",
properties: {
projectId: {
type: "string",
description: "The ID of the Stitch project",
},
prompt: {
type: "string",
description: "The prompt to generate the screen",
},
},
required: ["projectId", "prompt"],
},
},
{
name: "compare_themes",
description: "Generate variants of a screen and diff their configs",
inputSchema: {
type: "object",
properties: {
projectId: {
type: "string",
description: "The ID of the Stitch project",
},
screenId: {
type: "string",
description: "The ID of the base screen",
},
prompt: {
type: "string",
description: "The prompt for the variants",
},
},
required: ["projectId", "screenId", "prompt"],
},
},
{
name: "scaffold_project",
description: "Generate multiple screens and return them as a page manifest",
inputSchema: {
type: "object",
properties: {
projectId: {
type: "string",
description: "The ID of the Stitch project",
},
prompts: {
type: "array",
items: {
type: "string"
},
description: "An array of prompts to generate screens",
},
},
required: ["projectId", "prompts"],
},
},
],
};
});

async function extractTheme(htmlUrl: string): Promise<string> {
let html = "";
if (htmlUrl.startsWith("http")) {
const res = await fetch(htmlUrl);
html = await res.text();
} else {
html = htmlUrl;
}
const themeMatch = html.match(
/<script id="tailwind-config">([\s\S]*?)<\/script>/
);
return themeMatch ? themeMatch[1].trim() : "No theme found";
}

async function extractBody(htmlUrl: string): Promise<string> {
let html = "";
if (htmlUrl.startsWith("http")) {
const res = await fetch(htmlUrl);
html = await res.text();
} else {
html = htmlUrl;
}
const bodyMatch = html.match(/<body[^>]*>([\s\S]*?)<\/body>/);
return bodyMatch ? bodyMatch[1].trim() : "No body found";
}

server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "generate_and_extract") {
const { projectId, prompt } = request.params.arguments as any;
try {
const project = stitch.project(projectId);
const screen = await project.generate(prompt);
const htmlUrl = await screen.getHtml();
const imageUrl = await screen.getImage();
const theme = await extractTheme(htmlUrl);
const body = await extractBody(htmlUrl);

return {
content: [
{
type: "text",
text: JSON.stringify(
{
theme,
body,
imageUrl,
screenId: screen.id,
},
null,
2
),
},
],
};
} catch (e: any) {
return {
content: [
{
type: "text",
text: `Error generating screen: ${e.message}`,
},
],
isError: true,
};
}
}

if (request.params.name === "compare_themes") {
const { projectId, screenId, prompt } = request.params.arguments as any;
try {
const project = stitch.project(projectId);
const screen = await project.getScreen(screenId);
const variants = await screen.variants(prompt, { variantCount: 2 });

const configs = await Promise.all(
variants.map(async (v) => {
const htmlUrl = await v.getHtml();
return await extractTheme(htmlUrl);
})
);

return {
content: [
{
type: "text",
text: JSON.stringify(
{
variants: variants.map((v, i) => ({
screenId: v.id,
theme: configs[i],
})),
},
null,
2
),
},
],
};
} catch (e: any) {
return {
content: [
{
type: "text",
text: `Error comparing themes: ${e.message}`,
},
],
isError: true,
};
}
}

if (request.params.name === "scaffold_project") {
const { projectId, prompts } = request.params.arguments as { projectId: string, prompts: string[] };
try {
const project = stitch.project(projectId);

const results = await Promise.allSettled(
prompts.map(p => project.generate(p))
);

const manifest = [];
for (const [index, result] of results.entries()) {
if (result.status === "fulfilled") {
const screen = result.value;
manifest.push({
prompt: prompts[index],
screenId: screen.id,
imageUrl: await screen.getImage()
});
} else {
manifest.push({
prompt: prompts[index],
error: result.reason.message
});
}
}

return {
content: [
{
type: "text",
text: JSON.stringify({ manifest }, null, 2),
},
],
};
} catch (e: any) {
return {
content: [
{
type: "text",
text: `Error scaffolding project: ${e.message}`,
},
],
isError: true,
};
}
}

throw new Error(`Tool not found: ${request.params.name}`);
});

async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Stitch Design MCP Server running on stdio");
}

main().catch(console.error);
Loading