@polymorphism-tech/morph-spec 4.20.0 → 4.21.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.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  > Spec-driven development framework for multi-stack projects. Turns feature requests into implementation-ready code through structured, AI-orchestrated phases.
4
4
 
5
5
  **Package:** `@polymorphism-tech/morph-spec`
6
- **Version:** 4.20.0
6
+ **Version:** 4.21.0
7
7
  **Requires:** Node.js 18+, Claude Code
8
8
 
9
9
  ---
@@ -364,4 +364,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
364
364
 
365
365
  ---
366
366
 
367
- *morph-spec v4.20.0 by [Polymorphism Tech](https://polymorphism.tech)*
367
+ *morph-spec v4.21.0 by [Polymorphism Tech](https://polymorphism.tech)*
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morph-spec",
3
- "version": "4.20.0",
3
+ "version": "4.21.0",
4
4
  "displayName": "MORPH-SPEC Framework",
5
5
  "description": "Spec-driven development with 38 agents and 8-phase workflow for .NET/Blazor/Next.js/Azure",
6
6
  "publisher": "polymorphism-tech",
@@ -196,4 +196,4 @@ These files are never edited directly. Use CLI commands or `morph-spec update` i
196
196
 
197
197
  ---
198
198
 
199
- *morph-spec v4.20.0 by Polymorphism Tech*
199
+ *morph-spec v4.21.0 by Polymorphism Tech*
@@ -197,4 +197,4 @@ morph-spec doctor
197
197
 
198
198
  ---
199
199
 
200
- *morph-spec v4.20.0 by Polymorphism Tech*
200
+ *morph-spec v4.21.0 by Polymorphism Tech*
@@ -164,20 +164,33 @@
164
164
  "testCall": "Call list_projects to verify connectivity",
165
165
  "testParams": {},
166
166
  "successIndicator": "Returns a list of projects (possibly empty) without auth errors",
167
- "failureHint": "Stitch MCP not responding. Ensure @_davideast/stitch-mcp is installed or configure official MCP: claude mcp add stitch --transport http https://stitch.googleapis.com/mcp"
167
+ "failureHint": "Stitch MCP not responding. Verify gcloud auth: run 'gcloud auth application-default print-access-token'. Check .claude/stitch-proxy.cjs exists and GCP project ID is correct."
168
168
  },
169
169
  "install": {
170
- "autoInstall": true,
170
+ "autoInstall": false,
171
171
  "transport": "stdio",
172
172
  "config": {
173
- "command": "cmd",
174
- "args": ["/c", "npx", "-y", "@_davideast/stitch-mcp@latest", "proxy"]
173
+ "command": "node",
174
+ "args": [".claude/stitch-proxy.cjs"]
175
175
  },
176
- "credentials": [],
177
- "prerequisites": [],
176
+ "credentials": [
177
+ {
178
+ "name": "GCP Project ID",
179
+ "envVar": "STITCH_GCP_PROJECT",
180
+ "secret": false,
181
+ "helpUrl": "https://console.cloud.google.com/projectselector2/home/dashboard"
182
+ }
183
+ ],
184
+ "prerequisites": [
185
+ {
186
+ "name": "gcloud CLI",
187
+ "checkCommand": "gcloud --version",
188
+ "installUrl": "https://cloud.google.com/sdk/docs/install"
189
+ }
190
+ ],
178
191
  "stackRelevance": ["nextjs", "nextjs-neon", "vite"],
179
- "warnings": ["Community MCP tool. Official alternative: claude mcp add stitch --transport http https://stitch.googleapis.com/mcp (requires gcloud auth)"],
180
- "setupGuide": "Auto-installed via npx. For official Google MCP: claude mcp add stitch --transport http https://stitch.googleapis.com/mcp"
192
+ "warnings": ["Requires gcloud CLI with application-default credentials. Run 'gcloud auth application-default login' before setup.", "Add .claude/stitch-proxy.cjs to .gitignore (contains your GCP project ID)."],
193
+ "setupGuide": "Run: morph-spec mcp setup stitch (generates OAuth proxy script)"
181
194
  },
182
195
  "companionSkills": {
183
196
  "repo": "google-labs-code/stitch-skills",
@@ -10,7 +10,7 @@ allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion,
10
10
  mcp__playwright__browser_navigate, mcp__playwright__browser_resize,
11
11
  mcp__playwright__browser_take_screenshot, mcp__playwright__browser_snapshot,
12
12
  mcp__playwright__browser_evaluate, mcp__playwright__browser_console_messages
13
- cliVersion: "4.20.0"
13
+ cliVersion: "4.21.0"
14
14
  stacks:
15
15
  - nextjs
16
16
  ---
@@ -3,7 +3,7 @@ name: morph-phase-codebase-analysis
3
3
  description: MORPH-SPEC Design sub-phase that analyzes existing codebase and database schema, producing schema-analysis.md with real column names, types, relationships, and field mismatches. Use at the start of Design phase before generating contracts.cs to prevent incorrect field names or types.
4
4
  user-invocable: false
5
5
  allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion
6
- cliVersion: "4.20.0"
6
+ cliVersion: "4.21.0"
7
7
  ---
8
8
 
9
9
  # MORPH Codebase Analysis - Design Sub-phase
@@ -4,7 +4,7 @@ description: MORPH-SPEC Phase 2 (Design). Schema-first interactive design: reads
4
4
  argument-hint: "[feature-name]"
5
5
  user-invocable: false
6
6
  allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion, Agent
7
- cliVersion: "4.20.0"
7
+ cliVersion: "4.21.0"
8
8
  ---
9
9
 
10
10
  # MORPH Design — Phase 2
@@ -5,7 +5,7 @@ argument-hint: "[feature-name]"
5
5
  context: fork
6
6
  agent: general-purpose
7
7
  user-invocable: false
8
- cliVersion: "4.20.0"
8
+ cliVersion: "4.21.0"
9
9
  allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion, Agent
10
10
  ---
11
11
 
@@ -4,7 +4,7 @@ description: MORPH-SPEC Phase 3 (Plan). Generates a detailed implementation plan
4
4
  argument-hint: "[feature-name]"
5
5
  user-invocable: false
6
6
  allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion
7
- cliVersion: "4.20.0"
7
+ cliVersion: "4.21.0"
8
8
  ---
9
9
 
10
10
  # MORPH Plan — Phase 3
@@ -4,7 +4,7 @@ description: MORPH-SPEC Phase 4 (Tasks). Breaks approved spec into bottom-up ord
4
4
  argument-hint: "[feature-name]"
5
5
  user-invocable: false
6
6
  allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion
7
- cliVersion: "4.20.0"
7
+ cliVersion: "4.21.0"
8
8
  ---
9
9
 
10
10
  # MORPH Tasks — Phase 4
@@ -4,7 +4,7 @@ description: MORPH-SPEC Phase 2 (UI/UX). Creates design-system.md, mockups.md, c
4
4
  argument-hint: "[feature-name]"
5
5
  user-invocable: false
6
6
  allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion
7
- cliVersion: "4.20.0"
7
+ cliVersion: "4.21.0"
8
8
  ---
9
9
 
10
10
  # MORPH UI/UX Design - Phase 2
@@ -4,7 +4,7 @@ description: Guided workflow for mid-implementation scope escalation — analyze
4
4
  user-invocable: true
5
5
  argument-hint: "[feature-name]"
6
6
  allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion
7
- cliVersion: "4.20.0"
7
+ cliVersion: "4.21.0"
8
8
  ---
9
9
 
10
10
  # Scope Escalation Workflow
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.21.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...`));
@@ -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