@polymorphism-tech/morph-spec 4.20.0 → 4.22.0

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.
Files changed (28) hide show
  1. package/README.md +2 -2
  2. package/claude-plugin.json +1 -1
  3. package/docs/CHEATSHEET.md +1 -1
  4. package/docs/QUICKSTART.md +1 -1
  5. package/framework/agents.json +15 -0
  6. package/framework/hooks/shared/phase-utils.js +3 -0
  7. package/framework/phases.json +1 -1
  8. package/framework/skills/level-0-meta/mcp-registry.json +56 -10
  9. package/framework/skills/level-0-meta/morph-frontend-review/SKILL.md +1 -1
  10. package/framework/skills/level-0-meta/morph-gemini-image/SKILL.md +290 -0
  11. package/framework/skills/level-0-meta/morph-post-implementation/SKILL.md +19 -0
  12. package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +1 -1
  13. package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +10 -10
  14. package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +146 -5
  15. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/implementer-prompt.md +8 -0
  16. package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +1 -1
  17. package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +1 -1
  18. package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +216 -9
  19. package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +1 -1
  20. package/framework/standards/STANDARDS.json +32 -0
  21. package/framework/standards/frontend/design-system/ai-image-generation.md +244 -0
  22. package/framework/standards/integration/mcp/mcp-tools.md +48 -0
  23. package/package.json +1 -1
  24. package/src/commands/mcp/mcp-setup.js +99 -2
  25. package/src/core/paths/output-schema.js +1 -0
  26. package/src/lib/agents/output-collector.js +1 -0
  27. package/src/lib/detectors/index.js +1 -1
  28. package/src/lib/installers/mcp-installer.js +140 -1
@@ -0,0 +1,244 @@
1
+ # AI Image Generation
2
+
3
+ > **Scope:** nextjs
4
+ > **Layer:** 1 (load when relevant)
5
+ > **Keywords:** image, gemini, asset, ai-generation, placeholder, mcp, next/image
6
+ > **Load When:** feature has UI components requiring image assets
7
+
8
+ ## When to Use AI-Generated Images
9
+
10
+ | Use Case | Approach | Examples |
11
+ |----------|----------|---------|
12
+ | **AI generation** | Gemini Image MCP | Hero images, brand illustrations, feature-specific visuals, empty states, background panels |
13
+ | **Stock photos** | External sources | Generic content (people in offices, nature scenes, stock portraits) |
14
+ | **SVG/CSS** | Code-based | Icons, patterns, gradients, geometric shapes, loading animations |
15
+ | **Skip** | Other tools | Screenshots (Playwright), diagrams (Mermaid), logos (vector tools) |
16
+
17
+ **Decision rule:** If the image is unique to the project's brand/aesthetic and doesn't exist as stock, use AI generation. If it's generic and widely available, use stock. If it's geometric or icon-based, use SVG/CSS.
18
+
19
+ ## Gemini Image MCP API
20
+
21
+ ```javascript
22
+ // Generate a new image
23
+ mcp__gemini_image_mcp__generate_image({
24
+ prompt: string, // Text description (max 5000 chars, narrative style)
25
+ aspectRatio?: "1:1" | "2:3" | "3:2" | "3:4" | "4:3" | "9:16" | "16:9",
26
+ outputPath?: string, // File path to save (e.g., "public/images/feature/hero.png")
27
+ model?: string, // Default: gemini-2.5-flash-image
28
+ })
29
+
30
+ // Edit an existing image
31
+ mcp__gemini_image_mcp__edit_image({
32
+ imageSource: string, // Path to image or image ID
33
+ prompt: string, // Short, targeted edit instructions (1-2 sentences)
34
+ outputPath?: string,
35
+ })
36
+
37
+ // Continue editing last image
38
+ mcp__gemini_image_mcp__continue_editing({
39
+ prompt: string, // One adjustment at a time
40
+ })
41
+
42
+ // List generated images
43
+ mcp__gemini_image_mcp__list_images({
44
+ type?: "generated" | "edited" | "all",
45
+ limit?: number,
46
+ })
47
+
48
+ // Get image details
49
+ mcp__gemini_image_mcp__get_image_info({
50
+ imageId?: string, // Defaults to last image
51
+ })
52
+ ```
53
+
54
+ ## Prompt Engineering Patterns
55
+
56
+ ### Design-System-Aware Prompts
57
+
58
+ Read `design-system.md` or `globals.css` to extract project tokens. Inject into prompts:
59
+
60
+ - **Background color** → use as image background
61
+ - **Primary/accent colors** → use as highlight colors
62
+ - **Font pair** → mention for text elements
63
+ - **Aesthetic direction** → "Modern Luxury", "Kinetic Atelier", etc.
64
+
65
+ ```
66
+ "{subject description}. Dark charcoal background (#10131a) with copper (#D2802D)
67
+ and gold (#FFB779) accent colors. {style modifiers}. Premium luxury aesthetic.
68
+ {composition rules}."
69
+ ```
70
+
71
+ ### Component-Contextual Prompts
72
+
73
+ Describe what the image will BE in the UI, not just the subject:
74
+
75
+ - **Hero image** → "floating tablet showing app dashboard on dark background" (not just "fitness dashboard")
76
+ - **Auth background** → "gym equipment with moody lighting" (matches brand feel)
77
+ - **Empty state** → "artistic wireframe body silhouette" (matches feature context)
78
+
79
+ ### Narrative Style
80
+
81
+ Gemini responds best to coherent paragraphs. Build prompts by layering:
82
+
83
+ 1. **Subject** — hyper-specific description
84
+ 2. **Environment** — foreground, mid-ground, background (layered for depth)
85
+ 3. **Lighting** — multiple sources with different colors/directions
86
+ 4. **Style** — photography terms ("85mm portrait lens") or art style ("cel-shaded")
87
+ 5. **Color palette** — dominant colors and contrast relationships
88
+ 6. **Composition** — camera angle, framing, negative space
89
+
90
+ **Never:** keyword spam ("beautiful, stunning, 4k, masterpiece"), JSON prompts, negative prompts ("no cars").
91
+
92
+ ## Iterative Editing Workflows
93
+
94
+ **Cardinal rule: generation demands depth; editing demands restraint.**
95
+
96
+ ### Adding / Removing Elements
97
+ One sentence. Don't describe how to integrate — the model handles that.
98
+ ```
99
+ "Add a knitted wizard hat to the cat."
100
+ ```
101
+
102
+ ### Background Replacement
103
+ State the new background simply. Don't over-describe.
104
+ ```
105
+ "Replace the background with a tropical beach at sunset."
106
+ ```
107
+
108
+ ### Inpainting (Targeted Changes)
109
+ Name what changes and what stays in one line.
110
+ ```
111
+ "Change the blue sofa to a brown leather chesterfield. Keep everything else."
112
+ ```
113
+
114
+ ### Style Transfer
115
+ Name the target style.
116
+ ```
117
+ "Transform this into Van Gogh's Starry Night style."
118
+ ```
119
+
120
+ ### Iterative Refinement with `continue_editing`
121
+ One change at a time:
122
+ - "Make the lighting warmer"
123
+ - "More contrast in the shadows"
124
+ - "Zoom out a bit"
125
+ - "More serious expression"
126
+
127
+ ### Text Rendering
128
+ The model frequently misspells words. After generating any image with text:
129
+ 1. View the result
130
+ 2. If text is wrong: `continue_editing` with "Fix the text — it should read '{correct text}' exactly. Keep everything else."
131
+ 3. Expect 2-3 iterations for correct text
132
+
133
+ ## Aspect Ratio Guide
134
+
135
+ | Use Case | Ratio | Examples |
136
+ |----------|-------|---------|
137
+ | Hero / banner | `16:9` | Landing page hero, YouTube thumbnail, desktop wallpaper |
138
+ | Card / blog header | `3:2` | Feature cards, blog post headers, landscape photo |
139
+ | Avatar / icon | `1:1` | Profile pictures, social media posts, square icons |
140
+ | Auth panel / sidebar | `3:4` | Login brand image, Pinterest-style, portrait poster |
141
+ | Portrait | `2:3` | Book cover, tall illustration, portrait photo |
142
+ | Story / reel | `9:16` | Mobile wallpaper, social story, vertical banner |
143
+ | Standard display | `4:3` | Documentation, slides, standard display |
144
+
145
+ ## File Naming & Directory
146
+
147
+ **Convention:** `public/images/{feature}/{purpose}.png`
148
+
149
+ - Lowercase, kebab-case, descriptive names
150
+ - Group by feature, not by type
151
+ - Examples:
152
+ - `public/images/ui-redesign/hero-mockup.png`
153
+ - `public/images/ui-redesign/gym-equipment.png`
154
+ - `public/images/ui-redesign/body-analysis.png`
155
+
156
+ ## Next.js Image Integration
157
+
158
+ ```tsx
159
+ import Image from 'next/image';
160
+
161
+ // Above-the-fold hero image (priority for LCP)
162
+ <Image
163
+ src="/images/{feature}/hero-mockup.png"
164
+ alt="AI fitness analysis dashboard"
165
+ width={1440}
166
+ height={810}
167
+ priority
168
+ sizes="100vw"
169
+ />
170
+
171
+ // Card image with responsive sizing
172
+ <Image
173
+ src="/images/{feature}/card-image.png"
174
+ alt="Feature card illustration"
175
+ width={600}
176
+ height={400}
177
+ sizes="(max-width: 768px) 100vw, 50vw"
178
+ className="rounded-lg"
179
+ />
180
+
181
+ // Background image with fill
182
+ <div className="relative h-full">
183
+ <Image
184
+ src="/images/{feature}/auth-background.png"
185
+ alt=""
186
+ fill
187
+ className="object-cover"
188
+ priority={false}
189
+ />
190
+ </div>
191
+ ```
192
+
193
+ ## `assets.md` Format
194
+
195
+ The `assets.md` file is created during the UI/UX phase (Step 4.5) and consumed by the implement phase.
196
+
197
+ ```markdown
198
+ # Image Assets — {feature}
199
+
200
+ ## Design Context
201
+ - Aesthetic: {from design-system.md}
202
+ - Primary: {--color-primary value}
203
+ - Background: {--color-background value}
204
+ - Accent: {--color-accent value}
205
+ - Font pair: {display + body fonts}
206
+
207
+ ## Assets
208
+
209
+ ### IMG-001: {Descriptive Name}
210
+ - **Purpose:** {What this image represents in the UI}
211
+ - **Route:** {App route where it appears}
212
+ - **Component:** {React component name}
213
+ - **Aspect Ratio:** {1:1 | 3:2 | 16:9 | etc.}
214
+ - **Output Path:** `public/images/{feature}/{kebab-name}.png`
215
+ - **Prompt:** "{Narrative prompt with design system colors and aesthetic direction injected}"
216
+ - **Priority:** {high | medium | low}
217
+ ```
218
+
219
+ **Example entry:**
220
+
221
+ ```markdown
222
+ ### IMG-001: Hero Mockup
223
+ - **Purpose:** App dashboard floating on dark background for landing page hero
224
+ - **Route:** /
225
+ - **Component:** HeroSection
226
+ - **Aspect Ratio:** 16:9
227
+ - **Output Path:** `public/images/ui-redesign/hero-mockup.png`
228
+ - **Prompt:** "A floating tablet showing a modern AI fitness analysis dashboard on a dark charcoal background (#10131a). Copper (#D2802D) and gold (#FFB779) accent lighting creates warm rim highlights around the device edges. The screen displays body metrics with glassmorphism cards. Premium luxury aesthetic, photorealistic, cinematic lighting with a key light from the left and cool blue fill from the right."
229
+ - **Priority:** high
230
+ ```
231
+
232
+ ## Quality Checklist
233
+
234
+ - [ ] Colors match design system tokens (no random colors in prompts)
235
+ - [ ] No text artifacts or misspellings (if text in image)
236
+ - [ ] Correct aspect ratio for intended use
237
+ - [ ] Appropriate file size (< 500KB for web, < 200KB for cards)
238
+ - [ ] Descriptive alt text written for accessibility
239
+ - [ ] `<Image>` component with proper width/height dimensions
240
+ - [ ] `priority` prop set for above-the-fold images
241
+ - [ ] Build compiles after integration (`npm run build`)
242
+ - [ ] Image visible in dev server at correct route
243
+
244
+ *MORPH-SPEC by Polymorphism Tech*
@@ -81,6 +81,53 @@ const docs = await mcp__context7__query_docs({
81
81
  // WebFetch for specific component API pages
82
82
  ```
83
83
 
84
+ ### Gemini Image MCP — AI Image Generation
85
+
86
+ > Available in: **UI/UX** (preview generation), **Implement** (production assets)
87
+
88
+ | Tool | Use Case | Example |
89
+ |------|----------|---------|
90
+ | `generate_image` | Create new image from text prompt | `mcp__gemini-image-mcp__generate_image({ prompt, aspectRatio, outputPath })` |
91
+ | `edit_image` | Modify existing image | `mcp__gemini-image-mcp__edit_image({ imageSource, prompt })` |
92
+ | `continue_editing` | Iterate on last generated/edited image | `mcp__gemini-image-mcp__continue_editing({ prompt })` |
93
+ | `list_images` | Browse image generation history | `mcp__gemini-image-mcp__list_images({ type: "all" })` |
94
+ | `get_image_info` | Get metadata about a specific image | `mcp__gemini-image-mcp__get_image_info({ imageId })` |
95
+
96
+ ```javascript
97
+ // Generate a hero image using design system tokens
98
+ // Read design-system.md first to get project colors and aesthetic direction
99
+ const image = await mcp__gemini_image_mcp__generate_image({
100
+ prompt: "A floating tablet showing a modern dashboard interface on a dark charcoal background with copper and gold accent lighting. Premium luxury aesthetic, photorealistic, cinematic lighting with warm rim light.",
101
+ aspectRatio: "16:9",
102
+ outputPath: "public/images/my-feature/hero-mockup.png"
103
+ });
104
+
105
+ // Edit an existing image (keep edits short and targeted)
106
+ await mcp__gemini_image_mcp__edit_image({
107
+ imageSource: "public/images/my-feature/hero-mockup.png",
108
+ prompt: "Make the lighting warmer and add a subtle copper glow around the tablet edges."
109
+ });
110
+
111
+ // Iterate with continue_editing (one change at a time)
112
+ await mcp__gemini_image_mcp__continue_editing({
113
+ prompt: "More contrast in the shadows."
114
+ });
115
+
116
+ // Fallback: Manual image creation, stock photos, or placeholder divs
117
+ ```
118
+
119
+ **Use cases by phase:**
120
+
121
+ | Phase | Use Case | Tool |
122
+ |-------|----------|------|
123
+ | UI/UX (2) | Generate preview images for design approval | `generate_image` |
124
+ | Implement (7) | Generate production assets from assets.md specs | `generate_image` + `edit_image` |
125
+ | Implement (7) | Iterate on generated images for quality | `continue_editing` |
126
+
127
+ > **Standard:** See `frontend/design-system/ai-image-generation.md` for prompt engineering patterns, aspect ratio guide, and `assets.md` format.
128
+
129
+ ---
130
+
84
131
  ### Phase 3 (Design) — Schema & Architecture
85
132
 
86
133
  | Tool | Use Case | Example |
@@ -246,6 +293,7 @@ await mcp__vercel__manage_env_vars({
246
293
  | Container ops | Docker CLI (`docker ps`, `docker logs`, `docker compose`) | — |
247
294
  | Cloud resources | Azure CLI (`az resource`, `az deployment`) | — |
248
295
  | Vercel deployments | Vercel MCP | Bash `vercel` CLI |
296
+ | AI image generation | Gemini Image MCP | Manual creation or stock photos |
249
297
 
250
298
  **Rule:** MCP tools provide structured data (JSON responses). Native tools require manual parsing. **Always prefer MCP when available** — fall back to native when not. **Exceptions:** Neon, Docker, and Azure always use their respective CLIs directly — never MCP.
251
299
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polymorphism-tech/morph-spec",
3
- "version": "4.20.0",
3
+ "version": "4.22.0",
4
4
  "description": "MORPH-SPEC: AI-First development framework with validation pipeline and multi-stack support",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -13,6 +13,8 @@
13
13
  import chalk from 'chalk';
14
14
  import inquirer from 'inquirer';
15
15
  import ora from 'ora';
16
+ import { readFileSync, existsSync } from 'fs';
17
+ import { join } from 'path';
16
18
  import {
17
19
  orchestrateMcpSetup,
18
20
  installAutoMcps,
@@ -21,15 +23,34 @@ import {
21
23
  generateSetupInstructions,
22
24
  formatMcpStatusTable,
23
25
  loadMcpRegistry,
24
- isRemoteMcp
26
+ isRemoteMcp,
27
+ generateStitchProxy,
28
+ patchConfigForPlatform
25
29
  } from '../../lib/installers/mcp-installer.js';
26
30
  import { detectClaudeConfig } from '../../lib/detectors/claude-config-detector.js';
27
31
  import { detectProject } from '../../lib/detectors/index.js';
32
+ import { installMcpServers } from '../../utils/claude-settings-manager.js';
28
33
 
29
34
  /**
30
- * Detect the current project's stack
35
+ * Detect the current project's stack.
36
+ * Prefers .morph/config/config.json (set during morph-spec init) over filesystem re-detection.
31
37
  */
32
38
  async function detectStack(targetPath) {
39
+ // 1. Prefer the authoritative config (handles compound stacks like nextjs+dotnet)
40
+ try {
41
+ const configPath = join(targetPath, '.morph', 'config', 'config.json');
42
+ if (existsSync(configPath)) {
43
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'));
44
+ const configStack = config.project?.stack;
45
+ if (configStack && configStack !== 'unknown') {
46
+ return configStack;
47
+ }
48
+ }
49
+ } catch {
50
+ // Fall through to filesystem detection
51
+ }
52
+
53
+ // 2. Fallback: detect from filesystem
33
54
  try {
34
55
  const results = await detectProject(targetPath, { conversation: false, generateStandards: false });
35
56
  return results.structure?.stack || 'unknown';
@@ -186,6 +207,60 @@ async function setupSpecificMcp(targetPath, name, stack, existingMcps) {
186
207
 
187
208
  const credentials = mcpEntry.install.credentials || [];
188
209
 
210
+ // Stitch MCP — custom OAuth proxy setup
211
+ if (name === 'stitch' && !mcpEntry.install.autoInstall) {
212
+ console.log(chalk.cyan(`\n Setting up Stitch MCP (OAuth proxy)\n`));
213
+ console.log(chalk.gray(` Usage: ${mcpEntry.usage}`));
214
+ console.log('');
215
+
216
+ const gcpCred = credentials.find(c => c.envVar === 'STITCH_GCP_PROJECT');
217
+ const { gcpProject } = await inquirer.prompt([{
218
+ type: 'input',
219
+ name: 'gcpProject',
220
+ message: gcpCred ? `${gcpCred.name}:` : 'GCP Project ID:',
221
+ }]);
222
+
223
+ if (!gcpProject || !gcpProject.trim()) {
224
+ console.log(chalk.red(' GCP Project ID is required.'));
225
+ console.log('');
226
+ return;
227
+ }
228
+
229
+ const spinner = ora('Generating Stitch proxy...').start();
230
+ const { proxyPath } = generateStitchProxy(targetPath, gcpProject.trim());
231
+ spinner.succeed(`Proxy script generated: ${proxyPath}`);
232
+
233
+ const config = { ...mcpEntry.install.config };
234
+ const result = await installMcpServers(targetPath, { stitch: patchConfigForPlatform(config) });
235
+ if (result.added.length > 0) {
236
+ console.log(chalk.green(` ✓ Stitch MCP configured (using OAuth proxy)`));
237
+ } else {
238
+ console.log(chalk.yellow(` ⚠ Stitch MCP already configured`));
239
+ }
240
+
241
+ if (mcpEntry.companionSkills?.installCommand) {
242
+ const { installSkills } = await inquirer.prompt([{
243
+ type: 'confirm',
244
+ name: 'installSkills',
245
+ message: 'Install Stitch companion skills (design, loop, etc.)?',
246
+ default: true
247
+ }]);
248
+ if (installSkills) {
249
+ const skillResults = installCompanionSkills({ stitch: mcpEntry });
250
+ for (const r of skillResults) {
251
+ if (r.success) {
252
+ console.log(chalk.green(` ✓ Companion skills installed: ${r.repo}`));
253
+ } else {
254
+ console.log(chalk.yellow(` ⚠ Failed to install companion skills: ${r.repo}`));
255
+ }
256
+ }
257
+ }
258
+ }
259
+
260
+ console.log('');
261
+ return;
262
+ }
263
+
189
264
  // Auto-installable — just install
190
265
  if (mcpEntry.install.autoInstall) {
191
266
  const spinner = ora(`Installing ${name}...`).start();
@@ -349,6 +424,28 @@ async function interactiveWizard(targetPath, stack, existingMcps) {
349
424
  }]);
350
425
 
351
426
  if (setupNow) {
427
+ // Stitch — custom OAuth proxy flow
428
+ if (name === 'stitch' && !entry.install.autoInstall) {
429
+ const gcpCred = entry.install.credentials?.find(c => c.envVar === 'STITCH_GCP_PROJECT');
430
+ const { gcpProject } = await inquirer.prompt([{
431
+ type: 'input',
432
+ name: 'gcpProject',
433
+ message: gcpCred ? `${gcpCred.name}:` : 'GCP Project ID:',
434
+ }]);
435
+ if (gcpProject?.trim()) {
436
+ const { proxyPath } = generateStitchProxy(targetPath, gcpProject.trim());
437
+ console.log(chalk.green(` ✓ Proxy generated: ${proxyPath}`));
438
+ const config = { ...entry.install.config };
439
+ await installMcpServers(targetPath, { stitch: patchConfigForPlatform(config) });
440
+ console.log(chalk.green(` ✓ Stitch configured (OAuth proxy)`));
441
+ const skillResults = installCompanionSkills({ stitch: entry });
442
+ for (const r of skillResults) {
443
+ if (r.success) console.log(chalk.green(` ✓ Companion skills: ${r.repo}`));
444
+ }
445
+ }
446
+ continue;
447
+ }
448
+
352
449
  // Remote MCPs — launch OAuth via claude mcp add
353
450
  if (isRemoteMcp(entry.install.config)) {
354
451
  console.log(chalk.cyan(` Launching ${capitalizedName} OAuth...`));
@@ -35,6 +35,7 @@ export const OUTPUT_SCHEMA = {
35
35
  uiMockups: { filename: 'mockups.md', phaseDir: '1-ui', phase: 'uiux', protected: true, approvalGate: 'uiux' },
36
36
  uiComponents: { filename: 'components.md', phaseDir: '1-ui', phase: 'uiux', protected: true, approvalGate: 'uiux' },
37
37
  uiFlows: { filename: 'flows.md', phaseDir: '1-ui', phase: 'uiux', protected: true, approvalGate: 'uiux' },
38
+ stitchHtml: { filename: 'stitch-screen-map.json', phaseDir: '1-ui', phase: 'uiux', protected: true, approvalGate: 'uiux' },
38
39
  decisions: { filename: 'decisions.md', phaseDir: '2-design', phase: 'design', protected: false },
39
40
  recap: { filename: 'recap.md', phaseDir: '5-implement', phase: 'implement', protected: false },
40
41
  };
@@ -26,6 +26,7 @@ export const SOURCE_WEIGHTS = {
26
26
  uiMockups: 1.0,
27
27
  uiComponents: 1.0,
28
28
  uiFlows: 1.0,
29
+ stitchHtml: 1.0,
29
30
  recap: 1.0,
30
31
  };
31
32
 
@@ -30,7 +30,7 @@ export function detectProject(targetPath, opts = {}) {
30
30
  try {
31
31
  // ── Package.json analysis ──────────────────────────────────────────────
32
32
  // Collect deps from root and common monorepo subfolders
33
- const pkgLocations = ['.', 'src/frontend', 'frontend', 'apps/web', 'packages/app'];
33
+ const pkgLocations = ['.', 'src/frontend', 'src/web', 'frontend', 'apps/web', 'packages/app'];
34
34
  const allDeps = {};
35
35
  let rootPkg = null;
36
36
  let scripts = [];
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import { join } from 'path';
10
- import { readFileSync, existsSync } from 'fs';
10
+ import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs';
11
11
  import { execSync } from 'child_process';
12
12
  import { installMcpServers } from '../../utils/claude-settings-manager.js';
13
13
 
@@ -173,6 +173,145 @@ export function installCompanionSkills(mcpsInstalled, exec = execSync) {
173
173
  return results;
174
174
  }
175
175
 
176
+ /**
177
+ * Generate the Stitch OAuth proxy template script.
178
+ * @param {string} gcpProject - GCP project ID for x-goog-user-project header
179
+ * @returns {string} CJS proxy script content
180
+ */
181
+ export function getStitchProxyTemplate(gcpProject) {
182
+ return `/**
183
+ * Stitch MCP OAuth Proxy
184
+ * Generated by morph-spec mcp setup stitch
185
+ *
186
+ * Bridges stdio JSON-RPC to https://stitch.googleapis.com/mcp
187
+ * using gcloud application-default credentials (OAuth2 bearer token).
188
+ */
189
+ const { execSync } = require('child_process');
190
+ const https = require('https');
191
+
192
+ const GCP_PROJECT = '${gcpProject}';
193
+ const STITCH_HOST = 'stitch.googleapis.com';
194
+ const STITCH_PATH = '/mcp';
195
+ const TOKEN_TTL_MS = 50 * 60 * 1000; // 50 minutes
196
+
197
+ let cachedToken = null;
198
+ let tokenExpiresAt = 0;
199
+
200
+ function getAccessToken() {
201
+ if (cachedToken && Date.now() < tokenExpiresAt) return cachedToken;
202
+ try {
203
+ cachedToken = execSync('gcloud auth application-default print-access-token', {
204
+ encoding: 'utf-8',
205
+ timeout: 10000,
206
+ stdio: ['pipe', 'pipe', 'pipe']
207
+ }).trim();
208
+ tokenExpiresAt = Date.now() + TOKEN_TTL_MS;
209
+ return cachedToken;
210
+ } catch (err) {
211
+ process.stderr.write('[stitch-proxy] Failed to get access token. Run: gcloud auth application-default login\\n');
212
+ throw err;
213
+ }
214
+ }
215
+
216
+ function clearTokenCache() {
217
+ cachedToken = null;
218
+ tokenExpiresAt = 0;
219
+ }
220
+
221
+ function sendRequest(body) {
222
+ return new Promise((resolve, reject) => {
223
+ const token = getAccessToken();
224
+ const data = JSON.stringify(body);
225
+ const options = {
226
+ hostname: STITCH_HOST,
227
+ path: STITCH_PATH,
228
+ method: 'POST',
229
+ headers: {
230
+ 'Content-Type': 'application/json',
231
+ 'Content-Length': Buffer.byteLength(data),
232
+ 'Authorization': \`Bearer \${token}\`,
233
+ 'x-goog-user-project': GCP_PROJECT,
234
+ },
235
+ };
236
+
237
+ const req = https.request(options, (res) => {
238
+ let responseData = '';
239
+ res.on('data', (chunk) => { responseData += chunk; });
240
+ res.on('end', () => {
241
+ if (res.statusCode === 401) {
242
+ // Token expired — retry once with fresh token
243
+ clearTokenCache();
244
+ const retryToken = getAccessToken();
245
+ options.headers['Authorization'] = \`Bearer \${retryToken}\`;
246
+ const retryData = JSON.stringify(body);
247
+ options.headers['Content-Length'] = Buffer.byteLength(retryData);
248
+ const retryReq = https.request(options, (retryRes) => {
249
+ let retryResponse = '';
250
+ retryRes.on('data', (chunk) => { retryResponse += chunk; });
251
+ retryRes.on('end', () => resolve(retryResponse));
252
+ });
253
+ retryReq.on('error', reject);
254
+ retryReq.write(retryData);
255
+ retryReq.end();
256
+ } else {
257
+ resolve(responseData);
258
+ }
259
+ });
260
+ });
261
+
262
+ req.on('error', reject);
263
+ req.write(data);
264
+ req.end();
265
+ });
266
+ }
267
+
268
+ // Buffer stdin and split on newlines (JSON-RPC messages)
269
+ let buffer = '';
270
+ process.stdin.setEncoding('utf-8');
271
+ process.stdin.on('data', async (chunk) => {
272
+ buffer += chunk;
273
+ const lines = buffer.split('\\n');
274
+ buffer = lines.pop(); // keep incomplete line in buffer
275
+
276
+ for (const line of lines) {
277
+ if (!line.trim()) continue;
278
+ try {
279
+ const request = JSON.parse(line);
280
+ const response = await sendRequest(request);
281
+ process.stdout.write(response + '\\n');
282
+ } catch (err) {
283
+ const errorResponse = JSON.stringify({
284
+ jsonrpc: '2.0',
285
+ id: null,
286
+ error: { code: -32603, message: err.message || 'Proxy error' }
287
+ });
288
+ process.stdout.write(errorResponse + '\\n');
289
+ }
290
+ }
291
+ });
292
+
293
+ process.stderr.write(\`[stitch-proxy] Ready — GCP project: \${GCP_PROJECT}\\n\`);
294
+ `;
295
+ }
296
+
297
+ /**
298
+ * Generate and write the Stitch OAuth proxy script to the target project.
299
+ * @param {string} targetPath - Project root directory
300
+ * @param {string} gcpProject - GCP project ID
301
+ * @returns {{ proxyPath: string }} Path to the generated proxy script
302
+ */
303
+ export function generateStitchProxy(targetPath, gcpProject) {
304
+ const claudeDir = join(targetPath, '.claude');
305
+ if (!existsSync(claudeDir)) {
306
+ mkdirSync(claudeDir, { recursive: true });
307
+ }
308
+
309
+ const proxyPath = join(claudeDir, 'stitch-proxy.cjs');
310
+ writeFileSync(proxyPath, getStitchProxyTemplate(gcpProject), 'utf-8');
311
+
312
+ return { proxyPath };
313
+ }
314
+
176
315
  /**
177
316
  * Install a single MCP with provided credentials
178
317
  * @param {string} targetPath - Project root directory