mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-05 16:13:08 +08:00
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:
@@ -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<string, string>;
|
||||
// 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<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({
|
||||
mode,
|
||||
@@ -81,12 +219,18 @@ export function McpServerDialog({
|
||||
// Fetch templates from backend
|
||||
const { templates, isLoading: templatesLoading } = useMcpTemplates();
|
||||
|
||||
// Transport type state
|
||||
const [transportType, setTransportType] = useState<McpTransportType>('stdio');
|
||||
|
||||
// Form state
|
||||
const [formData, setFormData] = useState<McpServerFormData>({
|
||||
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<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({
|
||||
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<string, string>
|
||||
value: string | boolean | string[] | Record<string, string> | 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<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 {
|
||||
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({
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Template Selector */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'mcp.dialog.form.template' })}
|
||||
</label>
|
||||
<Select value={selectedTemplate} onValueChange={handleTemplateSelect} disabled={templatesLoading}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue
|
||||
placeholder={templatesLoading
|
||||
? formatMessage({ id: 'mcp.templates.loading' })
|
||||
: formatMessage({ id: 'mcp.dialog.form.templatePlaceholder' })
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{templates.length === 0 ? (
|
||||
<SelectItem value="__empty__" disabled>
|
||||
{formatMessage({ id: 'mcp.templates.empty.title' })}
|
||||
</SelectItem>
|
||||
) : (
|
||||
templates.map((template) => (
|
||||
<SelectItem key={template.name} value={template.name}>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{template.name}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{template.description || formatMessage({ id: 'mcp.dialog.form.templatePlaceholder' })}
|
||||
</span>
|
||||
</div>
|
||||
{/* Template Selector - Only for STDIO */}
|
||||
{transportType === 'stdio' && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'mcp.dialog.form.template' })}
|
||||
</label>
|
||||
<Select value={selectedTemplate} onValueChange={handleTemplateSelect} disabled={templatesLoading}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue
|
||||
placeholder={templatesLoading
|
||||
? formatMessage({ id: 'mcp.templates.loading' })
|
||||
: formatMessage({ id: 'mcp.dialog.form.templatePlaceholder' })
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{templates.length === 0 ? (
|
||||
<SelectItem value="__empty__" disabled>
|
||||
{formatMessage({ id: 'mcp.templates.empty.title' })}
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
) : (
|
||||
templates.map((template) => (
|
||||
<SelectItem key={template.name} value={template.name}>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{template.name}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{template.description || formatMessage({ id: 'mcp.dialog.form.templatePlaceholder' })}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Name */}
|
||||
<div className="space-y-2">
|
||||
@@ -375,87 +634,192 @@ export function McpServerDialog({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Command */}
|
||||
{/* Transport Type Selector */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'mcp.dialog.form.command' })}
|
||||
<span className="text-destructive ml-1">*</span>
|
||||
{formatMessage({ id: 'mcp.dialog.form.transportType' })}
|
||||
</label>
|
||||
<Input
|
||||
value={formData.command}
|
||||
onChange={(e) => handleFieldChange('command', e.target.value)}
|
||||
placeholder={formatMessage({ id: 'mcp.dialog.form.commandPlaceholder' })}
|
||||
error={!!errors.command}
|
||||
/>
|
||||
{errors.command && (
|
||||
<p className="text-sm text-destructive">{errors.command}</p>
|
||||
)}
|
||||
<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>
|
||||
|
||||
{/* Args */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'mcp.dialog.form.args' })}
|
||||
</label>
|
||||
<Input
|
||||
value={argsInput}
|
||||
onChange={(e) => handleArgsChange(e.target.value)}
|
||||
placeholder={formatMessage({ id: 'mcp.dialog.form.argsPlaceholder' })}
|
||||
error={!!errors.args}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'mcp.dialog.form.argsHint' })}
|
||||
</p>
|
||||
{formData.args.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{formData.args.map((arg, idx) => (
|
||||
<Badge key={idx} variant="secondary" className="font-mono text-xs">
|
||||
{arg}
|
||||
</Badge>
|
||||
))}
|
||||
{/* STDIO Fields */}
|
||||
{transportType === 'stdio' && (
|
||||
<>
|
||||
{/* Command */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'mcp.dialog.form.command' })}
|
||||
<span className="text-destructive ml-1">*</span>
|
||||
</label>
|
||||
<Input
|
||||
value={formData.command}
|
||||
onChange={(e) => handleFieldChange('command', e.target.value)}
|
||||
placeholder={formatMessage({ id: 'mcp.dialog.form.commandPlaceholder' })}
|
||||
error={!!errors.command}
|
||||
/>
|
||||
{errors.command && (
|
||||
<p className="text-sm text-destructive">{errors.command}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{errors.args && (
|
||||
<p className="text-sm text-destructive">{errors.args}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Environment Variables */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'mcp.dialog.form.env' })}
|
||||
</label>
|
||||
<textarea
|
||||
value={envInput}
|
||||
onChange={(e) => handleEnvChange(e.target.value)}
|
||||
placeholder={formatMessage({ id: 'mcp.dialog.form.envPlaceholder' })}
|
||||
className={cn(
|
||||
'flex min-h-[100px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
errors.env && 'border-destructive focus-visible:ring-destructive'
|
||||
)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'mcp.dialog.form.envHint' })}
|
||||
</p>
|
||||
{Object.keys(formData.env).length > 0 && (
|
||||
<div className="space-y-1 mt-2">
|
||||
{Object.entries(formData.env).map(([key, value]) => (
|
||||
<div key={key} className="flex items-center gap-2 text-sm">
|
||||
<Badge variant="outline" className="font-mono">
|
||||
{key}
|
||||
</Badge>
|
||||
<span className="text-muted-foreground">=</span>
|
||||
<code className="text-xs bg-muted px-2 py-1 rounded flex-1 overflow-x-auto">
|
||||
{value}
|
||||
</code>
|
||||
{/* Args */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'mcp.dialog.form.args' })}
|
||||
</label>
|
||||
<Input
|
||||
value={argsInput}
|
||||
onChange={(e) => handleArgsChange(e.target.value)}
|
||||
placeholder={formatMessage({ id: 'mcp.dialog.form.argsPlaceholder' })}
|
||||
error={!!errors.args}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'mcp.dialog.form.argsHint' })}
|
||||
</p>
|
||||
{formData.args.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{formData.args.map((arg, idx) => (
|
||||
<Badge key={idx} variant="secondary" className="font-mono text-xs">
|
||||
{arg}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
{errors.args && (
|
||||
<p className="text-sm text-destructive">{errors.args}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{errors.env && (
|
||||
<p className="text-sm text-destructive">{errors.env}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Environment Variables */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'mcp.dialog.form.env' })}
|
||||
</label>
|
||||
<textarea
|
||||
value={envInput}
|
||||
onChange={(e) => handleEnvChange(e.target.value)}
|
||||
placeholder={formatMessage({ id: 'mcp.dialog.form.envPlaceholder' })}
|
||||
className={cn(
|
||||
'flex min-h-[100px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
errors.env && 'border-destructive focus-visible:ring-destructive'
|
||||
)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'mcp.dialog.form.envHint' })}
|
||||
</p>
|
||||
{Object.keys(formData.env).length > 0 && (
|
||||
<div className="space-y-1 mt-2">
|
||||
{Object.entries(formData.env).map(([key, value]) => (
|
||||
<div key={key} className="flex items-center gap-2 text-sm">
|
||||
<Badge variant="outline" className="font-mono">
|
||||
{key}
|
||||
</Badge>
|
||||
<span className="text-muted-foreground">=</span>
|
||||
<code className="text-xs bg-muted px-2 py-1 rounded flex-1 overflow-x-auto">
|
||||
{value}
|
||||
</code>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{errors.env && (
|
||||
<p className="text-sm text-destructive">{errors.env}</p>
|
||||
)}
|
||||
</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 */}
|
||||
<div className="space-y-2">
|
||||
@@ -522,19 +886,21 @@ export function McpServerDialog({
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Save as Template */}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="save-as-template"
|
||||
checked={saveAsTemplate}
|
||||
onChange={(e) => setSaveAsTemplate(e.target.checked)}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
<label htmlFor="save-as-template" className="text-sm font-medium text-foreground cursor-pointer">
|
||||
{formatMessage({ id: 'mcp.templates.actions.saveAsTemplate' })}
|
||||
</label>
|
||||
</div>
|
||||
{/* Save as Template - Only for STDIO */}
|
||||
{transportType === 'stdio' && (
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="save-as-template"
|
||||
checked={saveAsTemplate}
|
||||
onChange={(e) => setSaveAsTemplate(e.target.checked)}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
<label htmlFor="save-as-template" className="text-sm font-medium text-foreground cursor-pointer">
|
||||
{formatMessage({ id: 'mcp.templates.actions.saveAsTemplate' })}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
|
||||
@@ -3178,13 +3178,66 @@ export async function fetchReviewSession(sessionId: string): Promise<ReviewSessi
|
||||
|
||||
// ========== MCP API ==========
|
||||
|
||||
export interface McpServer {
|
||||
/**
|
||||
* Base fields shared by all MCP server types
|
||||
*/
|
||||
interface McpServerBase {
|
||||
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;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
enabled: boolean;
|
||||
scope: 'project' | 'global';
|
||||
cwd?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
@@ -3257,17 +3310,80 @@ function findProjectConfigKey(projects: Record<string, unknown>, projectPath?: s
|
||||
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)) {
|
||||
return { command: '' };
|
||||
// Default to STDIO with empty command
|
||||
return { transport: 'stdio', command: '' };
|
||||
}
|
||||
|
||||
const command =
|
||||
typeof config.command === 'string'
|
||||
? config.command
|
||||
: typeof config.url === 'string'
|
||||
? config.url
|
||||
: '';
|
||||
// Detect HTTP transport by presence of url field (both Claude and Codex formats)
|
||||
const hasUrl = typeof config.url === 'string' && config.url.trim() !== '';
|
||||
const hasHttpType = config.type === 'http' || config.transport === 'http';
|
||||
|
||||
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)
|
||||
? config.args.filter((arg): arg is string => typeof arg === 'string')
|
||||
@@ -3281,11 +3397,24 @@ function normalizeServerConfig(config: unknown): { command: string; args?: strin
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
const cwd = typeof config.cwd === 'string' ? config.cwd : undefined;
|
||||
|
||||
const result: Omit<StdioMcpServer, 'name' | 'enabled' | 'scope'> = {
|
||||
transport: 'stdio',
|
||||
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;
|
||||
}
|
||||
|
||||
function toServerConfig(server: { command: string; args?: string[]; env?: Record<string, string> }): UnknownRecord {
|
||||
const config: UnknownRecord = { 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;
|
||||
/**
|
||||
* Convert McpServer to raw config format for persistence
|
||||
* Handles both STDIO and HTTP server types
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update MCP server configuration
|
||||
* Supports both STDIO and HTTP server types
|
||||
*/
|
||||
export async function updateMcpServer(
|
||||
serverName: string,
|
||||
@@ -3389,15 +3569,20 @@ export async function updateMcpServer(
|
||||
if (!config.scope) {
|
||||
throw new Error('updateMcpServer: scope is required');
|
||||
}
|
||||
if (typeof config.command !== 'string' || !config.command.trim()) {
|
||||
throw new Error('updateMcpServer: command 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()) {
|
||||
throw new Error('updateMcpServer: command is required for STDIO servers');
|
||||
}
|
||||
}
|
||||
|
||||
const serverConfig = toServerConfig({
|
||||
command: config.command,
|
||||
args: config.args,
|
||||
env: config.env,
|
||||
});
|
||||
const serverConfig = toServerConfig(config);
|
||||
|
||||
if (config.scope === 'global') {
|
||||
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);
|
||||
return [...servers.project, ...servers.global].find((s) => s.name === serverName) ?? {
|
||||
name: serverName,
|
||||
command: config.command,
|
||||
transport: config.transport ?? 'stdio',
|
||||
...(config.transport === 'http' ? { url: config.url! } : { command: config.command! }),
|
||||
args: config.args,
|
||||
env: config.env,
|
||||
enabled: config.enabled ?? true,
|
||||
scope: config.scope,
|
||||
};
|
||||
} as McpServer;
|
||||
}
|
||||
|
||||
return {
|
||||
name: serverName,
|
||||
command: config.command,
|
||||
transport: config.transport ?? 'stdio',
|
||||
...(config.transport === 'http' ? { url: config.url! } : { command: config.command! }),
|
||||
args: config.args,
|
||||
env: config.env,
|
||||
enabled: config.enabled ?? true,
|
||||
scope: config.scope,
|
||||
};
|
||||
} as McpServer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new MCP server
|
||||
* Supports both STDIO and HTTP server types
|
||||
*/
|
||||
export async function createMcpServer(
|
||||
server: McpServer,
|
||||
@@ -3464,8 +3652,17 @@ export async function createMcpServer(
|
||||
if (!server.name?.trim()) {
|
||||
throw new Error('createMcpServer: name is required');
|
||||
}
|
||||
if (!server.command?.trim()) {
|
||||
throw new Error('createMcpServer: command 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()) {
|
||||
throw new Error('createMcpServer: command is required for STDIO servers');
|
||||
}
|
||||
}
|
||||
|
||||
const serverName = server.name.trim();
|
||||
|
||||
@@ -84,7 +84,25 @@
|
||||
"envPlaceholder": "Key=value pairs (one per line), e.g.,\nAPI_KEY=your_key\nDEBUG=true",
|
||||
"envHint": "Enter one key=value pair per line",
|
||||
"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": {
|
||||
"npx-stdio": "NPX STDIO",
|
||||
@@ -94,7 +112,9 @@
|
||||
"validation": {
|
||||
"nameRequired": "Server name is required",
|
||||
"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": {
|
||||
"save": "Save",
|
||||
|
||||
@@ -84,7 +84,25 @@
|
||||
"envPlaceholder": "键=值对(每行一个),例如:\nAPI_KEY=your_key\nDEBUG=true",
|
||||
"envHint": "每行输入一个键=值对",
|
||||
"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": {
|
||||
"npx-stdio": "NPX STDIO",
|
||||
@@ -94,7 +112,9 @@
|
||||
"validation": {
|
||||
"nameRequired": "服务器名称不能为空",
|
||||
"nameExists": "已存在同名服务器",
|
||||
"commandRequired": "命令不能为空"
|
||||
"commandRequired": "命令不能为空",
|
||||
"urlRequired": "服务器地址不能为空",
|
||||
"urlInvalid": "请输入有效的 URL 地址"
|
||||
},
|
||||
"actions": {
|
||||
"save": "保存",
|
||||
|
||||
@@ -331,14 +331,40 @@ function addCodexMcpServer(serverName: string, serverConfig: Record<string, any>
|
||||
}
|
||||
|
||||
// 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) {
|
||||
codexServerConfig.url = serverConfig.url;
|
||||
|
||||
// Codex format: bearer_token_env_var
|
||||
if (serverConfig.bearer_token_env_var) {
|
||||
codexServerConfig.bearer_token_env_var = serverConfig.bearer_token_env_var;
|
||||
}
|
||||
|
||||
// Codex format: http_headers
|
||||
if (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
|
||||
|
||||
Reference in New Issue
Block a user