mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-11 17:21:03 +08:00
ccw: fix view JSON errors and WSL MCP install
This commit is contained in:
@@ -22,42 +22,81 @@ interface SwitchWorkspaceResult {
|
||||
* Provides better error messages when response is not JSON (e.g., proxy errors)
|
||||
*/
|
||||
async function safeParseJson<T>(response: Response, endpoint: string): Promise<T> {
|
||||
const contentType = response.headers.get('content-type');
|
||||
const contentType = response.headers.get('content-type') || 'unknown';
|
||||
const isJson = contentType.includes('application/json');
|
||||
|
||||
// Check if response is JSON
|
||||
if (!contentType?.includes('application/json')) {
|
||||
// Get response text for error message (truncated to avoid huge output)
|
||||
const truncate = (text: string, maxLen: number = 200): string => {
|
||||
const trimmed = text.trim();
|
||||
if (trimmed.length <= maxLen) return trimmed;
|
||||
return `${trimmed.slice(0, maxLen)}…`;
|
||||
};
|
||||
|
||||
const isApiKeyProxyMessage = (text: string): boolean => {
|
||||
return /APIKEY|api\s*key|apiKey/i.test(text);
|
||||
};
|
||||
|
||||
// If response claims to not be JSON, surface a helpful error with a preview.
|
||||
if (!isJson) {
|
||||
const text = await response.text();
|
||||
const preview = text.substring(0, 200);
|
||||
const preview = truncate(text);
|
||||
|
||||
// Detect common proxy errors
|
||||
if (text.includes('APIKEY') || text.includes('api key') || text.includes('apiKey')) {
|
||||
if (isApiKeyProxyMessage(text)) {
|
||||
throw new Error(
|
||||
`Request to ${endpoint} was intercepted by a proxy requiring API key. ` +
|
||||
`Request to ${endpoint} was intercepted by a proxy requiring an API key. ` +
|
||||
`Check HTTP_PROXY/HTTPS_PROXY environment variables. ` +
|
||||
`Response: ${preview}`
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unexpected response from ${endpoint} (expected JSON, got: ${contentType || 'unknown'}). ` +
|
||||
`Unexpected response from ${endpoint} (expected JSON, got: ${contentType}). ` +
|
||||
`This may indicate a proxy or network issue. Response: ${preview}`
|
||||
);
|
||||
}
|
||||
|
||||
// Check for HTTP errors
|
||||
// Read text once so we can provide good errors even when JSON parsing fails.
|
||||
const text = await response.text();
|
||||
const preview = truncate(text);
|
||||
|
||||
// Check for HTTP errors first; try to parse error JSON if possible.
|
||||
if (!response.ok) {
|
||||
let errorMessage = response.statusText;
|
||||
try {
|
||||
const body = await response.json() as { error?: string; message?: string };
|
||||
errorMessage = body.error || body.message || response.statusText;
|
||||
} catch {
|
||||
// Ignore JSON parse errors for error response
|
||||
if (isApiKeyProxyMessage(text)) {
|
||||
throw new Error(
|
||||
`Request to ${endpoint} was intercepted by a proxy requiring an API key. ` +
|
||||
`Check HTTP_PROXY/HTTPS_PROXY environment variables. ` +
|
||||
`Response: ${preview}`
|
||||
);
|
||||
}
|
||||
throw new Error(`HTTP ${response.status}: ${errorMessage}`);
|
||||
|
||||
let errorMessage = response.statusText;
|
||||
if (text.trim()) {
|
||||
try {
|
||||
const body = JSON.parse(text) as { error?: string; message?: string };
|
||||
errorMessage = body.error || body.message || response.statusText;
|
||||
} catch {
|
||||
errorMessage = `${response.statusText} (invalid JSON body)`;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`HTTP ${response.status}: ${errorMessage}${preview ? `. Response: ${preview}` : ''}`);
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>;
|
||||
try {
|
||||
return JSON.parse(text) as T;
|
||||
} catch {
|
||||
if (isApiKeyProxyMessage(text)) {
|
||||
throw new Error(
|
||||
`Request to ${endpoint} was intercepted by a proxy requiring an API key. ` +
|
||||
`Check HTTP_PROXY/HTTPS_PROXY environment variables. ` +
|
||||
`Response: ${preview}`
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unexpected response from ${endpoint} (invalid JSON despite content-type: ${contentType}). ` +
|
||||
`This may indicate a proxy or network issue. Response: ${preview}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1210,40 +1210,68 @@ export async function handleMcpRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Install CCW MCP server to project
|
||||
// API: Install CCW MCP server (global or project scope)
|
||||
if (pathname === '/api/mcp-install-ccw' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
if (!isRecord(body)) {
|
||||
return { error: 'Invalid request body', status: 400 };
|
||||
}
|
||||
|
||||
const projectPath = body.projectPath;
|
||||
if (typeof projectPath !== 'string' || !projectPath.trim()) {
|
||||
return { error: 'projectPath is required', status: 400 };
|
||||
const rawScope = body.scope;
|
||||
const scope = rawScope === 'global' || rawScope === 'project' ? rawScope : undefined;
|
||||
|
||||
// Backward compatibility: allow legacy fields at top-level as well as body.env
|
||||
const envInput = (isRecord(body.env) ? body.env : body) as Record<string, unknown>;
|
||||
|
||||
const projectPath = typeof body.projectPath === 'string' ? body.projectPath : undefined;
|
||||
|
||||
const enableSandbox = envInput.enableSandbox === true;
|
||||
|
||||
const enabledToolsRaw = envInput.enabledTools;
|
||||
let enabledToolsEnv: string;
|
||||
if (enabledToolsRaw === undefined || enabledToolsRaw === null) {
|
||||
enabledToolsEnv = 'write_file,edit_file,read_file,core_memory,ask_question,smart_search';
|
||||
} else if (Array.isArray(enabledToolsRaw)) {
|
||||
enabledToolsEnv = enabledToolsRaw.filter((t): t is string => typeof t === 'string').join(',');
|
||||
} else if (typeof enabledToolsRaw === 'string') {
|
||||
enabledToolsEnv = enabledToolsRaw;
|
||||
} else {
|
||||
enabledToolsEnv = 'write_file,edit_file,read_file,core_memory,ask_question,smart_search';
|
||||
}
|
||||
|
||||
// Check if sandbox should be enabled
|
||||
const enableSandbox = body.enableSandbox === true;
|
||||
const projectRoot = typeof envInput.projectRoot === 'string' ? envInput.projectRoot : undefined;
|
||||
|
||||
// Parse enabled tools from request body
|
||||
const enabledTools = Array.isArray(body.enabledTools) && body.enabledTools.length > 0
|
||||
? (body.enabledTools as string[]).join(',')
|
||||
: 'write_file,edit_file,read_file,core_memory,ask_question,smart_search';
|
||||
const allowedDirsRaw = envInput.allowedDirs;
|
||||
let allowedDirsEnv: string | undefined;
|
||||
if (Array.isArray(allowedDirsRaw)) {
|
||||
allowedDirsEnv = allowedDirsRaw.filter((d): d is string => typeof d === 'string').join(',');
|
||||
} else if (typeof allowedDirsRaw === 'string') {
|
||||
allowedDirsEnv = allowedDirsRaw;
|
||||
}
|
||||
|
||||
// Generate CCW MCP server config
|
||||
// Use cmd /c on Windows to inherit Claude Code's working directory
|
||||
// Generate CCW MCP server config using *server-side* platform detection.
|
||||
// On WSL/Linux, this ensures we produce `npx -y ccw-mcp` even when the browser runs on Windows.
|
||||
const isWin = process.platform === 'win32';
|
||||
const env: Record<string, string> = { CCW_ENABLED_TOOLS: enabledToolsEnv };
|
||||
if (projectRoot) env.CCW_PROJECT_ROOT = projectRoot;
|
||||
if (allowedDirsEnv) env.CCW_ALLOWED_DIRS = allowedDirsEnv;
|
||||
if (enableSandbox) env.CCW_ENABLE_SANDBOX = '1';
|
||||
|
||||
const ccwMcpConfig: Record<string, any> = {
|
||||
command: isWin ? "cmd" : "npx",
|
||||
args: isWin ? ["/c", "npx", "-y", "ccw-mcp"] : ["-y", "ccw-mcp"],
|
||||
env: {
|
||||
CCW_ENABLED_TOOLS: enabledTools,
|
||||
...(enableSandbox && { CCW_ENABLE_SANDBOX: "1" })
|
||||
}
|
||||
command: isWin ? 'cmd' : 'npx',
|
||||
args: isWin ? ['/c', 'npx', '-y', 'ccw-mcp'] : ['-y', 'ccw-mcp'],
|
||||
env
|
||||
};
|
||||
|
||||
// Use existing addMcpServerToProject to install CCW MCP
|
||||
return addMcpServerToProject(projectPath, 'ccw-tools', ccwMcpConfig);
|
||||
const resolvedScope: 'global' | 'project' = scope ?? (projectPath ? 'project' : 'global');
|
||||
if (resolvedScope === 'project') {
|
||||
if (!projectPath || !projectPath.trim()) {
|
||||
return { error: 'projectPath is required for project scope', status: 400 };
|
||||
}
|
||||
return addMcpServerToProject(projectPath, 'ccw-tools', ccwMcpConfig);
|
||||
}
|
||||
|
||||
return addGlobalMcpServer('ccw-tools', ccwMcpConfig);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user