Codex MCP installation (#345)

This commit is contained in:
Solomon
2025-08-08 18:24:04 +01:00
committed by GitHub
parent 1999986304
commit 0e7d4ddbdf
9 changed files with 343 additions and 135 deletions

View File

@@ -146,7 +146,8 @@ const shouldRenderMarkdown = (entryType: NormalizedEntryType) => {
entryType.tool_name.toLowerCase() === 'search' ||
entryType.tool_name.toLowerCase() === 'webfetch' ||
entryType.tool_name.toLowerCase() === 'web_fetch' ||
entryType.tool_name.toLowerCase() === 'task'))
entryType.tool_name.toLowerCase() === 'task' ||
entryType.tool_name.toLowerCase().startsWith('mcp_')))
);
};

View File

@@ -0,0 +1,233 @@
// Strategy pattern implementation for MCP server configuration handling
// across different base coding agents (Claude, Amp, Gemini, Opencode, Codex)
import { BaseCodingAgent } from 'shared/types';
export interface McpConfigStrategy {
// Get the default empty configuration structure for this executor (as JSON string for textarea)
getDefaultConfig(): string;
// Create the full configuration structure from servers data
createFullConfig(servers: Record<string, any>): Record<string, any>;
// Validate the full configuration structure
validateFullConfig(config: Record<string, any>): void;
// Extract the servers object from the full configuration for API calls
extractServersForApi(fullConfig: Record<string, any>): Record<string, any>;
// Create the vibe-kanban MCP server configuration for this executor
createVibeKanbanConfig(): Record<string, any>;
// Add vibe-kanban configuration to existing config
addVibeKanbanToConfig(
existingConfig: Record<string, any>,
vibeKanbanConfig: Record<string, any>
): Record<string, any>;
}
/**
* Standard MCP configuration strategy for Claude, Gemini, etc.
* Uses JSON with top-level "mcpServers"
*/
export class StandardMcpStrategy implements McpConfigStrategy {
getDefaultConfig(): string {
return '{\n "mcpServers": {\n }\n}';
}
createFullConfig(servers: Record<string, any>): Record<string, any> {
return { mcpServers: servers };
}
validateFullConfig(config: Record<string, any>): void {
if (!config.mcpServers || typeof config.mcpServers !== 'object') {
throw new Error('Configuration must contain an "mcpServers" object');
}
}
extractServersForApi(fullConfig: Record<string, any>): Record<string, any> {
return fullConfig.mcpServers;
}
createVibeKanbanConfig(): Record<string, any> {
return {
command: 'npx',
args: ['-y', 'vibe-kanban', '--mcp'],
};
}
addVibeKanbanToConfig(
existingConfig: Record<string, any>,
vibeKanbanConfig: Record<string, any>
): Record<string, any> {
return {
...existingConfig,
mcpServers: {
...(existingConfig.mcpServers || {}),
vibe_kanban: vibeKanbanConfig,
},
};
}
}
/**
* AMP-specific MCP configuration strategy
* Uses flat key "amp.mcpServers" in JSON
*/
export class AmpMcpStrategy implements McpConfigStrategy {
getDefaultConfig(): string {
return '{\n "amp.mcpServers": {\n }\n}';
}
createFullConfig(servers: Record<string, any>): Record<string, any> {
return { 'amp.mcpServers': servers };
}
validateFullConfig(config: Record<string, any>): void {
if (
!config['amp.mcpServers'] ||
typeof config['amp.mcpServers'] !== 'object'
) {
throw new Error(
'AMP configuration must contain an "amp.mcpServers" object'
);
}
}
extractServersForApi(fullConfig: Record<string, any>): Record<string, any> {
return fullConfig['amp.mcpServers'];
}
createVibeKanbanConfig(): Record<string, any> {
return {
command: 'npx',
args: ['-y', 'vibe-kanban', '--mcp'],
};
}
addVibeKanbanToConfig(
existingConfig: Record<string, any>,
vibeKanbanConfig: Record<string, any>
): Record<string, any> {
return {
...existingConfig,
'amp.mcpServers': {
...(existingConfig['amp.mcpServers'] || {}),
vibe_kanban: vibeKanbanConfig,
},
};
}
}
/**
* Opencode (SST Opencode)-specific MCP configuration strategy
* Uses JSON with top-level "mcp" plus $schema
*/
export class OpencodeMcpStrategy implements McpConfigStrategy {
getDefaultConfig(): string {
return '{\n "mcp": {\n }, "$schema": "https://opencode.ai/config.json"\n}';
}
createFullConfig(servers: Record<string, any>): Record<string, any> {
return {
mcp: servers,
$schema: 'https://opencode.ai/config.json',
};
}
validateFullConfig(config: Record<string, any>): void {
if (!config.mcp || typeof config.mcp !== 'object') {
throw new Error('Configuration must contain an "mcp" object');
}
}
extractServersForApi(fullConfig: Record<string, any>): Record<string, any> {
return fullConfig.mcp;
}
createVibeKanbanConfig(): Record<string, any> {
return {
type: 'local',
command: ['npx', '-y', 'vibe-kanban', '--mcp'],
enabled: true,
};
}
addVibeKanbanToConfig(
existingConfig: Record<string, any>,
vibeKanbanConfig: Record<string, any>
): Record<string, any> {
return {
...existingConfig,
mcp: {
...(existingConfig.mcp || {}),
vibe_kanban: vibeKanbanConfig,
},
};
}
}
/**
* Codex-specific MCP configuration strategy
* Frontend works with JSON using key "mcp_servers"; backend converts to TOML.
*/
export class CodexMcpStrategy implements McpConfigStrategy {
getDefaultConfig(): string {
// Although Codex uses TOML on disk, the frontend textarea is JSON.
return '{\n "mcp_servers": {\n }\n}';
}
createFullConfig(servers: Record<string, any>): Record<string, any> {
return { mcp_servers: servers };
}
validateFullConfig(config: Record<string, any>): void {
if (!config.mcp_servers || typeof config.mcp_servers !== 'object') {
throw new Error('Configuration must contain an "mcp_servers" object');
}
}
extractServersForApi(fullConfig: Record<string, any>): Record<string, any> {
return fullConfig.mcp_servers;
}
createVibeKanbanConfig(): Record<string, any> {
return {
command: 'npx',
args: ['-y', 'vibe-kanban', '--mcp'],
};
}
addVibeKanbanToConfig(
existingConfig: Record<string, any>,
vibeKanbanConfig: Record<string, any>
): Record<string, any> {
return {
...existingConfig,
mcp_servers: {
...(existingConfig.mcp_servers || {}),
vibe_kanban: vibeKanbanConfig,
},
};
}
}
/**
* Factory to get the appropriate MCP strategy for a BaseCodingAgent
*/
export function getMcpStrategyByAgent(
agent: BaseCodingAgent
): McpConfigStrategy {
switch (agent) {
case BaseCodingAgent.AMP:
return new AmpMcpStrategy();
case BaseCodingAgent.OPENCODE:
return new OpencodeMcpStrategy();
case BaseCodingAgent.CODEX:
return new CodexMcpStrategy();
case BaseCodingAgent.CLAUDE_CODE:
case BaseCodingAgent.GEMINI:
default:
return new StandardMcpStrategy();
}
}

View File

@@ -18,9 +18,10 @@ import { Label } from '@/components/ui/label';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Textarea } from '@/components/ui/textarea';
import { Loader2 } from 'lucide-react';
import { BaseCodingAgent, AgentProfile } from 'shared/types';
import { AgentProfile } from 'shared/types';
import { useUserSystem } from '@/components/config-provider';
import { mcpServersApi } from '../lib/api';
import { getMcpStrategyByAgent } from '../lib/mcp-strategies';
export function McpServers() {
const { config, profiles } = useUserSystem();
@@ -55,13 +56,9 @@ export function McpServers() {
setMcpLoading(true);
setMcpError(null);
// Set default empty config based on agent type
const defaultConfig =
profile.agent === BaseCodingAgent.AMP
? '{\n "amp.mcpServers": {\n }\n}'
: profile.agent === BaseCodingAgent.OPENCODE
? '{\n "mcp": {\n }, "$schema": "https://opencode.ai/config.json"\n}'
: '{\n "mcpServers": {\n }\n}';
// Set default empty config based on agent type using strategy
const strategy = getMcpStrategyByAgent(profile.agent);
const defaultConfig = strategy.getDefaultConfig();
setMcpServers(defaultConfig);
setMcpConfigPath('');
@@ -73,21 +70,9 @@ export function McpServers() {
const servers = data.servers || {};
const configPath = data.config_path || '';
// Create the full configuration structure based on agent type
let fullConfig;
if (profile.agent === BaseCodingAgent.AMP) {
// For AMP, use the amp.mcpServers structure
fullConfig = { 'amp.mcpServers': servers };
} else if (profile.agent === BaseCodingAgent.OPENCODE) {
fullConfig = {
mcp: servers,
$schema: 'https://opencode.ai/config.json',
};
} else {
// For other executors, use the standard mcpServers structure
fullConfig = { mcpServers: servers };
}
// Create the full configuration structure using strategy
const strategy = getMcpStrategyByAgent(profile.agent);
const fullConfig = strategy.createFullConfig(servers);
const configJson = JSON.stringify(fullConfig, null, 2);
setMcpServers(configJson);
setMcpConfigPath(configPath);
@@ -116,27 +101,15 @@ export function McpServers() {
if (value.trim() && selectedProfile) {
try {
const config = JSON.parse(value);
// Validate that the config has the expected structure based on agent type
if (selectedProfile.agent === BaseCodingAgent.AMP) {
if (
!config['amp.mcpServers'] ||
typeof config['amp.mcpServers'] !== 'object'
) {
setMcpError(
'AMP configuration must contain an "amp.mcpServers" object'
);
}
} else if (selectedProfile.agent === BaseCodingAgent.OPENCODE) {
if (!config.mcp || typeof config.mcp !== 'object') {
setMcpError('Configuration must contain an "mcp" object');
}
} else {
if (!config.mcpServers || typeof config.mcpServers !== 'object') {
setMcpError('Configuration must contain an "mcpServers" object');
}
}
// Validate that the config has the expected structure using strategy
const strategy = getMcpStrategyByAgent(selectedProfile.agent);
strategy.validateFullConfig(config);
} catch (err) {
setMcpError('Invalid JSON format');
if (err instanceof SyntaxError) {
setMcpError('Invalid JSON format');
} else {
setMcpError(err instanceof Error ? err.message : 'Validation error');
}
}
}
};
@@ -148,46 +121,15 @@ export function McpServers() {
// Parse existing configuration
const existingConfig = mcpServers.trim() ? JSON.parse(mcpServers) : {};
// Always use production MCP installation instructions
const vibeKanbanConfig =
selectedProfile.agent === BaseCodingAgent.OPENCODE
? {
type: 'local',
command: ['npx', '-y', 'vibe-kanban', '--mcp'],
enabled: true,
}
: {
command: 'npx',
args: ['-y', 'vibe-kanban', '--mcp'],
};
// Use strategy to create vibe-kanban configuration
const strategy = getMcpStrategyByAgent(selectedProfile.agent);
const vibeKanbanConfig = strategy.createVibeKanbanConfig();
// Add vibe_kanban to the existing configuration
let updatedConfig;
if (selectedProfile.agent === BaseCodingAgent.AMP) {
updatedConfig = {
...existingConfig,
'amp.mcpServers': {
...(existingConfig['amp.mcpServers'] || {}),
vibe_kanban: vibeKanbanConfig,
},
};
} else if (selectedProfile.agent === BaseCodingAgent.OPENCODE) {
updatedConfig = {
...existingConfig,
mcp: {
...(existingConfig.mcp || {}),
vibe_kanban: vibeKanbanConfig,
},
};
} else {
updatedConfig = {
...existingConfig,
mcpServers: {
...(existingConfig.mcpServers || {}),
vibe_kanban: vibeKanbanConfig,
},
};
}
// Add vibe_kanban to the existing configuration using strategy
const updatedConfig = strategy.addVibeKanbanToConfig(
existingConfig,
vibeKanbanConfig
);
// Update the textarea with the new configuration
const configJson = JSON.stringify(updatedConfig, null, 2);
@@ -211,37 +153,12 @@ export function McpServers() {
try {
const fullConfig = JSON.parse(mcpServers);
// Validate that the config has the expected structure based on agent type
let mcpServersConfig;
if (selectedProfile.agent === BaseCodingAgent.AMP) {
if (
!fullConfig['amp.mcpServers'] ||
typeof fullConfig['amp.mcpServers'] !== 'object'
) {
throw new Error(
'AMP configuration must contain an "amp.mcpServers" object'
);
}
// Extract just the inner servers object for the API - backend will handle nesting
mcpServersConfig = fullConfig['amp.mcpServers'];
} else if (selectedProfile.agent === BaseCodingAgent.OPENCODE) {
if (!fullConfig.mcp || typeof fullConfig.mcp !== 'object') {
throw new Error('Configuration must contain an "mcp" object');
}
// Extract just the mcp part for the API
mcpServersConfig = fullConfig.mcp;
} else {
if (
!fullConfig.mcpServers ||
typeof fullConfig.mcpServers !== 'object'
) {
throw new Error(
'Configuration must contain an "mcpServers" object'
);
}
// Extract just the mcpServers part for the API
mcpServersConfig = fullConfig.mcpServers;
}
// Use strategy to validate and extract servers config
const strategy = getMcpStrategyByAgent(selectedProfile.agent);
strategy.validateFullConfig(fullConfig);
// Extract just the servers object for the API - backend will handle nesting/format
const mcpServersConfig = strategy.extractServersForApi(fullConfig);
await mcpServersApi.save(selectedProfile.agent, mcpServersConfig);
@@ -349,7 +266,8 @@ export function McpServers() {
<p>{mcpError}</p>
<p className="mt-1">
To use MCP servers, please select a different profile
(Claude, Amp, or Gemini) above.
that supports MCP (Claude, Amp, Gemini, Codex, or
Opencode) above.
</p>
</div>
</div>