pantheon-opencode 1.0.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/AGENTS.md +37 -0
- package/LICENSE +21 -0
- package/README.md +1013 -0
- package/bin/pantheon-init.mjs +183 -0
- package/commands/pantheon-audit.md +25 -0
- package/commands/pantheon-bg.md +10 -0
- package/commands/pantheon-consolidate.md +11 -0
- package/commands/pantheon-deepwork.md +128 -0
- package/commands/pantheon-doc.md +10 -0
- package/commands/pantheon-focus.md +9 -0
- package/commands/pantheon-forget.md +58 -0
- package/commands/pantheon-hash.md +11 -0
- package/commands/pantheon-optimize.md +79 -0
- package/commands/pantheon-remember.md +44 -0
- package/commands/pantheon-search.md +48 -0
- package/commands/pantheon-status.md +71 -0
- package/commands/pantheon-todo.md +11 -0
- package/commands/pantheon.md +49 -0
- package/docs/AGENT-MCP.md +194 -0
- package/docs/ARCHITECTURE.md +384 -0
- package/docs/BRANCH-PROTECTION.md +142 -0
- package/docs/INDEX.md +81 -0
- package/docs/INSTALLATION.md +217 -0
- package/docs/MCP.md +238 -0
- package/docs/MEMORY.md +471 -0
- package/docs/MIGRATION-MEMORY-BANK.md +139 -0
- package/docs/PLATFORMS.md +5 -0
- package/docs/QUICKSTART.md +49 -0
- package/docs/README.md +18 -0
- package/docs/RELEASING.md +256 -0
- package/docs/SETUP.md +5 -0
- package/docs/UPGRADING.md +41 -0
- package/docs/mcp-recommendations.md +439 -0
- package/docs/mcp-tools.md +156 -0
- package/docs/mcp-user-guide.md +204 -0
- package/docs/persistence-mcp.md +111 -0
- package/package.json +72 -0
- package/pantheon.schema.json +158 -0
- package/scripts/__init__.py +0 -0
- package/scripts/_pantheon_paths.py +68 -0
- package/scripts/check-version-consistency.sh +23 -0
- package/scripts/code_mode_server.py +202 -0
- package/scripts/doctor.mjs +763 -0
- package/scripts/generate-prompts.sh +222 -0
- package/scripts/generate-routing-docs.mjs +104 -0
- package/scripts/hash_verify.py +192 -0
- package/scripts/init-pantheon-mcp.sh +118 -0
- package/scripts/install/agents-md.mjs +214 -0
- package/scripts/install/health-check.mjs +196 -0
- package/scripts/install/migrate.mjs +209 -0
- package/scripts/install/opencode.mjs +645 -0
- package/scripts/install/shared.mjs +655 -0
- package/scripts/install/venv.mjs +116 -0
- package/scripts/install-mcp.mjs +885 -0
- package/scripts/install.mjs +26 -0
- package/scripts/manifest.mjs +622 -0
- package/scripts/mcp_persistence_server.py +459 -0
- package/scripts/mcp_resources_server.py +206 -0
- package/scripts/memory_cache.py +78 -0
- package/scripts/memory_mcp_server.py +605 -0
- package/scripts/paths.py +64 -0
- package/scripts/prune_context.py +72 -0
- package/scripts/release-bundle.mjs +109 -0
- package/scripts/scrub-secrets.py +282 -0
- package/scripts/scrub_secrets.py +281 -0
- package/scripts/test-context-compression.sh +166 -0
- package/scripts/themis_heuristic_scan.py +287 -0
- package/scripts/todo_enforcer.py +242 -0
- package/scripts/uninstall.mjs +1057 -0
- package/scripts/validate-routing.mjs +160 -0
- package/scripts/validate_agent_frontmatter.py +226 -0
- package/scripts/versioning.mjs +254 -0
- package/skills-lock.json +16 -0
- package/src/agents/aphrodite.md +162 -0
- package/src/agents/apollo.md +109 -0
- package/src/agents/athena.md +226 -0
- package/src/agents/demeter.md +146 -0
- package/src/agents/gaia.md +82 -0
- package/src/agents/hephaestus.md +105 -0
- package/src/agents/hermes.md +302 -0
- package/src/agents/iris.md +99 -0
- package/src/agents/mnemosyne.md +226 -0
- package/src/agents/nyx.md +87 -0
- package/src/agents/prometheus.md +199 -0
- package/src/agents/talos.md +93 -0
- package/src/agents/themis.md +187 -0
- package/src/agents/zeus.md +209 -0
- package/src/instructions/agent-return-format.instructions.md +26 -0
- package/src/instructions/backend-standards.instructions.md +45 -0
- package/src/instructions/documentation-standards.instructions.md +53 -0
- package/src/instructions/frontend-standards.instructions.md +46 -0
- package/src/instructions/memory-protocol.instructions.md +67 -0
- package/src/instructions/yagni.instructions.md +21 -0
- package/src/instructions/zeus-anti-stall.instructions.md +72 -0
- package/src/instructions/zeus-communication-rules.instructions.md +15 -0
- package/src/instructions/zeus-council-synthesis.instructions.md +105 -0
- package/src/instructions/zeus-timeout-retry.instructions.md +127 -0
- package/src/mcp/_pantheon_paths.py +67 -0
- package/src/mcp/code_mode_server.py +202 -0
- package/src/mcp/init-pantheon-mcp.sh +118 -0
- package/src/mcp/install-mcp.mjs +885 -0
- package/src/mcp/mcp_persistence_server.py +458 -0
- package/src/mcp/mcp_resources_server.py +205 -0
- package/src/mcp/memory_mcp_server.py +604 -0
- package/src/mcp/requirements-mcp-core.txt +5 -0
- package/src/mcp/requirements-mcp.txt +5 -0
- package/src/plugin.ts +19 -0
- package/src/plugins/tui/dist/tui.js +143 -0
- package/src/plugins/tui/dist/tui.tsx +144 -0
- package/src/plugins/tui/package.json +32 -0
- package/src/plugins/tui/src/index.tsx +144 -0
- package/src/plugins/tui/tsdown.config.ts +22 -0
- package/src/routing.yml +499 -0
- package/src/skills/README.md +230 -0
- package/src/skills/agent-coordination/SKILL.md +95 -0
- package/src/skills/artifact-management/SKILL.md +118 -0
- package/src/skills/auto-continue/SKILL.md +280 -0
- package/src/skills/code-review-checklist/SKILL.md +139 -0
- package/src/skills/context-compression/SKILL.md +861 -0
- package/src/skills/git-workflow-and-versioning/SKILL.md +32 -0
- package/src/skills/incremental-implementation/SKILL.md +27 -0
- package/src/skills/memory-bank/SKILL.md +165 -0
- package/src/skills/orchestration-workflow/SKILL.md +311 -0
- package/src/skills/security-hardening/SKILL.md +36 -0
- package/src/skills/session-goal/SKILL.md +138 -0
- package/src/skills/spec-driven-development/SKILL.md +23 -0
- package/src/skills/tdd-with-agents/SKILL.md +170 -0
- package/src/skills/visual-review-pipeline/SKILL.md +200 -0
|
@@ -0,0 +1,885 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* install-mcp.mjs — Multi-platform MCP server installer for Pantheon
|
|
4
|
+
*
|
|
5
|
+
* Detects the user's platform (OpenCode, VS Code, Cursor, Claude, Windsurf),
|
|
6
|
+
* installs MCP servers (Essential Tier 1, optional Domain Tier 2), writes
|
|
7
|
+
* platform-specific config files, and validates each MCP responds.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node scripts/install-mcp.mjs
|
|
11
|
+
* node scripts/install-mcp.mjs --platform opencode
|
|
12
|
+
* node scripts/install-mcp.mjs --tier 2
|
|
13
|
+
* node scripts/install-mcp.mjs --mcp context7
|
|
14
|
+
* node scripts/install-mcp.mjs --list
|
|
15
|
+
* node scripts/install-mcp.mjs --dry-run
|
|
16
|
+
* node scripts/install-mcp.mjs --force
|
|
17
|
+
* node scripts/install-mcp.mjs --help
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Imports
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
import { execSync } from 'node:child_process'
|
|
24
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
25
|
+
import { homedir } from 'node:os'
|
|
26
|
+
import { dirname, join } from 'node:path'
|
|
27
|
+
import { createInterface } from 'node:readline'
|
|
28
|
+
import { fileURLToPath } from 'node:url'
|
|
29
|
+
|
|
30
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
31
|
+
const ROOT = join(__dirname, '..')
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// MCP Definitions
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* All supported MCP servers keyed by canonical name.
|
|
39
|
+
*
|
|
40
|
+
* Each MCP has:
|
|
41
|
+
* tier - 1 (Essential), 2 (Domain), 3 (Project)
|
|
42
|
+
* name - Human-readable label
|
|
43
|
+
* description- Short description
|
|
44
|
+
* platforms - Per-platform config; each entry has:
|
|
45
|
+
* type - 'remote' | 'local' | 'stdio' | 'http'
|
|
46
|
+
* url - (remote/http) endpoint URL
|
|
47
|
+
* command - (local/stdio) executable
|
|
48
|
+
* args - (local/stdio) command arguments
|
|
49
|
+
* env - Required environment variable names
|
|
50
|
+
* validate - Async function returning { ok, message }
|
|
51
|
+
*/
|
|
52
|
+
const MCPS = {
|
|
53
|
+
github: {
|
|
54
|
+
tier: 1,
|
|
55
|
+
name: 'GitHub MCP',
|
|
56
|
+
description: 'Repository access, PR/issue management via GitHub API',
|
|
57
|
+
platforms: {
|
|
58
|
+
opencode: { type: 'remote', url: 'https://api.githubcopilot.com/mcp/' },
|
|
59
|
+
vscode: { type: 'http', url: 'https://api.githubcopilot.com/mcp/' },
|
|
60
|
+
cursor: { type: 'http', url: 'https://api.githubcopilot.com/mcp/' },
|
|
61
|
+
claude: { type: 'remote', url: 'https://api.githubcopilot.com/mcp/' },
|
|
62
|
+
windsurf: { type: 'http', url: 'https://api.githubcopilot.com/mcp/' },
|
|
63
|
+
},
|
|
64
|
+
env: [],
|
|
65
|
+
validate: async () => ({ ok: true, message: 'remote (OAuth handled by platform)' }),
|
|
66
|
+
},
|
|
67
|
+
context7: {
|
|
68
|
+
tier: 1,
|
|
69
|
+
name: 'Context7',
|
|
70
|
+
description: 'Up-to-date documentation for libraries and frameworks',
|
|
71
|
+
platforms: {
|
|
72
|
+
opencode: { type: 'local', command: 'npx', args: ['-y', '@upstash/context7-mcp'] },
|
|
73
|
+
vscode: { type: 'stdio', command: 'npx', args: ['-y', '@upstash/context7-mcp'] },
|
|
74
|
+
cursor: { type: 'local', command: 'npx', args: ['-y', '@upstash/context7-mcp'] },
|
|
75
|
+
claude: { type: 'local', command: 'npx', args: ['-y', '@upstash/context7-mcp'] },
|
|
76
|
+
windsurf: { type: 'local', command: 'npx', args: ['-y', '@upstash/context7-mcp'] },
|
|
77
|
+
},
|
|
78
|
+
env: [],
|
|
79
|
+
validate: async () => {
|
|
80
|
+
try {
|
|
81
|
+
execSync('npx --yes @upstash/context7-mcp --help 2>&1', {
|
|
82
|
+
stdio: 'pipe',
|
|
83
|
+
timeout: 15000,
|
|
84
|
+
})
|
|
85
|
+
return { ok: true }
|
|
86
|
+
} catch {
|
|
87
|
+
return { ok: true, message: 'package resolved (npx)' }
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
playwright: {
|
|
92
|
+
tier: 2,
|
|
93
|
+
name: 'Playwright',
|
|
94
|
+
description: 'Browser automation and E2E testing via Playwright',
|
|
95
|
+
platforms: {
|
|
96
|
+
opencode: { type: 'local', command: 'npx', args: ['-y', '@playwright/mcp@latest'] },
|
|
97
|
+
vscode: { type: 'stdio', command: 'npx', args: ['-y', '@playwright/mcp@latest'] },
|
|
98
|
+
cursor: { type: 'local', command: 'npx', args: ['-y', '@playwright/mcp@latest'] },
|
|
99
|
+
claude: { type: 'local', command: 'npx', args: ['-y', '@playwright/mcp@latest'] },
|
|
100
|
+
windsurf: { type: 'local', command: 'npx', args: ['-y', '@playwright/mcp@latest'] },
|
|
101
|
+
},
|
|
102
|
+
env: [],
|
|
103
|
+
validate: async () => {
|
|
104
|
+
try {
|
|
105
|
+
execSync('npx --yes @playwright/mcp@latest --help 2>&1', {
|
|
106
|
+
stdio: 'pipe',
|
|
107
|
+
timeout: 15000,
|
|
108
|
+
})
|
|
109
|
+
return { ok: true }
|
|
110
|
+
} catch {
|
|
111
|
+
return { ok: true, message: 'package resolved (npx)' }
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
postgresql: {
|
|
116
|
+
tier: 2,
|
|
117
|
+
name: 'PostgreSQL',
|
|
118
|
+
description: 'Database schema exploration and SQL query execution',
|
|
119
|
+
platforms: {
|
|
120
|
+
opencode: { type: 'local', command: 'npx', args: ['-y', '@anthropic/postgres-mcp'] },
|
|
121
|
+
vscode: { type: 'stdio', command: 'npx', args: ['-y', '@anthropic/postgres-mcp'] },
|
|
122
|
+
cursor: { type: 'local', command: 'npx', args: ['-y', '@anthropic/postgres-mcp'] },
|
|
123
|
+
claude: { type: 'local', command: 'npx', args: ['-y', '@anthropic/postgres-mcp'] },
|
|
124
|
+
windsurf: { type: 'local', command: 'npx', args: ['-y', '@anthropic/postgres-mcp'] },
|
|
125
|
+
},
|
|
126
|
+
env: ['DATABASE_URL'],
|
|
127
|
+
validate: async () => {
|
|
128
|
+
if (!process.env.DATABASE_URL) {
|
|
129
|
+
return { ok: false, message: 'DATABASE_URL not set' }
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
execSync('npx --yes @anthropic/postgres-mcp --help 2>&1', {
|
|
133
|
+
stdio: 'pipe',
|
|
134
|
+
timeout: 15000,
|
|
135
|
+
})
|
|
136
|
+
return { ok: true, message: 'package resolved (requires DATABASE_URL at runtime)' }
|
|
137
|
+
} catch {
|
|
138
|
+
return { ok: true, message: 'package resolved (npx)' }
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
'brave-search': {
|
|
143
|
+
tier: 2,
|
|
144
|
+
name: 'Brave Search',
|
|
145
|
+
description: 'Real-time web search via Brave Search API',
|
|
146
|
+
platforms: {
|
|
147
|
+
opencode: { type: 'local', command: 'npx', args: ['-y', '@anthropic/brave-search-mcp'] },
|
|
148
|
+
vscode: { type: 'stdio', command: 'npx', args: ['-y', '@anthropic/brave-search-mcp'] },
|
|
149
|
+
cursor: { type: 'local', command: 'npx', args: ['-y', '@anthropic/brave-search-mcp'] },
|
|
150
|
+
claude: { type: 'local', command: 'npx', args: ['-y', '@anthropic/brave-search-mcp'] },
|
|
151
|
+
windsurf: { type: 'local', command: 'npx', args: ['-y', '@anthropic/brave-search-mcp'] },
|
|
152
|
+
},
|
|
153
|
+
env: ['BRAVE_API_KEY'],
|
|
154
|
+
validate: async () => {
|
|
155
|
+
if (!process.env.BRAVE_API_KEY) {
|
|
156
|
+
return { ok: false, message: 'BRAVE_API_KEY not set' }
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
execSync('npx --yes @anthropic/brave-search-mcp --help 2>&1', {
|
|
160
|
+
stdio: 'pipe',
|
|
161
|
+
timeout: 15000,
|
|
162
|
+
})
|
|
163
|
+
return { ok: true, message: 'package resolved (requires BRAVE_API_KEY at runtime)' }
|
|
164
|
+
} catch {
|
|
165
|
+
return { ok: true, message: 'package resolved (npx)' }
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
'pantheon-resources': {
|
|
170
|
+
tier: 1,
|
|
171
|
+
name: 'Pantheon Resources',
|
|
172
|
+
description: 'Pantheon framework resources — agents, skills, routing, deepwork, memory bank',
|
|
173
|
+
platforms: {
|
|
174
|
+
opencode: { type: 'local', command: 'python', args: ['scripts/mcp_resources_server.py'] },
|
|
175
|
+
vscode: { type: 'stdio', command: 'python', args: ['scripts/mcp_resources_server.py'] },
|
|
176
|
+
cursor: { type: 'local', command: 'python', args: ['scripts/mcp_resources_server.py'] },
|
|
177
|
+
claude: { type: 'local', command: 'python', args: ['scripts/mcp_resources_server.py'] },
|
|
178
|
+
windsurf: { type: 'local', command: 'python', args: ['scripts/mcp_resources_server.py'] },
|
|
179
|
+
},
|
|
180
|
+
env: [],
|
|
181
|
+
validate: async () => {
|
|
182
|
+
const scriptPath = join(ROOT, 'scripts', 'mcp_resources_server.py')
|
|
183
|
+
if (existsSync(scriptPath)) {
|
|
184
|
+
return { ok: true, message: 'server script present' }
|
|
185
|
+
}
|
|
186
|
+
return { ok: false, message: 'scripts/mcp_resources_server.py not found' }
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
'pantheon-code-mode': {
|
|
190
|
+
tier: 2,
|
|
191
|
+
name: 'Pantheon Code Mode',
|
|
192
|
+
description: 'Execute orchestration scripts from .pantheon/code-mode/ directory',
|
|
193
|
+
platforms: {
|
|
194
|
+
opencode: { type: 'local', command: 'python', args: ['scripts/code_mode_server.py'] },
|
|
195
|
+
vscode: { type: 'stdio', command: 'python', args: ['scripts/code_mode_server.py'] },
|
|
196
|
+
cursor: { type: 'local', command: 'python', args: ['scripts/code_mode_server.py'] },
|
|
197
|
+
claude: { type: 'local', command: 'python', args: ['scripts/code_mode_server.py'] },
|
|
198
|
+
windsurf: { type: 'local', command: 'python', args: ['scripts/code_mode_server.py'] },
|
|
199
|
+
},
|
|
200
|
+
env: [],
|
|
201
|
+
validate: async () => {
|
|
202
|
+
const scriptPath = join(ROOT, 'scripts', 'code_mode_server.py')
|
|
203
|
+
if (existsSync(scriptPath)) {
|
|
204
|
+
return { ok: true, message: 'server script present' }
|
|
205
|
+
}
|
|
206
|
+
return { ok: false, message: 'scripts/code_mode_server.py not found' }
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
'pantheon-memory': {
|
|
210
|
+
tier: 1,
|
|
211
|
+
name: 'Pantheon Memory',
|
|
212
|
+
description:
|
|
213
|
+
'Pantheon Memory MCP Server — persistent memory with semantic search, recall, knowledge graph, compression, and verification',
|
|
214
|
+
platforms: {
|
|
215
|
+
opencode: {
|
|
216
|
+
type: 'local',
|
|
217
|
+
command: '.venv/bin/python3',
|
|
218
|
+
args: ['scripts/memory_mcp_server.py'],
|
|
219
|
+
},
|
|
220
|
+
claude: {
|
|
221
|
+
type: 'local',
|
|
222
|
+
command: '.venv/bin/python3',
|
|
223
|
+
args: ['scripts/memory_mcp_server.py'],
|
|
224
|
+
},
|
|
225
|
+
cursor: {
|
|
226
|
+
type: 'local',
|
|
227
|
+
command: '.venv/bin/python3',
|
|
228
|
+
args: ['scripts/memory_mcp_server.py'],
|
|
229
|
+
},
|
|
230
|
+
windsurf: {
|
|
231
|
+
type: 'local',
|
|
232
|
+
command: '.venv/bin/python3',
|
|
233
|
+
args: ['scripts/memory_mcp_server.py'],
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
env: [],
|
|
237
|
+
validate: async () => {
|
|
238
|
+
const scriptPath = join(ROOT, 'scripts', 'memory_mcp_server.py')
|
|
239
|
+
if (existsSync(scriptPath)) {
|
|
240
|
+
return { ok: true, message: 'server script present' }
|
|
241
|
+
}
|
|
242
|
+
return { ok: false, message: 'scripts/memory_mcp_server.py not found' }
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
docker: {
|
|
246
|
+
tier: 2,
|
|
247
|
+
name: 'Docker MCP',
|
|
248
|
+
description: 'Container lifecycle management via Docker CLI',
|
|
249
|
+
platforms: {
|
|
250
|
+
opencode: { type: 'local', command: 'docker' },
|
|
251
|
+
vscode: { type: 'stdio', command: 'docker' },
|
|
252
|
+
cursor: { type: 'local', command: 'docker' },
|
|
253
|
+
claude: { type: 'local', command: 'docker' },
|
|
254
|
+
windsurf: { type: 'local', command: 'docker' },
|
|
255
|
+
},
|
|
256
|
+
env: [],
|
|
257
|
+
validate: async () => {
|
|
258
|
+
try {
|
|
259
|
+
execSync('docker info --format "{{.ServerVersion}}" 2>&1', {
|
|
260
|
+
stdio: 'pipe',
|
|
261
|
+
timeout: 10000,
|
|
262
|
+
})
|
|
263
|
+
return { ok: true, message: 'Docker daemon reachable' }
|
|
264
|
+
} catch {
|
|
265
|
+
return { ok: false, message: 'Docker daemon not reachable or not installed' }
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
// Platform Metadata
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
const PLATFORMS = {
|
|
276
|
+
opencode: {
|
|
277
|
+
label: 'OpenCode',
|
|
278
|
+
configFile: 'opencode.json',
|
|
279
|
+
configPaths: [
|
|
280
|
+
join(ROOT, 'opencode.json'),
|
|
281
|
+
join(process.cwd(), 'opencode.json'),
|
|
282
|
+
join(homedir(), '.config', 'opencode', 'opencode.json'),
|
|
283
|
+
],
|
|
284
|
+
// OpenCode uses "mcp" key for servers (not "mcpServers")
|
|
285
|
+
configKey: 'mcp',
|
|
286
|
+
detect: () =>
|
|
287
|
+
existsSync(join(ROOT, 'opencode.json')) ||
|
|
288
|
+
existsSync(join(process.cwd(), 'opencode.json')) ||
|
|
289
|
+
existsSync(join(homedir(), '.config', 'opencode', 'opencode.json')),
|
|
290
|
+
},
|
|
291
|
+
vscode: {
|
|
292
|
+
label: 'VS Code',
|
|
293
|
+
configFile: '.vscode/mcp.json',
|
|
294
|
+
configPaths: [join(ROOT, '.vscode', 'mcp.json'), join(process.cwd(), '.vscode', 'mcp.json')],
|
|
295
|
+
// VS Code uses "servers" key
|
|
296
|
+
configKey: 'servers',
|
|
297
|
+
detect: () =>
|
|
298
|
+
existsSync(join(ROOT, '.vscode', 'mcp.json')) ||
|
|
299
|
+
existsSync(join(process.cwd(), '.vscode', 'mcp.json')),
|
|
300
|
+
},
|
|
301
|
+
cursor: {
|
|
302
|
+
label: 'Cursor',
|
|
303
|
+
configFile: '.cursor/mcp.json',
|
|
304
|
+
configPaths: [join(ROOT, '.cursor', 'mcp.json'), join(process.cwd(), '.cursor', 'mcp.json')],
|
|
305
|
+
configKey: 'mcpServers',
|
|
306
|
+
detect: () =>
|
|
307
|
+
existsSync(join(ROOT, '.cursor', 'mcp.json')) ||
|
|
308
|
+
existsSync(join(process.cwd(), '.cursor', 'mcp.json')),
|
|
309
|
+
},
|
|
310
|
+
claude: {
|
|
311
|
+
label: 'Claude',
|
|
312
|
+
configFile: '.mcp.json',
|
|
313
|
+
configPaths: [
|
|
314
|
+
join(ROOT, '.mcp.json'),
|
|
315
|
+
join(process.cwd(), '.mcp.json'),
|
|
316
|
+
join(homedir(), '.claude', 'settings.json'),
|
|
317
|
+
],
|
|
318
|
+
configKey: 'mcpServers',
|
|
319
|
+
detect: () =>
|
|
320
|
+
existsSync(join(ROOT, '.mcp.json')) ||
|
|
321
|
+
existsSync(join(process.cwd(), '.mcp.json')) ||
|
|
322
|
+
existsSync(join(homedir(), '.claude', 'settings.json')),
|
|
323
|
+
},
|
|
324
|
+
windsurf: {
|
|
325
|
+
label: 'Windsurf',
|
|
326
|
+
configFile: '~/.codeium/windsurf/mcp_config.json',
|
|
327
|
+
configPaths: [join(homedir(), '.codeium', 'windsurf', 'mcp_config.json')],
|
|
328
|
+
configKey: 'mcpServers',
|
|
329
|
+
detect: () => existsSync(join(homedir(), '.codeium', 'windsurf', 'mcp_config.json')),
|
|
330
|
+
},
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
// Platform Config Writers
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Build the MCP server entry for a given platform.
|
|
339
|
+
* Each platform has a slightly different JSON shape.
|
|
340
|
+
*
|
|
341
|
+
* @param {string} mcpKey - Canonical MCP key (e.g. 'github', 'context7')
|
|
342
|
+
* @param {string} platformName - Platform key (e.g. 'opencode', 'vscode')
|
|
343
|
+
* @returns {object|null} - The config entry, or null if unsupported
|
|
344
|
+
*/
|
|
345
|
+
function buildMcpEntry(mcpKey, platformName) {
|
|
346
|
+
const mcp = MCPS[mcpKey]
|
|
347
|
+
if (!mcp) return null
|
|
348
|
+
const platform = mcp.platforms[platformName]
|
|
349
|
+
if (!platform) return null
|
|
350
|
+
|
|
351
|
+
// Collect env vars that are set for inclusion in config
|
|
352
|
+
const env = {}
|
|
353
|
+
for (const key of mcp.env) {
|
|
354
|
+
const val = process.env[key]
|
|
355
|
+
if (val) {
|
|
356
|
+
env[key] = `\${${key}}`
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
switch (platform.type) {
|
|
361
|
+
case 'remote':
|
|
362
|
+
case 'http': {
|
|
363
|
+
// OpenCode uses "mcp" key with { type, url, enabled }
|
|
364
|
+
if (platformName === 'opencode') {
|
|
365
|
+
return {
|
|
366
|
+
type: 'remote',
|
|
367
|
+
url: platform.url,
|
|
368
|
+
enabled: true,
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
// VS Code http format: { type: "http", url }
|
|
372
|
+
if (platformName === 'vscode') {
|
|
373
|
+
return { type: 'http', url: platform.url }
|
|
374
|
+
}
|
|
375
|
+
// Cursor/Claude/Windsurf http format
|
|
376
|
+
return { type: 'http', url: platform.url }
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
case 'local': {
|
|
380
|
+
// OpenCode uses "mcp" key with { type: "local", command: [...], enabled }
|
|
381
|
+
if (platformName === 'opencode') {
|
|
382
|
+
const entry = {
|
|
383
|
+
type: 'local',
|
|
384
|
+
command: [platform.command, ...(platform.args || [])],
|
|
385
|
+
enabled: true,
|
|
386
|
+
}
|
|
387
|
+
if (Object.keys(env).length > 0) {
|
|
388
|
+
entry.environment = env
|
|
389
|
+
}
|
|
390
|
+
return entry
|
|
391
|
+
}
|
|
392
|
+
// Cursor/Claude/Windsurf format: { command, args?, env? }
|
|
393
|
+
const entry = {
|
|
394
|
+
command: platform.command,
|
|
395
|
+
...(platform.args ? { args: platform.args } : {}),
|
|
396
|
+
}
|
|
397
|
+
if (Object.keys(env).length > 0) {
|
|
398
|
+
entry.env = env
|
|
399
|
+
}
|
|
400
|
+
return entry
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
case 'stdio': {
|
|
404
|
+
// VS Code stdio format: { type: "stdio", command, args? }
|
|
405
|
+
return {
|
|
406
|
+
type: 'stdio',
|
|
407
|
+
command: platform.command,
|
|
408
|
+
...(platform.args ? { args: platform.args } : {}),
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
default:
|
|
413
|
+
return null
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Read a JSON config file, returning the parsed object or {}.
|
|
419
|
+
*/
|
|
420
|
+
function readConfig(filePath) {
|
|
421
|
+
try {
|
|
422
|
+
if (existsSync(filePath)) {
|
|
423
|
+
const raw = readFileSync(filePath, 'utf8')
|
|
424
|
+
return JSON.parse(raw)
|
|
425
|
+
}
|
|
426
|
+
} catch (err) {
|
|
427
|
+
console.warn(` ⚠️ Could not parse ${filePath}: ${err.message}`)
|
|
428
|
+
}
|
|
429
|
+
return {}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Write the MCP server entry to the platform config file.
|
|
434
|
+
*
|
|
435
|
+
* @param {string} platformName - Platform key
|
|
436
|
+
* @param {string} mcpKey - MCP canonical key
|
|
437
|
+
* @param {boolean} dryRun - If true, don't write anything
|
|
438
|
+
* @param {boolean} force - Overwrite existing entry
|
|
439
|
+
* @returns {{ status: string, path?: string, reason?: string, entry?: object }}
|
|
440
|
+
*/
|
|
441
|
+
function writeMcpConfig(platformName, mcpKey, dryRun, force) {
|
|
442
|
+
const platform = PLATFORMS[platformName]
|
|
443
|
+
const mcp = MCPS[mcpKey]
|
|
444
|
+
if (!platform || !mcp) {
|
|
445
|
+
return { status: 'error', reason: 'unknown platform or mcp' }
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Find the best config path (first existing, or first fallback)
|
|
449
|
+
let configPath = null
|
|
450
|
+
for (const p of platform.configPaths) {
|
|
451
|
+
if (existsSync(p)) {
|
|
452
|
+
configPath = p
|
|
453
|
+
break
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (!configPath) {
|
|
457
|
+
configPath = platform.configPaths[0]
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Read existing config
|
|
461
|
+
const config = readConfig(configPath)
|
|
462
|
+
const configExisted = Object.keys(config).length > 0
|
|
463
|
+
|
|
464
|
+
// The key under which MCP servers are stored (e.g. 'mcp', 'servers', 'mcpServers')
|
|
465
|
+
const key = platform.configKey
|
|
466
|
+
|
|
467
|
+
// Ensure the section exists
|
|
468
|
+
if (!config[key]) {
|
|
469
|
+
config[key] = {}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Check if the MCP already has an entry
|
|
473
|
+
if (config[key][mcpKey] && !force) {
|
|
474
|
+
return {
|
|
475
|
+
status: 'skipped',
|
|
476
|
+
reason: `Already exists in ${platform.configFile} (use --force to overwrite)`,
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Build the platform-specific entry
|
|
481
|
+
const entry = buildMcpEntry(mcpKey, platformName)
|
|
482
|
+
if (!entry) {
|
|
483
|
+
return {
|
|
484
|
+
status: 'error',
|
|
485
|
+
reason: `${mcp.name} does not support ${platform.label}`,
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (dryRun) {
|
|
490
|
+
return { status: 'dry-run', entry }
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Backup existing config before modifying
|
|
494
|
+
if (configExisted) {
|
|
495
|
+
const backupPath = `${configPath}.backup`
|
|
496
|
+
if (!existsSync(backupPath)) {
|
|
497
|
+
try {
|
|
498
|
+
copyFileSync(configPath, backupPath)
|
|
499
|
+
} catch {
|
|
500
|
+
// Non-fatal: continue even if backup fails
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Write the MCP entry
|
|
506
|
+
config[key][mcpKey] = entry
|
|
507
|
+
|
|
508
|
+
// Ensure directory exists
|
|
509
|
+
const dir = dirname(configPath)
|
|
510
|
+
if (!existsSync(dir)) {
|
|
511
|
+
mkdirSync(dir, { recursive: true })
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8')
|
|
515
|
+
|
|
516
|
+
return { status: 'installed', path: configPath }
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ---------------------------------------------------------------------------
|
|
520
|
+
// CLI Argument Parsing
|
|
521
|
+
// ---------------------------------------------------------------------------
|
|
522
|
+
|
|
523
|
+
function parseArgs(argv) {
|
|
524
|
+
const args = {
|
|
525
|
+
platforms: [],
|
|
526
|
+
tier: 1,
|
|
527
|
+
mcp: null,
|
|
528
|
+
list: false,
|
|
529
|
+
dryRun: false,
|
|
530
|
+
force: false,
|
|
531
|
+
help: false,
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
for (let i = 2; i < argv.length; i++) {
|
|
535
|
+
switch (argv[i]) {
|
|
536
|
+
case '--platform':
|
|
537
|
+
args.platforms = argv[++i].split(',').map((s) => s.trim().toLowerCase())
|
|
538
|
+
break
|
|
539
|
+
case '--tier':
|
|
540
|
+
args.tier = parseInt(argv[++i], 10)
|
|
541
|
+
break
|
|
542
|
+
case '--mcp':
|
|
543
|
+
args.mcp = argv[++i].toLowerCase()
|
|
544
|
+
break
|
|
545
|
+
case '--list':
|
|
546
|
+
args.list = true
|
|
547
|
+
break
|
|
548
|
+
case '--dry-run':
|
|
549
|
+
args.dryRun = true
|
|
550
|
+
break
|
|
551
|
+
case '--force':
|
|
552
|
+
args.force = true
|
|
553
|
+
break
|
|
554
|
+
case '--help':
|
|
555
|
+
case '-h':
|
|
556
|
+
args.help = true
|
|
557
|
+
break
|
|
558
|
+
default:
|
|
559
|
+
console.warn(`⚠️ Unknown option: ${argv[i]}`)
|
|
560
|
+
break
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// If no platforms specified and not --list, auto-detect
|
|
565
|
+
if (args.platforms.length === 0 && !args.list && !args.help) {
|
|
566
|
+
const detected = detectPlatforms()
|
|
567
|
+
if (detected.length > 0) {
|
|
568
|
+
args.platforms = detected
|
|
569
|
+
} else {
|
|
570
|
+
// Default to all platforms if none detected
|
|
571
|
+
args.platforms = Object.keys(PLATFORMS)
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return args
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function detectPlatforms() {
|
|
579
|
+
const detected = []
|
|
580
|
+
for (const [name, platform] of Object.entries(PLATFORMS)) {
|
|
581
|
+
if (platform.detect()) {
|
|
582
|
+
detected.push(name)
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return detected
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// ---------------------------------------------------------------------------
|
|
589
|
+
// Interactive Prompt
|
|
590
|
+
// ---------------------------------------------------------------------------
|
|
591
|
+
|
|
592
|
+
function askQuestion(query) {
|
|
593
|
+
return new Promise((resolve) => {
|
|
594
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
595
|
+
rl.question(query, (answer) => {
|
|
596
|
+
rl.close()
|
|
597
|
+
resolve(answer.trim().toLowerCase())
|
|
598
|
+
})
|
|
599
|
+
})
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
async function promptForTier2Mcps() {
|
|
603
|
+
const selected = []
|
|
604
|
+
const tier2Mcps = Object.entries(MCPS).filter(([, mcp]) => mcp.tier === 2)
|
|
605
|
+
|
|
606
|
+
console.log('\nDomain (Tier 2) — ? [y/N]:')
|
|
607
|
+
|
|
608
|
+
for (const [key, mcp] of tier2Mcps) {
|
|
609
|
+
const envHints = mcp.env.length > 0 ? ` (needs: ${mcp.env.join(', ')})` : ''
|
|
610
|
+
const answer = await askQuestion(` ${mcp.name}${envHints}? [y/N] `)
|
|
611
|
+
if (answer === 'y' || answer === 'yes') {
|
|
612
|
+
selected.push(key)
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return selected
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// ---------------------------------------------------------------------------
|
|
620
|
+
// Validation
|
|
621
|
+
// ---------------------------------------------------------------------------
|
|
622
|
+
|
|
623
|
+
async function validateMcp(mcpKey) {
|
|
624
|
+
const mcp = MCPS[mcpKey]
|
|
625
|
+
if (!mcp) return { ok: false, message: 'unknown MCP' }
|
|
626
|
+
try {
|
|
627
|
+
return await mcp.validate()
|
|
628
|
+
} catch (err) {
|
|
629
|
+
return { ok: false, message: err.message }
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// ---------------------------------------------------------------------------
|
|
634
|
+
// List Available MCPs
|
|
635
|
+
// ---------------------------------------------------------------------------
|
|
636
|
+
|
|
637
|
+
function listMcps() {
|
|
638
|
+
console.log('\nAvailable MCP Servers:\n')
|
|
639
|
+
console.log('Essential (Tier 1):')
|
|
640
|
+
for (const [key, mcp] of Object.entries(MCPS)) {
|
|
641
|
+
if (mcp.tier === 1) {
|
|
642
|
+
console.log(` ${key.padEnd(16)} ${mcp.name.padEnd(20)} ${mcp.description}`)
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
console.log('\nDomain (Tier 2):')
|
|
646
|
+
for (const [key, mcp] of Object.entries(MCPS)) {
|
|
647
|
+
if (mcp.tier === 2) {
|
|
648
|
+
const envInfo = mcp.env.length > 0 ? ` [env: ${mcp.env.join(', ')}]` : ''
|
|
649
|
+
console.log(` ${key.padEnd(16)} ${mcp.name.padEnd(20)} ${mcp.description}${envInfo}`)
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
console.log(`\nPlatforms supported: ${Object.keys(PLATFORMS).join(', ')}`)
|
|
653
|
+
console.log('')
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// ---------------------------------------------------------------------------
|
|
657
|
+
// Help
|
|
658
|
+
// ---------------------------------------------------------------------------
|
|
659
|
+
|
|
660
|
+
function showHelp() {
|
|
661
|
+
console.log(`
|
|
662
|
+
install-mcp.mjs — Multi-platform MCP server installer for Pantheon
|
|
663
|
+
|
|
664
|
+
Usage:
|
|
665
|
+
node scripts/install-mcp.mjs Auto-detect platform, install Tier 1
|
|
666
|
+
node scripts/install-mcp.mjs --platform opencode Install for specific platform
|
|
667
|
+
node scripts/install-mcp.mjs --tier 2 Install Tier 2 (interactive prompt)
|
|
668
|
+
node scripts/install-mcp.mjs --mcp context7 Install a specific MCP only
|
|
669
|
+
node scripts/install-mcp.mjs --list List available MCPs
|
|
670
|
+
node scripts/install-mcp.mjs --dry-run Preview changes without writing
|
|
671
|
+
node scripts/install-mcp.mjs --force Overwrite existing MCP entries
|
|
672
|
+
node scripts/install-mcp.mjs --platform opencode --mcp github,context7
|
|
673
|
+
|
|
674
|
+
Options:
|
|
675
|
+
--platform <name> Target platform (opencode|vscode|cursor|claude|windsurf).
|
|
676
|
+
Comma-separated for multiple. Auto-detects if omitted.
|
|
677
|
+
--tier <n> 1=Essential (default), 2=Domain, 3=Project
|
|
678
|
+
--mcp <name> Install specific MCP(s) only. Comma-separated.
|
|
679
|
+
--list List available MCPs and exit
|
|
680
|
+
--dry-run Show what would be installed without writing
|
|
681
|
+
--force Overwrite existing MCP entries
|
|
682
|
+
--help, -h Show this help
|
|
683
|
+
|
|
684
|
+
Platforms:
|
|
685
|
+
opencode → opencode.json (mcp key)
|
|
686
|
+
vscode → .vscode/mcp.json (servers key)
|
|
687
|
+
cursor → .cursor/mcp.json (mcpServers key)
|
|
688
|
+
claude → .mcp.json or ~/.claude/settings.json (mcpServers key)
|
|
689
|
+
windsurf → ~/.codeium/windsurf/mcp_config.json (mcpServers key)
|
|
690
|
+
|
|
691
|
+
Environment Variables:
|
|
692
|
+
DATABASE_URL Required for PostgreSQL MCP (postgresql+ssl://...)
|
|
693
|
+
BRAVE_API_KEY Required for Brave Search MCP
|
|
694
|
+
|
|
695
|
+
Examples:
|
|
696
|
+
node scripts/install-mcp.mjs Auto-detect + install essentials
|
|
697
|
+
node scripts/install-mcp.mjs --platform opencode --tier 2 OpenCode + interactive Tier 2
|
|
698
|
+
node scripts/install-mcp.mjs --mcp docker Install only Docker MCP
|
|
699
|
+
node scripts/install-mcp.mjs --platform vscode --dry-run Preview VS Code changes
|
|
700
|
+
`)
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// ---------------------------------------------------------------------------
|
|
704
|
+
// Progress / Logging Helpers
|
|
705
|
+
// ---------------------------------------------------------------------------
|
|
706
|
+
|
|
707
|
+
const progress = {
|
|
708
|
+
installed: [],
|
|
709
|
+
skipped: [],
|
|
710
|
+
errors: [],
|
|
711
|
+
validations: {},
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function logResult(platformLabel, mcpKey, result) {
|
|
715
|
+
const mcp = MCPS[mcpKey]
|
|
716
|
+
const label = mcp ? mcp.name : mcpKey
|
|
717
|
+
switch (result.status) {
|
|
718
|
+
case 'installed':
|
|
719
|
+
console.log(` ✅ ${label} — config written to ${result.path}`)
|
|
720
|
+
progress.installed.push({ platform: platformLabel, mcp: mcpKey })
|
|
721
|
+
break
|
|
722
|
+
case 'skipped':
|
|
723
|
+
console.log(` ⏭️ ${label} — ${result.reason}`)
|
|
724
|
+
progress.skipped.push({ platform: platformLabel, mcp: mcpKey, reason: result.reason })
|
|
725
|
+
break
|
|
726
|
+
case 'dry-run': {
|
|
727
|
+
const desc =
|
|
728
|
+
result.entry?.type === 'remote' || result.entry?.type === 'http'
|
|
729
|
+
? `remote → ${result.entry.url}`
|
|
730
|
+
: `local → ${result.entry.command} ${(result.entry.args || []).join(' ')}`
|
|
731
|
+
console.log(` 📋 ${label} — would install (${desc})`)
|
|
732
|
+
break
|
|
733
|
+
}
|
|
734
|
+
default:
|
|
735
|
+
console.log(` ❌ ${label} — ${result.reason}`)
|
|
736
|
+
progress.errors.push({ platform: platformLabel, mcp: mcpKey, reason: result.reason })
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function logValidation(mcpKey, result) {
|
|
741
|
+
const mcp = MCPS[mcpKey]
|
|
742
|
+
const label = mcp ? mcp.name : mcpKey
|
|
743
|
+
if (result.ok) {
|
|
744
|
+
const extra = result.message ? ` — ${result.message}` : ''
|
|
745
|
+
console.log(` ✅ ${label}${extra}`)
|
|
746
|
+
} else {
|
|
747
|
+
const extra = result.message ? ` — ${result.message}` : ''
|
|
748
|
+
console.log(` ❌ ${label}${extra}`)
|
|
749
|
+
}
|
|
750
|
+
progress.validations[mcpKey] = result
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// ---------------------------------------------------------------------------
|
|
754
|
+
// Main
|
|
755
|
+
// ---------------------------------------------------------------------------
|
|
756
|
+
|
|
757
|
+
async function main() {
|
|
758
|
+
const args = parseArgs(process.argv)
|
|
759
|
+
|
|
760
|
+
if (args.help) {
|
|
761
|
+
showHelp()
|
|
762
|
+
process.exit(0)
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
if (args.list) {
|
|
766
|
+
listMcps()
|
|
767
|
+
process.exit(0)
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Determine which MCPs to install
|
|
771
|
+
let mcpKeys = []
|
|
772
|
+
|
|
773
|
+
if (args.mcp) {
|
|
774
|
+
// Specific MCP(s) requested
|
|
775
|
+
mcpKeys = args.mcp.split(',').map((s) => s.trim().toLowerCase())
|
|
776
|
+
for (const key of mcpKeys) {
|
|
777
|
+
if (!MCPS[key]) {
|
|
778
|
+
console.error(`❌ Unknown MCP: "${key}". Use --list to see available MCPs.`)
|
|
779
|
+
process.exit(1)
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
} else {
|
|
783
|
+
// Tier 1 — always included
|
|
784
|
+
const tier1Mcps = Object.entries(MCPS)
|
|
785
|
+
.filter(([, mcp]) => mcp.tier === 1)
|
|
786
|
+
.map(([key]) => key)
|
|
787
|
+
mcpKeys.push(...tier1Mcps)
|
|
788
|
+
|
|
789
|
+
// Tier 2 — interactive prompt
|
|
790
|
+
if (args.tier >= 2) {
|
|
791
|
+
const tier2Selected = await promptForTier2Mcps()
|
|
792
|
+
mcpKeys.push(...tier2Selected)
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (mcpKeys.length === 0) {
|
|
797
|
+
console.log('No MCPs selected. Nothing to install.\n')
|
|
798
|
+
process.exit(0)
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Determine target platforms
|
|
802
|
+
const platforms = args.platforms
|
|
803
|
+
const platformLabels = platforms.map((p) => PLATFORMS[p]?.label || p)
|
|
804
|
+
|
|
805
|
+
if (args.dryRun) {
|
|
806
|
+
console.log('\n🔍 Dry-run mode — no files will be written\n')
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
console.log(
|
|
810
|
+
`\n🔍 ${platforms.length === 1 ? 'Platform' : 'Platforms'}: ${platformLabels.join(', ')}`,
|
|
811
|
+
)
|
|
812
|
+
console.log(
|
|
813
|
+
`📦 Installing ${mcpKeys.length} MCP(s): ${mcpKeys.map((k) => MCPS[k]?.name || k).join(', ')}\n`,
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
// Install per platform
|
|
817
|
+
for (const platformName of platforms) {
|
|
818
|
+
const platform = PLATFORMS[platformName]
|
|
819
|
+
if (!platform) {
|
|
820
|
+
console.warn(` ⚠️ Unknown platform: ${platformName} — skipping`)
|
|
821
|
+
continue
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
console.log(`🔧 ${platform.label} (${platform.configFile}):`)
|
|
825
|
+
|
|
826
|
+
for (const mcpKey of mcpKeys) {
|
|
827
|
+
const result = writeMcpConfig(platformName, mcpKey, args.dryRun, args.force)
|
|
828
|
+
logResult(platform.label, mcpKey, result)
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
console.log('')
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Validation
|
|
835
|
+
if (!args.dryRun) {
|
|
836
|
+
console.log('🔍 Validating MCPs...\n')
|
|
837
|
+
for (const key of mcpKeys) {
|
|
838
|
+
const result = await validateMcp(key)
|
|
839
|
+
logValidation(key, result)
|
|
840
|
+
}
|
|
841
|
+
console.log('')
|
|
842
|
+
} else {
|
|
843
|
+
console.log('🔍 Validation skipped in dry-run mode\n')
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// Summary
|
|
847
|
+
console.log('='.repeat(60))
|
|
848
|
+
console.log('📋 Installation Summary')
|
|
849
|
+
console.log('='.repeat(60))
|
|
850
|
+
console.log(` Platforms: ${platformLabels.join(', ')}`)
|
|
851
|
+
console.log(` MCPs: ${mcpKeys.length} requested`)
|
|
852
|
+
console.log(` Installed: ${progress.installed.length}`)
|
|
853
|
+
console.log(` Skipped: ${progress.skipped.length}`)
|
|
854
|
+
console.log(` Errors: ${progress.errors.length}`)
|
|
855
|
+
|
|
856
|
+
if (progress.installed.length > 0) {
|
|
857
|
+
console.log('\n ✅ Installed:')
|
|
858
|
+
for (const item of progress.installed) {
|
|
859
|
+
console.log(` - ${item.mcp} → ${item.platform}`)
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if (progress.skipped.length > 0 && !args.dryRun) {
|
|
864
|
+
console.log('\n ⏭️ Skipped:')
|
|
865
|
+
for (const item of progress.skipped) {
|
|
866
|
+
console.log(` - ${item.mcp} (${item.platform}): ${item.reason}`)
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
if (progress.errors.length > 0) {
|
|
871
|
+
console.log('\n ❌ Errors:')
|
|
872
|
+
for (const item of progress.errors) {
|
|
873
|
+
console.log(` - ${item.mcp} (${item.platform}): ${item.reason}`)
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
console.log('\n⚠️ Reminder: Restart your editor/CLI tool for MCP changes to take effect.')
|
|
878
|
+
console.log(' For VS Code: Cmd+Shift+P → "Developer: Reload Window"')
|
|
879
|
+
console.log(' For OpenCode: Just restart the session')
|
|
880
|
+
console.log('')
|
|
881
|
+
|
|
882
|
+
process.exit(progress.errors.length > 0 ? 1 : 0)
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
main()
|