@polymorphism-tech/morph-spec 8.37.1 → 8.40.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/CLAUDE.md +37 -13
- package/README.md +2 -2
- package/bin/morph-spec.js +75 -49
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +6 -5
- package/framework/MORPH.md +25 -22
- package/framework/agents.json +96 -36
- package/framework/docs/CLI.md +54 -17
- package/framework/evals/rubrics/code-quality.md +5 -0
- package/framework/evals/rubrics/condensed.md +6 -2
- package/framework/evals/rubrics/contracts.md +20 -0
- package/framework/evals/rubrics/test-coverage.md +17 -6
- package/framework/hooks/README.md +9 -3
- package/framework/hooks/claude-code/core/graph-refresh-kick.js +52 -0
- package/framework/hooks/claude-code/core/telemetry-log.js +172 -87
- package/framework/hooks/claude-code/core/validate-completion.js +16 -90
- package/framework/hooks/claude-code/core/worktree-context.js +10 -0
- package/framework/hooks/shared/command-tokens.js +96 -0
- package/framework/hooks/shared/git-exclude.js +44 -0
- package/framework/hooks/shared/git-paths.js +52 -0
- package/framework/hooks/shared/graph-freshness.js +380 -0
- package/framework/hooks/shared/graph-refresh.js +416 -0
- package/framework/hooks/shared/morph-cli.js +17 -5
- package/framework/hooks/shared/redact-secrets.js +55 -0
- package/framework/hooks/shared/telemetry-logger.js +25 -2
- package/framework/hooks/shared/transcript-usage.js +80 -0
- package/framework/mcp/registry.json +33 -2
- package/framework/rubrics/backend.json +2 -2
- package/framework/rubrics/default.json +2 -2
- package/framework/rubrics/frontend.json +2 -2
- package/framework/rules/csharp-standards.md +1 -0
- package/framework/rules/infrastructure-standards.md +0 -1
- package/framework/rules/morph-workflow.md +10 -4
- package/framework/rules/nextjs-standards.md +1 -0
- package/framework/schemas/config.schema.json +41 -1
- package/framework/schemas/feature.schema.json +5 -1
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +40 -11
- package/framework/skills/core/morph-brainstorming/SKILL.md +16 -7
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +44 -10
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +73 -15
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +51 -13
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +39 -13
- package/framework/standards/STANDARDS.json +189 -14
- package/framework/standards/ai-agents/context-providers.md +16 -26
- package/framework/standards/ai-agents/durable-workflows-hitl.md +15 -21
- package/framework/standards/ai-agents/modalities-image-gen.md +21 -25
- package/framework/standards/ai-agents/modalities-speech-to-text.md +4 -15
- package/framework/standards/ai-agents/modalities-text-to-speech.md +2 -11
- package/framework/standards/ai-agents/modalities-vision.md +1 -18
- package/framework/standards/ai-agents/prompt-sources.md +0 -4
- package/framework/standards/ai-agents/testing-ai.md +2 -1
- package/framework/standards/architecture/code-comments.md +100 -0
- package/framework/standards/backend/dotnet/testing.md +22 -0
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +63 -50
- package/framework/standards/frontend/admin-app/data-table.md +562 -0
- package/framework/standards/frontend/admin-app/feedback.md +236 -0
- package/framework/standards/frontend/admin-app/forms.md +232 -0
- package/framework/standards/frontend/admin-app/page-structure.md +203 -0
- package/framework/standards/frontend/design-system/ai-image-generation.md +5 -5
- package/framework/standards/frontend/nextjs/components.md +37 -58
- package/framework/standards/frontend/nextjs/data-fetching.md +1 -0
- package/framework/standards/frontend/nextjs/forms.md +31 -30
- package/framework/standards/frontend/nextjs/state-management.md +2 -1
- package/framework/standards/frontend/nextjs/testing.md +18 -0
- package/framework/standards/integration/mcp/mcp-tools.md +48 -117
- package/framework/templates/code/dotnet/abstractions/result.cs +0 -2
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +26 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +28 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +99 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +2 -44
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +40 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +54 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +53 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +192 -281
- package/framework/templates/code/dotnet/database/migration.cs +49 -83
- package/framework/templates/code/dotnet/database/vector-entity.cs +2 -46
- package/framework/templates/code/dotnet/extensions/result-extensions.cs +0 -2
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +11 -15
- package/framework/templates/code/dotnet/jobs/agent.cs +3 -51
- package/framework/templates/code/dotnet/jobs/job.cs +2 -97
- package/framework/templates/code/dotnet/test.cs +2 -87
- package/framework/templates/code/typescript/contracts.ts +140 -167
- package/framework/templates/context/design-system.md +3 -3
- package/framework/templates/docs/proposal.md +11 -0
- package/framework/templates/docs/spec.md +15 -0
- package/framework/templates/docs/ui-mockups.md +115 -98
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +1 -48
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +3 -79
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +1 -70
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +1 -47
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +2 -99
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +0 -6
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +3 -88
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +1 -41
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +0 -21
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +1 -25
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +2 -63
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +0 -30
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +1 -52
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +3 -78
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +4 -109
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +0 -22
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +1 -36
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +2 -57
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +0 -75
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +2 -59
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +4 -111
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +0 -34
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +1 -57
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +0 -19
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +2 -74
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +2 -33
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +1 -43
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +1 -96
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +0 -13
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +0 -27
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +0 -33
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +0 -31
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +3 -75
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +0 -42
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +0 -39
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +1 -55
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +3 -59
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +0 -46
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +3 -28
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +7 -61
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +2 -37
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +1 -23
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +2 -82
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +1 -68
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +1 -62
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +2 -62
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +1 -25
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +0 -124
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +1 -73
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +1 -66
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +0 -41
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +2 -112
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +2 -43
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +5 -110
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +7 -103
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +1 -42
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +1 -30
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +1 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +0 -48
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +1 -29
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +2 -38
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +1 -14
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +4 -69
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +1 -10
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +1 -57
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +0 -26
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +1 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +0 -32
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +0 -14
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +0 -32
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +0 -37
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +0 -78
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +0 -53
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +0 -44
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +0 -44
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +0 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +6 -48
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +1 -13
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +2 -71
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +5 -100
- package/framework/templates/feature/decisions.md +124 -122
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +39 -43
- package/framework/templates/frontend/nextjs/env.mjs.hbs +29 -32
- package/framework/templates/frontend/nextjs/page.tsx.hbs +21 -22
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +42 -54
- package/framework/templates/integrations/asaas-client.cs +332 -387
- package/framework/templates/integrations/asaas-webhook.cs +290 -351
- package/framework/templates/integrations/neon-auth-config.cs +9 -105
- package/framework/templates/project/validate.js +97 -120
- package/package.json +2 -2
- package/src/commands/dashboard/dashboard.js +80 -53
- package/src/commands/graph/affected.js +4 -3
- package/src/commands/graph/build.js +109 -18
- package/src/commands/graph/explain.js +8 -5
- package/src/commands/graph/status.js +14 -8
- package/src/commands/mcp/mcp-setup.js +37 -9
- package/src/commands/observability/cost.js +148 -18
- package/src/commands/project/doctor.js +85 -15
- package/src/commands/project/init.js +6 -3
- package/src/commands/project/install-plugin-cmd.js +1 -1
- package/src/commands/project/update.js +30 -0
- package/src/commands/project/worktree.js +73 -90
- package/src/commands/state/advance.js +18 -0
- package/src/commands/state/approve.js +15 -0
- package/src/commands/state/archive.js +62 -9
- package/src/commands/state/finish.js +62 -14
- package/src/commands/state/gate-check.js +8 -1
- package/src/commands/state/gate-decision.js +8 -4
- package/src/commands/state/report-check.js +90 -9
- package/src/commands/state/usecase.js +66 -0
- package/src/commands/tasks/dag.js +17 -2
- package/src/commands/validation/validate-feature.js +6 -1
- package/src/commands/validation/verify.js +20 -1
- package/src/dashboard/app/App.jsx +80 -16
- package/src/dashboard/app/components/ActivityFeed.jsx +172 -0
- package/src/dashboard/app/components/CommandPalette.jsx +155 -0
- package/src/dashboard/app/components/DetailPanel.jsx +47 -11
- package/src/dashboard/app/components/EntityLink.jsx +55 -0
- package/src/dashboard/app/components/ErrorBoundary.jsx +54 -0
- package/src/dashboard/app/components/FlowGraph.jsx +19 -17
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +53 -0
- package/src/dashboard/app/components/Markdown.jsx +47 -0
- package/src/dashboard/app/components/PhaseRail.jsx +111 -0
- package/src/dashboard/app/components/SearchFilter.jsx +40 -22
- package/src/dashboard/app/components/Sidebar.jsx +146 -37
- package/src/dashboard/app/hooks/useMorphQuery.js +54 -0
- package/src/dashboard/app/hooks/usePref.js +38 -0
- package/src/dashboard/app/lib/format.js +69 -0
- package/src/dashboard/app/lib/phases.js +56 -0
- package/src/dashboard/app/lib/project.js +50 -0
- package/src/dashboard/app/pages/Activity.jsx +24 -0
- package/src/dashboard/app/pages/AgentMap.jsx +371 -370
- package/src/dashboard/app/pages/FeatureDetail.jsx +604 -0
- package/src/dashboard/app/pages/Features.jsx +400 -0
- package/src/dashboard/app/pages/Guide.jsx +146 -0
- package/src/dashboard/app/pages/HookInspector.jsx +357 -322
- package/src/dashboard/app/pages/PhasePipeline.jsx +277 -408
- package/src/dashboard/app/pages/Portfolio.jsx +156 -0
- package/src/dashboard/app/pages/Rubrics.jsx +101 -0
- package/src/dashboard/app/pages/RulesMap.jsx +300 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +483 -366
- package/src/dashboard/app/pages/Standards.jsx +407 -332
- package/src/dashboard/app/styles/index.css +187 -47
- package/src/dashboard/index.html +14 -12
- package/src/dashboard/package.json +32 -24
- package/src/dashboard/scanner/activity-scanner.js +351 -0
- package/src/dashboard/scanner/agents-scanner.js +63 -1
- package/src/dashboard/scanner/docs-scanner.js +59 -0
- package/src/dashboard/scanner/feature-status.js +10 -0
- package/src/dashboard/scanner/features-scanner.js +360 -0
- package/src/dashboard/scanner/hooks-scanner.js +8 -1
- package/src/dashboard/scanner/index.js +14 -7
- package/src/dashboard/scanner/phases-scanner.js +14 -15
- package/src/dashboard/scanner/portfolio.js +153 -0
- package/src/dashboard/scanner/project-api.js +195 -0
- package/src/dashboard/scanner/rules-scanner.js +42 -37
- package/src/dashboard/scanner/skills-scanner.js +105 -43
- package/src/dashboard/scanner/standards-scanner.js +4 -1
- package/src/dashboard/vite-plugin-morph-data.js +134 -17
- package/src/lib/ai/comment-stripper.js +101 -4
- package/src/lib/detectors/claude-config-detector.js +2 -7
- package/src/lib/detectors/index.js +5 -1
- package/src/lib/evaluator-report.js +47 -0
- package/src/lib/generators/handoff-generator.js +4 -18
- package/src/lib/generators/settings-generator.js +208 -209
- package/src/lib/git/default-branch.js +52 -0
- package/src/lib/git/tree-fingerprint.js +58 -0
- package/src/lib/git/tree-state.js +167 -0
- package/src/lib/graph/god-nodes.js +12 -3
- package/src/lib/graph/graph-health.js +48 -2
- package/src/lib/graph/graph-loader.js +99 -25
- package/src/lib/graph/graphifyignore.js +145 -28
- package/src/lib/graph/sanitize.js +117 -11
- package/src/lib/installers/mcp-installer.js +724 -719
- package/src/lib/installers/memory-installer.js +1 -2
- package/src/lib/projects/registry.js +154 -0
- package/src/lib/risk-detector.js +90 -0
- package/src/lib/specs/acervo-format.js +70 -0
- package/src/lib/specs/acervo-lifecycle.js +264 -0
- package/src/lib/specs/adr-index.js +169 -0
- package/src/lib/specs/revocations.js +156 -0
- package/src/lib/specs/screen-checklist.js +357 -0
- package/src/lib/specs/usecase-check.js +20 -16
- package/src/lib/specs/usecase-promoter.js +103 -38
- package/src/lib/specs/usecase-registry.js +7 -7
- package/src/lib/tasks/tasks-json-parser.js +2 -3
- package/src/lib/telemetry/metrics.js +386 -0
- package/src/lib/telemetry/telemetry-reader.js +161 -51
- package/src/lib/telemetry/verify-event.js +79 -0
- package/src/lib/validators/code-budget/code-budget-validator.js +200 -0
- package/src/lib/validators/code-budget/comment-scanner.js +75 -0
- package/src/lib/validators/code-budget/index.js +9 -0
- package/src/lib/validators/reuse/reuse-validator.js +4 -2
- package/src/lib/validators/reuse/symbol-scanner.js +27 -22
- package/src/lib/validators/validation-runner.js +21 -5
- package/src/lib/validators/verify-runner.js +11 -0
- package/src/lib/worktree/finish-backup.js +739 -0
- package/src/lib/worktree/ignored-triage.js +15 -8
- package/src/lib/worktree/worktree-audit.js +5 -0
- package/src/scripts/install-plugin.js +1 -1
- package/src/scripts/setup-infra.js +31 -22
- package/src/utils/agents-installer.js +338 -24
- package/src/utils/banner.js +1 -1
- package/src/utils/hooks-installer.js +75 -15
- package/src/commands/index.js +0 -13
- package/src/commands/project/checkpoint.js +0 -209
- package/src/core/state/phase-state-machine.js +0 -197
- package/src/dashboard/app/hooks/useDetailPanel.js +0 -15
- package/src/dashboard/app/pages/LiveState.jsx +0 -206
- package/src/dashboard/scanner/squads-scanner.js +0 -13
- package/src/dashboard/scanner/state-scanner.js +0 -85
- package/src/lib/generators/index.js +0 -10
- package/src/lib/hooks/hook-executor.js +0 -167
- package/src/lib/validators/checkpoint-persistence.js +0 -39
- package/src/lib/validators/shared/emit-validator-dispatch.js +0 -14
- package/src/lib/validators/shared/index.js +0 -12
- package/src/lib/validators/shared/issue-counter.js +0 -14
- package/src/lib/validators/shared/result-formatter.js +0 -124
|
@@ -1,719 +1,724 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MCP Installer
|
|
3
|
-
*
|
|
4
|
-
* Core logic for installing and configuring MCP servers during morph-spec init
|
|
5
|
-
* and via `morph-spec mcp setup`. Handles auto-installable (credential-free)
|
|
6
|
-
* MCPs, prerequisite checking, credential collection, and setup guidance.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { join } from 'path';
|
|
10
|
-
import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
11
|
-
import { execSync, spawn } from 'child_process';
|
|
12
|
-
import { homedir } from 'os';
|
|
13
|
-
import { installMcpServers } from '../../utils/claude-settings-manager.js';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Load the enhanced MCP registry (v3.0.0)
|
|
17
|
-
* @returns {Object} Registry with install blocks
|
|
18
|
-
*/
|
|
19
|
-
export function loadMcpRegistry() {
|
|
20
|
-
const registryPath = join(
|
|
21
|
-
import.meta.dirname, '..', '..', '..', 'framework', 'mcp', 'registry.json'
|
|
22
|
-
);
|
|
23
|
-
const raw = readFileSync(registryPath, 'utf-8');
|
|
24
|
-
return JSON.parse(raw);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Filter MCPs by stack relevance
|
|
29
|
-
* @param {Object} registry - Full MCP registry
|
|
30
|
-
* @param {string} stack - Detected project stack (e.g., 'nextjs-neon', 'blazor')
|
|
31
|
-
* @
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
*
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
*
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
let
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
if (
|
|
279
|
-
process.stdout.write(
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
* @
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
// Step
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
steps.push({ step: '
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
);
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
*
|
|
529
|
-
* @
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
*
|
|
594
|
-
* @param {
|
|
595
|
-
* @
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
const
|
|
604
|
-
const
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
1
|
+
/**
|
|
2
|
+
* MCP Installer
|
|
3
|
+
*
|
|
4
|
+
* Core logic for installing and configuring MCP servers during morph-spec init
|
|
5
|
+
* and via `morph-spec mcp setup`. Handles auto-installable (credential-free)
|
|
6
|
+
* MCPs, prerequisite checking, credential collection, and setup guidance.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
11
|
+
import { execSync, spawn } from 'child_process';
|
|
12
|
+
import { homedir } from 'os';
|
|
13
|
+
import { installMcpServers } from '../../utils/claude-settings-manager.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Load the enhanced MCP registry (v3.0.0)
|
|
17
|
+
* @returns {Object} Registry with install blocks
|
|
18
|
+
*/
|
|
19
|
+
export function loadMcpRegistry() {
|
|
20
|
+
const registryPath = join(
|
|
21
|
+
import.meta.dirname, '..', '..', '..', 'framework', 'mcp', 'registry.json'
|
|
22
|
+
);
|
|
23
|
+
const raw = readFileSync(registryPath, 'utf-8');
|
|
24
|
+
return JSON.parse(raw);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Filter MCPs by stack relevance
|
|
29
|
+
* @param {Object} registry - Full MCP registry
|
|
30
|
+
* @param {string} stack - Detected project stack (e.g., 'nextjs-neon', 'blazor')
|
|
31
|
+
* @param {string[]} [integrations] - `project.integrations` (e.g., ['neon']). The
|
|
32
|
+
* detectors put a database or payment provider here, never in the stack string,
|
|
33
|
+
* so an MCP relevant to it (neon, asaas) is matched against this list too.
|
|
34
|
+
* @returns {Object} Filtered map of mcpName → mcpEntry
|
|
35
|
+
*/
|
|
36
|
+
export function filterByStack(registry, stack, integrations = []) {
|
|
37
|
+
const result = {};
|
|
38
|
+
const normalizedStack = (stack || '').toLowerCase();
|
|
39
|
+
const tags = integrations.map(i => String(i).toLowerCase());
|
|
40
|
+
|
|
41
|
+
for (const [name, entry] of Object.entries(registry.mcps)) {
|
|
42
|
+
if (!entry.install) continue;
|
|
43
|
+
const relevance = entry.install.stackRelevance || [];
|
|
44
|
+
|
|
45
|
+
if (relevance.includes('*')) {
|
|
46
|
+
result[name] = entry;
|
|
47
|
+
} else if (relevance.some(s => (normalizedStack && normalizedStack.includes(s)) || tags.includes(s))) {
|
|
48
|
+
result[name] = entry;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Match detected MCP servers against the registry
|
|
57
|
+
* @param {Array} existingServers - From detectClaudeConfig().mcpServers
|
|
58
|
+
* @param {Object} registry - MCP registry
|
|
59
|
+
* @returns {Object} Map of mcpName → { installed: true, source }
|
|
60
|
+
*/
|
|
61
|
+
export function getInstalledMcps(existingServers, registry) {
|
|
62
|
+
const installed = {};
|
|
63
|
+
const registryNames = Object.keys(registry.mcps);
|
|
64
|
+
|
|
65
|
+
for (const server of existingServers) {
|
|
66
|
+
const serverName = server.name.toLowerCase();
|
|
67
|
+
for (const regName of registryNames) {
|
|
68
|
+
if (serverName.includes(regName)) {
|
|
69
|
+
installed[regName] = { installed: true, source: server.source };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return installed;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check if prerequisites are met for an MCP entry
|
|
79
|
+
* @param {Object} mcpEntry - MCP entry from registry
|
|
80
|
+
* @returns {Array} Array of { name, met, installUrl } for each prerequisite
|
|
81
|
+
*/
|
|
82
|
+
export function checkPrerequisites(mcpEntry) {
|
|
83
|
+
const prereqs = mcpEntry.install?.prerequisites || [];
|
|
84
|
+
const results = [];
|
|
85
|
+
|
|
86
|
+
for (const prereq of prereqs) {
|
|
87
|
+
let met = false;
|
|
88
|
+
try {
|
|
89
|
+
execSync(prereq.checkCommand, { stdio: 'ignore', timeout: 5000 });
|
|
90
|
+
met = true;
|
|
91
|
+
} catch {
|
|
92
|
+
// Command failed or not found
|
|
93
|
+
}
|
|
94
|
+
results.push({
|
|
95
|
+
name: prereq.name,
|
|
96
|
+
met,
|
|
97
|
+
installUrl: prereq.installUrl
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return results;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Check if an MCP config uses remote HTTP transport (no local process).
|
|
106
|
+
* @param {Object} config - MCP server config
|
|
107
|
+
* @returns {boolean}
|
|
108
|
+
*/
|
|
109
|
+
export function isRemoteMcp(config) {
|
|
110
|
+
return config.type === 'http' || config.url != null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Patch MCP config for the current platform.
|
|
115
|
+
* On Windows, npx must be invoked via `cmd /c` to avoid ENOENT errors.
|
|
116
|
+
* Registry already stores cmd/c format; this handles legacy configs that still use npx directly.
|
|
117
|
+
* Remote HTTP MCPs are returned as-is (no local process to patch).
|
|
118
|
+
* @param {Object} config - MCP server config { command, args, env? } or { type: "http", url }
|
|
119
|
+
* @returns {Object} Patched config (new object, original unmodified)
|
|
120
|
+
*/
|
|
121
|
+
export function patchConfigForPlatform(config) {
|
|
122
|
+
// Remote MCPs need no platform patching
|
|
123
|
+
if (isRemoteMcp(config)) return config;
|
|
124
|
+
if (config.command !== 'npx') return config;
|
|
125
|
+
return {
|
|
126
|
+
...config,
|
|
127
|
+
command: 'cmd',
|
|
128
|
+
args: ['/c', 'npx', ...(config.args || [])]
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Install auto-installable MCPs (no credentials, no prerequisites)
|
|
134
|
+
* @param {string} targetPath - Project root directory
|
|
135
|
+
* @param {Object} mcpsToInstall - Map of mcpName → mcpEntry
|
|
136
|
+
* @returns {Promise<Object>} Result from installMcpServers
|
|
137
|
+
*/
|
|
138
|
+
export async function installAutoMcps(targetPath, mcpsToInstall) {
|
|
139
|
+
const servers = {};
|
|
140
|
+
|
|
141
|
+
for (const [name, entry] of Object.entries(mcpsToInstall)) {
|
|
142
|
+
let config = { ...entry.install.config };
|
|
143
|
+
// Remote MCPs don't have env to strip
|
|
144
|
+
if (!isRemoteMcp(config)) {
|
|
145
|
+
// Only include env if it has actual values
|
|
146
|
+
if (config.env && Object.values(config.env).every(v => !v)) {
|
|
147
|
+
delete config.env;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
servers[name] = patchConfigForPlatform(config);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return installMcpServers(targetPath, servers);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Install companion skills for MCPs that define them (e.g., Stitch agent skills).
|
|
158
|
+
* Reads the `companionSkills` field from registry entries and runs the install command.
|
|
159
|
+
* @param {Object} mcpsInstalled - Map of mcpName → mcpEntry that were just installed
|
|
160
|
+
* @param {Function} exec - execSync function (for testing)
|
|
161
|
+
* @returns {Array<{name: string, repo: string, success: boolean}>}
|
|
162
|
+
*/
|
|
163
|
+
export function installCompanionSkills(mcpsInstalled, exec = execSync) {
|
|
164
|
+
const results = [];
|
|
165
|
+
|
|
166
|
+
for (const [name, entry] of Object.entries(mcpsInstalled)) {
|
|
167
|
+
const companion = entry.companionSkills;
|
|
168
|
+
if (!companion?.installCommand) continue;
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
exec(companion.installCommand, { stdio: 'inherit', timeout: 30000 });
|
|
172
|
+
results.push({ name, repo: companion.repo, success: true });
|
|
173
|
+
} catch {
|
|
174
|
+
results.push({ name, repo: companion.repo, success: false });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return results;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Generate the Stitch OAuth proxy template script.
|
|
183
|
+
* Cross-platform (Windows gcloud.cmd + Unix gcloud), fetch()-based,
|
|
184
|
+
* with readline, session tracking, and graceful shutdown.
|
|
185
|
+
* @param {string} gcpProject - GCP project ID for x-goog-user-project header
|
|
186
|
+
* @returns {string} CJS proxy script content
|
|
187
|
+
*/
|
|
188
|
+
export function getStitchProxyTemplate(gcpProject) {
|
|
189
|
+
return `#!/usr/bin/env node
|
|
190
|
+
/**
|
|
191
|
+
* Stitch MCP stdio-to-HTTP proxy with Google auth
|
|
192
|
+
* Generated by morph-spec mcp setup stitch
|
|
193
|
+
*/
|
|
194
|
+
const { execSync } = require('child_process');
|
|
195
|
+
const { createInterface } = require('readline');
|
|
196
|
+
|
|
197
|
+
const STITCH_URL = 'https://stitch.googleapis.com/mcp';
|
|
198
|
+
const PROJECT_ID = '${gcpProject}';
|
|
199
|
+
|
|
200
|
+
// Cross-platform gcloud path: Windows uses gcloud.cmd from Cloud SDK install
|
|
201
|
+
const GCLOUD = process.platform === 'win32'
|
|
202
|
+
? (process.env.LOCALAPPDATA || '') + '/Google/Cloud SDK/google-cloud-sdk/bin/gcloud.cmd'
|
|
203
|
+
: 'gcloud';
|
|
204
|
+
|
|
205
|
+
let cachedToken = null;
|
|
206
|
+
let tokenExpiry = 0;
|
|
207
|
+
let pendingRequests = 0;
|
|
208
|
+
let stdinClosed = false;
|
|
209
|
+
let sessionId = null;
|
|
210
|
+
|
|
211
|
+
function getToken() {
|
|
212
|
+
if (cachedToken && Date.now() < tokenExpiry) return cachedToken;
|
|
213
|
+
try {
|
|
214
|
+
cachedToken = execSync(\`"\${GCLOUD}" auth application-default print-access-token\`, {
|
|
215
|
+
encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 10000
|
|
216
|
+
}).trim();
|
|
217
|
+
tokenExpiry = Date.now() + 50 * 60 * 1000; // 50-minute TTL
|
|
218
|
+
return cachedToken;
|
|
219
|
+
} catch {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function maybeExit() {
|
|
225
|
+
if (stdinClosed && pendingRequests === 0) process.exit(0);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function handleMessage(line) {
|
|
229
|
+
let msg;
|
|
230
|
+
try { msg = JSON.parse(line); } catch { return; }
|
|
231
|
+
|
|
232
|
+
// Notifications (no id) don't need responses
|
|
233
|
+
if (msg.id === undefined && msg.method === 'notifications/initialized') return;
|
|
234
|
+
|
|
235
|
+
const token = getToken();
|
|
236
|
+
if (!token) {
|
|
237
|
+
if (msg.id !== undefined) {
|
|
238
|
+
process.stdout.write(JSON.stringify({
|
|
239
|
+
jsonrpc: '2.0', id: msg.id,
|
|
240
|
+
error: { code: -32000, message: 'Failed to get gcloud auth token. Run: gcloud auth application-default login' }
|
|
241
|
+
}) + '\\n');
|
|
242
|
+
}
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const headers = {
|
|
247
|
+
'Content-Type': 'application/json',
|
|
248
|
+
'Authorization': \`Bearer \${token}\`,
|
|
249
|
+
'x-goog-user-project': PROJECT_ID,
|
|
250
|
+
};
|
|
251
|
+
if (sessionId) headers['mcp-session-id'] = sessionId;
|
|
252
|
+
|
|
253
|
+
pendingRequests++;
|
|
254
|
+
try {
|
|
255
|
+
const res = await fetch(STITCH_URL, { method: 'POST', headers, body: JSON.stringify(msg) });
|
|
256
|
+
|
|
257
|
+
// Track sticky session
|
|
258
|
+
const sid = res.headers.get('mcp-session-id');
|
|
259
|
+
if (sid) sessionId = sid;
|
|
260
|
+
|
|
261
|
+
// Retry once on 401 (expired token)
|
|
262
|
+
if (res.status === 401) {
|
|
263
|
+
cachedToken = null;
|
|
264
|
+
tokenExpiry = 0;
|
|
265
|
+
const retryToken = getToken();
|
|
266
|
+
if (retryToken) {
|
|
267
|
+
headers['Authorization'] = \`Bearer \${retryToken}\`;
|
|
268
|
+
const retryRes = await fetch(STITCH_URL, { method: 'POST', headers, body: JSON.stringify(msg) });
|
|
269
|
+
const retryText = await retryRes.text();
|
|
270
|
+
if (retryText.trim()) {
|
|
271
|
+
process.stdout.write(retryText.endsWith('\\n') ? retryText : retryText + '\\n');
|
|
272
|
+
}
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const text = await res.text();
|
|
278
|
+
if (text.trim()) {
|
|
279
|
+
process.stdout.write(text.endsWith('\\n') ? text : text + '\\n');
|
|
280
|
+
}
|
|
281
|
+
} catch (err) {
|
|
282
|
+
if (msg.id !== undefined) {
|
|
283
|
+
process.stdout.write(JSON.stringify({
|
|
284
|
+
jsonrpc: '2.0', id: msg.id,
|
|
285
|
+
error: { code: -32000, message: \`Stitch API error: \${err.message}\` }
|
|
286
|
+
}) + '\\n');
|
|
287
|
+
}
|
|
288
|
+
} finally {
|
|
289
|
+
pendingRequests--;
|
|
290
|
+
maybeExit();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const rl = createInterface({ input: process.stdin });
|
|
295
|
+
rl.on('line', handleMessage);
|
|
296
|
+
rl.on('close', () => { stdinClosed = true; maybeExit(); });
|
|
297
|
+
`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Generate and write the Stitch OAuth proxy script to the target project.
|
|
302
|
+
* @param {string} targetPath - Project root directory
|
|
303
|
+
* @param {string} gcpProject - GCP project ID
|
|
304
|
+
* @returns {{ proxyPath: string }} Path to the generated proxy script
|
|
305
|
+
*/
|
|
306
|
+
export function generateStitchProxy(targetPath, gcpProject) {
|
|
307
|
+
const claudeDir = join(targetPath, '.claude');
|
|
308
|
+
if (!existsSync(claudeDir)) {
|
|
309
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const proxyPath = join(claudeDir, 'stitch-proxy.cjs');
|
|
313
|
+
writeFileSync(proxyPath, getStitchProxyTemplate(gcpProject), 'utf-8');
|
|
314
|
+
|
|
315
|
+
return { proxyPath };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Resolve the gcloud CLI binary path.
|
|
320
|
+
* Checks: system PATH → bundled at ~/.stitch-mcp/ → null.
|
|
321
|
+
* @param {Function} exec - execSync function (for testing)
|
|
322
|
+
* @returns {{ gcloudPath: string|null, source: string }}
|
|
323
|
+
*/
|
|
324
|
+
export function findGcloud(exec = execSync) {
|
|
325
|
+
// Try system PATH first
|
|
326
|
+
try {
|
|
327
|
+
exec('gcloud --version', { stdio: ['pipe', 'pipe', 'pipe'], timeout: 10000 });
|
|
328
|
+
return { gcloudPath: 'gcloud', source: 'system' };
|
|
329
|
+
} catch { /* not on PATH */ }
|
|
330
|
+
|
|
331
|
+
// Windows: try Cloud SDK default install location
|
|
332
|
+
if (process.platform === 'win32') {
|
|
333
|
+
const winPath = join(process.env.LOCALAPPDATA || '', 'Google', 'Cloud SDK', 'google-cloud-sdk', 'bin', 'gcloud.cmd');
|
|
334
|
+
if (existsSync(winPath)) {
|
|
335
|
+
return { gcloudPath: winPath, source: 'windows-sdk' };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Try bundled at ~/.stitch-mcp/
|
|
340
|
+
const bundledDir = join(homedir(), '.stitch-mcp');
|
|
341
|
+
const bundledBin = process.platform === 'win32'
|
|
342
|
+
? join(bundledDir, 'google-cloud-sdk', 'bin', 'gcloud.cmd')
|
|
343
|
+
: join(bundledDir, 'google-cloud-sdk', 'bin', 'gcloud');
|
|
344
|
+
if (existsSync(bundledBin)) {
|
|
345
|
+
return { gcloudPath: bundledBin, source: 'bundled' };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return { gcloudPath: null, source: 'not-found' };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Fully automated Stitch MCP setup orchestrator.
|
|
353
|
+
*
|
|
354
|
+
* Steps:
|
|
355
|
+
* 1. Find gcloud CLI
|
|
356
|
+
* 2. Verify ADC credentials
|
|
357
|
+
* 3. Detect GCP project
|
|
358
|
+
* 4. Enable stitch.googleapis.com API
|
|
359
|
+
* 5. Generate proxy script
|
|
360
|
+
* 6. Install MCP config in .mcp.json
|
|
361
|
+
* 7. Health-check the proxy
|
|
362
|
+
*
|
|
363
|
+
* @param {string} targetPath - Project root directory
|
|
364
|
+
* @param {Object} options
|
|
365
|
+
* @param {string} [options.gcpProject] - GCP project ID (auto-detected if omitted)
|
|
366
|
+
* @param {Function} [options._exec] - execSync override for testing
|
|
367
|
+
* @returns {Promise<Object>} Step-by-step results
|
|
368
|
+
*/
|
|
369
|
+
export async function setupStitch(targetPath, options = {}) {
|
|
370
|
+
const exec = options._exec || execSync;
|
|
371
|
+
const steps = [];
|
|
372
|
+
|
|
373
|
+
// Step 1: Find gcloud
|
|
374
|
+
const { gcloudPath, source: gcloudSource } = findGcloud(exec);
|
|
375
|
+
if (!gcloudPath) {
|
|
376
|
+
steps.push({ step: 'find-gcloud', success: false, error: 'gcloud CLI not found. Install from: https://cloud.google.com/sdk/docs/install' });
|
|
377
|
+
return { success: false, steps };
|
|
378
|
+
}
|
|
379
|
+
steps.push({ step: 'find-gcloud', success: true, gcloudPath, source: gcloudSource });
|
|
380
|
+
|
|
381
|
+
const gcloudCmd = gcloudPath.includes(' ') ? `"${gcloudPath}"` : gcloudPath;
|
|
382
|
+
|
|
383
|
+
// Step 2: Verify ADC credentials
|
|
384
|
+
try {
|
|
385
|
+
exec(`${gcloudCmd} auth application-default print-access-token`, {
|
|
386
|
+
encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 15000
|
|
387
|
+
});
|
|
388
|
+
steps.push({ step: 'verify-adc', success: true });
|
|
389
|
+
} catch {
|
|
390
|
+
steps.push({ step: 'verify-adc', success: false, error: 'No ADC credentials. Run: gcloud auth application-default login' });
|
|
391
|
+
return { success: false, steps };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Step 3: Detect GCP project
|
|
395
|
+
let gcpProject = options.gcpProject;
|
|
396
|
+
if (!gcpProject) {
|
|
397
|
+
// Try config.json
|
|
398
|
+
const configPath = join(targetPath, '.morph', 'config', 'config.json');
|
|
399
|
+
if (existsSync(configPath)) {
|
|
400
|
+
try {
|
|
401
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
402
|
+
gcpProject = config.gcpProject || config.project?.gcpProject;
|
|
403
|
+
} catch { /* invalid config */ }
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (!gcpProject) {
|
|
407
|
+
// Try gcloud config
|
|
408
|
+
try {
|
|
409
|
+
gcpProject = exec(`${gcloudCmd} config get-value project`, {
|
|
410
|
+
encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 10000
|
|
411
|
+
}).trim();
|
|
412
|
+
if (!gcpProject || gcpProject === '(unset)') gcpProject = null;
|
|
413
|
+
} catch { /* gcloud config failed */ }
|
|
414
|
+
}
|
|
415
|
+
if (!gcpProject) {
|
|
416
|
+
steps.push({ step: 'detect-project', success: false, error: 'Could not detect GCP project. Pass it explicitly or set via: gcloud config set project <id>' });
|
|
417
|
+
return { success: false, steps };
|
|
418
|
+
}
|
|
419
|
+
steps.push({ step: 'detect-project', success: true, gcpProject });
|
|
420
|
+
|
|
421
|
+
// Step 4: Enable Stitch API
|
|
422
|
+
try {
|
|
423
|
+
exec(`${gcloudCmd} services enable stitch.googleapis.com --project=${gcpProject}`, {
|
|
424
|
+
encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 30000
|
|
425
|
+
});
|
|
426
|
+
steps.push({ step: 'enable-api', success: true });
|
|
427
|
+
} catch (err) {
|
|
428
|
+
steps.push({ step: 'enable-api', success: false, error: `Failed to enable stitch API: ${err.message}` });
|
|
429
|
+
// Non-fatal — API might already be enabled and user may lack permissions to re-enable
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Step 5: Generate proxy
|
|
433
|
+
const { proxyPath } = generateStitchProxy(targetPath, gcpProject);
|
|
434
|
+
steps.push({ step: 'generate-proxy', success: true, proxyPath });
|
|
435
|
+
|
|
436
|
+
// Step 6: Install MCP config in .mcp.json
|
|
437
|
+
const mcpResult = await installMcpServers(targetPath, {
|
|
438
|
+
stitch: { type: 'stdio', command: 'node', args: ['.claude/stitch-proxy.cjs'] }
|
|
439
|
+
});
|
|
440
|
+
steps.push({ step: 'install-mcp', success: true, added: mcpResult.added, skipped: mcpResult.skipped });
|
|
441
|
+
|
|
442
|
+
// Step 7: Health-check
|
|
443
|
+
const healthResult = await healthCheckStitchProxy(proxyPath);
|
|
444
|
+
steps.push({ step: 'health-check', ...healthResult });
|
|
445
|
+
|
|
446
|
+
const allCriticalPassed = steps.every(s =>
|
|
447
|
+
s.success || s.step === 'enable-api' || s.step === 'health-check'
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
return { success: allCriticalPassed, steps, gcpProject };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Health-check the Stitch proxy by sending an MCP initialize handshake.
|
|
455
|
+
* @param {string} proxyPath - Path to stitch-proxy.cjs
|
|
456
|
+
* @returns {Promise<Object>} { success, protocolVersion?, error? }
|
|
457
|
+
*/
|
|
458
|
+
export function healthCheckStitchProxy(proxyPath) {
|
|
459
|
+
return new Promise((resolve) => {
|
|
460
|
+
const initMsg = JSON.stringify({
|
|
461
|
+
jsonrpc: '2.0',
|
|
462
|
+
id: 1,
|
|
463
|
+
method: 'initialize',
|
|
464
|
+
params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'morph-spec', version: '1.0.0' } }
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
const child = spawn('node', [proxyPath], {
|
|
468
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
469
|
+
timeout: 20000,
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
let output = '';
|
|
473
|
+
let timedOut = false;
|
|
474
|
+
|
|
475
|
+
const timer = setTimeout(() => {
|
|
476
|
+
timedOut = true;
|
|
477
|
+
child.kill();
|
|
478
|
+
resolve({ success: false, error: 'Health-check timed out after 20s' });
|
|
479
|
+
}, 20000);
|
|
480
|
+
|
|
481
|
+
child.stdout.on('data', (data) => {
|
|
482
|
+
output += data.toString();
|
|
483
|
+
// Check if we got a response
|
|
484
|
+
const lines = output.split('\n').filter(l => l.trim());
|
|
485
|
+
for (const line of lines) {
|
|
486
|
+
try {
|
|
487
|
+
const resp = JSON.parse(line);
|
|
488
|
+
if (resp.result?.protocolVersion) {
|
|
489
|
+
clearTimeout(timer);
|
|
490
|
+
child.kill();
|
|
491
|
+
resolve({ success: true, protocolVersion: resp.result.protocolVersion });
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (resp.error) {
|
|
495
|
+
clearTimeout(timer);
|
|
496
|
+
child.kill();
|
|
497
|
+
resolve({ success: false, error: resp.error.message || 'MCP error' });
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
} catch { /* not valid JSON yet */ }
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
child.on('error', (err) => {
|
|
505
|
+
if (!timedOut) {
|
|
506
|
+
clearTimeout(timer);
|
|
507
|
+
resolve({ success: false, error: err.message });
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
child.on('close', (code) => {
|
|
512
|
+
if (!timedOut) {
|
|
513
|
+
clearTimeout(timer);
|
|
514
|
+
resolve({ success: false, error: output.trim()
|
|
515
|
+
? `Proxy exited with code ${code} — unexpected output`
|
|
516
|
+
: `Proxy exited with code ${code} and no output`
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// Send the initialize message
|
|
522
|
+
child.stdin.write(initMsg + '\n');
|
|
523
|
+
child.stdin.end();
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Install a single MCP with provided credentials
|
|
529
|
+
* @param {string} targetPath - Project root directory
|
|
530
|
+
* @param {string} name - MCP name
|
|
531
|
+
* @param {Object} mcpEntry - MCP entry from registry
|
|
532
|
+
* @param {Object} credentialValues - Map of envVar → value
|
|
533
|
+
* @returns {Promise<Object>} Result from installMcpServers
|
|
534
|
+
*/
|
|
535
|
+
export async function installMcpWithCredentials(targetPath, name, mcpEntry, credentialValues) {
|
|
536
|
+
const config = patchConfigForPlatform({ ...mcpEntry.install.config });
|
|
537
|
+
config.env = { ...config.env, ...credentialValues };
|
|
538
|
+
return installMcpServers(targetPath, { [name]: config });
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Generate setup instructions for a credential-requiring MCP
|
|
543
|
+
* @param {string} name - MCP name
|
|
544
|
+
* @param {Object} mcpEntry - MCP entry from registry
|
|
545
|
+
* @returns {Object} { configSnippet, credentialUrls, cliCommand }
|
|
546
|
+
*/
|
|
547
|
+
export function generateSetupInstructions(name, mcpEntry) {
|
|
548
|
+
const config = mcpEntry.install.config;
|
|
549
|
+
const credentials = mcpEntry.install.credentials || [];
|
|
550
|
+
const setupGuide = mcpEntry.install.setupGuide;
|
|
551
|
+
|
|
552
|
+
// Remote HTTP MCPs — show claude mcp add command instead of config snippet
|
|
553
|
+
if (isRemoteMcp(config)) {
|
|
554
|
+
const configSnippet = JSON.stringify({ [name]: config }, null, 2);
|
|
555
|
+
const credentialUrls = credentials.map(c => ({
|
|
556
|
+
name: c.name,
|
|
557
|
+
envVar: c.envVar,
|
|
558
|
+
helpUrl: c.helpUrl
|
|
559
|
+
}));
|
|
560
|
+
return {
|
|
561
|
+
configSnippet,
|
|
562
|
+
credentialUrls,
|
|
563
|
+
cliCommand: setupGuide || `claude mcp add --transport http ${name} ${config.url}`,
|
|
564
|
+
isRemote: true
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Build the config snippet with placeholder values
|
|
569
|
+
const snippetConfig = { ...config };
|
|
570
|
+
if (credentials.length > 0) {
|
|
571
|
+
snippetConfig.env = {};
|
|
572
|
+
for (const cred of credentials) {
|
|
573
|
+
snippetConfig.env[cred.envVar] = cred.secret ? 'YOUR_KEY' : 'YOUR_VALUE';
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const configSnippet = JSON.stringify({ [name]: snippetConfig }, null, 2);
|
|
578
|
+
|
|
579
|
+
const credentialUrls = credentials.map(c => ({
|
|
580
|
+
name: c.name,
|
|
581
|
+
envVar: c.envVar,
|
|
582
|
+
helpUrl: c.helpUrl
|
|
583
|
+
}));
|
|
584
|
+
|
|
585
|
+
return {
|
|
586
|
+
configSnippet,
|
|
587
|
+
credentialUrls,
|
|
588
|
+
cliCommand: `morph-spec mcp setup ${name}`
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Full orchestrator for MCP setup during init or `mcp setup`
|
|
594
|
+
* @param {string} targetPath - Project root directory
|
|
595
|
+
* @param {string} stack - Detected project stack
|
|
596
|
+
* @param {Array} existingMcps - From detectClaudeConfig().mcpServers
|
|
597
|
+
* @param {Object} options
|
|
598
|
+
* @param {boolean} options.autoOnly - Only handle auto-installable MCPs
|
|
599
|
+
* @param {string[]} [options.integrations] - `project.integrations`, matched like the stack
|
|
600
|
+
* @returns {Object} { autoInstallable, alreadyInstalled, needsManualSetup, notRelevant, prerequisitesMissing }
|
|
601
|
+
*/
|
|
602
|
+
export function orchestrateMcpSetup(targetPath, stack, existingMcps, options = {}) {
|
|
603
|
+
const registry = loadMcpRegistry();
|
|
604
|
+
const relevant = filterByStack(registry, stack, options.integrations ?? []);
|
|
605
|
+
const installed = getInstalledMcps(existingMcps || [], registry);
|
|
606
|
+
|
|
607
|
+
const autoInstallable = {};
|
|
608
|
+
const alreadyInstalled = {};
|
|
609
|
+
const needsManualSetup = {};
|
|
610
|
+
const notRelevant = {};
|
|
611
|
+
const prerequisitesMissing = {};
|
|
612
|
+
|
|
613
|
+
// Categorize all MCPs
|
|
614
|
+
for (const [name, entry] of Object.entries(registry.mcps)) {
|
|
615
|
+
if (!entry.install) continue;
|
|
616
|
+
|
|
617
|
+
// Check if already installed
|
|
618
|
+
if (installed[name]) {
|
|
619
|
+
alreadyInstalled[name] = { entry, source: installed[name].source };
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Check if relevant to stack
|
|
624
|
+
if (!relevant[name]) {
|
|
625
|
+
notRelevant[name] = entry;
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Check prerequisites
|
|
630
|
+
const prereqResults = checkPrerequisites(entry);
|
|
631
|
+
const unmetPrereqs = prereqResults.filter(p => !p.met);
|
|
632
|
+
if (unmetPrereqs.length > 0) {
|
|
633
|
+
prerequisitesMissing[name] = { entry, unmet: unmetPrereqs };
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Categorize by install type
|
|
638
|
+
if (entry.install.autoInstall) {
|
|
639
|
+
autoInstallable[name] = entry;
|
|
640
|
+
} else if (!options.autoOnly) {
|
|
641
|
+
needsManualSetup[name] = entry;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return {
|
|
646
|
+
registry,
|
|
647
|
+
autoInstallable,
|
|
648
|
+
alreadyInstalled,
|
|
649
|
+
needsManualSetup,
|
|
650
|
+
notRelevant,
|
|
651
|
+
prerequisitesMissing
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Format MCP status table for display
|
|
657
|
+
* @param {Object} orchestration - Result from orchestrateMcpSetup
|
|
658
|
+
* @param {Object} justInstalled - Map of names just installed during this run
|
|
659
|
+
* @returns {Array<Object>} Array of { name, status, detail }
|
|
660
|
+
*/
|
|
661
|
+
export function formatMcpStatusTable(orchestration, justInstalled = {}) {
|
|
662
|
+
const rows = [];
|
|
663
|
+
|
|
664
|
+
const { autoInstallable, alreadyInstalled, needsManualSetup, notRelevant, prerequisitesMissing, registry } = orchestration;
|
|
665
|
+
|
|
666
|
+
// Installed during this run
|
|
667
|
+
for (const [name, entry] of Object.entries(justInstalled)) {
|
|
668
|
+
const mcpEntry = registry.mcps[name];
|
|
669
|
+
rows.push({
|
|
670
|
+
name,
|
|
671
|
+
status: 'installed',
|
|
672
|
+
detail: mcpEntry ? mcpEntry.usage : ''
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Auto-installable but not yet installed (and not just-installed)
|
|
677
|
+
for (const [name, entry] of Object.entries(autoInstallable)) {
|
|
678
|
+
if (justInstalled[name]) continue;
|
|
679
|
+
rows.push({
|
|
680
|
+
name,
|
|
681
|
+
status: 'available',
|
|
682
|
+
detail: `Run: morph-spec mcp setup --auto`
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Already installed
|
|
687
|
+
for (const [name, { source }] of Object.entries(alreadyInstalled)) {
|
|
688
|
+
rows.push({
|
|
689
|
+
name,
|
|
690
|
+
status: 'already_configured',
|
|
691
|
+
detail: `(${source})`
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Needs manual setup
|
|
696
|
+
for (const [name, entry] of Object.entries(needsManualSetup)) {
|
|
697
|
+
rows.push({
|
|
698
|
+
name,
|
|
699
|
+
status: 'needs_setup',
|
|
700
|
+
detail: `Run: morph-spec mcp setup ${name}`
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Prerequisites missing
|
|
705
|
+
for (const [name, { unmet }] of Object.entries(prerequisitesMissing)) {
|
|
706
|
+
const missing = unmet.map(p => p.name).join(', ');
|
|
707
|
+
rows.push({
|
|
708
|
+
name,
|
|
709
|
+
status: 'prereq_missing',
|
|
710
|
+
detail: `Missing: ${missing}`
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Not relevant
|
|
715
|
+
for (const [name] of Object.entries(notRelevant)) {
|
|
716
|
+
rows.push({
|
|
717
|
+
name,
|
|
718
|
+
status: 'not_relevant',
|
|
719
|
+
detail: 'not relevant to stack'
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return rows;
|
|
724
|
+
}
|