@slycode/slycode 0.2.18 → 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.
- package/bin/sly-kanban.js +3 -3
- package/dist/data/scaffold-templates/mcp.json +1 -6
- package/dist/scripts/kanban.js +9 -3
- package/dist/web/.next/BUILD_ID +1 -1
- package/dist/web/.next/build-manifest.json +2 -2
- package/dist/web/.next/server/app/_global-error.html +2 -2
- package/dist/web/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/web/.next/server/app/_not-found.html +1 -1
- package/dist/web/.next/server/app/_not-found.rsc +2 -2
- package/dist/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/dist/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/dist/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/dist/web/.next/server/app/api/cli-assets/sync/route.js +3 -2
- package/dist/web/.next/server/app/api/cli-assets/sync/route.js.nft.json +1 -1
- package/dist/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/.next/server/app/project/[id]/page_client-reference-manifest.js +1 -1
- package/dist/web/.next/server/chunks/[root-of-the-server]__12f6cd6f._.js +1 -1
- package/dist/web/.next/server/chunks/[root-of-the-server]__15fc9266._.js +1 -1
- package/dist/web/.next/server/chunks/[root-of-the-server]__279e9bf3._.js +1 -1
- package/dist/web/.next/server/chunks/[root-of-the-server]__2d1f0ed9._.js +1 -1
- package/dist/web/.next/server/chunks/[root-of-the-server]__3b9d3e43._.js +42 -23
- package/dist/web/.next/server/chunks/[root-of-the-server]__47dd878e._.js +1 -1
- package/dist/web/.next/server/chunks/[root-of-the-server]__5b8c9374._.js +3 -0
- package/dist/web/.next/server/chunks/[root-of-the-server]__5e08b942._.js +1 -1
- package/dist/web/.next/server/chunks/[root-of-the-server]__71bb3374._.js +1 -1
- package/dist/web/.next/server/chunks/[root-of-the-server]__7603305e._.js +1 -1
- package/dist/web/.next/server/chunks/[root-of-the-server]__7c476ad6._.js +1 -1
- package/dist/web/.next/server/chunks/[root-of-the-server]__846ca56f._.js +1 -1
- package/dist/web/.next/server/chunks/[root-of-the-server]__98d88050._.js +1 -1
- package/dist/web/.next/server/chunks/[root-of-the-server]__b273cc05._.js +1 -1
- package/dist/web/.next/server/chunks/[root-of-the-server]__d6362272._.js +1 -1
- package/dist/web/.next/server/chunks/[root-of-the-server]__de1277ee._.js +1 -1
- package/dist/web/.next/server/chunks/[root-of-the-server]__f3e501b6._.js +1 -1
- package/dist/web/.next/server/chunks/[root-of-the-server]__f97e93fa._.js +2 -2
- package/dist/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_234fda9c.js +3 -0
- package/dist/web/.next/server/chunks/src_lib_asset-scanner_ts_c599fd1c._.js +1 -1
- package/dist/web/.next/server/chunks/ssr/src_components_Dashboard_tsx_efc4dc27._.js +1 -1
- package/dist/web/.next/server/chunks/ssr/src_lib_registry_ts_2fc87c9c._.js +1 -1
- package/dist/web/.next/server/pages/404.html +1 -1
- package/dist/web/.next/server/pages/500.html +2 -2
- package/dist/web/.next/static/chunks/747f5e5f9dcf2621.css +1 -0
- package/dist/web/.next/static/chunks/f55f3c8c1a52f80c.js +1 -0
- package/dist/web/src/app/api/cli-assets/assistant/route.ts +30 -13
- package/dist/web/src/app/api/cli-assets/store/route.ts +5 -3
- package/dist/web/src/app/api/cli-assets/sync/route.ts +31 -1
- package/dist/web/src/app/api/file/route.ts +2 -1
- package/dist/web/src/components/AssetViewer.tsx +99 -0
- package/dist/web/src/components/CliAssetsTab.tsx +2 -2
- package/dist/web/src/components/StoreView.tsx +168 -61
- package/dist/web/src/lib/asset-scanner.ts +1 -1
- package/dist/web/src/lib/mcp-common.ts +76 -37
- package/dist/web/src/lib/provider-paths.ts +1 -1
- package/dist/web/src/lib/store-scanner.ts +1 -1
- package/dist/web/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/templates/kanban-seed.json +1 -1
- package/dist/web/.next/server/chunks/[root-of-the-server]__3c5ef8ec._.js +0 -3
- package/dist/web/.next/static/chunks/2744d72103f49934.css +0 -1
- package/dist/web/.next/static/chunks/e8b318caa49fce00.js +0 -1
- /package/dist/web/.next/static/{O_wqVAO7cnu-Tz0pWsDsk → qMss0q1Ox38k4U6oxip2H}/_buildManifest.js +0 -0
- /package/dist/web/.next/static/{O_wqVAO7cnu-Tz0pWsDsk → qMss0q1Ox38k4U6oxip2H}/_clientMiddlewareManifest.json +0 -0
- /package/dist/web/.next/static/{O_wqVAO7cnu-Tz0pWsDsk → 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 [
|
|
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'
|
|
52
|
-
{ key: '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, '.
|
|
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
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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.
|
|
96
|
-
|
|
97
|
-
lines.push(`
|
|
98
|
-
|
|
99
|
-
lines.push(
|
|
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
|
-
):
|
|
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
|
-
):
|
|
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):
|
|
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
|
-
//
|
|
210
|
-
|
|
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
|
|
234
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
289
|
+
// Stop skipping when we hit a different section header
|
|
290
|
+
if (skipping && trimmed.startsWith('[')) {
|
|
252
291
|
skipping = false;
|
|
253
292
|
}
|
|
254
293
|
|
|
@@ -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 {
|