@slycode/slycode 0.2.17 → 0.2.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/bin/sly-kanban.js +3 -3
  2. package/dist/data/scaffold-templates/mcp.json +1 -6
  3. package/dist/scripts/kanban.js +9 -3
  4. package/dist/web/.next/BUILD_ID +1 -1
  5. package/dist/web/.next/build-manifest.json +2 -2
  6. package/dist/web/.next/server/app/_global-error.html +2 -2
  7. package/dist/web/.next/server/app/_global-error.rsc +1 -1
  8. package/dist/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/dist/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/dist/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  11. package/dist/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  12. package/dist/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/dist/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/dist/web/.next/server/app/_not-found.html +1 -1
  15. package/dist/web/.next/server/app/_not-found.rsc +3 -3
  16. package/dist/web/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  17. package/dist/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  18. package/dist/web/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  19. package/dist/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  20. package/dist/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  21. package/dist/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  22. package/dist/web/.next/server/app/api/cli-assets/sync/route.js +3 -2
  23. package/dist/web/.next/server/app/api/cli-assets/sync/route.js.nft.json +1 -1
  24. package/dist/web/.next/server/app/page_client-reference-manifest.js +1 -1
  25. package/dist/web/.next/server/app/project/[id]/page_client-reference-manifest.js +1 -1
  26. package/dist/web/.next/server/chunks/[root-of-the-server]__12f6cd6f._.js +1 -1
  27. package/dist/web/.next/server/chunks/[root-of-the-server]__15fc9266._.js +1 -1
  28. package/dist/web/.next/server/chunks/[root-of-the-server]__279e9bf3._.js +1 -1
  29. package/dist/web/.next/server/chunks/[root-of-the-server]__2d1f0ed9._.js +1 -1
  30. package/dist/web/.next/server/chunks/[root-of-the-server]__3b9d3e43._.js +42 -23
  31. package/dist/web/.next/server/chunks/[root-of-the-server]__47dd878e._.js +1 -1
  32. package/dist/web/.next/server/chunks/[root-of-the-server]__5b8c9374._.js +3 -0
  33. package/dist/web/.next/server/chunks/[root-of-the-server]__5e08b942._.js +1 -1
  34. package/dist/web/.next/server/chunks/[root-of-the-server]__71bb3374._.js +1 -1
  35. package/dist/web/.next/server/chunks/[root-of-the-server]__7603305e._.js +1 -1
  36. package/dist/web/.next/server/chunks/[root-of-the-server]__7c476ad6._.js +1 -1
  37. package/dist/web/.next/server/chunks/[root-of-the-server]__846ca56f._.js +1 -1
  38. package/dist/web/.next/server/chunks/[root-of-the-server]__98d88050._.js +1 -1
  39. package/dist/web/.next/server/chunks/[root-of-the-server]__b273cc05._.js +1 -1
  40. package/dist/web/.next/server/chunks/[root-of-the-server]__d6362272._.js +1 -1
  41. package/dist/web/.next/server/chunks/[root-of-the-server]__de1277ee._.js +1 -1
  42. package/dist/web/.next/server/chunks/[root-of-the-server]__f3e501b6._.js +1 -1
  43. package/dist/web/.next/server/chunks/[root-of-the-server]__f97e93fa._.js +2 -2
  44. package/dist/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_234fda9c.js +3 -0
  45. package/dist/web/.next/server/chunks/src_lib_asset-scanner_ts_c599fd1c._.js +1 -1
  46. package/dist/web/.next/server/chunks/ssr/[root-of-the-server]__193dbb5b._.js +1 -1
  47. package/dist/web/.next/server/chunks/ssr/src_components_Dashboard_tsx_efc4dc27._.js +1 -1
  48. package/dist/web/.next/server/chunks/ssr/src_lib_registry_ts_2fc87c9c._.js +1 -1
  49. package/dist/web/.next/server/pages/404.html +1 -1
  50. package/dist/web/.next/server/pages/500.html +2 -2
  51. package/dist/web/.next/static/chunks/747f5e5f9dcf2621.css +1 -0
  52. package/dist/web/.next/static/chunks/f55f3c8c1a52f80c.js +1 -0
  53. package/dist/web/src/app/api/cli-assets/assistant/route.ts +30 -13
  54. package/dist/web/src/app/api/cli-assets/store/route.ts +5 -3
  55. package/dist/web/src/app/api/cli-assets/sync/route.ts +31 -1
  56. package/dist/web/src/app/api/file/route.ts +2 -1
  57. package/dist/web/src/app/layout.tsx +1 -1
  58. package/dist/web/src/components/AssetViewer.tsx +99 -0
  59. package/dist/web/src/components/CliAssetsTab.tsx +2 -2
  60. package/dist/web/src/components/StoreView.tsx +168 -61
  61. package/dist/web/src/lib/asset-scanner.ts +1 -1
  62. package/dist/web/src/lib/mcp-common.ts +76 -37
  63. package/dist/web/src/lib/provider-paths.ts +1 -1
  64. package/dist/web/src/lib/store-scanner.ts +1 -1
  65. package/dist/web/tsconfig.tsbuildinfo +1 -1
  66. package/package.json +3 -2
  67. package/templates/kanban-seed.json +1 -1
  68. package/dist/web/.next/server/chunks/[root-of-the-server]__3c5ef8ec._.js +0 -3
  69. package/dist/web/.next/static/chunks/2744d72103f49934.css +0 -1
  70. package/dist/web/.next/static/chunks/e8b318caa49fce00.js +0 -1
  71. /package/dist/web/.next/static/{zp7O3ZtAOySM7ONwqP-kQ → qMss0q1Ox38k4U6oxip2H}/_buildManifest.js +0 -0
  72. /package/dist/web/.next/static/{zp7O3ZtAOySM7ONwqP-kQ → qMss0q1Ox38k4U6oxip2H}/_clientMiddlewareManifest.json +0 -0
  73. /package/dist/web/.next/static/{zp7O3ZtAOySM7ONwqP-kQ → qMss0q1Ox38k4U6oxip2H}/_ssgManifest.js +0 -0
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
- import { useState } from 'react';
4
- import type { StoreData, StoreAssetInfo, AssetType } from '@/lib/types';
3
+ import { useState, useEffect } from 'react';
4
+ import type { StoreData, StoreAssetInfo, AssetType, ProviderId, Project } from '@/lib/types';
5
5
  import { AssetViewer } from './AssetViewer';
6
6
 
7
7
  interface StoreViewProps {
@@ -21,7 +21,8 @@ export function StoreView({ data, onFix, onAssistant, onRefresh }: StoreViewProp
21
21
  const [viewingAsset, setViewingAsset] = useState<StoreAssetInfo | null>(null);
22
22
  const [confirmDelete, setConfirmDelete] = useState<{ name: string; type: AssetType } | null>(null);
23
23
  const [deleting, setDeleting] = useState(false);
24
- const [expandedSections, setExpandedSections] = useState({
24
+ const [deployTarget, setDeployTarget] = useState<{ name: string } | null>(null);
25
+ const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({
25
26
  skills: true,
26
27
  agents: true,
27
28
  mcp: true,
@@ -48,8 +49,9 @@ export function StoreView({ data, onFix, onAssistant, onRefresh }: StoreViewProp
48
49
  }
49
50
 
50
51
  const sections = [
51
- { key: 'skills' as const, label: 'Skills', assets: data.skills },
52
- { key: 'agents' as const, label: 'Agents', assets: data.agents },
52
+ { key: 'skills', label: 'Skills', assets: data.skills },
53
+ { key: 'agents', label: 'Agents', assets: data.agents },
54
+ { key: 'mcp', label: 'MCP Configs', assets: data.mcp },
53
55
  ];
54
56
 
55
57
  return (
@@ -114,6 +116,15 @@ export function StoreView({ data, onFix, onAssistant, onRefresh }: StoreViewProp
114
116
  </td>
115
117
  <td className="px-4 py-2 text-right">
116
118
  <div className="flex items-center justify-end gap-1">
119
+ {asset.type === 'mcp' && (
120
+ <button
121
+ onClick={() => setDeployTarget({ name: asset.name })}
122
+ className="rounded border border-neon-blue-400/30 bg-neon-blue-400/10 px-2 py-1 text-xs font-medium text-neon-blue-400 hover:bg-neon-blue-400/20"
123
+ title="Deploy to project"
124
+ >
125
+ Deploy
126
+ </button>
127
+ )}
117
128
  <button
118
129
  onClick={() => setConfirmDelete({ name: asset.name, type: asset.type })}
119
130
  className="rounded border border-red-400/30 bg-red-400/10 px-2 py-1 text-xs font-medium text-red-500 hover:bg-red-400/20"
@@ -155,62 +166,6 @@ export function StoreView({ data, onFix, onAssistant, onRefresh }: StoreViewProp
155
166
  </div>
156
167
  ))}
157
168
 
158
- {/* MCP section */}
159
- {data.mcp.length > 0 && (
160
- <div className="rounded-lg border border-void-200 bg-white shadow-(--shadow-card) dark:border-void-700 dark:bg-void-850">
161
- <button
162
- onClick={() => toggleSection('mcp')}
163
- className="flex w-full items-center justify-between px-4 py-3"
164
- >
165
- <div className="flex items-center gap-2">
166
- <h3 className="text-sm font-semibold text-void-900 dark:text-void-100">MCP Configs</h3>
167
- <span className="rounded-full bg-void-100 px-2 py-0.5 text-xs text-void-600 dark:bg-void-700 dark:text-void-300">
168
- {data.mcp.length}
169
- </span>
170
- </div>
171
- <svg
172
- className={`h-4 w-4 text-void-400 transition-transform ${expandedSections.mcp ? 'rotate-180' : ''}`}
173
- fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}
174
- >
175
- <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
176
- </svg>
177
- </button>
178
- {expandedSections.mcp && (
179
- <div className="border-t border-void-100 dark:border-void-700">
180
- <table className="w-full text-sm">
181
- <thead>
182
- <tr className="border-b border-void-200 dark:border-void-700">
183
- <th className="px-4 py-2 text-left text-xs font-medium text-void-500 dark:text-void-400">Name</th>
184
- <th className="px-4 py-2 text-left text-xs font-medium text-void-500 dark:text-void-400">Description</th>
185
- <th className="px-4 py-2 text-left text-xs font-medium text-void-500 dark:text-void-400">Version</th>
186
- </tr>
187
- </thead>
188
- <tbody>
189
- {data.mcp.map(mcp => (
190
- <tr key={mcp.name} className="border-b border-void-100 dark:border-void-800">
191
- <td className="px-4 py-2 font-medium text-void-900 dark:text-void-100">
192
- <span className="flex items-center gap-2">
193
- {mcp.name}
194
- <span className={`rounded px-1.5 py-0.5 text-[10px] font-medium ${typeBadgeColors.mcp}`}>
195
- mcp
196
- </span>
197
- </span>
198
- </td>
199
- <td className="px-4 py-2 text-void-500 dark:text-void-400">
200
- {(mcp.frontmatter?.description as string) || '-'}
201
- </td>
202
- <td className="px-4 py-2 text-void-500 dark:text-void-400">
203
- {mcp.frontmatter?.version ? `v${mcp.frontmatter.version}` : '-'}
204
- </td>
205
- </tr>
206
- ))}
207
- </tbody>
208
- </table>
209
- </div>
210
- )}
211
- </div>
212
- )}
213
-
214
169
  {/* Delete confirmation dialog */}
215
170
  {confirmDelete && (
216
171
  <div
@@ -248,6 +203,158 @@ export function StoreView({ data, onFix, onAssistant, onRefresh }: StoreViewProp
248
203
  onClose={() => setViewingAsset(null)}
249
204
  />
250
205
  )}
206
+
207
+ {/* MCP deploy dialog */}
208
+ {deployTarget && (
209
+ <McpDeployDialog
210
+ mcpName={deployTarget.name}
211
+ onClose={() => setDeployTarget(null)}
212
+ onDeployed={() => { setDeployTarget(null); onRefresh?.(); }}
213
+ />
214
+ )}
215
+ </div>
216
+ );
217
+ }
218
+
219
+ /**
220
+ * Deploy an MCP from the store to a project.
221
+ */
222
+ function McpDeployDialog({ mcpName, onClose, onDeployed }: {
223
+ mcpName: string;
224
+ onClose: () => void;
225
+ onDeployed: () => void;
226
+ }) {
227
+ const [projects, setProjects] = useState<Project[]>([]);
228
+ const [selectedProject, setSelectedProject] = useState('');
229
+ const [selectedProvider, setSelectedProvider] = useState<ProviderId>('claude');
230
+ const [deploying, setDeploying] = useState(false);
231
+ const [result, setResult] = useState<{ success: boolean; message: string } | null>(null);
232
+
233
+ useEffect(() => {
234
+ fetch('/api/dashboard')
235
+ .then(r => r.json())
236
+ .then(data => {
237
+ if (data.projects) setProjects(data.projects);
238
+ })
239
+ .catch(() => {});
240
+ }, []);
241
+
242
+ async function handleDeploy() {
243
+ if (!selectedProject) return;
244
+ setDeploying(true);
245
+ setResult(null);
246
+ try {
247
+ const res = await fetch('/api/cli-assets/sync', {
248
+ method: 'POST',
249
+ headers: { 'Content-Type': 'application/json' },
250
+ body: JSON.stringify({
251
+ changes: [{
252
+ assetName: mcpName,
253
+ assetType: 'mcp',
254
+ projectId: selectedProject,
255
+ action: 'deploy',
256
+ provider: selectedProvider,
257
+ source: 'store',
258
+ }],
259
+ }),
260
+ });
261
+ const data = await res.json();
262
+ const firstResult = data.results?.[0];
263
+ if (res.ok && firstResult?.success && firstResult?.error) {
264
+ // success=true but has error message means "already present — skipped"
265
+ setResult({ success: true, message: firstResult.error });
266
+ } else if (res.ok && firstResult?.success) {
267
+ setResult({ success: true, message: `Deployed to ${projects.find(p => p.id === selectedProject)?.name || selectedProject}` });
268
+ setTimeout(onDeployed, 1200);
269
+ } else {
270
+ setResult({ success: false, message: data.results?.[0]?.error || data.error || 'Deploy failed' });
271
+ }
272
+ } catch {
273
+ setResult({ success: false, message: 'Network error' });
274
+ }
275
+ setDeploying(false);
276
+ }
277
+
278
+ const providers: { id: ProviderId; label: string }[] = [
279
+ { id: 'claude', label: 'Claude' },
280
+ { id: 'codex', label: 'Codex' },
281
+ { id: 'gemini', label: 'Gemini' },
282
+ ];
283
+
284
+ return (
285
+ <div
286
+ className="fixed inset-0 z-50 flex items-center justify-center bg-black/60"
287
+ onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
288
+ >
289
+ <div className="mx-4 w-full max-w-sm rounded-lg border border-void-700 bg-void-850 p-5 shadow-(--shadow-overlay)">
290
+ <h3 className="text-sm font-semibold text-void-100">
291
+ Deploy MCP: <span className="text-neon-blue-400">{mcpName}</span>
292
+ </h3>
293
+
294
+ <div className="mt-4 space-y-3">
295
+ {/* Project picker */}
296
+ <div>
297
+ <label className="mb-1 block text-xs font-medium text-void-400">Project</label>
298
+ <select
299
+ value={selectedProject}
300
+ onChange={(e) => setSelectedProject(e.target.value)}
301
+ className="w-full rounded-md border border-void-700 bg-void-900 px-3 py-2 text-sm text-void-200 focus:border-neon-blue-400/50 focus:outline-none"
302
+ >
303
+ <option value="">Select project...</option>
304
+ {projects.map(p => (
305
+ <option key={p.id} value={p.id}>{p.name}</option>
306
+ ))}
307
+ </select>
308
+ </div>
309
+
310
+ {/* Provider picker */}
311
+ <div>
312
+ <label className="mb-1 block text-xs font-medium text-void-400">Provider</label>
313
+ <div className="flex gap-1">
314
+ {providers.map(p => (
315
+ <button
316
+ key={p.id}
317
+ onClick={() => setSelectedProvider(p.id)}
318
+ className={`rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${
319
+ selectedProvider === p.id
320
+ ? 'border border-neon-blue-400/40 bg-neon-blue-400/20 text-neon-blue-400'
321
+ : 'border border-void-700 text-void-400 hover:border-void-600 hover:text-void-200'
322
+ }`}
323
+ >
324
+ {p.label}
325
+ </button>
326
+ ))}
327
+ </div>
328
+ </div>
329
+
330
+ {/* Result message */}
331
+ {result && (
332
+ <div className={`rounded-md border p-2 text-xs ${
333
+ result.success
334
+ ? 'border-green-400/30 bg-green-400/10 text-green-400'
335
+ : 'border-red-400/30 bg-red-400/10 text-red-400'
336
+ }`}>
337
+ {result.message}
338
+ </div>
339
+ )}
340
+ </div>
341
+
342
+ <div className="mt-4 flex justify-end gap-2">
343
+ <button
344
+ onClick={onClose}
345
+ className="rounded px-3 py-1.5 text-sm text-void-400 hover:text-void-200"
346
+ >
347
+ Cancel
348
+ </button>
349
+ <button
350
+ onClick={handleDeploy}
351
+ disabled={!selectedProject || deploying}
352
+ className="rounded bg-neon-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-neon-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
353
+ >
354
+ {deploying ? 'Deploying...' : 'Deploy'}
355
+ </button>
356
+ </div>
357
+ </div>
251
358
  </div>
252
359
  );
253
360
  }
@@ -350,7 +350,7 @@ export function getAssetPath(projectPath: string, assetType: AssetType, assetNam
350
350
  case 'agent':
351
351
  return path.join(projectPath, '.claude', 'agents', `${assetName}.md`);
352
352
  case 'mcp':
353
- return path.join(projectPath, '.claude', 'settings.json');
353
+ return path.join(projectPath, '.mcp.json');
354
354
  default:
355
355
  return path.join(projectPath, '.claude', 'skills', assetName);
356
356
  }
@@ -19,11 +19,12 @@ import { getProviderMcpConfigPath } from './provider-paths';
19
19
 
20
20
  export interface CommonMcpConfig {
21
21
  name: string;
22
- command: string;
22
+ command?: string;
23
23
  args?: string[];
24
24
  env?: Record<string, string>;
25
+ url?: string;
26
+ headers?: Record<string, string>;
25
27
  timeout?: number;
26
- transport?: string;
27
28
  version?: string;
28
29
  description?: string;
29
30
  updated?: string;
@@ -54,12 +55,20 @@ export function parseMcpFromStore(jsonPath: string): CommonMcpConfig | null {
54
55
  * Claude uses JSON: { "mcpServers": { "name": { command, args, env } } }
55
56
  */
56
57
  export function transformToClaudeMcp(config: CommonMcpConfig): Record<string, unknown> {
57
- const entry: Record<string, unknown> = {
58
- command: config.command,
59
- };
60
- if (config.args?.length) entry.args = config.args;
61
- if (config.env && Object.keys(config.env).length > 0) entry.env = config.env;
62
- if (config.timeout) entry.timeout = config.timeout;
58
+ const entry: Record<string, unknown> = {};
59
+
60
+ if (config.url) {
61
+ // HTTP transport
62
+ entry.type = 'http';
63
+ entry.url = config.url;
64
+ if (config.headers && Object.keys(config.headers).length > 0) entry.headers = config.headers;
65
+ } else if (config.command) {
66
+ // Stdio transport
67
+ entry.command = config.command;
68
+ if (config.args?.length) entry.args = config.args;
69
+ if (config.env && Object.keys(config.env).length > 0) entry.env = config.env;
70
+ if (config.timeout) entry.timeout = config.timeout;
71
+ }
63
72
 
64
73
  return { [config.name]: entry };
65
74
  }
@@ -69,11 +78,18 @@ export function transformToClaudeMcp(config: CommonMcpConfig): Record<string, un
69
78
  * Gemini also uses JSON, similar to Claude.
70
79
  */
71
80
  export function transformToGeminiMcp(config: CommonMcpConfig): Record<string, unknown> {
72
- const entry: Record<string, unknown> = {
73
- command: config.command,
74
- };
75
- if (config.args?.length) entry.args = config.args;
76
- if (config.env && Object.keys(config.env).length > 0) entry.env = config.env;
81
+ const entry: Record<string, unknown> = {};
82
+
83
+ if (config.url) {
84
+ // HTTP transport — Gemini uses httpUrl (NOT url; url means SSE in Gemini)
85
+ entry.httpUrl = config.url;
86
+ if (config.headers && Object.keys(config.headers).length > 0) entry.headers = config.headers;
87
+ } else if (config.command) {
88
+ // Stdio transport
89
+ entry.command = config.command;
90
+ if (config.args?.length) entry.args = config.args;
91
+ if (config.env && Object.keys(config.env).length > 0) entry.env = config.env;
92
+ }
77
93
 
78
94
  return { [config.name]: entry };
79
95
  }
@@ -85,18 +101,30 @@ export function transformToGeminiMcp(config: CommonMcpConfig): Record<string, un
85
101
  export function transformToCodexMcp(config: CommonMcpConfig): string {
86
102
  const lines: string[] = [];
87
103
  lines.push(`[mcp_servers.${config.name}]`);
88
- lines.push(`command = "${config.command}"`);
89
-
90
- if (config.args?.length) {
91
- const argsStr = config.args.map(a => `"${a}"`).join(', ');
92
- lines.push(`args = [${argsStr}]`);
93
- }
94
104
 
95
- if (config.env && Object.keys(config.env).length > 0) {
96
- lines.push('');
97
- lines.push(`[mcp_servers.${config.name}.env]`);
98
- for (const [key, value] of Object.entries(config.env)) {
99
- lines.push(`${key} = "${value}"`);
105
+ if (config.url) {
106
+ // HTTP transport — Codex uses url + http_headers (not headers)
107
+ lines.push(`url = "${config.url}"`);
108
+ if (config.headers && Object.keys(config.headers).length > 0) {
109
+ lines.push('');
110
+ lines.push(`[mcp_servers.${config.name}.http_headers]`);
111
+ for (const [key, value] of Object.entries(config.headers)) {
112
+ lines.push(`${key} = "${value}"`);
113
+ }
114
+ }
115
+ } else if (config.command) {
116
+ // Stdio transport
117
+ lines.push(`command = "${config.command}"`);
118
+ if (config.args?.length) {
119
+ const argsStr = config.args.map(a => `"${a}"`).join(', ');
120
+ lines.push(`args = [${argsStr}]`);
121
+ }
122
+ if (config.env && Object.keys(config.env).length > 0) {
123
+ lines.push('');
124
+ lines.push(`[mcp_servers.${config.name}.env]`);
125
+ for (const [key, value] of Object.entries(config.env)) {
126
+ lines.push(`${key} = "${value}"`);
127
+ }
100
128
  }
101
129
  }
102
130
 
@@ -111,20 +139,22 @@ export function transformToCodexMcp(config: CommonMcpConfig): string {
111
139
  * Activate an MCP server in a project for a specific provider.
112
140
  * Reads the provider's config file, merges the MCP entry, writes back.
113
141
  */
142
+ export type ActivateResult = 'deployed' | 'already_exists';
143
+
114
144
  export function activateMcp(
115
145
  projectPath: string,
116
146
  provider: ProviderId,
117
147
  config: CommonMcpConfig,
118
- ): void {
148
+ ): ActivateResult {
119
149
  const configPath = getProviderMcpConfigPath(projectPath, provider);
120
150
  if (!configPath) {
121
151
  throw new Error(`Provider '${provider}' does not have an MCP config path`);
122
152
  }
123
153
 
124
154
  if (provider === 'codex') {
125
- activateCodexMcp(configPath, config);
155
+ return activateCodexMcp(configPath, config);
126
156
  } else {
127
- activateJsonMcp(configPath, config, provider);
157
+ return activateJsonMcp(configPath, config, provider);
128
158
  }
129
159
  }
130
160
 
@@ -154,7 +184,7 @@ function activateJsonMcp(
154
184
  configPath: string,
155
185
  config: CommonMcpConfig,
156
186
  provider: ProviderId,
157
- ): void {
187
+ ): ActivateResult {
158
188
  let existing: Record<string, unknown> = {};
159
189
  try {
160
190
  if (fs.existsSync(configPath)) {
@@ -165,6 +195,10 @@ function activateJsonMcp(
165
195
  }
166
196
 
167
197
  const mcpServers = (existing.mcpServers || {}) as Record<string, unknown>;
198
+
199
+ // Skip if an entry with this name already exists
200
+ if (config.name in mcpServers) return 'already_exists';
201
+
168
202
  const transformed = provider === 'claude'
169
203
  ? transformToClaudeMcp(config)
170
204
  : transformToGeminiMcp(config);
@@ -174,6 +208,7 @@ function activateJsonMcp(
174
208
 
175
209
  fs.mkdirSync(path.dirname(configPath), { recursive: true });
176
210
  fs.writeFileSync(configPath, JSON.stringify(existing, null, 2) + '\n');
211
+ return 'deployed';
177
212
  }
178
213
 
179
214
  function deactivateJsonMcp(configPath: string, mcpName: string): void {
@@ -196,7 +231,7 @@ function deactivateJsonMcp(configPath: string, mcpName: string): void {
196
231
  // TOML-based MCP (Codex)
197
232
  // ============================================================================
198
233
 
199
- function activateCodexMcp(configPath: string, config: CommonMcpConfig): void {
234
+ function activateCodexMcp(configPath: string, config: CommonMcpConfig): ActivateResult {
200
235
  let content = '';
201
236
  try {
202
237
  if (fs.existsSync(configPath)) {
@@ -206,8 +241,9 @@ function activateCodexMcp(configPath: string, config: CommonMcpConfig): void {
206
241
  content = '';
207
242
  }
208
243
 
209
- // Remove existing section for this MCP if present
210
- content = removeTomlSection(content, `mcp_servers.${config.name}`);
244
+ // Skip if this MCP section already exists
245
+ const sectionHeader = `[mcp_servers.${config.name}]`;
246
+ if (content.includes(sectionHeader)) return 'already_exists';
211
247
 
212
248
  // Append new section
213
249
  const tomlSection = transformToCodexMcp(config);
@@ -215,6 +251,7 @@ function activateCodexMcp(configPath: string, config: CommonMcpConfig): void {
215
251
 
216
252
  fs.mkdirSync(path.dirname(configPath), { recursive: true });
217
253
  fs.writeFileSync(configPath, content);
254
+ return 'deployed' as const;
218
255
  }
219
256
 
220
257
  function deactivateCodexMcp(configPath: string, mcpName: string): void {
@@ -230,25 +267,27 @@ function deactivateCodexMcp(configPath: string, mcpName: string): void {
230
267
  }
231
268
 
232
269
  /**
233
- * Remove a TOML section and its contents.
234
- * Simple approach: find [section.name] header, remove lines until next [section] or EOF.
270
+ * Remove a TOML section and all its subsections.
271
+ * Matches [sectionName] and any [sectionName.*] (e.g. .env, .http_headers).
272
+ * Removes lines until the next unrelated [section] or EOF.
235
273
  */
236
274
  function removeTomlSection(content: string, sectionName: string): string {
237
275
  const lines = content.split('\n');
238
276
  const result: string[] = [];
239
- const header = `[${sectionName}]`;
240
- const envHeader = `[${sectionName}.env]`;
277
+ const prefix = `[${sectionName}`;
241
278
  let skipping = false;
242
279
 
243
280
  for (const line of lines) {
244
281
  const trimmed = line.trim();
245
282
 
246
- if (trimmed === header || trimmed === envHeader) {
283
+ // Match [sectionName] or [sectionName.anything]
284
+ if (trimmed.startsWith(prefix) && (trimmed === `${prefix}]` || trimmed.startsWith(`${prefix}.`))) {
247
285
  skipping = true;
248
286
  continue;
249
287
  }
250
288
 
251
- if (skipping && trimmed.startsWith('[') && trimmed !== header && trimmed !== envHeader) {
289
+ // Stop skipping when we hit a different section header
290
+ if (skipping && trimmed.startsWith('[')) {
252
291
  skipping = false;
253
292
  }
254
293
 
@@ -18,7 +18,7 @@ const PROVIDER_PATHS: Record<ProviderId, ProviderAssetPaths> = {
18
18
  claude: {
19
19
  skills: '.claude/skills',
20
20
  agents: '.claude/agents',
21
- mcpConfig: '.claude/settings.json',
21
+ mcpConfig: '.mcp.json',
22
22
  },
23
23
  agents: {
24
24
  skills: '.agents/skills',
@@ -109,7 +109,7 @@ function scanStoreMcp(storePath: string): StoreAssetInfo[] {
109
109
  description: parsed.description,
110
110
  updated: parsed.updated,
111
111
  },
112
- isValid: !!(parsed.name && parsed.command),
112
+ isValid: !!(parsed.name && (parsed.command || parsed.url)),
113
113
  });
114
114
  }
115
115
  } catch {