Add comprehensive documentation templates and schemas for project documentation workflow

- Introduced agent instruction template for generating documentation tasks.
- Created CSV schema for tasks including definitions and example entries.
- Defined documentation dimensions across five waves with specific focus areas.
- Established quality standards for documentation, including completeness, accuracy, clarity, and context integration.
This commit is contained in:
catlog22
2026-03-04 15:43:24 +08:00
parent ab9b8ecbc0
commit a82e45fcf1
18 changed files with 2035 additions and 1802 deletions

View File

@@ -11,11 +11,13 @@ import {
ChevronDown,
ChevronUp,
Lock,
Link,
} from 'lucide-react';
import { Card } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
import { cn } from '@/lib/utils';
import type { McpServer } from '@/lib/api';
import { isHttpMcpServer, isStdioMcpServer } from '@/lib/api';
// ========== Types ==========
@@ -40,6 +42,20 @@ export function CodexMcpCard({
}: CodexMcpCardProps) {
const { formatMessage } = useIntl();
const isHttp = isHttpMcpServer(server);
const isStdio = isStdioMcpServer(server);
// Get display text for server summary line
const getServerSummary = () => {
if (isHttp) {
return server.url;
}
if (isStdio) {
return `${server.command || ''} ${server.args?.join(' ') || ''}`.trim();
}
return '';
};
return (
<Card className={cn('overflow-hidden', !enabled && 'opacity-60')}>
{/* Header */}
@@ -63,6 +79,13 @@ export function CodexMcpCard({
<span className="text-sm font-medium text-foreground">
{server.name}
</span>
{/* Transport type badge */}
{isHttp && (
<Badge variant="outline" className="text-xs text-blue-600 border-blue-300">
<Link className="w-3 h-3 mr-1" />
{formatMessage({ id: 'mcp.transport.http' })}
</Badge>
)}
{/* Read-only badge */}
<Badge variant="secondary" className="text-xs flex items-center gap-1">
<Lock className="w-3 h-3" />
@@ -75,8 +98,8 @@ export function CodexMcpCard({
</Badge>
)}
</div>
<p className="text-sm text-muted-foreground mt-1 font-mono">
{server.command} {server.args?.join(' ') || ''}
<p className="text-sm text-muted-foreground mt-1 font-mono truncate max-w-md" title={getServerSummary()}>
{getServerSummary()}
</p>
</div>
</div>
@@ -100,44 +123,101 @@ export function CodexMcpCard({
{/* Expanded Content */}
{isExpanded && (
<div className="border-t border-border p-4 space-y-3 bg-muted/30">
{/* Command details */}
<div>
<p className="text-xs text-muted-foreground mb-1">{formatMessage({ id: 'mcp.command' })}</p>
<code className="text-sm bg-background px-2 py-1 rounded block overflow-x-auto">
{server.command}
</code>
</div>
{/* Args */}
{server.args && server.args.length > 0 && (
<div>
<p className="text-xs text-muted-foreground mb-1">{formatMessage({ id: 'mcp.args' })}</p>
<div className="flex flex-wrap gap-1">
{server.args.map((arg, idx) => (
<Badge key={idx} variant="outline" className="font-mono text-xs">
{arg}
</Badge>
))}
{/* HTTP Server Details */}
{isHttp && (
<>
{/* URL */}
<div>
<p className="text-xs text-muted-foreground mb-1">{formatMessage({ id: 'mcp.http.url' })}</p>
<code className="text-sm bg-background px-2 py-1 rounded block overflow-x-auto break-all">
{server.url}
</code>
</div>
</div>
{/* HTTP Headers - show count only for read-only card */}
{(server.headers || server.httpHeaders || server.bearerTokenEnvVar) && (
<div>
<p className="text-xs text-muted-foreground mb-1">{formatMessage({ id: 'mcp.http.headers' })}</p>
<div className="space-y-1">
{server.headers && Object.entries(server.headers).map(([name]) => (
<div key={name} className="flex items-center gap-2 text-sm">
<Badge variant="secondary" className="font-mono">{name}</Badge>
<span className="text-muted-foreground">=</span>
<code className="text-xs bg-background px-2 py-1 rounded flex-1 overflow-x-auto">
****
</code>
</div>
))}
{server.httpHeaders && Object.entries(server.httpHeaders).map(([name]) => (
<div key={name} className="flex items-center gap-2 text-sm">
<Badge variant="secondary" className="font-mono">{name}</Badge>
<span className="text-muted-foreground">=</span>
<code className="text-xs bg-background px-2 py-1 rounded flex-1 overflow-x-auto">
****
</code>
</div>
))}
{server.bearerTokenEnvVar && (
<div className="flex items-center gap-2 text-sm">
<Badge variant="secondary" className="font-mono">Authorization</Badge>
<span className="text-muted-foreground">=</span>
<code className="text-xs bg-background px-2 py-1 rounded flex-1 overflow-x-auto">
Bearer $${server.bearerTokenEnvVar}
</code>
<Badge variant="outline" className="text-xs text-blue-500">
{formatMessage({ id: 'mcp.http.envVar' })}
</Badge>
</div>
)}
</div>
</div>
)}
</>
)}
{/* Environment variables */}
{server.env && Object.keys(server.env).length > 0 && (
<div>
<p className="text-xs text-muted-foreground mb-1">{formatMessage({ id: 'mcp.env' })}</p>
<div className="space-y-1">
{Object.entries(server.env).map(([key, value]) => (
<div key={key} className="flex items-center gap-2 text-sm">
<Badge variant="secondary" className="font-mono">{key}</Badge>
<span className="text-muted-foreground">=</span>
<code className="text-xs bg-background px-2 py-1 rounded flex-1 overflow-x-auto">
{value as string}
</code>
</div>
))}
{/* STDIO Server Details */}
{isStdio && (
<>
{/* Command details */}
<div>
<p className="text-xs text-muted-foreground mb-1">{formatMessage({ id: 'mcp.command' })}</p>
<code className="text-sm bg-background px-2 py-1 rounded block overflow-x-auto">
{server.command}
</code>
</div>
</div>
{/* Args */}
{server.args && server.args.length > 0 && (
<div>
<p className="text-xs text-muted-foreground mb-1">{formatMessage({ id: 'mcp.args' })}</p>
<div className="flex flex-wrap gap-1">
{server.args.map((arg: string, idx: number) => (
<Badge key={idx} variant="outline" className="font-mono text-xs">
{arg}
</Badge>
))}
</div>
</div>
)}
{/* Environment variables */}
{server.env && Object.keys(server.env).length > 0 && (
<div>
<p className="text-xs text-muted-foreground mb-1">{formatMessage({ id: 'mcp.env' })}</p>
<div className="space-y-1">
{Object.entries(server.env).map(([key, value]) => (
<div key={key} className="flex items-center gap-2 text-sm">
<Badge variant="secondary" className="font-mono">{key}</Badge>
<span className="text-muted-foreground">=</span>
<code className="text-xs bg-background px-2 py-1 rounded flex-1 overflow-x-auto">
{value as string}
</code>
</div>
))}
</div>
</div>
)}
</>
)}
{/* Read-only notice */}

View File

@@ -15,6 +15,9 @@ import {
Edit3,
Trash2,
Lock,
Link,
Eye,
EyeOff,
} from 'lucide-react';
import {
AlertDialog,
@@ -32,6 +35,7 @@ import { Card } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
import { cn } from '@/lib/utils';
import type { McpServer } from '@/lib/api';
import { isHttpMcpServer, isStdioMcpServer } from '@/lib/api';
// ========== Types ==========
@@ -54,6 +58,14 @@ export interface CodexMcpEditableCardProps {
// ========== Component ==========
/**
* Mask a header value for display (show first 4 chars + ***)
*/
function maskHeaderValue(value: string): string {
if (!value || value.length <= 4) return '****';
return value.substring(0, 4) + '****';
}
export function CodexMcpEditableCard({
server,
enabled,
@@ -67,6 +79,58 @@ export function CodexMcpEditableCard({
}: CodexMcpEditableCardProps) {
const { formatMessage } = useIntl();
const [isConfirmDeleteOpen, setIsConfirmDeleteOpen] = useState(false);
const [showHeaderValues, setShowHeaderValues] = useState<Record<string, boolean>>({});
const isHttp = isHttpMcpServer(server);
const isStdio = isStdioMcpServer(server);
// Get display text for server summary line
const getServerSummary = () => {
if (isHttp) {
return server.url;
}
return `${server.command || ''} ${server.args?.join(' ') || ''}`.trim();
};
// Get all headers for HTTP server (Codex format: http_headers, env_http_headers, bearerTokenEnvVar)
const getHttpHeaders = (): Array<{ name: string; value: string; isEnvVar?: boolean }> => {
if (!isHttp) return [];
const headers: Array<{ name: string; value: string; isEnvVar?: boolean }> = [];
// Codex format: httpHeaders object
if (server.httpHeaders) {
Object.entries(server.httpHeaders).forEach(([name, value]) => {
headers.push({ name, value });
});
}
// Codex format: bearerTokenEnvVar (adds Authorization header)
if (server.bearerTokenEnvVar) {
headers.push({
name: 'Authorization',
value: `Bearer $${server.bearerTokenEnvVar}`,
isEnvVar: true,
});
}
// Claude format: headers object (for compatibility)
if (server.headers) {
Object.entries(server.headers).forEach(([name, value]) => {
headers.push({ name, value });
});
}
return headers;
};
// Toggle header value visibility
const toggleHeaderValue = (headerName: string) => {
setShowHeaderValues(prev => ({
...prev,
[headerName]: !prev[headerName]
}));
};
// Handle toggle with optimistic update
const handleToggle = async () => {
@@ -111,6 +175,13 @@ export function CodexMcpEditableCard({
<span className="text-sm font-medium text-foreground">
{server.name}
</span>
{/* Transport type badge */}
{isHttp && (
<Badge variant="outline" className="text-xs text-blue-600 border-blue-300">
<Link className="w-3 h-3 mr-1" />
{formatMessage({ id: 'mcp.transport.http' })}
</Badge>
)}
{isEditable ? (
<>
{/* Editable badge with actions */}
@@ -141,8 +212,8 @@ export function CodexMcpEditableCard({
</>
)}
</div>
<p className="text-sm text-muted-foreground mt-1 font-mono">
{server.command} {server.args?.join(' ') || ''}
<p className="text-sm text-muted-foreground mt-1 font-mono truncate max-w-md" title={getServerSummary()}>
{getServerSummary()}
</p>
</div>
</div>
@@ -234,44 +305,100 @@ export function CodexMcpEditableCard({
{/* Expanded Content */}
{isExpanded && (
<div className="border-t border-border p-4 space-y-3 bg-muted/30">
{/* Command details */}
<div>
<p className="text-xs text-muted-foreground mb-1">{formatMessage({ id: 'mcp.command' })}</p>
<code className="text-sm bg-background px-2 py-1 rounded block overflow-x-auto">
{server.command}
</code>
</div>
{/* Args */}
{server.args && server.args.length > 0 && (
<div>
<p className="text-xs text-muted-foreground mb-1">{formatMessage({ id: 'mcp.args' })}</p>
<div className="flex flex-wrap gap-1">
{server.args.map((arg, idx) => (
<Badge key={idx} variant="outline" className="font-mono text-xs">
{arg}
</Badge>
))}
{/* HTTP Server Details */}
{isHttp && (
<>
{/* URL */}
<div>
<p className="text-xs text-muted-foreground mb-1">{formatMessage({ id: 'mcp.http.url' })}</p>
<code className="text-sm bg-background px-2 py-1 rounded block overflow-x-auto break-all">
{server.url}
</code>
</div>
</div>
{/* HTTP Headers */}
{getHttpHeaders().length > 0 && (
<div>
<p className="text-xs text-muted-foreground mb-1">{formatMessage({ id: 'mcp.http.headers' })}</p>
<div className="space-y-1">
{getHttpHeaders().map((header) => (
<div key={header.name} className="flex items-center gap-2 text-sm">
<Badge variant="secondary" className="font-mono">{header.name}</Badge>
<span className="text-muted-foreground">=</span>
<code className="text-xs bg-background px-2 py-1 rounded flex-1 overflow-x-auto">
{showHeaderValues[header.name] ? header.value : maskHeaderValue(header.value)}
</code>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0"
onClick={() => toggleHeaderValue(header.name)}
title={showHeaderValues[header.name]
? formatMessage({ id: 'mcp.http.hideValue' })
: formatMessage({ id: 'mcp.http.showValue' })}
>
{showHeaderValues[header.name] ? (
<EyeOff className="w-3 h-3 text-muted-foreground" />
) : (
<Eye className="w-3 h-3 text-muted-foreground" />
)}
</Button>
{header.isEnvVar && (
<Badge variant="outline" className="text-xs text-blue-500">
{formatMessage({ id: 'mcp.http.envVar' })}
</Badge>
)}
</div>
))}
</div>
</div>
)}
</>
)}
{/* Environment variables */}
{server.env && Object.keys(server.env).length > 0 && (
<div>
<p className="text-xs text-muted-foreground mb-1">{formatMessage({ id: 'mcp.env' })}</p>
<div className="space-y-1">
{Object.entries(server.env).map(([key, value]) => (
<div key={key} className="flex items-center gap-2 text-sm">
<Badge variant="secondary" className="font-mono">{key}</Badge>
<span className="text-muted-foreground">=</span>
<code className="text-xs bg-background px-2 py-1 rounded flex-1 overflow-x-auto">
{value as string}
</code>
</div>
))}
{/* STDIO Server Details */}
{isStdio && (
<>
{/* Command details */}
<div>
<p className="text-xs text-muted-foreground mb-1">{formatMessage({ id: 'mcp.command' })}</p>
<code className="text-sm bg-background px-2 py-1 rounded block overflow-x-auto">
{server.command}
</code>
</div>
</div>
{/* Args */}
{server.args && server.args.length > 0 && (
<div>
<p className="text-xs text-muted-foreground mb-1">{formatMessage({ id: 'mcp.args' })}</p>
<div className="flex flex-wrap gap-1">
{server.args.map((arg, idx) => (
<Badge key={idx} variant="outline" className="font-mono text-xs">
{arg}
</Badge>
))}
</div>
</div>
)}
{/* Environment variables */}
{server.env && Object.keys(server.env).length > 0 && (
<div>
<p className="text-xs text-muted-foreground mb-1">{formatMessage({ id: 'mcp.env' })}</p>
<div className="space-y-1">
{Object.entries(server.env).map(([key, value]) => (
<div key={key} className="flex items-center gap-2 text-sm">
<Badge variant="secondary" className="font-mono">{key}</Badge>
<span className="text-muted-foreground">=</span>
<code className="text-xs bg-background px-2 py-1 rounded flex-1 overflow-x-auto">
{value as string}
</code>
</div>
))}
</div>
</div>
)}
</>
)}
{/* Notice based on editable state */}

View File

@@ -17,7 +17,7 @@ import {
import { Checkbox } from '@/components/ui/Checkbox';
import { Badge } from '@/components/ui/Badge';
import { useMcpServers } from '@/hooks';
import { crossCliCopy, fetchCodexMcpServers } from '@/lib/api';
import { crossCliCopy, fetchCodexMcpServers, isHttpMcpServer, isStdioMcpServer } from '@/lib/api';
import { cn } from '@/lib/utils';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
@@ -41,7 +41,8 @@ export interface CrossCliCopyButtonProps {
interface ServerCheckboxItem {
name: string;
command: string;
/** Display text - command for STDIO, URL for HTTP */
displayText: string;
enabled: boolean;
selected: boolean;
}
@@ -78,7 +79,7 @@ export function CrossCliCopyButton({
setServerItems(
servers.map((s) => ({
name: s.name,
command: s.command,
displayText: isHttpMcpServer(s) ? s.url : (isStdioMcpServer(s) ? s.command : ''),
enabled: s.enabled,
selected: false,
}))
@@ -91,7 +92,7 @@ export function CrossCliCopyButton({
setServerItems(
(codex.servers ?? []).map((s) => ({
name: s.name,
command: s.command,
displayText: isHttpMcpServer(s) ? s.url : (isStdioMcpServer(s) ? s.command : ''),
enabled: s.enabled,
selected: false,
}))
@@ -281,7 +282,7 @@ export function CrossCliCopyButton({
)}
</div>
<p className="text-xs text-muted-foreground font-mono truncate">
{server.command}
{server.displayText}
</p>
</label>
</div>

View File

@@ -10,7 +10,7 @@ import { Checkbox } from '@/components/ui/Checkbox';
import { Badge } from '@/components/ui/Badge';
import { Button } from '@/components/ui/Button';
import { useMcpServers } from '@/hooks';
import { crossCliCopy, fetchCodexMcpServers } from '@/lib/api';
import { crossCliCopy, fetchCodexMcpServers, isHttpMcpServer, isStdioMcpServer } from '@/lib/api';
import { cn } from '@/lib/utils';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
@@ -25,7 +25,8 @@ export interface CrossCliSyncPanelProps {
interface ServerCheckboxItem {
name: string;
command: string;
/** Display text - command for STDIO, URL for HTTP */
displayText: string;
enabled: boolean;
selected: boolean;
}
@@ -64,7 +65,7 @@ export function CrossCliSyncPanel({ onSuccess, className }: CrossCliSyncPanelPro
setCodexServers(
(codex.servers ?? []).map((s) => ({
name: s.name,
command: s.command,
displayText: isHttpMcpServer(s) ? s.url : (isStdioMcpServer(s) ? s.command : ''),
enabled: s.enabled,
selected: false,
}))
@@ -323,7 +324,7 @@ export function CrossCliSyncPanel({ onSuccess, className }: CrossCliSyncPanelPro
)}
</div>
<p className="text-xs text-muted-foreground font-mono truncate">
{server.command}
{server.displayText}
</p>
</label>
</div>
@@ -421,7 +422,7 @@ export function CrossCliSyncPanel({ onSuccess, className }: CrossCliSyncPanelPro
)}
</div>
<p className="text-xs text-muted-foreground font-mono truncate">
{server.command}
{server.displayText}
</p>
</label>
</div>

View File

@@ -32,6 +32,8 @@ import {
saveMcpTemplate,
type McpServer,
type McpProjectConfigType,
isStdioMcpServer,
isHttpMcpServer,
} from '@/lib/api';
import { mcpServersKeys, useMcpTemplates } from '@/hooks';
import { cn } from '@/lib/utils';
@@ -122,11 +124,6 @@ function HttpHeadersInput({ headers, onChange, disabled }: HttpHeadersInputProps
});
};
const maskValue = (value: string) => {
if (!value) return '';
return '*'.repeat(Math.min(value.length, 8));
};
return (
<div className="space-y-2">
{headers.map((header) => (
@@ -246,9 +243,8 @@ export function McpServerDialog({
// 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';
// Use type guard to check for HTTP server
if (isHttpMcpServer(serverData)) return 'http';
return 'stdio';
}, []);
@@ -258,50 +254,56 @@ export function McpServerDialog({
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
// Parse HTTP headers if present (for HTTP servers)
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 (isHttpMcpServer(server)) {
// HTTP server - extract headers
if (server.httpHeaders) {
Object.entries(server.httpHeaders).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,
}
if (server.envHttpHeaders) {
// envHttpHeaders is an array of header names that get values from env vars
server.envHttpHeaders.forEach((headerName, idx) => {
httpHeaders.push({
id: `header-env-${idx}`,
name: headerName,
value: '', // Env var name is not stored in value
isEnvVar: true,
});
});
});
}
}
// Get STDIO fields safely using type guard
const stdioCommand = isStdioMcpServer(server) ? server.command : '';
const stdioArgs = isStdioMcpServer(server) ? (server.args || []) : [];
const stdioEnv = isStdioMcpServer(server) ? (server.env || {}) : {};
// Get HTTP fields safely using type guard
const httpUrl = isHttpMcpServer(server) ? server.url : '';
const httpBearerToken = isHttpMcpServer(server) ? (server.bearerTokenEnvVar || '') : '';
setFormData({
name: server.name,
command: server.command || '',
args: server.args || [],
env: server.env || {},
url: extendedServer.url || '',
command: stdioCommand,
args: stdioArgs,
env: stdioEnv,
url: httpUrl,
headers: httpHeaders,
bearerTokenEnvVar: extendedServer.bearer_token_env_var || '',
bearerTokenEnvVar: httpBearerToken,
scope: server.scope,
enabled: server.enabled,
});
setArgsInput((server.args || []).join(', '));
setArgsInput(stdioArgs.join(', '));
setEnvInput(
Object.entries(server.env || {})
Object.entries(stdioEnv)
.map(([k, v]) => `${k}=${v}`)
.join('\n')
);
@@ -488,50 +490,46 @@ export function McpServerDialog({
return;
}
// 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,
};
// Build server config based on transport type using discriminated union
let serverConfig: McpServer;
if (transportType === 'stdio') {
serverConfig.command = formData.command;
serverConfig.args = formData.args;
serverConfig.env = formData.env;
serverConfig = {
name: formData.name,
transport: 'stdio',
command: formData.command,
args: formData.args.length > 0 ? formData.args : undefined,
env: Object.keys(formData.env).length > 0 ? formData.env : undefined,
scope: formData.scope,
enabled: formData.enabled,
};
} else {
// HTTP transport
serverConfig.url = formData.url;
serverConfig.command = ''; // Empty command for HTTP servers
// Separate headers into static and env-based
// HTTP transport - separate headers into static and env-based
const httpHeaders: Record<string, string> = {};
const envHttpHeaders: Record<string, string> = {};
const envHttpHeaders: string[] = [];
formData.headers.forEach((h) => {
if (h.name.trim()) {
if (h.isEnvVar) {
envHttpHeaders[h.name.trim()] = h.value.trim();
// For env-based headers, store the header name that will be populated from env var
envHttpHeaders.push(h.name.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();
}
serverConfig = {
name: formData.name,
transport: 'http',
url: formData.url,
headers: Object.keys(httpHeaders).length > 0 ? httpHeaders : undefined,
httpHeaders: Object.keys(httpHeaders).length > 0 ? httpHeaders : undefined,
envHttpHeaders: envHttpHeaders.length > 0 ? envHttpHeaders : undefined,
bearerTokenEnvVar: formData.bearerTokenEnvVar.trim() || undefined,
scope: formData.scope,
enabled: formData.enabled,
};
}
// Save as template if checked (only for STDIO)

View File

@@ -19,12 +19,27 @@ import {
import { useProjectOperations, useMcpServerMutations } from '@/hooks';
import { cn } from '@/lib/utils';
import type { McpServer } from '@/lib/api';
import { isHttpMcpServer, isStdioMcpServer } from '@/lib/api';
// ========== Types ==========
export interface OtherProjectServer extends McpServer {
/**
* Server from another project
* Contains base server fields plus project metadata
*/
export interface OtherProjectServer {
name: string;
enabled: boolean;
/** Display text - command for STDIO, URL for HTTP */
displayText: string;
/** Transport type */
transport: 'stdio' | 'http';
/** Project path */
projectPath: string;
/** Project name */
projectName: string;
/** Original server data */
originalServer: McpServer;
}
export interface OtherProjectsSectionProps {
@@ -63,12 +78,17 @@ export function OtherProjectsSection({
for (const [path, serverList] of Object.entries(response.servers)) {
const projectName = path.split(/[/\\]/).filter(Boolean).pop() || path;
for (const server of (serverList as Omit<McpServer, 'scope'>[])) {
for (const server of (serverList as McpServer[])) {
servers.push({
...server,
scope: 'project',
name: server.name,
enabled: server.enabled,
displayText: isHttpMcpServer(server)
? server.url
: (isStdioMcpServer(server) ? server.command : ''),
transport: server.transport,
projectPath: path,
projectName,
originalServer: server,
});
}
}
@@ -88,14 +108,14 @@ export function OtherProjectsSection({
// Generate a unique name by combining project name and server name
const uniqueName = `${server.projectName}-${server.name}`.toLowerCase().replace(/\s+/g, '-');
await createServer({
// Create server based on transport type
const serverToCreate: McpServer = {
...server.originalServer,
name: uniqueName,
command: server.command,
args: server.args,
env: server.env,
scope: 'project',
enabled: server.enabled,
});
};
await createServer(serverToCreate);
onImportSuccess?.(uniqueName, server.projectPath);
} catch (error) {
@@ -203,7 +223,7 @@ export function OtherProjectsSection({
)}
</div>
<p className="text-xs text-muted-foreground font-mono truncate">
{server.command} {(server.args || []).join(' ')}
{server.displayText}
</p>
<p className="text-xs text-muted-foreground truncate">
<span className="font-medium">{server.projectName}</span>