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.
This commit is contained in:
catlog22
2026-03-04 15:08:17 +08:00
parent f389e3e6dd
commit ab9b8ecbc0
11 changed files with 807 additions and 178 deletions

View File

@@ -2,10 +2,12 @@
// MCP Server Dialog Component // MCP Server Dialog Component
// ======================================== // ========================================
// Add/Edit dialog for MCP server configuration with dynamic template loading // 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 { useIntl } from 'react-intl';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Eye, EyeOff, Plus, Trash2, Globe, Terminal } from 'lucide-react';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -38,6 +40,15 @@ import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
// ========== Types ========== // ========== Types ==========
export type McpTransportType = 'stdio' | 'http';
export interface HttpHeader {
id: string;
name: string;
value: string;
isEnvVar: boolean;
}
export interface McpServerDialogProps { export interface McpServerDialogProps {
mode: 'add' | 'edit'; mode: 'add' | 'edit';
server?: McpServer; server?: McpServer;
@@ -51,9 +62,15 @@ export type { McpTemplate } from '@/types/store';
interface McpServerFormData { interface McpServerFormData {
name: string; name: string;
// STDIO fields
command: string; command: string;
args: string[]; args: string[];
env: Record<string, string>; env: Record<string, string>;
// HTTP fields
url: string;
headers: HttpHeader[];
bearerTokenEnvVar: string;
// Common fields
scope: 'project' | 'global'; scope: 'project' | 'global';
enabled: boolean; enabled: boolean;
} }
@@ -63,9 +80,130 @@ interface FormErrors {
command?: string; command?: string;
args?: string; args?: string;
env?: 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<Set<string>>(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 (
<div className="space-y-2">
{headers.map((header) => (
<div key={header.id} className="flex items-center gap-2">
{/* Header Name */}
<Input
value={header.name}
onChange={(e) => handleUpdate(header.id, 'name', e.target.value)}
placeholder={formatMessage({ id: 'mcp.dialog.form.http.headerName' })}
className="flex-1"
disabled={disabled}
/>
{/* Header Value */}
<div className="relative flex-1">
<Input
type={revealedIds.has(header.id) ? 'text' : 'password'}
value={header.value}
onChange={(e) => handleUpdate(header.id, 'value', e.target.value)}
placeholder={formatMessage({ id: 'mcp.dialog.form.http.headerValue' })}
className="pr-8"
disabled={disabled}
/>
<Button
type="button"
variant="ghost"
size="icon"
className="absolute right-0 top-0 h-full px-2"
onClick={() => toggleReveal(header.id)}
disabled={disabled}
>
{revealedIds.has(header.id) ? (
<EyeOff className="w-4 h-4" />
) : (
<Eye className="w-4 h-4" />
)}
</Button>
</div>
{/* Env Var Toggle */}
<label className="flex items-center gap-1 text-xs text-muted-foreground whitespace-nowrap">
<input
type="checkbox"
checked={header.isEnvVar}
onChange={(e) => handleUpdate(header.id, 'isEnvVar', e.target.checked)}
className="w-3 h-3"
disabled={disabled}
/>
{formatMessage({ id: 'mcp.dialog.form.http.isEnvVar' })}
</label>
{/* Delete Button */}
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => handleRemove(header.id)}
disabled={disabled}
className="text-destructive"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
))}
<Button
type="button"
variant="outline"
size="sm"
onClick={handleAdd}
disabled={disabled}
className="w-full"
>
<Plus className="w-4 h-4 mr-1" />
{formatMessage({ id: 'mcp.dialog.form.http.addHeader' })}
</Button>
</div>
);
}
// ========== Main Component ==========
export function McpServerDialog({ export function McpServerDialog({
mode, mode,
@@ -81,12 +219,18 @@ export function McpServerDialog({
// Fetch templates from backend // Fetch templates from backend
const { templates, isLoading: templatesLoading } = useMcpTemplates(); const { templates, isLoading: templatesLoading } = useMcpTemplates();
// Transport type state
const [transportType, setTransportType] = useState<McpTransportType>('stdio');
// Form state // Form state
const [formData, setFormData] = useState<McpServerFormData>({ const [formData, setFormData] = useState<McpServerFormData>({
name: '', name: '',
command: '', command: '',
args: [], args: [],
env: {}, env: {},
url: '',
headers: [],
bearerTokenEnvVar: '',
scope: 'project', scope: 'project',
enabled: true, enabled: true,
}); });
@@ -99,14 +243,59 @@ export function McpServerDialog({
const [saveAsTemplate, setSaveAsTemplate] = useState(false); const [saveAsTemplate, setSaveAsTemplate] = useState(false);
const projectConfigType: McpProjectConfigType = configType === 'claude-json' ? 'claude' : 'mcp'; 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) // Initialize form from server prop (edit mode)
useEffect(() => { useEffect(() => {
if (server && mode === 'edit') { if (server && mode === 'edit') {
const detectedType = detectTransportType(server);
setTransportType(detectedType);
const extendedServer = server as McpServer & {
url?: string;
http_headers?: Record<string, string>;
env_http_headers?: Record<string, string>;
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({ setFormData({
name: server.name, name: server.name,
command: server.command, command: server.command || '',
args: server.args || [], args: server.args || [],
env: server.env || {}, env: server.env || {},
url: extendedServer.url || '',
headers: httpHeaders,
bearerTokenEnvVar: extendedServer.bearer_token_env_var || '',
scope: server.scope, scope: server.scope,
enabled: server.enabled, enabled: server.enabled,
}); });
@@ -118,11 +307,15 @@ export function McpServerDialog({
); );
} else { } else {
// Reset form for add mode // Reset form for add mode
setTransportType('stdio');
setFormData({ setFormData({
name: '', name: '',
command: '', command: '',
args: [], args: [],
env: {}, env: {},
url: '',
headers: [],
bearerTokenEnvVar: '',
scope: 'project', scope: 'project',
enabled: true, enabled: true,
}); });
@@ -132,7 +325,7 @@ export function McpServerDialog({
setSelectedTemplate(''); setSelectedTemplate('');
setSaveAsTemplate(false); setSaveAsTemplate(false);
setErrors({}); setErrors({});
}, [server, mode, open]); }, [server, mode, open, detectTransportType]);
// Mutations // Mutations
const createMutation = useMutation({ const createMutation = useMutation({
@@ -182,7 +375,7 @@ export function McpServerDialog({
const handleFieldChange = ( const handleFieldChange = (
field: keyof McpServerFormData, field: keyof McpServerFormData,
value: string | boolean | string[] | Record<string, string> value: string | boolean | string[] | Record<string, string> | HttpHeader[]
) => { ) => {
setFormData((prev) => ({ ...prev, [field]: value })); setFormData((prev) => ({ ...prev, [field]: value }));
// Clear error for this field // 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 validateForm = (): boolean => {
const newErrors: FormErrors = {}; const newErrors: FormErrors = {};
@@ -231,10 +441,24 @@ export function McpServerDialog({
newErrors.name = formatMessage({ id: 'mcp.dialog.validation.nameRequired' }); newErrors.name = formatMessage({ id: 'mcp.dialog.validation.nameRequired' });
} }
// Command required // Transport-specific validation
if (transportType === 'stdio') {
// Command required for STDIO
if (!formData.command.trim()) { if (!formData.command.trim()) {
newErrors.command = formatMessage({ id: 'mcp.dialog.validation.commandRequired' }); 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); setErrors(newErrors);
return Object.keys(newErrors).length === 0; return Object.keys(newErrors).length === 0;
@@ -264,8 +488,54 @@ export function McpServerDialog({
return; return;
} }
// Save as template if checked // Build server config based on transport type
if (saveAsTemplate) { const serverConfig: McpServer & {
url?: string;
http_headers?: Record<string, string>;
env_http_headers?: Record<string, string>;
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<string, string> = {};
const envHttpHeaders: Record<string, string> = {};
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 { try {
await saveMcpTemplate({ await saveMcpTemplate({
name: formData.name, name: formData.name,
@@ -283,26 +553,13 @@ export function McpServerDialog({
if (mode === 'add') { if (mode === 'add') {
createMutation.mutate({ createMutation.mutate({
server: { server: serverConfig,
name: formData.name,
command: formData.command,
args: formData.args,
env: formData.env,
scope: formData.scope,
enabled: formData.enabled,
},
configType: formData.scope === 'project' ? projectConfigType : undefined, configType: formData.scope === 'project' ? projectConfigType : undefined,
}); });
} else { } else {
updateMutation.mutate({ updateMutation.mutate({
serverName: server!.name, serverName: server!.name,
config: { config: serverConfig,
command: formData.command,
args: formData.args,
env: formData.env,
scope: formData.scope,
enabled: formData.enabled,
},
configType: formData.scope === 'project' ? projectConfigType : undefined, configType: formData.scope === 'project' ? projectConfigType : undefined,
}); });
} }
@@ -322,7 +579,8 @@ export function McpServerDialog({
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
{/* Template Selector */} {/* Template Selector - Only for STDIO */}
{transportType === 'stdio' && (
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium text-foreground"> <label className="text-sm font-medium text-foreground">
{formatMessage({ id: 'mcp.dialog.form.template' })} {formatMessage({ id: 'mcp.dialog.form.template' })}
@@ -356,6 +614,7 @@ export function McpServerDialog({
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
)}
{/* Name */} {/* Name */}
<div className="space-y-2"> <div className="space-y-2">
@@ -375,6 +634,51 @@ export function McpServerDialog({
)} )}
</div> </div>
{/* Transport Type Selector */}
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">
{formatMessage({ id: 'mcp.dialog.form.transportType' })}
</label>
<div className="flex gap-4">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="transportType"
value="stdio"
checked={transportType === 'stdio'}
onChange={() => handleTransportTypeChange('stdio')}
className="w-4 h-4"
disabled={mode === 'edit'}
/>
<Terminal className="w-4 h-4 text-muted-foreground" />
<span className="text-sm">
{formatMessage({ id: 'mcp.dialog.form.transportStdio' })}
</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="transportType"
value="http"
checked={transportType === 'http'}
onChange={() => handleTransportTypeChange('http')}
className="w-4 h-4"
disabled={mode === 'edit'}
/>
<Globe className="w-4 h-4 text-muted-foreground" />
<span className="text-sm">
{formatMessage({ id: 'mcp.dialog.form.transportHttp' })}
</span>
</label>
</div>
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'mcp.dialog.form.transportHint' })}
</p>
</div>
{/* STDIO Fields */}
{transportType === 'stdio' && (
<>
{/* Command */} {/* Command */}
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium text-foreground"> <label className="text-sm font-medium text-foreground">
@@ -456,6 +760,66 @@ export function McpServerDialog({
<p className="text-sm text-destructive">{errors.env}</p> <p className="text-sm text-destructive">{errors.env}</p>
)} )}
</div> </div>
</>
)}
{/* HTTP Fields */}
{transportType === 'http' && (
<>
{/* URL */}
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">
{formatMessage({ id: 'mcp.dialog.form.http.url' })}
<span className="text-destructive ml-1">*</span>
</label>
<Input
value={formData.url}
onChange={(e) => handleFieldChange('url', e.target.value)}
placeholder={formatMessage({ id: 'mcp.dialog.form.http.urlPlaceholder' })}
error={!!errors.url}
/>
{errors.url && (
<p className="text-sm text-destructive">{errors.url}</p>
)}
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'mcp.dialog.form.http.urlHint' })}
</p>
</div>
{/* Bearer Token Env Var */}
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">
{formatMessage({ id: 'mcp.dialog.form.http.bearerToken' })}
</label>
<Input
value={formData.bearerTokenEnvVar}
onChange={(e) => handleFieldChange('bearerTokenEnvVar', e.target.value)}
placeholder={formatMessage({ id: 'mcp.dialog.form.http.bearerTokenPlaceholder' })}
/>
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'mcp.dialog.form.http.bearerTokenHint' })}
</p>
</div>
{/* HTTP Headers */}
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">
{formatMessage({ id: 'mcp.dialog.form.http.headers' })}
</label>
<HttpHeadersInput
headers={formData.headers}
onChange={handleHeadersChange}
disabled={isPending}
/>
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'mcp.dialog.form.http.headersHint' })}
</p>
{errors.headers && (
<p className="text-sm text-destructive">{errors.headers}</p>
)}
</div>
</>
)}
{/* Scope */} {/* Scope */}
<div className="space-y-2"> <div className="space-y-2">
@@ -522,7 +886,8 @@ export function McpServerDialog({
</label> </label>
</div> </div>
{/* Save as Template */} {/* Save as Template - Only for STDIO */}
{transportType === 'stdio' && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <input
type="checkbox" type="checkbox"
@@ -535,6 +900,7 @@ export function McpServerDialog({
{formatMessage({ id: 'mcp.templates.actions.saveAsTemplate' })} {formatMessage({ id: 'mcp.templates.actions.saveAsTemplate' })}
</label> </label>
</div> </div>
)}
</div> </div>
<DialogFooter> <DialogFooter>

View File

@@ -3178,13 +3178,66 @@ export async function fetchReviewSession(sessionId: string): Promise<ReviewSessi
// ========== MCP API ========== // ========== MCP API ==========
export interface McpServer { /**
* Base fields shared by all MCP server types
*/
interface McpServerBase {
name: string; name: string;
enabled: boolean;
scope: 'project' | 'global';
}
/**
* STDIO-based MCP server (traditional command-based)
* Uses child process communication via stdin/stdout
*/
export interface StdioMcpServer extends McpServerBase {
transport: 'stdio';
command: string; command: string;
args?: string[]; args?: string[];
env?: Record<string, string>; env?: Record<string, string>;
enabled: boolean; cwd?: string;
scope: 'project' | 'global'; }
/**
* HTTP-based MCP server (remote/streamable)
* Uses HTTP/HTTPS transport for remote MCP servers
*
* Supports two config formats:
* - Claude format: { type: 'http', url, headers }
* - Codex format: { url, bearer_token_env_var, http_headers, env_http_headers }
*/
export interface HttpMcpServer extends McpServerBase {
transport: 'http';
url: string;
/** HTTP headers to include in requests (Claude format) */
headers?: Record<string, string>;
/** Environment variable name containing bearer token (Codex format) */
bearerTokenEnvVar?: string;
/** Static HTTP headers (Codex format) */
httpHeaders?: Record<string, string>;
/** Environment variable names whose values are injected as headers (Codex format) */
envHttpHeaders?: string[];
}
/**
* Discriminated union type for MCP server configurations
* Use type guards to distinguish between STDIO and HTTP servers
*/
export type McpServer = StdioMcpServer | HttpMcpServer;
/**
* Type guard to check if a server is STDIO-based
*/
export function isStdioMcpServer(server: McpServer): server is StdioMcpServer {
return server.transport === 'stdio';
}
/**
* Type guard to check if a server is HTTP-based
*/
export function isHttpMcpServer(server: McpServer): server is HttpMcpServer {
return server.transport === 'http';
} }
export interface McpServerConflict { export interface McpServerConflict {
@@ -3257,17 +3310,80 @@ function findProjectConfigKey(projects: Record<string, unknown>, projectPath?: s
return projectPath in projects ? projectPath : null; return projectPath in projects ? projectPath : null;
} }
function normalizeServerConfig(config: unknown): { command: string; args?: string[]; env?: Record<string, string> } { /**
* Normalize raw server config to discriminated union type
* Preserves HTTP-specific fields instead of flattening to command field
*
* Supports dual-format parsing:
* - Claude format: { type: 'http', url, headers }
* - Codex format: { url, bearer_token_env_var, http_headers, env_http_headers }
*/
function normalizeServerConfig(config: unknown): Omit<StdioMcpServer, 'name' | 'enabled' | 'scope'> | Omit<HttpMcpServer, 'name' | 'enabled' | 'scope'> {
if (!isUnknownRecord(config)) { if (!isUnknownRecord(config)) {
return { command: '' }; // Default to STDIO with empty command
return { transport: 'stdio', command: '' };
} }
const command = // Detect HTTP transport by presence of url field (both Claude and Codex formats)
typeof config.command === 'string' const hasUrl = typeof config.url === 'string' && config.url.trim() !== '';
? config.command const hasHttpType = config.type === 'http' || config.transport === 'http';
: typeof config.url === 'string'
? config.url if (hasUrl || hasHttpType) {
: ''; // HTTP-based server (Claude or Codex format)
const url = typeof config.url === 'string' ? config.url : '';
// Parse Claude format headers: { headers: { "Authorization": "Bearer xxx" } }
const headers = isUnknownRecord(config.headers)
? Object.fromEntries(
Object.entries(config.headers).flatMap(([key, value]) =>
typeof value === 'string' ? [[key, value]] : []
)
)
: undefined;
// Parse Codex format fields
const bearerTokenEnvVar = typeof config.bearer_token_env_var === 'string'
? config.bearer_token_env_var
: undefined;
// Parse Codex http_headers: { http_headers: { "Authorization": "Bearer xxx" } }
const httpHeaders = isUnknownRecord(config.http_headers)
? Object.fromEntries(
Object.entries(config.http_headers).flatMap(([key, value]) =>
typeof value === 'string' ? [[key, value]] : []
)
)
: undefined;
// Parse Codex env_http_headers: { env_http_headers: ["API_KEY", "SECRET"] }
const envHttpHeaders = Array.isArray(config.env_http_headers)
? config.env_http_headers.filter((item): item is string => typeof item === 'string')
: undefined;
const result: Omit<HttpMcpServer, 'name' | 'enabled' | 'scope'> = {
transport: 'http',
url,
};
// Only add optional fields if they have values
if (headers && Object.keys(headers).length > 0) {
result.headers = headers;
}
if (bearerTokenEnvVar) {
result.bearerTokenEnvVar = bearerTokenEnvVar;
}
if (httpHeaders && Object.keys(httpHeaders).length > 0) {
result.httpHeaders = httpHeaders;
}
if (envHttpHeaders && envHttpHeaders.length > 0) {
result.envHttpHeaders = envHttpHeaders;
}
return result;
}
// STDIO-based server (traditional command format)
const command = typeof config.command === 'string' ? config.command : '';
const args = Array.isArray(config.args) const args = Array.isArray(config.args)
? config.args.filter((arg): arg is string => typeof arg === 'string') ? config.args.filter((arg): arg is string => typeof arg === 'string')
@@ -3281,11 +3397,24 @@ function normalizeServerConfig(config: unknown): { command: string; args?: strin
) )
: undefined; : undefined;
return { const cwd = typeof config.cwd === 'string' ? config.cwd : undefined;
const result: Omit<StdioMcpServer, 'name' | 'enabled' | 'scope'> = {
transport: 'stdio',
command, command,
args: args && args.length > 0 ? args : undefined,
env: env && Object.keys(env).length > 0 ? env : undefined,
}; };
if (args && args.length > 0) {
result.args = args;
}
if (env && Object.keys(env).length > 0) {
result.env = env;
}
if (cwd) {
result.cwd = cwd;
}
return result;
} }
/** /**
@@ -3371,15 +3500,66 @@ function requireProjectPath(projectPath: string | undefined, ctx: string): strin
return trimmed; return trimmed;
} }
function toServerConfig(server: { command: string; args?: string[]; env?: Record<string, string> }): UnknownRecord { /**
const config: UnknownRecord = { command: server.command }; * Convert McpServer to raw config format for persistence
if (server.args && server.args.length > 0) config.args = server.args; * Handles both STDIO and HTTP server types
if (server.env && Object.keys(server.env).length > 0) config.env = server.env; */
function toServerConfig(server: Partial<McpServer>): UnknownRecord {
// Check if this is an HTTP server
if (server.transport === 'http') {
const config: UnknownRecord = { url: server.url };
// Claude format: type field
config.type = 'http';
// Claude format: headers
if (server.headers && Object.keys(server.headers).length > 0) {
config.headers = server.headers;
}
// Codex format: bearer_token_env_var
if (server.bearerTokenEnvVar) {
config.bearer_token_env_var = server.bearerTokenEnvVar;
}
// Codex format: http_headers
if (server.httpHeaders && Object.keys(server.httpHeaders).length > 0) {
config.http_headers = server.httpHeaders;
}
// Codex format: env_http_headers
if (server.envHttpHeaders && server.envHttpHeaders.length > 0) {
config.env_http_headers = server.envHttpHeaders;
}
return config;
}
// STDIO server (default)
const config: UnknownRecord = {};
if (typeof server.command === 'string') {
config.command = server.command;
}
if (server.args && server.args.length > 0) {
config.args = server.args;
}
if (server.env && Object.keys(server.env).length > 0) {
config.env = server.env;
}
if (server.cwd) {
config.cwd = server.cwd;
}
return config; return config;
} }
/** /**
* Update MCP server configuration * Update MCP server configuration
* Supports both STDIO and HTTP server types
*/ */
export async function updateMcpServer( export async function updateMcpServer(
serverName: string, serverName: string,
@@ -3389,15 +3569,20 @@ export async function updateMcpServer(
if (!config.scope) { if (!config.scope) {
throw new Error('updateMcpServer: scope is required'); throw new Error('updateMcpServer: scope is required');
} }
// Validate based on transport type
if (config.transport === 'http') {
if (typeof config.url !== 'string' || !config.url.trim()) {
throw new Error('updateMcpServer: url is required for HTTP servers');
}
} else {
// STDIO server (default)
if (typeof config.command !== 'string' || !config.command.trim()) { if (typeof config.command !== 'string' || !config.command.trim()) {
throw new Error('updateMcpServer: command is required'); throw new Error('updateMcpServer: command is required for STDIO servers');
}
} }
const serverConfig = toServerConfig({ const serverConfig = toServerConfig(config);
command: config.command,
args: config.args,
env: config.env,
});
if (config.scope === 'global') { if (config.scope === 'global') {
const result = await fetchApi<{ success?: boolean; error?: string }>('/api/mcp-add-global-server', { const result = await fetchApi<{ success?: boolean; error?: string }>('/api/mcp-add-global-server', {
@@ -3436,26 +3621,29 @@ export async function updateMcpServer(
const servers = await fetchMcpServers(options.projectPath); const servers = await fetchMcpServers(options.projectPath);
return [...servers.project, ...servers.global].find((s) => s.name === serverName) ?? { return [...servers.project, ...servers.global].find((s) => s.name === serverName) ?? {
name: serverName, name: serverName,
command: config.command, transport: config.transport ?? 'stdio',
...(config.transport === 'http' ? { url: config.url! } : { command: config.command! }),
args: config.args, args: config.args,
env: config.env, env: config.env,
enabled: config.enabled ?? true, enabled: config.enabled ?? true,
scope: config.scope, scope: config.scope,
}; } as McpServer;
} }
return { return {
name: serverName, name: serverName,
command: config.command, transport: config.transport ?? 'stdio',
...(config.transport === 'http' ? { url: config.url! } : { command: config.command! }),
args: config.args, args: config.args,
env: config.env, env: config.env,
enabled: config.enabled ?? true, enabled: config.enabled ?? true,
scope: config.scope, scope: config.scope,
}; } as McpServer;
} }
/** /**
* Create a new MCP server * Create a new MCP server
* Supports both STDIO and HTTP server types
*/ */
export async function createMcpServer( export async function createMcpServer(
server: McpServer, server: McpServer,
@@ -3464,8 +3652,17 @@ export async function createMcpServer(
if (!server.name?.trim()) { if (!server.name?.trim()) {
throw new Error('createMcpServer: name is required'); throw new Error('createMcpServer: name is required');
} }
// Validate based on transport type
if (server.transport === 'http') {
if (!server.url?.trim()) {
throw new Error('createMcpServer: url is required for HTTP servers');
}
} else {
// STDIO server (default)
if (!server.command?.trim()) { if (!server.command?.trim()) {
throw new Error('createMcpServer: command is required'); throw new Error('createMcpServer: command is required for STDIO servers');
}
} }
const serverName = server.name.trim(); const serverName = server.name.trim();

View File

@@ -84,7 +84,25 @@
"envPlaceholder": "Key=value pairs (one per line), e.g.,\nAPI_KEY=your_key\nDEBUG=true", "envPlaceholder": "Key=value pairs (one per line), e.g.,\nAPI_KEY=your_key\nDEBUG=true",
"envHint": "Enter one key=value pair per line", "envHint": "Enter one key=value pair per line",
"scope": "Scope", "scope": "Scope",
"enabled": "Enable this server" "enabled": "Enable this server",
"transportType": "Transport Type",
"transportStdio": "STDIO (Local Process)",
"transportHttp": "HTTP (Remote Server)",
"transportHint": "STDIO runs a local command, HTTP connects to a remote MCP server",
"http": {
"url": "Server URL",
"urlPlaceholder": "https://api.example.com/mcp",
"urlHint": "The MCP server endpoint URL",
"bearerToken": "Bearer Token Env Var",
"bearerTokenPlaceholder": "API_TOKEN",
"bearerTokenHint": "Environment variable name containing the bearer token for Authorization header",
"headers": "Custom Headers",
"headersHint": "Add custom HTTP headers for authentication or configuration",
"headerName": "Header Name",
"headerValue": "Header Value",
"isEnvVar": "Env Var",
"addHeader": "Add Header"
}
}, },
"templates": { "templates": {
"npx-stdio": "NPX STDIO", "npx-stdio": "NPX STDIO",
@@ -94,7 +112,9 @@
"validation": { "validation": {
"nameRequired": "Server name is required", "nameRequired": "Server name is required",
"nameExists": "A server with this name already exists", "nameExists": "A server with this name already exists",
"commandRequired": "Command is required" "commandRequired": "Command is required",
"urlRequired": "Server URL is required",
"urlInvalid": "Please enter a valid URL"
}, },
"actions": { "actions": {
"save": "Save", "save": "Save",

View File

@@ -84,7 +84,25 @@
"envPlaceholder": "键=值对(每行一个),例如:\nAPI_KEY=your_key\nDEBUG=true", "envPlaceholder": "键=值对(每行一个),例如:\nAPI_KEY=your_key\nDEBUG=true",
"envHint": "每行输入一个键=值对", "envHint": "每行输入一个键=值对",
"scope": "作用域", "scope": "作用域",
"enabled": "启用此服务器" "enabled": "启用此服务器",
"transportType": "传输类型",
"transportStdio": "STDIO (本地进程)",
"transportHttp": "HTTP (远程服务器)",
"transportHint": "STDIO 运行本地命令HTTP 连接到远程 MCP 服务器",
"http": {
"url": "服务器地址",
"urlPlaceholder": "https://api.example.com/mcp",
"urlHint": "MCP 服务器的 HTTP 端点地址",
"bearerToken": "Bearer Token 环境变量",
"bearerTokenPlaceholder": "MY_API_TOKEN",
"bearerTokenHint": "包含 Bearer Token 的环境变量名称,用于 Authorization 请求头",
"headers": "自定义请求头",
"headersHint": "添加用于认证或配置的自定义请求头。启用 '环境变量' 可引用环境变量。",
"headerName": "请求头名称",
"headerValue": "请求头值",
"isEnvVar": "环境变量",
"addHeader": "添加请求头"
}
}, },
"templates": { "templates": {
"npx-stdio": "NPX STDIO", "npx-stdio": "NPX STDIO",
@@ -94,7 +112,9 @@
"validation": { "validation": {
"nameRequired": "服务器名称不能为空", "nameRequired": "服务器名称不能为空",
"nameExists": "已存在同名服务器", "nameExists": "已存在同名服务器",
"commandRequired": "命令不能为空" "commandRequired": "命令不能为空",
"urlRequired": "服务器地址不能为空",
"urlInvalid": "请输入有效的 URL 地址"
}, },
"actions": { "actions": {
"save": "保存", "save": "保存",

View File

@@ -331,14 +331,40 @@ function addCodexMcpServer(serverName: string, serverConfig: Record<string, any>
} }
// Handle HTTP servers (url-based) // Handle HTTP servers (url-based)
// Supports dual-format parsing:
// - Claude format: { type: 'http', url, headers }
// - Codex format: { url, bearer_token_env_var, http_headers, env_http_headers }
if (serverConfig.url) { if (serverConfig.url) {
codexServerConfig.url = serverConfig.url; codexServerConfig.url = serverConfig.url;
// Codex format: bearer_token_env_var
if (serverConfig.bearer_token_env_var) { if (serverConfig.bearer_token_env_var) {
codexServerConfig.bearer_token_env_var = serverConfig.bearer_token_env_var; codexServerConfig.bearer_token_env_var = serverConfig.bearer_token_env_var;
} }
// Codex format: http_headers
if (serverConfig.http_headers) { if (serverConfig.http_headers) {
codexServerConfig.http_headers = serverConfig.http_headers; codexServerConfig.http_headers = serverConfig.http_headers;
} }
// Codex format: env_http_headers (array of env var names)
if (serverConfig.env_http_headers) {
codexServerConfig.env_http_headers = serverConfig.env_http_headers;
}
// Claude format: headers (convert to Codex http_headers for storage)
if (serverConfig.headers) {
// Merge with existing http_headers if present
codexServerConfig.http_headers = {
...(codexServerConfig.http_headers || {}),
...serverConfig.headers
};
}
// Claude format: type field (optional marker)
if (serverConfig.type) {
codexServerConfig.type = serverConfig.type;
}
} }
// Copy optional fields // Copy optional fields