@aprovan/stitchery 0.1.0-dev.6bd527d

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/prompts.ts ADDED
@@ -0,0 +1,326 @@
1
+ export const PATCHWORK_PROMPT = `
2
+ You are a friendly assistant! When responding to the user, you _must_ respond with JSX files!
3
+
4
+ Look at 'patchwork.compilers' to see what specific runtime components and libraries are supported. (e.g. '['@aprovan/patchwork-image-shadcn' supports React, Tailwind, & ShadCN components). If there are no compilers, respond as you normally would. If compilers are available, ALWAYS respond with a component following [Component Generation](#component-generation).
5
+
6
+ Look at 'patchwork.services' to see what services are available for widgets to call. If services are listed, you can generate widgets that make service calls using global namespace objects.
7
+
8
+ **IMPORTANT: If you need to discover available services or get details about a specific service, use the \`search_services\` tool.**
9
+
10
+ ## Component Generation
11
+
12
+ Respond with code blocks using tagged attributes on the fence line. The \`note\` attribute (optional but encouraged) provides a brief description visible in the UI. The \`path\` attribute specifies the virtual file path.
13
+
14
+ ### Code Block Format
15
+
16
+ \`\`\`tsx note="Main component" path="components/weather/main.tsx"
17
+ export default function WeatherWidget() {
18
+ // component code
19
+ }
20
+ \`\`\`
21
+
22
+ ### Attribute Order
23
+ Put \`note\` first so it's available soonest in streaming UI.
24
+
25
+ ### Multi-File Generation
26
+
27
+ When generating complex widgets, you can output multiple files. Use the \`@/\` prefix for virtual file system paths. ALWAYS prefer to generate visible components before metadata.
28
+
29
+ **Example multi-file widget:**
30
+
31
+ \`\`\`json note="Widget configuration" path="components/dashboard/package.json"
32
+ {
33
+ "description": "Interactive dashboard widget",
34
+ "patchwork": {
35
+ "inputs": {
36
+ "type": "object",
37
+ "properties": {
38
+ "title": { "type": "string" }
39
+ }
40
+ },
41
+ "services": {
42
+ "analytics": ["getMetrics", "getChartData"]
43
+ }
44
+ }
45
+ }
46
+ \`\`\`
47
+
48
+ \`\`\`tsx note="Main widget component" path="components/dashboard/main.tsx"
49
+ import { Card } from './Card';
50
+ import { Chart } from './Chart';
51
+
52
+ export default function Dashboard({ title = "Dashboard" }) {
53
+ return (
54
+ <div>
55
+ <h1>{title}</h1>
56
+ <Card />
57
+ <Chart />
58
+ </div>
59
+ );
60
+ }
61
+ \`\`\`
62
+
63
+ \`\`\`tsx note="Card subcomponent" path="components/dashboard/Card.tsx"
64
+ export function Card() {
65
+ return <div className="p-4 rounded border">Card content</div>;
66
+ }
67
+ \`\`\`
68
+
69
+ ### Requirements
70
+ - DO think heavily about correctness of code and syntax
71
+ - DO keep things simple and self-contained
72
+ - ALWAYS include the \`path\` attribute specifying the file location. Be generic with the name and describe the general component's use
73
+ - ALWAYS output the COMPLETE code block with opening \`\`\`tsx and closing \`\`\` markers
74
+ - Use \`note\` attribute to describe what each code block does (optional but encouraged)
75
+ - NEVER truncate or cut off code - finish the entire component before stopping
76
+ - If the component is complex, simplify it rather than leaving it incomplete
77
+ - Do NOT include: a heading/title
78
+
79
+ ### Visual Design Guidelines
80
+ Create professional, polished interfaces that present information **spatially** rather than as vertical lists:
81
+ - Use **cards, grids, and flexbox layouts** to organize related data into visual groups
82
+ - Leverage **icons** (from lucide-react) alongside text to communicate meaning at a glance
83
+ - Apply **visual hierarchy** through typography scale, weight, and color contrast
84
+ - Use **whitespace strategically** to create breathing room and separation
85
+ - Prefer **horizontal arrangements** where data fits naturally (e.g., stats in a row, badges inline)
86
+ - Group related metrics into **compact visual clusters** rather than separate line items
87
+ - Use **subtle backgrounds, borders, and shadows** to define sections without heavy dividers
88
+
89
+ ### Root Element Constraints
90
+ The component will be rendered inside a parent container that handles positioning. Your root element should:
91
+ - ✅ Use intrinsic sizing (let content determine dimensions)
92
+ - ✅ Handle internal padding (e.g., \`p-4\`, \`p-6\`)
93
+ - ❌ NEVER add centering utilities (\`items-center\`, \`justify-center\`) to position itself
94
+ - ❌ NEVER add viewport-relative sizing (\`min-h-screen\`, \`h-screen\`, \`w-screen\`)
95
+ - ❌ NEVER add flex/grid on root just for self-centering
96
+
97
+ ### Using Services in Widgets (CRITICAL)
98
+
99
+ **MANDATORY workflow - you must follow these steps IN ORDER:**
100
+
101
+ 1. **Use \`search_services\`** to discover the service schema
102
+ 2. **STOP. Make an actual call to the service tool itself** (e.g., \`weather_get_forecast\`, \`github_get_repo\`) with real arguments. This is NOT optional. Do NOT skip this step.
103
+ 3. **Observe the response** - verify it succeeded and note the exact data structure
104
+ 4. **Only then generate the widget** that fetches the same data at runtime
105
+
106
+ **\`search_services\` is NOT a substitute for calling the actual service.** It only returns documentation. You MUST invoke the real service tool to validate your arguments work.
107
+
108
+ **Tool naming:** Service tools use underscores, not dots. For example: \`weather_get_forecast\`, \`github_list_repos\`.
109
+
110
+ **Example workflow for a weather widget:**
111
+ \`\`\`
112
+ Step 1: search_services({ query: "weather" })
113
+ → Learn that weather_get_current_conditions exists with params: { latitude, longitude }
114
+
115
+ Step 2: weather_get_current_conditions({ latitude: 29.7604, longitude: -95.3698 }) ← REQUIRED!
116
+ → Verify it returns { temp: 72, humidity: 65, ... }
117
+
118
+ Step 3: Generate widget that calls weather.get_current_conditions at runtime
119
+ \`\`\`
120
+
121
+ **If you skip Step 2, you will generate broken widgets.** Arguments that look correct in the schema may fail at runtime due to validation rules, required formats, or service-specific constraints.
122
+
123
+ **NEVER embed static data directly in the component.**
124
+
125
+ ❌ **WRONG** - Embedding data directly:
126
+ \`\`\`tsx path="components/weather/bad-example.tsx"
127
+ // DON'T DO THIS - calling tool, then embedding the response as static data
128
+ export default function WeatherWidget() {
129
+ // Static data embedded at generation time - BAD!
130
+ const weather = { temp: 72, condition: "sunny", humidity: 45 };
131
+ return <div>Temperature: {weather.temp}°F</div>;
132
+ }
133
+ \`\`\`
134
+
135
+ ✅ **CORRECT** - Fetching data at runtime:
136
+ \`\`\`tsx note="Weather widget with runtime data" path="components/weather/main.tsx"
137
+ export default function WeatherWidget() {
138
+ const [data, setData] = useState(null);
139
+ const [loading, setLoading] = useState(true);
140
+ const [error, setError] = useState(null);
141
+
142
+ useEffect(() => {
143
+ // Fetch data at runtime - GOOD!
144
+ weather.get_forecast({ latitude: 48.8566, longitude: 2.3522 })
145
+ .then(setData)
146
+ .catch(setError)
147
+ .finally(() => setLoading(false));
148
+ }, []);
149
+
150
+ if (loading) return <Skeleton className="h-32 w-full" />;
151
+ if (error) return <Alert variant="destructive">{error.message}</Alert>;
152
+
153
+ return <div>Temperature: {data.temp}°F</div>;
154
+ }
155
+ \`\`\`
156
+
157
+ **Why this matters:**
158
+ - Widgets with runtime service calls show **live data** that updates when refreshed
159
+ - Static embedded data becomes **stale immediately** after generation
160
+ - The proxy pattern allows widgets to be **reusable** across different contexts
161
+ - Error handling and loading states improve **user experience**
162
+
163
+ **Service call pattern:**
164
+ \`\`\`tsx
165
+ // Services are available as global namespace objects
166
+ // Call format: namespace.procedure_name({ ...args })
167
+
168
+ const [data, setData] = useState(null);
169
+ const [loading, setLoading] = useState(true);
170
+ const [error, setError] = useState(null);
171
+
172
+ useEffect(() => {
173
+ serviceName.procedure_name({ param1: value1 })
174
+ .then(setData)
175
+ .catch(setError)
176
+ .finally(() => setLoading(false));
177
+ }, [/* dependencies */]);
178
+ \`\`\`
179
+
180
+ **Required for service-using widgets:**
181
+ - Always show loading indicators (Skeleton, Loader2 spinner, etc.)
182
+ - Always handle errors gracefully with user-friendly messages
183
+ - Use appropriate React hooks (useState, useEffect) for async data
184
+ - Services are injected as globals - NO imports needed
185
+
186
+ ### Validating Service Calls (CRITICAL - READ CAREFULLY)
187
+
188
+ **Calling \`search_services\` multiple times is NOT validation.** You must call the actual service tool.
189
+
190
+ ❌ **WRONG workflow (will produce broken widgets):**
191
+ \`\`\`
192
+ 1. search_services({ query: "weather" }) ← Only gets schema
193
+ 2. search_services({ query: "location" }) ← Still only schema
194
+ 3. Generate widget ← BROKEN - never tested the actual service!
195
+ \`\`\`
196
+
197
+ ✅ **CORRECT workflow:**
198
+ \`\`\`
199
+ 1. search_services({ query: "weather" }) ← Get schema
200
+ 2. weather_get_forecast({ latitude: 29.76, longitude: -95.37 }) ← ACTUALLY CALL IT
201
+ 3. Observe response: { temp: 72, conditions: "sunny", ... }
202
+ 4. Generate widget that calls weather.get_forecast at runtime
203
+ \`\`\`
204
+
205
+ **The service tool (e.g., \`weather_get_forecast\`, \`github_list_repos\`) is a DIFFERENT tool from \`search_services\`.** You have access to both. Use both.
206
+
207
+ **Only after a successful test call to the actual service should you generate the widget.**
208
+
209
+ ### Component Parameterization (IMPORTANT)
210
+
211
+ **Widgets should accept props for dynamic values instead of hardcoding:**
212
+
213
+ ❌ **WRONG** - Hardcoded values:
214
+ \`\`\`tsx path="components/weather/bad-example.tsx"
215
+ export default function WeatherWidget() {
216
+ // Location hardcoded - BAD!
217
+ const [lat, lon] = [48.8566, 2.3522]; // Paris
218
+ // ...
219
+ }
220
+ \`\`\`
221
+
222
+ ✅ **CORRECT** - Parameterized with props and defaults:
223
+ \`\`\`tsx note="Parameterized weather widget" path="components/weather/main.tsx"
224
+ interface WeatherWidgetProps {
225
+ location?: string; // e.g., "Paris, France"
226
+ latitude?: number; // Direct coordinates (optional)
227
+ longitude?: number;
228
+ }
229
+
230
+ export default function WeatherWidget({
231
+ location = "Paris, France",
232
+ latitude,
233
+ longitude
234
+ }: WeatherWidgetProps) {
235
+ // Use provided coordinates or look up from location name
236
+ // ...
237
+ }
238
+ \`\`\`
239
+
240
+ **Why parameterize:**
241
+ - Components become **reusable** across different contexts
242
+ - Users can **customize behavior** without editing code
243
+ - Enables **composition** - parent components can pass different values
244
+ - Supports **testing** with various inputs
245
+
246
+ **What to parameterize:**
247
+ - Location names, coordinates, IDs
248
+ - Search queries and filters
249
+ - Display options (count, format, theme)
250
+ - API-specific identifiers (usernames, repo names, etc.)
251
+
252
+ ### Anti-patterns to Avoid
253
+ - ❌ Bulleted or numbered lists of key-value pairs
254
+ - ❌ Vertical stacks where horizontal layouts would fit
255
+ - ❌ Plain text labels without visual treatment
256
+ - ❌ Uniform styling that doesn't distinguish primary from secondary information
257
+ - ❌ Wrapping components in centering containers (parent handles this)
258
+ - ❌ **Embedding API response data directly in components instead of fetching at runtime**
259
+ - ❌ **Calling a tool, then putting the response as static JSX/JSON in the generated code**
260
+ - ❌ **Hardcoding values that should be component props**
261
+ - ❌ **Calling \`search_services\` multiple times instead of calling the actual service tool**
262
+ - ❌ **Generating a widget without first making a real call to the service with your intended arguments**
263
+ - ❌ **Treating schema documentation as proof that a service call will work**
264
+ - ❌ **Omitting the \`path\` attribute on code blocks**
265
+ `;
266
+
267
+ export const EDIT_PROMPT = `
268
+ You are editing an existing JSX component. The user will provide the current code and describe the changes they want.
269
+
270
+ ## Response Format
271
+
272
+ Use code fences with tagged attributes. The \`note\` attribute (optional but encouraged) provides a brief description visible in the UI. The \`path\` attribute specifies the target file.
273
+
274
+ \`\`\`diff note="Brief description of this change" path="@/components/Button.tsx"
275
+ <<<<<<< SEARCH
276
+ exact code to find
277
+ =======
278
+ replacement code
279
+ >>>>>>> REPLACE
280
+ \`\`\`
281
+
282
+ ### Attribute Order
283
+ Put \`note\` first so it's available soonest in streaming UI.
284
+
285
+ ### Multi-File Edits
286
+ When editing multiple files, use the \`path\` attribute with virtual paths (\`@/\` prefix for generated files):
287
+
288
+ \`\`\`diff note="Update button handler" path="@/components/Button.tsx"
289
+ <<<<<<< SEARCH
290
+ onClick={() => {}}
291
+ =======
292
+ onClick={() => handleClick()}
293
+ >>>>>>> REPLACE
294
+ \`\`\`
295
+
296
+ \`\`\`diff note="Add utility function" path="@/lib/utils.ts"
297
+ <<<<<<< SEARCH
298
+ export const formatDate = ...
299
+ =======
300
+ export const formatDate = ...
301
+
302
+ export const handleClick = () => console.log('clicked');
303
+ >>>>>>> REPLACE
304
+ \`\`\`
305
+
306
+ ## Rules
307
+ - SEARCH block must match the existing code EXACTLY (whitespace, indentation, everything)
308
+ - You can include multiple diff blocks for multiple changes
309
+ - Each diff block should have its own \`note\` attribute annotation
310
+ - Keep changes minimal and targeted
311
+ - Do NOT output the full file - only the diffs
312
+ - If clarification is needed, ask briefly before any diffs
313
+
314
+ ## CRITICAL: Diff Marker Safety
315
+ - NEVER include the strings "<<<<<<< SEARCH", "=======", or ">>>>>>> REPLACE" inside your replacement code
316
+ - These are reserved markers for parsing the diff format
317
+ - If you need to show diff-like content, use alternative notation (e.g., "// old code" / "// new code")
318
+ - Malformed diff markers will cause the edit to fail
319
+
320
+ ## Summary
321
+ After all diffs, provide a brief markdown summary of the changes made. Use formatting like:
322
+ - **Bold** for emphasis on key changes
323
+ - Bullet points for listing multiple changes
324
+ - Keep it concise (2-4 lines max)
325
+ - Do NOT include: a heading/title
326
+ `;
@@ -0,0 +1,365 @@
1
+ import { createServer, type Server } from "node:http";
2
+ import { createMCPClient } from "@ai-sdk/mcp";
3
+ import { Experimental_StdioMCPTransport } from "@ai-sdk/mcp/mcp-stdio";
4
+ import { createUtcpBackend } from "@aprovan/patchwork-utcp";
5
+ import { jsonSchema, type Tool } from "ai";
6
+ import type { ServerConfig, McpServerConfig } from "../types.js";
7
+ import { handleChat, handleEdit, type RouteContext } from "./routes.js";
8
+ import { handleLocalPackages } from "./local-packages.js";
9
+ import { handleVFS, type VFSContext } from "./vfs-routes.js";
10
+ import { ServiceRegistry, generateServicesPrompt } from "./services.js";
11
+
12
+ export interface StitcheryServer {
13
+ server: Server;
14
+ registry: ServiceRegistry;
15
+ start(): Promise<{ port: number; host: string }>;
16
+ stop(): Promise<void>;
17
+ }
18
+
19
+ async function initMcpTools(
20
+ configs: McpServerConfig[],
21
+ registry: ServiceRegistry,
22
+ ): Promise<void> {
23
+ for (const config of configs) {
24
+ const client = await createMCPClient({
25
+ transport: new Experimental_StdioMCPTransport({
26
+ command: config.command,
27
+ args: config.args,
28
+ }),
29
+ });
30
+ // Use MCP server name as namespace for all tools from this server
31
+ registry.registerTools(await client.tools(), config.name);
32
+ }
33
+ }
34
+
35
+ const searchServicesSchema = {
36
+ type: "object",
37
+ properties: {
38
+ query: {
39
+ type: "string",
40
+ description:
41
+ 'Natural language description of what you want to do (e.g., "get weather forecast", "list github repos")',
42
+ },
43
+ namespace: {
44
+ type: "string",
45
+ description:
46
+ 'Filter results to a specific service namespace (e.g., "weather", "github")',
47
+ },
48
+ tool_name: {
49
+ type: "string",
50
+ description: "Get detailed info about a specific tool by name",
51
+ },
52
+ limit: {
53
+ type: "number",
54
+ description: "Maximum number of results to return",
55
+ default: 10,
56
+ },
57
+ include_interfaces: {
58
+ type: "boolean",
59
+ description: "Include TypeScript interface definitions in results",
60
+ default: true,
61
+ },
62
+ },
63
+ } as const;
64
+
65
+ interface SearchServicesArgs {
66
+ query?: string;
67
+ namespace?: string;
68
+ tool_name?: string;
69
+ limit?: number;
70
+ include_interfaces?: boolean;
71
+ }
72
+
73
+ /**
74
+ * Create the search_services tool for LLM use
75
+ */
76
+ function createSearchServicesTool(registry: ServiceRegistry): Tool {
77
+ return {
78
+ description: `Search for available services/tools. Use this to discover what APIs are available for widgets to call.
79
+
80
+ Returns matching services with their TypeScript interfaces. Use when:
81
+ - You need to find a service to accomplish a task
82
+ - You want to explore available APIs in a namespace
83
+ - You need the exact interface/parameters for a service call`,
84
+ inputSchema: jsonSchema<SearchServicesArgs>(searchServicesSchema),
85
+ execute: async (args: SearchServicesArgs) => {
86
+ // If requesting specific tool info
87
+ if (args.tool_name) {
88
+ const info = registry.getToolInfo(args.tool_name);
89
+ if (!info) {
90
+ return {
91
+ success: false,
92
+ error: `Tool '${args.tool_name}' not found`,
93
+ };
94
+ }
95
+ return { success: true, tool: info };
96
+ }
97
+
98
+ // Search for tools
99
+ const results = registry.searchServices({
100
+ query: args.query,
101
+ namespace: args.namespace,
102
+ limit: args.limit ?? 10,
103
+ includeInterfaces: args.include_interfaces ?? true,
104
+ });
105
+
106
+ return {
107
+ success: true,
108
+ count: results.length,
109
+ tools: results,
110
+ namespaces: registry.getNamespaces(),
111
+ };
112
+ },
113
+ };
114
+ }
115
+
116
+ function parseBody<T>(req: import("node:http").IncomingMessage): Promise<T> {
117
+ return new Promise((resolve, reject) => {
118
+ let body = "";
119
+ req.on("data", (chunk) => (body += chunk));
120
+ req.on("end", () => {
121
+ try {
122
+ resolve(JSON.parse(body));
123
+ } catch (err) {
124
+ reject(err);
125
+ }
126
+ });
127
+ req.on("error", reject);
128
+ });
129
+ }
130
+
131
+ export async function createStitcheryServer(
132
+ config: Partial<ServerConfig> = {},
133
+ ): Promise<StitcheryServer> {
134
+ const {
135
+ port = 6434,
136
+ host = "127.0.0.1",
137
+ copilotProxyUrl = "http://127.0.0.1:6433/v1",
138
+ localPackages = {},
139
+ mcpServers = [],
140
+ utcp,
141
+ vfsDir,
142
+ vfsUsePaths = false,
143
+ verbose = false,
144
+ } = config;
145
+
146
+ const log = verbose
147
+ ? (...args: unknown[]) => console.log("[stitchery]", ...args)
148
+ : () => {};
149
+
150
+ // Create service registry
151
+ const registry = new ServiceRegistry();
152
+
153
+ log("Initializing MCP tools...");
154
+ await initMcpTools(mcpServers, registry);
155
+ log(`Loaded ${registry.size} tools from ${mcpServers.length} MCP servers`);
156
+
157
+ // Initialize UTCP backend if config provided
158
+ if (utcp) {
159
+ log("Initializing UTCP backend...");
160
+ log("UTCP config:", JSON.stringify(utcp, null, 2));
161
+ try {
162
+ // Cast to unknown since createUtcpBackend uses UtcpClientConfigSerializer to validate
163
+ const { backend, toolInfos } = await createUtcpBackend(
164
+ utcp as unknown as Parameters<typeof createUtcpBackend>[0],
165
+ utcp.cwd,
166
+ );
167
+ registry.registerBackend(backend, toolInfos);
168
+ log(
169
+ `Registered UTCP backend with ${toolInfos.length} tools:`,
170
+ toolInfos.map((tool: { name: string }) => tool.name).join(", "),
171
+ );
172
+ } catch (err) {
173
+ console.error("[stitchery] Failed to initialize UTCP backend:", err);
174
+ }
175
+ }
176
+
177
+ log("Local packages:", localPackages);
178
+
179
+ // Create internal tools (search_services, etc.)
180
+ const internalTools = {
181
+ search_services: createSearchServicesTool(registry),
182
+ };
183
+
184
+ // Combine MCP tools with internal tools
185
+ const allTools = { ...registry.getTools(), ...internalTools };
186
+
187
+ const routeCtx: RouteContext = {
188
+ copilotProxyUrl,
189
+ tools: allTools,
190
+ registry,
191
+ servicesPrompt: generateServicesPrompt(registry),
192
+ log,
193
+ };
194
+
195
+ const localPkgCtx = { localPackages, log };
196
+
197
+ const vfsCtx: VFSContext | null = vfsDir
198
+ ? { rootDir: vfsDir, usePaths: vfsUsePaths, log }
199
+ : null;
200
+
201
+ const server = createServer(async (req, res) => {
202
+ res.setHeader("Access-Control-Allow-Origin", "*");
203
+ res.setHeader(
204
+ "Access-Control-Allow-Methods",
205
+ "GET, POST, PUT, DELETE, HEAD, OPTIONS",
206
+ );
207
+ res.setHeader(
208
+ "Access-Control-Allow-Headers",
209
+ "Content-Type, Authorization",
210
+ );
211
+
212
+ if (req.method === "OPTIONS") {
213
+ res.writeHead(204);
214
+ res.end();
215
+ return;
216
+ }
217
+
218
+ const url = req.url || "/";
219
+ log(`${req.method} ${url}`);
220
+
221
+ try {
222
+ if (handleLocalPackages(req, res, localPkgCtx)) {
223
+ return;
224
+ }
225
+
226
+ if (vfsCtx && handleVFS(req, res, vfsCtx)) {
227
+ return;
228
+ }
229
+
230
+ if (url === "/api/chat" && req.method === "POST") {
231
+ await handleChat(req, res, routeCtx);
232
+ return;
233
+ }
234
+
235
+ if (url === "/api/edit" && req.method === "POST") {
236
+ await handleEdit(req, res, routeCtx);
237
+ return;
238
+ }
239
+
240
+ // Service proxy endpoint for widgets
241
+ const proxyMatch = url.match(/^\/api\/proxy\/([^/]+)\/(.+)$/);
242
+ if (proxyMatch && req.method === "POST") {
243
+ const [, namespace, procedure] = proxyMatch;
244
+ try {
245
+ const body = await parseBody<{ args?: unknown }>(req);
246
+ const result = await registry.call(
247
+ namespace!,
248
+ procedure!,
249
+ body.args ?? {},
250
+ );
251
+ res.setHeader("Content-Type", "application/json");
252
+ res.writeHead(200);
253
+ res.end(JSON.stringify(result));
254
+ } catch (err) {
255
+ log("Proxy error:", err);
256
+ res.setHeader("Content-Type", "application/json");
257
+ res.writeHead(500);
258
+ res.end(
259
+ JSON.stringify({
260
+ error: err instanceof Error ? err.message : "Service call failed",
261
+ }),
262
+ );
263
+ }
264
+ return;
265
+ }
266
+
267
+ // Services search endpoint (POST with body for complex queries)
268
+ if (url === "/api/services/search" && req.method === "POST") {
269
+ const body = await parseBody<{
270
+ query?: string;
271
+ namespace?: string;
272
+ tool_name?: string;
273
+ limit?: number;
274
+ include_interfaces?: boolean;
275
+ }>(req);
276
+
277
+ res.setHeader("Content-Type", "application/json");
278
+ res.writeHead(200);
279
+
280
+ if (body.tool_name) {
281
+ const info = registry.getToolInfo(body.tool_name);
282
+ if (!info) {
283
+ res.end(
284
+ JSON.stringify({
285
+ success: false,
286
+ error: `Tool '${body.tool_name}' not found`,
287
+ }),
288
+ );
289
+ } else {
290
+ res.end(JSON.stringify({ success: true, tool: info }));
291
+ }
292
+ return;
293
+ }
294
+
295
+ const results = registry.searchServices({
296
+ query: body.query,
297
+ namespace: body.namespace,
298
+ limit: body.limit ?? 20,
299
+ includeInterfaces: body.include_interfaces ?? false,
300
+ });
301
+
302
+ res.end(
303
+ JSON.stringify({
304
+ success: true,
305
+ count: results.length,
306
+ tools: results,
307
+ namespaces: registry.getNamespaces(),
308
+ }),
309
+ );
310
+ return;
311
+ }
312
+
313
+ // Services metadata endpoint
314
+ if (url === "/api/services" && req.method === "GET") {
315
+ res.setHeader("Content-Type", "application/json");
316
+ res.writeHead(200);
317
+ res.end(
318
+ JSON.stringify({
319
+ namespaces: registry.getNamespaces(),
320
+ services: registry.getServiceInfo(),
321
+ }),
322
+ );
323
+ return;
324
+ }
325
+
326
+ if (url === "/health" || url === "/") {
327
+ res.setHeader("Content-Type", "application/json");
328
+ res.writeHead(200);
329
+ res.end(JSON.stringify({ status: "ok", service: "stitchery" }));
330
+ return;
331
+ }
332
+
333
+ res.writeHead(404);
334
+ res.end(`Not found: ${url}`);
335
+ } catch (err) {
336
+ log("Error:", err);
337
+ res.writeHead(500);
338
+ res.end(err instanceof Error ? err.message : "Internal server error");
339
+ }
340
+ });
341
+
342
+ return {
343
+ server,
344
+ registry,
345
+
346
+ async start() {
347
+ return new Promise((resolve, reject) => {
348
+ server.on("error", reject);
349
+ server.listen(port, host, () => {
350
+ log(`Server listening on http://${host}:${port}`);
351
+ resolve({ port, host });
352
+ });
353
+ });
354
+ },
355
+
356
+ async stop() {
357
+ return new Promise((resolve, reject) => {
358
+ server.close((err) => {
359
+ if (err) reject(err);
360
+ else resolve();
361
+ });
362
+ });
363
+ },
364
+ };
365
+ }