Skip to content

Commit dfe1537

Browse files
author
catlog22
committed
feat: Implement Cross-CLI Sync Panel for MCP servers
- Added CrossCliSyncPanel component for synchronizing MCP servers between Claude and Codex. - Implemented server selection, copy operations, and result handling. - Added tests for path mapping on Windows drives. - Created E2E tests for ask_question Answer Broker functionality. - Introduced MCP Tools Test Script for validating modified read_file and edit_file tools. - Updated path_mapper to ensure correct drive formatting on Windows. - Added .gitignore for ace-tool directory.
1 parent b9b2932 commit dfe1537

24 files changed

Lines changed: 1910 additions & 167 deletions

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<div align="center">
2+
new line
3+
new line
24

35
<!-- Animated Header -->
46
<img src="https://capsule-render.vercel.app/api?type=waving&color=gradient&customColorList=6,11,20&height=180&section=header&text=Claude%20Code%20Workflow&fontSize=42&fontColor=fff&animation=twinkling&fontAlignY=32&desc=Multi-Agent%20AI%20Development%20Framework&descAlignY=52&descSize=18"/>

ccw/frontend/src/components/mcp/AllProjectsTable.tsx

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
// ========================================
44
// Table component displaying all recent projects with MCP server statistics
55

6-
import { useState } from 'react';
6+
import { useState, useEffect } from 'react';
77
import { useIntl } from 'react-intl';
88
import { Folder, Clock, Database, ExternalLink } from 'lucide-react';
99
import { Card } from '@/components/ui/Card';
1010
import { Badge } from '@/components/ui/Badge';
1111
import { useProjectOperations } from '@/hooks';
1212
import { cn } from '@/lib/utils';
1313
import { formatDistanceToNow } from 'date-fns';
14+
import { fetchOtherProjectsServers } from '@/lib/api';
1415

1516
// ========== Types ==========
1617

@@ -32,6 +33,8 @@ export interface AllProjectsTableProps {
3233
className?: string;
3334
/** Maximum number of projects to display */
3435
maxProjects?: number;
36+
/** Project paths to display (if not provided, fetches from useProjectOperations) */
37+
projectPaths?: string[];
3538
}
3639

3740
// ========== Component ==========
@@ -41,29 +44,65 @@ export function AllProjectsTable({
4144
onOpenNewWindow,
4245
className,
4346
maxProjects,
47+
projectPaths: propProjectPaths,
4448
}: AllProjectsTableProps) {
4549
const { formatMessage } = useIntl();
4650
const [sortField, setSortField] = useState<'name' | 'serverCount' | 'lastModified'>('lastModified');
4751
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
52+
const [projectStats, setProjectStats] = useState<ProjectServerStats[]>([]);
53+
const [isStatsLoading, setIsStatsLoading] = useState(false);
4854

4955
const { projects, currentProject, isLoading } = useProjectOperations();
5056

51-
// Mock server counts since backend doesn't provide per-project stats
52-
// In production, this would come from a dedicated API endpoint
53-
const projectStats: ProjectServerStats[] = projects.slice(0, maxProjects).map((path) => {
54-
const isCurrent = path === currentProject;
55-
// Extract name from path (last segment)
56-
const name = path.split(/[/\\]/).filter(Boolean).pop() || path;
57+
// Use provided project paths or default to all projects
58+
const targetProjectPaths = propProjectPaths ?? projects;
59+
const displayProjects = maxProjects ? targetProjectPaths.slice(0, maxProjects) : targetProjectPaths;
5760

58-
return {
59-
name,
60-
path,
61-
serverCount: Math.floor(Math.random() * 10), // Mock data
62-
enabledCount: Math.floor(Math.random() * 8), // Mock data
63-
lastModified: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000).toISOString(),
64-
isCurrent,
61+
// Fetch real project server stats on mount
62+
useEffect(() => {
63+
const fetchStats = async () => {
64+
if (displayProjects.length === 0) {
65+
setProjectStats([]);
66+
return;
67+
}
68+
69+
setIsStatsLoading(true);
70+
try {
71+
const response = await fetchOtherProjectsServers(displayProjects);
72+
const stats: ProjectServerStats[] = displayProjects.map((path) => {
73+
const isCurrent = path === currentProject;
74+
const name = path.split(/[/\\]/).filter(Boolean).pop() || path;
75+
const servers = response.servers[path] ?? [];
76+
77+
return {
78+
name,
79+
path,
80+
serverCount: servers.length,
81+
enabledCount: servers.filter((s) => s.enabled).length,
82+
lastModified: undefined, // Backend doesn't provide this yet
83+
isCurrent,
84+
};
85+
});
86+
setProjectStats(stats);
87+
} catch (error) {
88+
console.error('Failed to fetch project server stats:', error);
89+
// Fallback to empty stats on error
90+
setProjectStats(
91+
displayProjects.map((path) => ({
92+
name: path.split(/[/\\]/).filter(Boolean).pop() || path,
93+
path,
94+
serverCount: 0,
95+
enabledCount: 0,
96+
isCurrent: path === currentProject,
97+
}))
98+
);
99+
} finally {
100+
setIsStatsLoading(false);
101+
}
65102
};
66-
});
103+
104+
void fetchStats();
105+
}, [displayProjects, currentProject]);
67106

68107
// Sort projects
69108
const sortedProjects = [...projectStats].sort((a, b) => {
@@ -107,7 +146,7 @@ export function AllProjectsTable({
107146
onOpenNewWindow?.(projectPath);
108147
};
109148

110-
if (isLoading) {
149+
if (isLoading || isStatsLoading) {
111150
return (
112151
<Card className={cn('p-8', className)}>
113152
<div className="flex items-center justify-center">

ccw/frontend/src/components/mcp/CcwToolsMcpCard.tsx

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ import {
3030
installCcwMcp,
3131
uninstallCcwMcp,
3232
updateCcwConfig,
33+
installCcwMcpToCodex,
34+
uninstallCcwMcpFromCodex,
35+
updateCcwConfigForCodex,
3336
} from '@/lib/api';
3437
import { mcpServersKeys } from '@/hooks';
3538
import { useQueryClient } from '@tanstack/react-query';
@@ -77,6 +80,8 @@ export interface CcwToolsMcpCardProps {
7780
onUpdateConfig: (config: Partial<CcwConfig>) => void;
7881
/** Callback when install/uninstall is triggered */
7982
onInstall: () => void;
83+
/** Installation target: Claude or Codex */
84+
target?: 'claude' | 'codex';
8085
}
8186

8287
// ========== Constants ==========
@@ -105,6 +110,7 @@ export function CcwToolsMcpCard({
105110
onToggleTool,
106111
onUpdateConfig,
107112
onInstall,
113+
target = 'claude',
108114
}: CcwToolsMcpCardProps) {
109115
const { formatMessage } = useIntl();
110116
const queryClient = useQueryClient();
@@ -117,22 +123,36 @@ export function CcwToolsMcpCard({
117123
const [isExpanded, setIsExpanded] = useState(false);
118124
const [installScope, setInstallScope] = useState<'global' | 'project'>('global');
119125

126+
const isCodex = target === 'codex';
127+
120128
// Mutations for install/uninstall
121129
const installMutation = useMutation({
122-
mutationFn: (params: { scope: 'global' | 'project'; projectPath?: string }) =>
123-
installCcwMcp(params.scope, params.projectPath),
130+
mutationFn: isCodex
131+
? () => installCcwMcpToCodex()
132+
: (params: { scope: 'global' | 'project'; projectPath?: string }) =>
133+
installCcwMcp(params.scope, params.projectPath),
124134
onSuccess: () => {
125-
queryClient.invalidateQueries({ queryKey: mcpServersKeys.all });
126-
queryClient.invalidateQueries({ queryKey: ['ccwMcpConfig'] });
135+
if (isCodex) {
136+
queryClient.invalidateQueries({ queryKey: ['codexMcpServers'] });
137+
queryClient.invalidateQueries({ queryKey: ['ccwMcpConfigCodex'] });
138+
} else {
139+
queryClient.invalidateQueries({ queryKey: mcpServersKeys.all });
140+
queryClient.invalidateQueries({ queryKey: ['ccwMcpConfig'] });
141+
}
127142
onInstall();
128143
},
129144
});
130145

131146
const uninstallMutation = useMutation({
132-
mutationFn: uninstallCcwMcp,
147+
mutationFn: isCodex ? uninstallCcwMcpFromCodex : uninstallCcwMcp,
133148
onSuccess: () => {
134-
queryClient.invalidateQueries({ queryKey: mcpServersKeys.all });
135-
queryClient.invalidateQueries({ queryKey: ['ccwMcpConfig'] });
149+
if (isCodex) {
150+
queryClient.invalidateQueries({ queryKey: ['codexMcpServers'] });
151+
queryClient.invalidateQueries({ queryKey: ['ccwMcpConfigCodex'] });
152+
} else {
153+
queryClient.invalidateQueries({ queryKey: mcpServersKeys.all });
154+
queryClient.invalidateQueries({ queryKey: ['ccwMcpConfig'] });
155+
}
136156
onInstall();
137157
},
138158
onError: (error) => {
@@ -141,9 +161,13 @@ export function CcwToolsMcpCard({
141161
});
142162

143163
const updateConfigMutation = useMutation({
144-
mutationFn: updateCcwConfig,
164+
mutationFn: isCodex ? updateCcwConfigForCodex : updateCcwConfig,
145165
onSuccess: () => {
146-
queryClient.invalidateQueries({ queryKey: mcpServersKeys.all });
166+
if (isCodex) {
167+
queryClient.invalidateQueries({ queryKey: ['codexMcpServers'] });
168+
} else {
169+
queryClient.invalidateQueries({ queryKey: mcpServersKeys.all });
170+
}
147171
},
148172
});
149173

@@ -170,10 +194,14 @@ export function CcwToolsMcpCard({
170194
};
171195

172196
const handleInstallClick = () => {
173-
installMutation.mutate({
174-
scope: installScope,
175-
projectPath: installScope === 'project' ? currentProjectPath : undefined,
176-
});
197+
if (isCodex) {
198+
(installMutation as any).mutate(undefined);
199+
} else {
200+
(installMutation as any).mutate({
201+
scope: installScope,
202+
projectPath: installScope === 'project' ? currentProjectPath : undefined,
203+
});
204+
}
177205
};
178206

179207
const handleUninstallClick = () => {
@@ -213,6 +241,11 @@ export function CcwToolsMcpCard({
213241
<Badge variant={isInstalled ? 'default' : 'secondary'} className="text-xs">
214242
{isInstalled ? formatMessage({ id: 'mcp.ccw.status.installed' }) : formatMessage({ id: 'mcp.ccw.status.notInstalled' })}
215243
</Badge>
244+
{isCodex && (
245+
<Badge variant="outline" className="text-xs text-blue-500">
246+
Codex
247+
</Badge>
248+
)}
216249
{isInstalled && (
217250
<Badge variant="outline" className="text-xs text-info">
218251
{formatMessage({ id: 'mcp.ccw.status.special' })}
@@ -388,8 +421,8 @@ export function CcwToolsMcpCard({
388421

389422
{/* Install/Uninstall Button */}
390423
<div className="pt-3 border-t border-border space-y-3">
391-
{/* Scope Selection */}
392-
{!isInstalled && (
424+
{/* Scope Selection - Claude only (Codex is always global) */}
425+
{!isInstalled && !isCodex && (
393426
<div className="space-y-2">
394427
<p className="text-xs font-medium text-muted-foreground uppercase">
395428
{formatMessage({ id: 'mcp.scope' })}
@@ -422,6 +455,12 @@ export function CcwToolsMcpCard({
422455
</div>
423456
</div>
424457
)}
458+
{/* Codex note */}
459+
{isCodex && !isInstalled && (
460+
<p className="text-xs text-muted-foreground">
461+
{formatMessage({ id: 'mcp.ccw.codexNote' })}
462+
</p>
463+
)}
425464
{!isInstalled ? (
426465
<Button
427466
onClick={handleInstallClick}
@@ -430,7 +469,7 @@ export function CcwToolsMcpCard({
430469
>
431470
{isPending
432471
? formatMessage({ id: 'mcp.ccw.actions.installing' })
433-
: formatMessage({ id: 'mcp.ccw.actions.install' })
472+
: formatMessage({ id: isCodex ? 'mcp.ccw.actions.installCodex' : 'mcp.ccw.actions.install' })
434473
}
435474
</Button>
436475
) : (

0 commit comments

Comments
 (0)