From ab9b8ecbc0dc34732f6226af826739113fa4a4b3 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Wed, 4 Mar 2026 15:08:17 +0800 Subject: [PATCH] Add comprehensive documentation for Numerical Analysis Workflow - Introduced agent instruction template for task assignments in numerical analysis. - Defined CSV schema for tasks, including input, computed, and output columns. - Specified analysis dimensions across six phases of the workflow. - Established phase topology for the diamond deep tree structure of the workflow. - Outlined quality standards for assessing analysis reports, including criteria and quality gates. --- .../numerical-analysis-workflow/SKILL.md | 0 .../instructions/agent-instruction.md | 0 .../schemas/tasks-schema.md | 0 .../specs/analysis-dimensions.md | 0 .../specs/phase-topology.md | 0 .../specs/quality-standards.md | 0 .../src/components/mcp/McpServerDialog.tsx | 652 ++++++++++++++---- ccw/frontend/src/lib/api.ts | 259 ++++++- ccw/frontend/src/locales/en/mcp-manager.json | 24 +- ccw/frontend/src/locales/zh/mcp-manager.json | 24 +- ccw/src/core/routes/mcp-routes.ts | 26 + 11 files changed, 807 insertions(+), 178 deletions(-) rename .codex/{skills => skill_lib}/numerical-analysis-workflow/SKILL.md (100%) rename .codex/{skills => skill_lib}/numerical-analysis-workflow/instructions/agent-instruction.md (100%) rename .codex/{skills => skill_lib}/numerical-analysis-workflow/schemas/tasks-schema.md (100%) rename .codex/{skills => skill_lib}/numerical-analysis-workflow/specs/analysis-dimensions.md (100%) rename .codex/{skills => skill_lib}/numerical-analysis-workflow/specs/phase-topology.md (100%) rename .codex/{skills => skill_lib}/numerical-analysis-workflow/specs/quality-standards.md (100%) diff --git a/.codex/skills/numerical-analysis-workflow/SKILL.md b/.codex/skill_lib/numerical-analysis-workflow/SKILL.md similarity index 100% rename from .codex/skills/numerical-analysis-workflow/SKILL.md rename to .codex/skill_lib/numerical-analysis-workflow/SKILL.md diff --git a/.codex/skills/numerical-analysis-workflow/instructions/agent-instruction.md b/.codex/skill_lib/numerical-analysis-workflow/instructions/agent-instruction.md similarity index 100% rename from .codex/skills/numerical-analysis-workflow/instructions/agent-instruction.md rename to .codex/skill_lib/numerical-analysis-workflow/instructions/agent-instruction.md diff --git a/.codex/skills/numerical-analysis-workflow/schemas/tasks-schema.md b/.codex/skill_lib/numerical-analysis-workflow/schemas/tasks-schema.md similarity index 100% rename from .codex/skills/numerical-analysis-workflow/schemas/tasks-schema.md rename to .codex/skill_lib/numerical-analysis-workflow/schemas/tasks-schema.md diff --git a/.codex/skills/numerical-analysis-workflow/specs/analysis-dimensions.md b/.codex/skill_lib/numerical-analysis-workflow/specs/analysis-dimensions.md similarity index 100% rename from .codex/skills/numerical-analysis-workflow/specs/analysis-dimensions.md rename to .codex/skill_lib/numerical-analysis-workflow/specs/analysis-dimensions.md diff --git a/.codex/skills/numerical-analysis-workflow/specs/phase-topology.md b/.codex/skill_lib/numerical-analysis-workflow/specs/phase-topology.md similarity index 100% rename from .codex/skills/numerical-analysis-workflow/specs/phase-topology.md rename to .codex/skill_lib/numerical-analysis-workflow/specs/phase-topology.md diff --git a/.codex/skills/numerical-analysis-workflow/specs/quality-standards.md b/.codex/skill_lib/numerical-analysis-workflow/specs/quality-standards.md similarity index 100% rename from .codex/skills/numerical-analysis-workflow/specs/quality-standards.md rename to .codex/skill_lib/numerical-analysis-workflow/specs/quality-standards.md diff --git a/ccw/frontend/src/components/mcp/McpServerDialog.tsx b/ccw/frontend/src/components/mcp/McpServerDialog.tsx index c522bb14..52fac045 100644 --- a/ccw/frontend/src/components/mcp/McpServerDialog.tsx +++ b/ccw/frontend/src/components/mcp/McpServerDialog.tsx @@ -2,10 +2,12 @@ // MCP Server Dialog Component // ======================================== // Add/Edit dialog for MCP server configuration with dynamic template loading +// Supports both STDIO and HTTP transport types -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { useIntl } from 'react-intl'; import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { Eye, EyeOff, Plus, Trash2, Globe, Terminal } from 'lucide-react'; import { Dialog, DialogContent, @@ -38,6 +40,15 @@ import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore'; // ========== Types ========== +export type McpTransportType = 'stdio' | 'http'; + +export interface HttpHeader { + id: string; + name: string; + value: string; + isEnvVar: boolean; +} + export interface McpServerDialogProps { mode: 'add' | 'edit'; server?: McpServer; @@ -51,9 +62,15 @@ export type { McpTemplate } from '@/types/store'; interface McpServerFormData { name: string; + // STDIO fields command: string; args: string[]; env: Record; + // HTTP fields + url: string; + headers: HttpHeader[]; + bearerTokenEnvVar: string; + // Common fields scope: 'project' | 'global'; enabled: boolean; } @@ -63,9 +80,130 @@ interface FormErrors { command?: string; args?: string; env?: string; + url?: string; + headers?: string; } -// ========== Component ========== +// ========== Helper Component: HttpHeadersInput ========== + +interface HttpHeadersInputProps { + headers: HttpHeader[]; + onChange: (headers: HttpHeader[]) => void; + disabled?: boolean; +} + +function HttpHeadersInput({ headers, onChange, disabled }: HttpHeadersInputProps) { + const { formatMessage } = useIntl(); + const [revealedIds, setRevealedIds] = useState>(new Set()); + + const generateId = () => `header-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; + + const handleAdd = () => { + onChange([...headers, { id: generateId(), name: '', value: '', isEnvVar: false }]); + }; + + const handleRemove = (id: string) => { + onChange(headers.filter((h) => h.id !== id)); + }; + + const handleUpdate = (id: string, field: keyof HttpHeader, value: string | boolean) => { + onChange(headers.map((h) => (h.id === id ? { ...h, [field]: value } : h))); + }; + + const toggleReveal = (id: string) => { + setRevealedIds((prev) => { + const next = new Set(prev); + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + return next; + }); + }; + + const maskValue = (value: string) => { + if (!value) return ''; + return '*'.repeat(Math.min(value.length, 8)); + }; + + return ( +
+ {headers.map((header) => ( +
+ {/* Header Name */} + handleUpdate(header.id, 'name', e.target.value)} + placeholder={formatMessage({ id: 'mcp.dialog.form.http.headerName' })} + className="flex-1" + disabled={disabled} + /> + {/* Header Value */} +
+ handleUpdate(header.id, 'value', e.target.value)} + placeholder={formatMessage({ id: 'mcp.dialog.form.http.headerValue' })} + className="pr-8" + disabled={disabled} + /> + +
+ {/* Env Var Toggle */} + + {/* Delete Button */} + +
+ ))} + +
+ ); +} + +// ========== Main Component ========== export function McpServerDialog({ mode, @@ -81,12 +219,18 @@ export function McpServerDialog({ // Fetch templates from backend const { templates, isLoading: templatesLoading } = useMcpTemplates(); + // Transport type state + const [transportType, setTransportType] = useState('stdio'); + // Form state const [formData, setFormData] = useState({ name: '', command: '', args: [], env: {}, + url: '', + headers: [], + bearerTokenEnvVar: '', scope: 'project', enabled: true, }); @@ -99,14 +243,59 @@ export function McpServerDialog({ const [saveAsTemplate, setSaveAsTemplate] = useState(false); const projectConfigType: McpProjectConfigType = configType === 'claude-json' ? 'claude' : 'mcp'; + // Helper to detect transport type from server data + const detectTransportType = useCallback((serverData: McpServer | undefined): McpTransportType => { + if (!serverData) return 'stdio'; + // If server has url field (from extended McpServer type), it's HTTP + const extendedServer = serverData as McpServer & { url?: string }; + if (extendedServer.url) return 'http'; + return 'stdio'; + }, []); + // Initialize form from server prop (edit mode) useEffect(() => { if (server && mode === 'edit') { + const detectedType = detectTransportType(server); + setTransportType(detectedType); + + const extendedServer = server as McpServer & { + url?: string; + http_headers?: Record; + env_http_headers?: Record; + bearer_token_env_var?: string; + }; + + // Parse HTTP headers if present + const httpHeaders: HttpHeader[] = []; + if (extendedServer.http_headers) { + Object.entries(extendedServer.http_headers).forEach(([name, value], idx) => { + httpHeaders.push({ + id: `header-http-${idx}`, + name, + value, + isEnvVar: false, + }); + }); + } + if (extendedServer.env_http_headers) { + Object.entries(extendedServer.env_http_headers).forEach(([name, envVar], idx) => { + httpHeaders.push({ + id: `header-env-${idx}`, + name, + value: envVar, + isEnvVar: true, + }); + }); + } + setFormData({ name: server.name, - command: server.command, + command: server.command || '', args: server.args || [], env: server.env || {}, + url: extendedServer.url || '', + headers: httpHeaders, + bearerTokenEnvVar: extendedServer.bearer_token_env_var || '', scope: server.scope, enabled: server.enabled, }); @@ -118,11 +307,15 @@ export function McpServerDialog({ ); } else { // Reset form for add mode + setTransportType('stdio'); setFormData({ name: '', command: '', args: [], env: {}, + url: '', + headers: [], + bearerTokenEnvVar: '', scope: 'project', enabled: true, }); @@ -132,7 +325,7 @@ export function McpServerDialog({ setSelectedTemplate(''); setSaveAsTemplate(false); setErrors({}); - }, [server, mode, open]); + }, [server, mode, open, detectTransportType]); // Mutations const createMutation = useMutation({ @@ -182,7 +375,7 @@ export function McpServerDialog({ const handleFieldChange = ( field: keyof McpServerFormData, - value: string | boolean | string[] | Record + value: string | boolean | string[] | Record | HttpHeader[] ) => { setFormData((prev) => ({ ...prev, [field]: value })); // Clear error for this field @@ -223,6 +416,23 @@ export function McpServerDialog({ } }; + const handleHeadersChange = (headers: HttpHeader[]) => { + setFormData((prev) => ({ ...prev, headers })); + if (errors.headers) { + setErrors((prev) => ({ ...prev, headers: undefined })); + } + }; + + const handleTransportTypeChange = (type: McpTransportType) => { + setTransportType(type); + // Clear relevant errors when switching + setErrors((prev) => ({ + ...prev, + command: undefined, + url: undefined, + })); + }; + const validateForm = (): boolean => { const newErrors: FormErrors = {}; @@ -231,9 +441,23 @@ export function McpServerDialog({ newErrors.name = formatMessage({ id: 'mcp.dialog.validation.nameRequired' }); } - // Command required - if (!formData.command.trim()) { - newErrors.command = formatMessage({ id: 'mcp.dialog.validation.commandRequired' }); + // Transport-specific validation + if (transportType === 'stdio') { + // Command required for STDIO + if (!formData.command.trim()) { + newErrors.command = formatMessage({ id: 'mcp.dialog.validation.commandRequired' }); + } + } else { + // URL required for HTTP + if (!formData.url.trim()) { + newErrors.url = formatMessage({ id: 'mcp.dialog.validation.urlRequired' }); + } + // Validate URL format + try { + new URL(formData.url); + } catch { + newErrors.url = formatMessage({ id: 'mcp.dialog.validation.urlInvalid' }); + } } setErrors(newErrors); @@ -264,8 +488,54 @@ export function McpServerDialog({ return; } - // Save as template if checked - if (saveAsTemplate) { + // Build server config based on transport type + const serverConfig: McpServer & { + url?: string; + http_headers?: Record; + env_http_headers?: Record; + bearer_token_env_var?: string; + } = { + name: formData.name, + scope: formData.scope, + enabled: formData.enabled, + }; + + if (transportType === 'stdio') { + serverConfig.command = formData.command; + serverConfig.args = formData.args; + serverConfig.env = formData.env; + } else { + // HTTP transport + serverConfig.url = formData.url; + serverConfig.command = ''; // Empty command for HTTP servers + + // Separate headers into static and env-based + const httpHeaders: Record = {}; + const envHttpHeaders: Record = {}; + + formData.headers.forEach((h) => { + if (h.name.trim()) { + if (h.isEnvVar) { + envHttpHeaders[h.name.trim()] = h.value.trim(); + } else { + httpHeaders[h.name.trim()] = h.value.trim(); + } + } + }); + + if (Object.keys(httpHeaders).length > 0) { + serverConfig.http_headers = httpHeaders; + } + if (Object.keys(envHttpHeaders).length > 0) { + serverConfig.env_http_headers = envHttpHeaders; + } + if (formData.bearerTokenEnvVar.trim()) { + serverConfig.bearer_token_env_var = formData.bearerTokenEnvVar.trim(); + } + } + + // Save as template if checked (only for STDIO) + if (saveAsTemplate && transportType === 'stdio') { try { await saveMcpTemplate({ name: formData.name, @@ -283,26 +553,13 @@ export function McpServerDialog({ if (mode === 'add') { createMutation.mutate({ - server: { - name: formData.name, - command: formData.command, - args: formData.args, - env: formData.env, - scope: formData.scope, - enabled: formData.enabled, - }, + server: serverConfig, configType: formData.scope === 'project' ? projectConfigType : undefined, }); } else { updateMutation.mutate({ serverName: server!.name, - config: { - command: formData.command, - args: formData.args, - env: formData.env, - scope: formData.scope, - enabled: formData.enabled, - }, + config: serverConfig, configType: formData.scope === 'project' ? projectConfigType : undefined, }); } @@ -322,40 +579,42 @@ export function McpServerDialog({
- {/* Template Selector */} -
- - + + + + + {templates.length === 0 ? ( + + {formatMessage({ id: 'mcp.templates.empty.title' })} - )) - )} - - -
+ ) : ( + templates.map((template) => ( + +
+ {template.name} + + {template.description || formatMessage({ id: 'mcp.dialog.form.templatePlaceholder' })} + +
+
+ )) + )} + + +
+ )} {/* Name */}
@@ -375,87 +634,192 @@ export function McpServerDialog({ )}
- {/* Command */} + {/* Transport Type Selector */}
- handleFieldChange('command', e.target.value)} - placeholder={formatMessage({ id: 'mcp.dialog.form.commandPlaceholder' })} - error={!!errors.command} - /> - {errors.command && ( -

{errors.command}

- )} +
+ + +
+

+ {formatMessage({ id: 'mcp.dialog.form.transportHint' })} +

- {/* Args */} -
- - handleArgsChange(e.target.value)} - placeholder={formatMessage({ id: 'mcp.dialog.form.argsPlaceholder' })} - error={!!errors.args} - /> -

- {formatMessage({ id: 'mcp.dialog.form.argsHint' })} -

- {formData.args.length > 0 && ( -
- {formData.args.map((arg, idx) => ( - - {arg} - - ))} + {/* STDIO Fields */} + {transportType === 'stdio' && ( + <> + {/* Command */} +
+ + handleFieldChange('command', e.target.value)} + placeholder={formatMessage({ id: 'mcp.dialog.form.commandPlaceholder' })} + error={!!errors.command} + /> + {errors.command && ( +

{errors.command}

+ )}
- )} - {errors.args && ( -

{errors.args}

- )} -
- {/* Environment Variables */} -
- -