Skip to content

Commit f6c7c14

Browse files
author
catlog22
committed
ccw: fix view JSON errors and WSL MCP install
1 parent dc1dc87 commit f6c7c14

6 files changed

Lines changed: 254 additions & 183 deletions

File tree

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,9 @@ export function CrossCliSyncPanel({ onSuccess, className }: CrossCliSyncPanelPro
324324
)}
325325
</div>
326326
<p className="text-xs text-muted-foreground font-mono truncate">
327-
{server.displayText}
327+
{isHttpMcpServer(server)
328+
? server.url
329+
: (isStdioMcpServer(server) ? server.command : '')}
328330
</p>
329331
</label>
330332
</div>

ccw/frontend/src/lib/api.mcp.test.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
crossCliCopy,
1010
fetchAllProjects,
1111
fetchOtherProjectsServers,
12+
isStdioMcpServer,
1213
type McpServer,
1314
} from './api';
1415

@@ -64,11 +65,14 @@ describe('MCP API (frontend ↔ backend contract)', () => {
6465
expect(global1?.scope).toBe('global');
6566

6667
const projOnly = result.project[0];
67-
expect(projOnly?.command).toBe('node');
68+
expect(isStdioMcpServer(projOnly)).toBe(true);
69+
if (isStdioMcpServer(projOnly)) {
70+
expect(projOnly.command).toBe('node');
71+
expect(projOnly.env).toEqual({ A: '1' });
72+
expect(projOnly.args).toEqual(['x']);
73+
}
6874
expect(projOnly?.enabled).toBe(true);
6975
expect(projOnly?.scope).toBe('project');
70-
expect(projOnly?.env).toEqual({ A: '1' });
71-
expect(projOnly?.args).toEqual(['x']);
7276
});
7377

7478
it('toggleMcpServer uses /api/mcp-toggle with { projectPath, serverName, enable }', async () => {
@@ -150,6 +154,7 @@ describe('MCP API (frontend ↔ backend contract)', () => {
150154

151155
const inputServer: McpServer = {
152156
name: 's1',
157+
transport: 'stdio',
153158
command: 'node',
154159
args: ['a'],
155160
env: { K: 'V' },
@@ -290,4 +295,3 @@ describe('MCP API (frontend ↔ backend contract)', () => {
290295
expect(res.servers['D:/a']?.[0]?.enabled).toBe(false);
291296
});
292297
});
293-

ccw/frontend/src/lib/api.ts

Lines changed: 99 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -3507,7 +3507,8 @@ function requireProjectPath(projectPath: string | undefined, ctx: string): strin
35073507
function toServerConfig(server: Partial<McpServer>): UnknownRecord {
35083508
// Check if this is an HTTP server
35093509
if (server.transport === 'http') {
3510-
const config: UnknownRecord = { url: server.url };
3510+
const url = 'url' in server && typeof server.url === 'string' ? server.url : '';
3511+
const config: UnknownRecord = { url };
35113512

35123513
// Claude format: type field
35133514
config.type = 'http';
@@ -3538,23 +3539,60 @@ function toServerConfig(server: Partial<McpServer>): UnknownRecord {
35383539
// STDIO server (default)
35393540
const config: UnknownRecord = {};
35403541

3541-
if (typeof server.command === 'string') {
3542-
config.command = server.command;
3543-
}
3542+
if ('command' in server && typeof server.command === 'string') config.command = server.command;
3543+
if ('args' in server && Array.isArray(server.args) && server.args.length > 0) config.args = server.args;
3544+
if ('env' in server && server.env && Object.keys(server.env).length > 0) config.env = server.env;
3545+
if ('cwd' in server && typeof server.cwd === 'string' && server.cwd.trim()) config.cwd = server.cwd;
35443546

3545-
if (server.args && server.args.length > 0) {
3546-
config.args = server.args;
3547-
}
3547+
return config;
3548+
}
35483549

3549-
if (server.env && Object.keys(server.env).length > 0) {
3550-
config.env = server.env;
3551-
}
3550+
function _buildFallbackServer(serverName: string, config: Partial<McpServer>): McpServer {
3551+
const transport = config.transport ?? 'stdio';
3552+
const enabled = config.enabled ?? true;
3553+
const scope = config.scope ?? 'project';
35523554

3553-
if (server.cwd) {
3554-
config.cwd = server.cwd;
3555+
if (transport === 'http') {
3556+
const url = 'url' in config && typeof config.url === 'string' ? config.url : '';
3557+
return {
3558+
name: serverName,
3559+
transport: 'http',
3560+
url,
3561+
enabled,
3562+
scope,
3563+
};
35553564
}
35563565

3557-
return config;
3566+
const command =
3567+
'command' in config && typeof config.command === 'string'
3568+
? config.command
3569+
: '';
3570+
3571+
const args =
3572+
'args' in config && Array.isArray(config.args)
3573+
? config.args
3574+
: undefined;
3575+
3576+
const env =
3577+
'env' in config && config.env && typeof config.env === 'object'
3578+
? (config.env as Record<string, string>)
3579+
: undefined;
3580+
3581+
const cwd =
3582+
'cwd' in config && typeof config.cwd === 'string'
3583+
? config.cwd
3584+
: undefined;
3585+
3586+
return {
3587+
name: serverName,
3588+
transport: 'stdio',
3589+
command,
3590+
args,
3591+
env,
3592+
cwd,
3593+
enabled,
3594+
scope,
3595+
};
35583596
}
35593597

35603598
/**
@@ -3572,12 +3610,14 @@ export async function updateMcpServer(
35723610

35733611
// Validate based on transport type
35743612
if (config.transport === 'http') {
3575-
if (typeof config.url !== 'string' || !config.url.trim()) {
3613+
const url = 'url' in config ? config.url : undefined;
3614+
if (typeof url !== 'string' || !url.trim()) {
35763615
throw new Error('updateMcpServer: url is required for HTTP servers');
35773616
}
35783617
} else {
35793618
// STDIO server (default)
3580-
if (typeof config.command !== 'string' || !config.command.trim()) {
3619+
const command = 'command' in config ? config.command : undefined;
3620+
if (typeof command !== 'string' || !command.trim()) {
35813621
throw new Error('updateMcpServer: command is required for STDIO servers');
35823622
}
35833623
}
@@ -3619,26 +3659,13 @@ export async function updateMcpServer(
36193659

36203660
if (options.projectPath) {
36213661
const servers = await fetchMcpServers(options.projectPath);
3622-
return [...servers.project, ...servers.global].find((s) => s.name === serverName) ?? {
3623-
name: serverName,
3624-
transport: config.transport ?? 'stdio',
3625-
...(config.transport === 'http' ? { url: config.url! } : { command: config.command! }),
3626-
args: config.args,
3627-
env: config.env,
3628-
enabled: config.enabled ?? true,
3629-
scope: config.scope,
3630-
} as McpServer;
3662+
return (
3663+
[...servers.project, ...servers.global].find((s) => s.name === serverName) ??
3664+
_buildFallbackServer(serverName, config)
3665+
);
36313666
}
36323667

3633-
return {
3634-
name: serverName,
3635-
transport: config.transport ?? 'stdio',
3636-
...(config.transport === 'http' ? { url: config.url! } : { command: config.command! }),
3637-
args: config.args,
3638-
env: config.env,
3639-
enabled: config.enabled ?? true,
3640-
scope: config.scope,
3641-
} as McpServer;
3668+
return _buildFallbackServer(serverName, config);
36423669
}
36433670

36443671
/**
@@ -3756,22 +3783,23 @@ export async function toggleMcpServer(
37563783
}
37573784

37583785
const servers = await fetchMcpServers(projectPath);
3759-
return [...servers.project, ...servers.global].find((s) => s.name === serverName) ?? {
3760-
name: serverName,
3761-
command: '',
3762-
enabled,
3763-
scope: 'project',
3764-
};
3786+
return (
3787+
[...servers.project, ...servers.global].find((s) => s.name === serverName) ?? {
3788+
name: serverName,
3789+
transport: 'stdio',
3790+
command: '',
3791+
enabled,
3792+
scope: 'project',
3793+
}
3794+
);
37653795
}
37663796

37673797
// ========== Codex MCP API ==========
37683798
/**
37693799
* Codex MCP Server - Read-only server with config path
37703800
* Extends McpServer with optional configPath field
37713801
*/
3772-
export interface CodexMcpServer extends McpServer {
3773-
configPath?: string;
3774-
}
3802+
export type CodexMcpServer = McpServer & { configPath?: string };
37753803

37763804
export interface CodexMcpServersResponse {
37773805
servers: CodexMcpServer[];
@@ -3958,13 +3986,16 @@ export async function fetchOtherProjectsServers(
39583986
servers[path] = Object.entries(projectServersRecord)
39593987
// Exclude globally-defined servers; this section is meant for project-local discovery
39603988
.filter(([name]) => !(name in userServers) && !(name in enterpriseServers))
3961-
.map(([name, raw]) => {
3989+
.flatMap(([name, raw]) => {
39623990
const normalized = normalizeServerConfig(raw);
3963-
return {
3991+
if (normalized.transport !== 'stdio') return [];
3992+
return [{
39643993
name,
3965-
...normalized,
3994+
command: normalized.command,
3995+
args: normalized.args,
3996+
env: normalized.env,
39663997
enabled: !disabledSet.has(name),
3967-
};
3998+
}];
39683999
});
39694000
}
39704001

@@ -4552,58 +4583,6 @@ export interface CcwMcpConfig {
45524583
installedScopes: ('global' | 'project')[];
45534584
}
45544585

4555-
/**
4556-
* Platform detection for cross-platform MCP config
4557-
*/
4558-
const isWindows = typeof navigator !== 'undefined' && navigator.platform?.toLowerCase().includes('win');
4559-
4560-
/**
4561-
* Build CCW MCP server config
4562-
*/
4563-
function buildCcwMcpServerConfig(config: {
4564-
enabledTools?: string[];
4565-
projectRoot?: string;
4566-
allowedDirs?: string;
4567-
enableSandbox?: boolean;
4568-
}): { command: string; args: string[]; env: Record<string, string> } {
4569-
const env: Record<string, string> = {};
4570-
4571-
// Only use default when enabledTools is undefined (not provided)
4572-
// When enabledTools is an empty array, set to empty string to disable all tools
4573-
console.log('[buildCcwMcpServerConfig] config.enabledTools:', config.enabledTools);
4574-
if (config.enabledTools !== undefined) {
4575-
env.CCW_ENABLED_TOOLS = config.enabledTools.join(',');
4576-
console.log('[buildCcwMcpServerConfig] Set CCW_ENABLED_TOOLS to:', env.CCW_ENABLED_TOOLS);
4577-
} else {
4578-
env.CCW_ENABLED_TOOLS = 'write_file,edit_file,read_file,core_memory,ask_question,smart_search';
4579-
console.log('[buildCcwMcpServerConfig] Using default CCW_ENABLED_TOOLS');
4580-
}
4581-
4582-
if (config.projectRoot) {
4583-
env.CCW_PROJECT_ROOT = config.projectRoot;
4584-
}
4585-
if (config.allowedDirs) {
4586-
env.CCW_ALLOWED_DIRS = config.allowedDirs;
4587-
}
4588-
if (config.enableSandbox) {
4589-
env.CCW_ENABLE_SANDBOX = '1';
4590-
}
4591-
4592-
// Cross-platform config
4593-
if (isWindows) {
4594-
return {
4595-
command: 'cmd',
4596-
args: ['/c', 'npx', '-y', 'ccw-mcp'],
4597-
env
4598-
};
4599-
}
4600-
return {
4601-
command: 'npx',
4602-
args: ['-y', 'ccw-mcp'],
4603-
env
4604-
};
4605-
}
4606-
46074586
/**
46084587
* Fetch CCW Tools MCP configuration by checking if ccw-tools server exists
46094588
*/
@@ -4698,13 +4677,14 @@ export async function updateCcwConfig(config: {
46984677
allowedDirs?: string;
46994678
enableSandbox?: boolean;
47004679
}): Promise<CcwMcpConfig> {
4701-
const serverConfig = buildCcwMcpServerConfig(config);
4702-
4703-
// Install/update to global config
4704-
const result = await addGlobalMcpServer('ccw-tools', serverConfig);
4705-
if (!result.success) {
4706-
throw new Error(result.error || 'Failed to update CCW config');
4707-
}
4680+
const result = await fetchApi<{ success?: boolean; error?: string }>('/api/mcp-install-ccw', {
4681+
method: 'POST',
4682+
body: JSON.stringify({
4683+
scope: 'global',
4684+
env: config,
4685+
}),
4686+
});
4687+
if (result?.error) throw new Error(result.error || 'Failed to update CCW config');
47084688

47094689
return fetchCcwMcpConfig();
47104690
}
@@ -4716,31 +4696,21 @@ export async function installCcwMcp(
47164696
scope: 'global' | 'project' = 'global',
47174697
projectPath?: string
47184698
): Promise<CcwMcpConfig> {
4719-
const serverConfig = buildCcwMcpServerConfig({
4720-
enabledTools: ['write_file', 'edit_file', 'read_file', 'core_memory', 'ask_question', 'smart_search'],
4721-
});
4699+
const path = scope === 'project' ? requireProjectPath(projectPath, 'installCcwMcp') : undefined;
47224700

4723-
if (scope === 'project' && projectPath) {
4724-
const result = await fetchApi<{ success?: boolean; error?: string }>('/api/mcp-copy-server', {
4725-
method: 'POST',
4726-
body: JSON.stringify({
4727-
projectPath,
4728-
serverName: 'ccw-tools',
4729-
serverConfig,
4730-
configType: 'mcp',
4731-
}),
4732-
});
4733-
if (result?.error) {
4734-
throw new Error(result.error || 'Failed to install CCW MCP to project');
4735-
}
4736-
} else {
4737-
const result = await addGlobalMcpServer('ccw-tools', serverConfig);
4738-
if (!result.success) {
4739-
throw new Error(result.error || 'Failed to install CCW MCP');
4740-
}
4741-
}
4701+
const result = await fetchApi<{ success?: boolean; error?: string }>('/api/mcp-install-ccw', {
4702+
method: 'POST',
4703+
body: JSON.stringify({
4704+
scope,
4705+
projectPath: path,
4706+
env: {
4707+
enabledTools: ['write_file', 'edit_file', 'read_file', 'core_memory', 'ask_question', 'smart_search'],
4708+
},
4709+
}),
4710+
});
4711+
if (result?.error) throw new Error(result.error || `Failed to install CCW MCP (${scope})`);
47424712

4743-
return fetchCcwMcpConfig();
4713+
return fetchCcwMcpConfig(path);
47444714
}
47454715

47464716
/**
@@ -4811,7 +4781,7 @@ export async function fetchCcwMcpConfigForCodex(): Promise<CcwMcpConfig> {
48114781
return { isInstalled: false, enabledTools: [], installedScopes: [] };
48124782
}
48134783

4814-
const env = ccwServer.env || {};
4784+
const env = isStdioMcpServer(ccwServer) ? (ccwServer.env || {}) : {};
48154785
// Note: CCW_ENABLED_TOOLS can be empty string (all tools disabled), 'all' (default set), or comma-separated list
48164786
const enabledToolsStr = env.CCW_ENABLED_TOOLS;
48174787
let enabledTools: string[];

0 commit comments

Comments
 (0)