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.
This commit is contained in:
catlog22
2026-02-08 23:19:19 +08:00
parent b9b2932f50
commit dfe153778c
24 changed files with 1911 additions and 168 deletions

View File

@@ -33,7 +33,7 @@ import { CcwToolsMcpCard } from '@/components/mcp/CcwToolsMcpCard';
import { McpTemplatesSection, TemplateSaveDialog } from '@/components/mcp/McpTemplatesSection';
import { RecommendedMcpSection } from '@/components/mcp/RecommendedMcpSection';
import { WindowsCompatibilityWarning } from '@/components/mcp/WindowsCompatibilityWarning';
import { CrossCliCopyButton } from '@/components/mcp/CrossCliCopyButton';
import { CrossCliSyncPanel } from '@/components/mcp/CrossCliSyncPanel';
import { AllProjectsTable } from '@/components/mcp/AllProjectsTable';
import { OtherProjectsSection } from '@/components/mcp/OtherProjectsSection';
import { TabsNavigation } from '@/components/ui/TabsNavigation';
@@ -41,7 +41,9 @@ import { useMcpServers, useMcpServerMutations, useNotifications } from '@/hooks'
import {
fetchCodexMcpServers,
fetchCcwMcpConfig,
fetchCcwMcpConfigForCodex,
updateCcwConfig,
updateCcwConfigForCodex,
codexRemoveServer,
codexToggleServer,
saveMcpTemplate,
@@ -255,6 +257,14 @@ export function McpManagerPage() {
staleTime: 5 * 60 * 1000, // 5 minutes
});
// Fetch CCW Tools MCP configuration (Codex mode only)
const ccwMcpCodexQuery = useQuery({
queryKey: ['ccwMcpConfigCodex'],
queryFn: fetchCcwMcpConfigForCodex,
enabled: cliMode === 'codex',
staleTime: 5 * 60 * 1000, // 5 minutes
});
const {
toggleServer,
deleteServer,
@@ -358,6 +368,32 @@ export function McpManagerPage() {
ccwMcpQuery.refetch();
};
// CCW MCP handlers for Codex mode
const ccwCodexConfig = ccwMcpCodexQuery.data ?? {
isInstalled: false,
enabledTools: [],
projectRoot: undefined,
allowedDirs: undefined,
disableSandbox: undefined,
};
const handleToggleCcwToolCodex = async (tool: string, enabled: boolean) => {
const updatedTools = enabled
? [...ccwCodexConfig.enabledTools, tool]
: ccwCodexConfig.enabledTools.filter((t) => t !== tool);
await updateCcwConfigForCodex({ enabledTools: updatedTools });
ccwMcpCodexQuery.refetch();
};
const handleUpdateCcwConfigCodex = async (config: Partial<CcwMcpConfig>) => {
await updateCcwConfigForCodex(config);
ccwMcpCodexQuery.refetch();
};
const handleCcwInstallCodex = () => {
ccwMcpCodexQuery.refetch();
};
// Template handlers
const handleInstallTemplate = (template: any) => {
setEditingServer({
@@ -617,7 +653,7 @@ export function McpManagerPage() {
</div>
)}
{/* CCW Tools MCP Card - Claude mode only */}
{/* CCW Tools MCP Card */}
{cliMode === 'claude' && (
<CcwToolsMcpCard
isInstalled={ccwConfig.isInstalled}
@@ -630,6 +666,19 @@ export function McpManagerPage() {
onInstall={handleCcwInstall}
/>
)}
{cliMode === 'codex' && (
<CcwToolsMcpCard
target="codex"
isInstalled={ccwCodexConfig.isInstalled}
enabledTools={ccwCodexConfig.enabledTools}
projectRoot={ccwCodexConfig.projectRoot}
allowedDirs={ccwCodexConfig.allowedDirs}
disableSandbox={ccwCodexConfig.disableSandbox}
onToggleTool={handleToggleCcwToolCodex}
onUpdateConfig={handleUpdateCcwConfigCodex}
onInstall={handleCcwInstallCodex}
/>
)}
{/* Servers List */}
{currentIsLoading ? (
@@ -680,37 +729,56 @@ export function McpManagerPage() {
{/* Tab Content: Cross-CLI */}
{activeTab === 'cross-cli' && (
<div className="mt-4 space-y-4">
{/* Cross-CLI Copy Button */}
<div className="flex items-center justify-between p-4 bg-muted rounded-lg">
<div className="flex-1">
<div className="mt-4 space-y-6">
{/* Section 1: Claude ↔ Codex 同步 */}
<section>
<div className="flex items-center gap-2 mb-3">
<RefreshCw className="w-4 h-4 text-muted-foreground" />
<h3 className="text-sm font-medium text-foreground">
{formatMessage({ id: 'mcp.crossCli.title' })}
{formatMessage({ id: 'mcp.sync.title' })}
</h3>
<p className="text-xs text-muted-foreground mt-1">
{formatMessage({ id: 'mcp.crossCli.selectServersHint' })}
</p>
</div>
<CrossCliCopyButton
currentMode={cliMode}
onSuccess={() => refetch()}
<Card className="p-4">
<CrossCliSyncPanel onSuccess={(count, direction) => refetch()} />
</Card>
</section>
{/* Section 2: 项目概览 */}
<section>
<div className="flex items-center gap-2 mb-3">
<Folder className="w-4 h-4 text-muted-foreground" />
<h3 className="text-sm font-medium text-foreground">
{formatMessage({ id: 'mcp.projects.title' })}
</h3>
</div>
<p className="text-xs text-muted-foreground mb-3">
{formatMessage({ id: 'mcp.projects.description' })}
</p>
<AllProjectsTable
maxProjects={10}
onProjectClick={(path) => console.log('Open project:', path)}
onOpenNewWindow={(path) => window.open(`/?project=${encodeURIComponent(path)}`, '_blank')}
/>
</div>
</section>
{/* All Projects Table */}
<AllProjectsTable
maxProjects={10}
onProjectClick={(path) => console.log('Open project:', path)}
onOpenNewWindow={(path) => window.open(`/?project=${encodeURIComponent(path)}`, '_blank')}
/>
{/* Other Projects Section */}
<OtherProjectsSection
onImportSuccess={(serverName, sourceProject) => {
console.log('Imported server:', serverName, 'from:', sourceProject);
refetch();
}}
/>
{/* Section 3: 跨项目导入 */}
<section>
<div className="flex items-center gap-2 mb-3">
<Globe className="w-4 h-4 text-muted-foreground" />
<h3 className="text-sm font-medium text-foreground">
{formatMessage({ id: 'mcp.crossProject.title' })}
</h3>
</div>
<p className="text-xs text-muted-foreground mb-3">
{formatMessage({ id: 'mcp.crossProject.description' })}
</p>
<OtherProjectsSection
onImportSuccess={(serverName, sourceProject) => {
console.log('Imported server:', serverName, 'from:', sourceProject);
refetch();
}}
/>
</section>
</div>
)}