feat: add MCP server support for AI agents

Add Model Context Protocol (MCP) server integration to extend AI agent
capabilities with external tools. This allows users to configure MCP
servers (stdio, SSE, HTTP) in global settings and have agents use them.

Note: MCP servers are currently configured globally. Per-project MCP
server configuration is planned for a future update.

Features:
- New MCP Servers settings section with full CRUD operations
- Import/Export JSON configs (Claude Code format compatible)
- Configurable permission settings:
  - Auto-approve MCP tools (bypass permission prompts)
  - Unrestricted tools (allow all tools when MCP enabled)
- Refresh button to reload from settings file

Implementation:
- Added MCPServerConfig and MCPToolInfo types
- Added store actions for MCP server management
- Updated claude-provider to use configurable MCP permissions
- Updated sdk-options factory functions for MCP support
- Added settings helpers for loading MCP configs
This commit is contained in:
M Zubair
2025-12-28 00:51:50 +01:00
parent f7a0365bee
commit 5f328a4c13
20 changed files with 1375 additions and 43 deletions

View File

@@ -28,6 +28,7 @@
"@automaker/prompts": "^1.0.0",
"@automaker/types": "^1.0.0",
"@automaker/utils": "^1.0.0",
"@modelcontextprotocol/sdk": "^1.25.1",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.2.1",

View File

@@ -18,7 +18,7 @@
import type { Options } from '@anthropic-ai/claude-agent-sdk';
import path from 'path';
import { resolveModelString } from '@automaker/model-resolver';
import { DEFAULT_MODELS, CLAUDE_MODEL_MAP } from '@automaker/types';
import { DEFAULT_MODELS, CLAUDE_MODEL_MAP, type McpServerConfig } from '@automaker/types';
import { isPathAllowed, PathNotAllowedError, getAllowedRootDirectory } from '@automaker/platform';
/**
@@ -136,6 +136,53 @@ function getBaseOptions(): Partial<Options> {
};
}
/**
* MCP permission options result
*/
interface McpPermissionOptions {
/** Whether tools should be restricted to a preset */
shouldRestrictTools: boolean;
/** Options to spread when MCP bypass is enabled */
bypassOptions: Partial<Options>;
/** Options to spread for MCP servers */
mcpServerOptions: Partial<Options>;
}
/**
* Build MCP-related options based on configuration.
* Centralizes the logic for determining permission modes and tool restrictions
* when MCP servers are configured.
*
* @param config - The SDK options config
* @returns Object with MCP permission settings to spread into final options
*/
function buildMcpOptions(config: CreateSdkOptionsConfig): McpPermissionOptions {
const hasMcpServers = config.mcpServers && Object.keys(config.mcpServers).length > 0;
// Default to true - this is a deliberate design choice for ease of use with MCP servers.
// Users can disable these in settings for stricter security.
const mcpAutoApprove = config.mcpAutoApproveTools ?? true;
const mcpUnrestricted = config.mcpUnrestrictedTools ?? true;
// Determine if we should bypass permissions based on settings
const shouldBypassPermissions = hasMcpServers && mcpAutoApprove;
// Determine if we should restrict tools (only when no MCP or unrestricted is disabled)
const shouldRestrictTools = !hasMcpServers || !mcpUnrestricted;
return {
shouldRestrictTools,
// Only include bypass options when MCP is configured and auto-approve is enabled
bypassOptions: shouldBypassPermissions
? {
permissionMode: 'bypassPermissions' as const,
// Required flag when using bypassPermissions mode
allowDangerouslySkipPermissions: true,
}
: {},
// Include MCP servers if configured
mcpServerOptions: config.mcpServers ? { mcpServers: config.mcpServers } : {},
};
}
/**
* Build system prompt configuration based on autoLoadClaudeMd setting.
* When autoLoadClaudeMd is true:
@@ -219,8 +266,25 @@ export interface CreateSdkOptionsConfig {
/** Enable sandbox mode for bash command isolation */
enableSandboxMode?: boolean;
/** MCP servers to make available to the agent */
mcpServers?: Record<string, McpServerConfig>;
/** Auto-approve MCP tool calls without permission prompts */
mcpAutoApproveTools?: boolean;
/** Allow unrestricted tools when MCP servers are enabled */
mcpUnrestrictedTools?: boolean;
}
// Re-export MCP types from @automaker/types for convenience
export type {
McpServerConfig,
McpStdioServerConfig,
McpSSEServerConfig,
McpHttpServerConfig,
} from '@automaker/types';
/**
* Create SDK options for spec generation
*
@@ -330,12 +394,18 @@ export function createChatOptions(config: CreateSdkOptionsConfig): Options {
// Build CLAUDE.md auto-loading options if enabled
const claudeMdOptions = buildClaudeMdOptions(config);
// Build MCP-related options
const mcpOptions = buildMcpOptions(config);
return {
...getBaseOptions(),
model: getModelForUseCase('chat', effectiveModel),
maxTurns: MAX_TURNS.standard,
cwd: config.cwd,
allowedTools: [...TOOL_PRESETS.chat],
// Only restrict tools if no MCP servers configured or unrestricted is disabled
...(mcpOptions.shouldRestrictTools && { allowedTools: [...TOOL_PRESETS.chat] }),
// Apply MCP bypass options if configured
...mcpOptions.bypassOptions,
...(config.enableSandboxMode && {
sandbox: {
enabled: true,
@@ -344,6 +414,7 @@ export function createChatOptions(config: CreateSdkOptionsConfig): Options {
}),
...claudeMdOptions,
...(config.abortController && { abortController: config.abortController }),
...mcpOptions.mcpServerOptions,
};
}
@@ -364,12 +435,18 @@ export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options {
// Build CLAUDE.md auto-loading options if enabled
const claudeMdOptions = buildClaudeMdOptions(config);
// Build MCP-related options
const mcpOptions = buildMcpOptions(config);
return {
...getBaseOptions(),
model: getModelForUseCase('auto', config.model),
maxTurns: MAX_TURNS.maximum,
cwd: config.cwd,
allowedTools: [...TOOL_PRESETS.fullAccess],
// Only restrict tools if no MCP servers configured or unrestricted is disabled
...(mcpOptions.shouldRestrictTools && { allowedTools: [...TOOL_PRESETS.fullAccess] }),
// Apply MCP bypass options if configured
...mcpOptions.bypassOptions,
...(config.enableSandboxMode && {
sandbox: {
enabled: true,
@@ -378,6 +455,7 @@ export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options {
}),
...claudeMdOptions,
...(config.abortController && { abortController: config.abortController }),
...mcpOptions.mcpServerOptions,
};
}
@@ -400,14 +478,27 @@ export function createCustomOptions(
// Build CLAUDE.md auto-loading options if enabled
const claudeMdOptions = buildClaudeMdOptions(config);
// Build MCP-related options
const mcpOptions = buildMcpOptions(config);
// For custom options: use explicit allowedTools if provided, otherwise use preset based on MCP settings
const effectiveAllowedTools = config.allowedTools
? [...config.allowedTools]
: mcpOptions.shouldRestrictTools
? [...TOOL_PRESETS.readOnly]
: undefined;
return {
...getBaseOptions(),
model: getModelForUseCase('default', config.model),
maxTurns: config.maxTurns ?? MAX_TURNS.maximum,
cwd: config.cwd,
allowedTools: config.allowedTools ? [...config.allowedTools] : [...TOOL_PRESETS.readOnly],
...(effectiveAllowedTools && { allowedTools: effectiveAllowedTools }),
...(config.sandbox && { sandbox: config.sandbox }),
// Apply MCP bypass options if configured
...mcpOptions.bypassOptions,
...claudeMdOptions,
...(config.abortController && { abortController: config.abortController }),
...mcpOptions.mcpServerOptions,
};
}

View File

@@ -4,6 +4,7 @@
import type { SettingsService } from '../services/settings-service.js';
import type { ContextFilesResult, ContextFileInfo } from '@automaker/utils';
import type { MCPServerConfig, McpServerConfig } from '@automaker/types';
/**
* Get the autoLoadClaudeMd setting, with project settings taking precedence over global.
@@ -136,3 +137,120 @@ function formatContextFileEntry(file: ContextFileInfo): string {
const descriptionInfo = file.description ? `\n**Purpose:** ${file.description}` : '';
return `${header}\n${pathInfo}${descriptionInfo}\n\n${file.content}`;
}
/**
* Get enabled MCP servers from global settings, converted to SDK format.
* Returns an empty object if settings service is not available or no servers are configured.
*
* @param settingsService - Optional settings service instance
* @param logPrefix - Prefix for log messages (e.g., '[AgentService]')
* @returns Promise resolving to MCP servers in SDK format (keyed by name)
*/
export async function getMCPServersFromSettings(
settingsService?: SettingsService | null,
logPrefix = '[SettingsHelper]'
): Promise<Record<string, McpServerConfig>> {
if (!settingsService) {
return {};
}
try {
const globalSettings = await settingsService.getGlobalSettings();
const mcpServers = globalSettings.mcpServers || [];
// Filter to only enabled servers and convert to SDK format
const enabledServers = mcpServers.filter((s) => s.enabled !== false);
if (enabledServers.length === 0) {
return {};
}
// Convert settings format to SDK format (keyed by name)
const sdkServers: Record<string, McpServerConfig> = {};
for (const server of enabledServers) {
sdkServers[server.name] = convertToSdkFormat(server);
}
console.log(
`${logPrefix} Loaded ${enabledServers.length} MCP server(s): ${enabledServers.map((s) => s.name).join(', ')}`
);
return sdkServers;
} catch (error) {
console.error(`${logPrefix} Failed to load MCP servers setting:`, error);
return {};
}
}
/**
* Get MCP permission settings from global settings.
*
* @param settingsService - Optional settings service instance
* @param logPrefix - Prefix for log messages (e.g., '[AgentService]')
* @returns Promise resolving to MCP permission settings
*/
export async function getMCPPermissionSettings(
settingsService?: SettingsService | null,
logPrefix = '[SettingsHelper]'
): Promise<{ mcpAutoApproveTools: boolean; mcpUnrestrictedTools: boolean }> {
// Default values (both enabled for backwards compatibility)
const defaults = { mcpAutoApproveTools: true, mcpUnrestrictedTools: true };
if (!settingsService) {
return defaults;
}
try {
const globalSettings = await settingsService.getGlobalSettings();
const result = {
mcpAutoApproveTools: globalSettings.mcpAutoApproveTools ?? true,
mcpUnrestrictedTools: globalSettings.mcpUnrestrictedTools ?? true,
};
console.log(
`${logPrefix} MCP permission settings: autoApprove=${result.mcpAutoApproveTools}, unrestricted=${result.mcpUnrestrictedTools}`
);
return result;
} catch (error) {
console.error(`${logPrefix} Failed to load MCP permission settings:`, error);
return defaults;
}
}
/**
* Convert a settings MCPServerConfig to SDK McpServerConfig format.
* Validates required fields and throws informative errors if missing.
*/
function convertToSdkFormat(server: MCPServerConfig): McpServerConfig {
if (server.type === 'sse') {
if (!server.url) {
throw new Error(`SSE MCP server "${server.name}" is missing a URL.`);
}
return {
type: 'sse',
url: server.url,
headers: server.headers,
};
}
if (server.type === 'http') {
if (!server.url) {
throw new Error(`HTTP MCP server "${server.name}" is missing a URL.`);
}
return {
type: 'http',
url: server.url,
headers: server.headers,
};
}
// Default to stdio
if (!server.command) {
throw new Error(`Stdio MCP server "${server.name}" is missing a command.`);
}
return {
type: 'stdio',
command: server.command,
args: server.args,
env: server.env,
};
}

View File

@@ -36,16 +36,32 @@ export class ClaudeProvider extends BaseProvider {
} = options;
// Build Claude SDK options
// MCP permission logic - determines how to handle tool permissions when MCP servers are configured.
// This logic mirrors buildMcpOptions() in sdk-options.ts but is applied here since
// the provider is the final point where SDK options are constructed.
const hasMcpServers = options.mcpServers && Object.keys(options.mcpServers).length > 0;
// Default to true - deliberate design choice for ease of use. Users can disable in settings.
const mcpAutoApprove = options.mcpAutoApproveTools ?? true;
const mcpUnrestricted = options.mcpUnrestrictedTools ?? true;
const defaultTools = ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch'];
const toolsToUse = allowedTools || defaultTools;
// Determine permission mode based on settings
const shouldBypassPermissions = hasMcpServers && mcpAutoApprove;
// Determine if we should restrict tools (only when no MCP or unrestricted is disabled)
const shouldRestrictTools = !hasMcpServers || !mcpUnrestricted;
const sdkOptions: Options = {
model,
systemPrompt,
maxTurns,
cwd,
allowedTools: toolsToUse,
permissionMode: 'default',
// Only restrict tools if explicitly set OR (no MCP / unrestricted disabled)
...(allowedTools && shouldRestrictTools && { allowedTools }),
...(!allowedTools && shouldRestrictTools && { allowedTools: defaultTools }),
// When MCP servers are configured and auto-approve is enabled, use bypassPermissions
permissionMode: shouldBypassPermissions ? 'bypassPermissions' : 'default',
// Required when using bypassPermissions mode
...(shouldBypassPermissions && { allowDangerouslySkipPermissions: true }),
abortController,
// Resume existing SDK session if we have a session ID
...(sdkSessionId && conversationHistory && conversationHistory.length > 0
@@ -55,6 +71,8 @@ export class ClaudeProvider extends BaseProvider {
...(options.settingSources && { settingSources: options.settingSources }),
// Forward sandbox configuration
...(options.sandbox && { sandbox: options.sandbox }),
// Forward MCP servers configuration
...(options.mcpServers && { mcpServers: options.mcpServers }),
};
// Build prompt payload

View File

@@ -1,41 +1,19 @@
/**
* Shared types for AI model providers
*
* Re-exports types from @automaker/types for consistency across the codebase.
*/
/**
* Configuration for a provider instance
*/
export interface ProviderConfig {
apiKey?: string;
cliPath?: string;
env?: Record<string, string>;
}
/**
* Message in conversation history
*/
export interface ConversationMessage {
role: 'user' | 'assistant';
content: string | Array<{ type: string; text?: string; source?: object }>;
}
/**
* Options for executing a query via a provider
*/
export interface ExecuteOptions {
prompt: string | Array<{ type: string; text?: string; source?: object }>;
model: string;
cwd: string;
systemPrompt?: string | { type: 'preset'; preset: 'claude_code'; append?: string };
maxTurns?: number;
allowedTools?: string[];
mcpServers?: Record<string, unknown>;
abortController?: AbortController;
conversationHistory?: ConversationMessage[]; // Previous messages for context
sdkSessionId?: string; // Claude SDK session ID for resuming conversations
settingSources?: Array<'user' | 'project' | 'local'>; // Claude filesystem settings to load
sandbox?: { enabled: boolean; autoAllowBashIfSandboxed?: boolean }; // Sandbox configuration
}
// Re-export all provider types from @automaker/types
export type {
ProviderConfig,
ConversationMessage,
ExecuteOptions,
McpServerConfig,
McpStdioServerConfig,
McpSSEServerConfig,
McpHttpServerConfig,
} from '@automaker/types';
/**
* Content block in a provider message (matches Claude SDK format)

View File

@@ -21,6 +21,8 @@ import {
getAutoLoadClaudeMdSetting,
getEnableSandboxModeSetting,
filterClaudeMdFromContext,
getMCPServersFromSettings,
getMCPPermissionSettings,
} from '../lib/settings-helpers.js';
interface Message {
@@ -227,6 +229,12 @@ export class AgentService {
'[AgentService]'
);
// Load MCP servers from settings (global setting only)
const mcpServers = await getMCPServersFromSettings(this.settingsService, '[AgentService]');
// Load MCP permission settings (global setting only)
const mcpPermissions = await getMCPPermissionSettings(this.settingsService, '[AgentService]');
// Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.)
const contextResult = await loadContextFiles({
projectPath: effectiveWorkDir,
@@ -252,6 +260,9 @@ export class AgentService {
abortController: session.abortController!,
autoLoadClaudeMd,
enableSandboxMode,
mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined,
mcpAutoApproveTools: mcpPermissions.mcpAutoApproveTools,
mcpUnrestrictedTools: mcpPermissions.mcpUnrestrictedTools,
});
// Extract model, maxTurns, and allowedTools from SDK options
@@ -275,6 +286,9 @@ export class AgentService {
settingSources: sdkOptions.settingSources,
sandbox: sdkOptions.sandbox, // Pass sandbox configuration
sdkSessionId: session.sdkSessionId, // Pass SDK session ID for resuming
mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined, // Pass MCP servers configuration
mcpAutoApproveTools: mcpPermissions.mcpAutoApproveTools, // Pass MCP auto-approve setting
mcpUnrestrictedTools: mcpPermissions.mcpUnrestrictedTools, // Pass MCP unrestricted tools setting
};
// Build prompt content with images

View File

@@ -36,6 +36,8 @@ import {
getAutoLoadClaudeMdSetting,
getEnableSandboxModeSetting,
filterClaudeMdFromContext,
getMCPServersFromSettings,
getMCPPermissionSettings,
} from '../lib/settings-helpers.js';
const execAsync = promisify(exec);
@@ -1841,6 +1843,12 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
// Load enableSandboxMode setting (global setting only)
const enableSandboxMode = await getEnableSandboxModeSetting(this.settingsService, '[AutoMode]');
// Load MCP servers from settings (global setting only)
const mcpServers = await getMCPServersFromSettings(this.settingsService, '[AutoMode]');
// Load MCP permission settings (global setting only)
const mcpPermissions = await getMCPPermissionSettings(this.settingsService, '[AutoMode]');
// Build SDK options using centralized configuration for feature implementation
const sdkOptions = createAutoModeOptions({
cwd: workDir,
@@ -1848,6 +1856,9 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
abortController,
autoLoadClaudeMd,
enableSandboxMode,
mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined,
mcpAutoApproveTools: mcpPermissions.mcpAutoApproveTools,
mcpUnrestrictedTools: mcpPermissions.mcpUnrestrictedTools,
});
// Extract model, maxTurns, and allowedTools from SDK options
@@ -1889,6 +1900,9 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
systemPrompt: sdkOptions.systemPrompt,
settingSources: sdkOptions.settingSources,
sandbox: sdkOptions.sandbox, // Pass sandbox configuration
mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined, // Pass MCP servers configuration
mcpAutoApproveTools: mcpPermissions.mcpAutoApproveTools, // Pass MCP auto-approve setting
mcpUnrestrictedTools: mcpPermissions.mcpUnrestrictedTools, // Pass MCP unrestricted tools setting
};
// Execute via provider
@@ -2116,6 +2130,9 @@ After generating the revised spec, output:
cwd: workDir,
allowedTools: allowedTools,
abortController,
mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined,
mcpAutoApproveTools: mcpPermissions.mcpAutoApproveTools,
mcpUnrestrictedTools: mcpPermissions.mcpUnrestrictedTools,
});
let revisionText = '';
@@ -2253,6 +2270,9 @@ After generating the revised spec, output:
cwd: workDir,
allowedTools: allowedTools,
abortController,
mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined,
mcpAutoApproveTools: mcpPermissions.mcpAutoApproveTools,
mcpUnrestrictedTools: mcpPermissions.mcpUnrestrictedTools,
});
let taskOutput = '';
@@ -2342,6 +2362,9 @@ Implement all the changes described in the plan above.`;
cwd: workDir,
allowedTools: allowedTools,
abortController,
mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined,
mcpAutoApproveTools: mcpPermissions.mcpAutoApproveTools,
mcpUnrestrictedTools: mcpPermissions.mcpUnrestrictedTools,
});
for await (const msg of continuationStream) {

View File

@@ -178,6 +178,13 @@ export function ContextView() {
// Ensure context directory exists
await api.mkdir(contextPath);
// Ensure metadata file exists (create empty one if not)
const metadataPath = `${contextPath}/context-metadata.json`;
const metadataExists = await api.exists(metadataPath);
if (!metadataExists) {
await api.writeFile(metadataPath, JSON.stringify({ files: {} }, null, 2));
}
// Load metadata for descriptions
const metadata = await loadMetadata();

View File

@@ -18,6 +18,7 @@ import { AudioSection } from './settings-view/audio/audio-section';
import { KeyboardShortcutsSection } from './settings-view/keyboard-shortcuts/keyboard-shortcuts-section';
import { FeatureDefaultsSection } from './settings-view/feature-defaults/feature-defaults-section';
import { DangerZoneSection } from './settings-view/danger-zone/danger-zone-section';
import { MCPServersSection } from './settings-view/mcp-servers';
import type { Project as SettingsProject, Theme } from './settings-view/shared/types';
import type { Project as ElectronProject } from '@/lib/electron';
@@ -116,6 +117,8 @@ export function SettingsView() {
{showUsageTracking && <ClaudeUsageSection />}
</div>
);
case 'mcp-servers':
return <MCPServersSection />;
case 'ai-enhancement':
return <AIEnhancementSection />;
case 'appearance':

View File

@@ -9,6 +9,7 @@ import {
FlaskConical,
Trash2,
Sparkles,
Plug,
} from 'lucide-react';
import type { SettingsViewId } from '../hooks/use-settings-view';
@@ -22,6 +23,7 @@ export interface NavigationItem {
export const NAV_ITEMS: NavigationItem[] = [
{ id: 'api-keys', label: 'API Keys', icon: Key },
{ id: 'claude', label: 'Claude', icon: Terminal },
{ id: 'mcp-servers', label: 'MCP Servers', icon: Plug },
{ id: 'ai-enhancement', label: 'AI Enhancement', icon: Sparkles },
{ id: 'appearance', label: 'Appearance', icon: Palette },
{ id: 'terminal', label: 'Terminal', icon: SquareTerminal },

View File

@@ -3,6 +3,7 @@ import { useState, useCallback } from 'react';
export type SettingsViewId =
| 'api-keys'
| 'claude'
| 'mcp-servers'
| 'ai-enhancement'
| 'appearance'
| 'terminal'

View File

@@ -0,0 +1 @@
export { MCPServersSection } from './mcp-servers-section';

View File

@@ -0,0 +1,647 @@
import { useState, useEffect } from 'react';
import { useAppStore } from '@/store/app-store';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
Plug,
Plus,
Pencil,
Trash2,
Terminal,
Globe,
FileJson,
Download,
RefreshCw,
} from 'lucide-react';
import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';
import { toast } from 'sonner';
import type { MCPServerConfig } from '@automaker/types';
import { syncSettingsToServer, loadMCPServersFromServer } from '@/hooks/use-settings-migration';
type ServerType = 'stdio' | 'sse' | 'http';
interface ServerFormData {
name: string;
description: string;
type: ServerType;
command: string;
args: string;
url: string;
}
const defaultFormData: ServerFormData = {
name: '',
description: '',
type: 'stdio',
command: '',
args: '',
url: '',
};
export function MCPServersSection() {
const {
mcpServers,
addMCPServer,
updateMCPServer,
removeMCPServer,
mcpAutoApproveTools,
mcpUnrestrictedTools,
setMcpAutoApproveTools,
setMcpUnrestrictedTools,
} = useAppStore();
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
const [editingServer, setEditingServer] = useState<MCPServerConfig | null>(null);
const [formData, setFormData] = useState<ServerFormData>(defaultFormData);
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null);
const [isImportDialogOpen, setIsImportDialogOpen] = useState(false);
const [importJson, setImportJson] = useState('');
const [isRefreshing, setIsRefreshing] = useState(false);
// Auto-load MCP servers from settings file on mount
useEffect(() => {
loadMCPServersFromServer().catch((error) => {
console.error('Failed to load MCP servers on mount:', error);
});
}, []);
const handleRefresh = async () => {
setIsRefreshing(true);
try {
const success = await loadMCPServersFromServer();
if (success) {
toast.success('MCP servers refreshed from settings');
} else {
toast.error('Failed to refresh MCP servers');
}
} catch (error) {
toast.error('Error refreshing MCP servers');
} finally {
setIsRefreshing(false);
}
};
const handleOpenAddDialog = () => {
setFormData(defaultFormData);
setEditingServer(null);
setIsAddDialogOpen(true);
};
const handleOpenEditDialog = (server: MCPServerConfig) => {
setFormData({
name: server.name,
description: server.description || '',
type: server.type || 'stdio',
command: server.command || '',
args: server.args?.join(' ') || '',
url: server.url || '',
});
setEditingServer(server);
setIsAddDialogOpen(true);
};
const handleCloseDialog = () => {
setIsAddDialogOpen(false);
setEditingServer(null);
setFormData(defaultFormData);
};
const handleSave = async () => {
if (!formData.name.trim()) {
toast.error('Server name is required');
return;
}
if (formData.type === 'stdio' && !formData.command.trim()) {
toast.error('Command is required for stdio servers');
return;
}
if ((formData.type === 'sse' || formData.type === 'http') && !formData.url.trim()) {
toast.error('URL is required for SSE/HTTP servers');
return;
}
const serverData: Omit<MCPServerConfig, 'id'> = {
name: formData.name.trim(),
description: formData.description.trim() || undefined,
type: formData.type,
enabled: editingServer?.enabled ?? true,
};
if (formData.type === 'stdio') {
serverData.command = formData.command.trim();
if (formData.args.trim()) {
serverData.args = formData.args.trim().split(/\s+/);
}
} else {
serverData.url = formData.url.trim();
}
if (editingServer) {
updateMCPServer(editingServer.id, serverData);
toast.success('MCP server updated');
} else {
addMCPServer(serverData);
toast.success('MCP server added');
}
await syncSettingsToServer();
handleCloseDialog();
};
const handleToggleEnabled = async (server: MCPServerConfig) => {
updateMCPServer(server.id, { enabled: !server.enabled });
await syncSettingsToServer();
toast.success(server.enabled ? 'Server disabled' : 'Server enabled');
};
const handleDelete = async (id: string) => {
removeMCPServer(id);
await syncSettingsToServer();
setDeleteConfirmId(null);
toast.success('MCP server removed');
};
const getServerIcon = (type: ServerType = 'stdio') => {
if (type === 'stdio') return Terminal;
return Globe;
};
const handleImportJson = async () => {
try {
const parsed = JSON.parse(importJson);
// Support both formats:
// 1. Claude Code format: { "mcpServers": { "name": { command, args, ... } } }
// 2. Direct format: { "name": { command, args, ... } }
const servers = parsed.mcpServers || parsed;
if (typeof servers !== 'object' || Array.isArray(servers)) {
toast.error('Invalid format: expected object with server configurations');
return;
}
let addedCount = 0;
let skippedCount = 0;
for (const [name, config] of Object.entries(servers)) {
if (typeof config !== 'object' || config === null) continue;
const serverConfig = config as Record<string, unknown>;
// Check if server with this name already exists
if (mcpServers.some((s) => s.name === name)) {
skippedCount++;
continue;
}
const serverData: Omit<MCPServerConfig, 'id'> = {
name,
type: (serverConfig.type as ServerType) || 'stdio',
enabled: true,
};
if (serverData.type === 'stdio') {
if (!serverConfig.command) {
console.warn(`Skipping ${name}: no command specified`);
skippedCount++;
continue;
}
serverData.command = serverConfig.command as string;
if (Array.isArray(serverConfig.args)) {
serverData.args = serverConfig.args as string[];
}
if (typeof serverConfig.env === 'object' && serverConfig.env !== null) {
serverData.env = serverConfig.env as Record<string, string>;
}
} else {
if (!serverConfig.url) {
console.warn(`Skipping ${name}: no url specified`);
skippedCount++;
continue;
}
serverData.url = serverConfig.url as string;
if (typeof serverConfig.headers === 'object' && serverConfig.headers !== null) {
serverData.headers = serverConfig.headers as Record<string, string>;
}
}
addMCPServer(serverData);
addedCount++;
}
await syncSettingsToServer();
if (addedCount > 0) {
toast.success(`Imported ${addedCount} MCP server${addedCount > 1 ? 's' : ''}`);
}
if (skippedCount > 0) {
toast.info(
`Skipped ${skippedCount} server${skippedCount > 1 ? 's' : ''} (already exist or invalid)`
);
}
if (addedCount === 0 && skippedCount === 0) {
toast.warning('No servers found in JSON');
}
setIsImportDialogOpen(false);
setImportJson('');
} catch (error) {
toast.error('Invalid JSON: ' + (error instanceof Error ? error.message : 'Parse error'));
}
};
const handleExportJson = () => {
const exportData: Record<string, Record<string, unknown>> = {};
for (const server of mcpServers) {
const serverConfig: Record<string, unknown> = {
type: server.type || 'stdio',
};
if (server.type === 'stdio' || !server.type) {
serverConfig.command = server.command;
if (server.args?.length) serverConfig.args = server.args;
if (server.env && Object.keys(server.env).length > 0) serverConfig.env = server.env;
} else {
serverConfig.url = server.url;
if (server.headers && Object.keys(server.headers).length > 0)
serverConfig.headers = server.headers;
}
exportData[server.name] = serverConfig;
}
const json = JSON.stringify({ mcpServers: exportData }, null, 2);
navigator.clipboard.writeText(json);
toast.success('Copied to clipboard');
};
return (
<div
className={cn(
'rounded-2xl overflow-hidden',
'border border-border/50',
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
'shadow-sm shadow-black/5'
)}
>
{/* Header */}
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
<div className="flex items-center justify-between">
<div>
<div className="flex items-center gap-3 mb-2">
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
<Plug className="w-5 h-5 text-brand-500" />
</div>
<h2 className="text-lg font-semibold text-foreground tracking-tight">MCP Servers</h2>
</div>
<p className="text-sm text-muted-foreground/80 ml-12">
Configure Model Context Protocol servers to extend agent capabilities.
</p>
</div>
<div className="flex items-center gap-2">
<Button
size="sm"
variant="ghost"
onClick={handleRefresh}
disabled={isRefreshing}
data-testid="refresh-mcp-servers-button"
>
<RefreshCw className={cn('w-4 h-4', isRefreshing && 'animate-spin')} />
</Button>
{mcpServers.length > 0 && (
<Button
size="sm"
variant="outline"
onClick={handleExportJson}
data-testid="export-mcp-servers-button"
>
<Download className="w-4 h-4 mr-2" />
Export
</Button>
)}
<Button
size="sm"
variant="outline"
onClick={() => setIsImportDialogOpen(true)}
data-testid="import-mcp-servers-button"
>
<FileJson className="w-4 h-4 mr-2" />
Import JSON
</Button>
<Button size="sm" onClick={handleOpenAddDialog} data-testid="add-mcp-server-button">
<Plus className="w-4 h-4 mr-2" />
Add Server
</Button>
</div>
</div>
</div>
{/* Permission Settings */}
{mcpServers.length > 0 && (
<div className="px-6 py-4 border-b border-border/50 bg-muted/20">
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="mcp-auto-approve" className="text-sm font-medium">
Auto-approve MCP tools
</Label>
<p className="text-xs text-muted-foreground">
Allow MCP tool calls without permission prompts (recommended)
</p>
</div>
<Switch
id="mcp-auto-approve"
checked={mcpAutoApproveTools}
onCheckedChange={async (checked) => {
setMcpAutoApproveTools(checked);
await syncSettingsToServer();
}}
data-testid="mcp-auto-approve-toggle"
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="mcp-unrestricted" className="text-sm font-medium">
Unrestricted tools
</Label>
<p className="text-xs text-muted-foreground">
Allow all tools when MCP is enabled (don't filter to default set)
</p>
</div>
<Switch
id="mcp-unrestricted"
checked={mcpUnrestrictedTools}
onCheckedChange={async (checked) => {
setMcpUnrestrictedTools(checked);
await syncSettingsToServer();
}}
data-testid="mcp-unrestricted-toggle"
/>
</div>
</div>
</div>
)}
{/* Server List */}
<div className="p-6">
{mcpServers.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<Plug className="w-12 h-12 mx-auto mb-3 opacity-50" />
<p className="text-sm">No MCP servers configured</p>
<p className="text-xs mt-1">Add a server to extend agent capabilities</p>
</div>
) : (
<div className="space-y-3">
{mcpServers.map((server) => {
const Icon = getServerIcon(server.type);
return (
<div
key={server.id}
className={cn(
'flex items-center justify-between p-4 rounded-xl border',
server.enabled !== false
? 'border-border/50 bg-accent/20'
: 'border-border/30 bg-muted/30 opacity-60'
)}
data-testid={`mcp-server-${server.id}`}
>
<div className="flex items-center gap-3">
<div
className={cn(
'w-8 h-8 rounded-lg flex items-center justify-center',
server.enabled !== false ? 'bg-brand-500/20' : 'bg-muted'
)}
>
<Icon className="w-4 h-4 text-brand-500" />
</div>
<div>
<div className="font-medium text-sm">{server.name}</div>
{server.description && (
<div className="text-xs text-muted-foreground">{server.description}</div>
)}
<div className="text-xs text-muted-foreground/60 mt-0.5">
{server.type === 'stdio'
? `${server.command}${server.args?.length ? ' ' + server.args.join(' ') : ''}`
: server.url}
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Switch
checked={server.enabled !== false}
onCheckedChange={() => handleToggleEnabled(server)}
data-testid={`mcp-server-toggle-${server.id}`}
/>
<Button
variant="ghost"
size="icon"
onClick={() => handleOpenEditDialog(server)}
data-testid={`mcp-server-edit-${server.id}`}
>
<Pencil className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="text-destructive hover:text-destructive"
onClick={() => setDeleteConfirmId(server.id)}
data-testid={`mcp-server-delete-${server.id}`}
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
);
})}
</div>
)}
</div>
{/* Add/Edit Dialog */}
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
<DialogContent data-testid="mcp-server-dialog">
<DialogHeader>
<DialogTitle>{editingServer ? 'Edit MCP Server' : 'Add MCP Server'}</DialogTitle>
<DialogDescription>
Configure an MCP server to extend agent capabilities with custom tools.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="server-name">Name</Label>
<Input
id="server-name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="my-mcp-server"
data-testid="mcp-server-name-input"
/>
</div>
<div className="space-y-2">
<Label htmlFor="server-description">Description (optional)</Label>
<Input
id="server-description"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="What this server provides..."
data-testid="mcp-server-description-input"
/>
</div>
<div className="space-y-2">
<Label htmlFor="server-type">Transport Type</Label>
<Select
value={formData.type}
onValueChange={(value: ServerType) => setFormData({ ...formData, type: value })}
>
<SelectTrigger id="server-type" data-testid="mcp-server-type-select">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="stdio">Stdio (subprocess)</SelectItem>
<SelectItem value="sse">SSE (Server-Sent Events)</SelectItem>
<SelectItem value="http">HTTP</SelectItem>
</SelectContent>
</Select>
</div>
{formData.type === 'stdio' ? (
<>
<div className="space-y-2">
<Label htmlFor="server-command">Command</Label>
<Input
id="server-command"
value={formData.command}
onChange={(e) => setFormData({ ...formData, command: e.target.value })}
placeholder="npx, node, python, etc."
data-testid="mcp-server-command-input"
/>
</div>
<div className="space-y-2">
<Label htmlFor="server-args">Arguments (space-separated)</Label>
<Input
id="server-args"
value={formData.args}
onChange={(e) => setFormData({ ...formData, args: e.target.value })}
placeholder="-y @modelcontextprotocol/server-filesystem"
data-testid="mcp-server-args-input"
/>
</div>
</>
) : (
<div className="space-y-2">
<Label htmlFor="server-url">URL</Label>
<Input
id="server-url"
value={formData.url}
onChange={(e) => setFormData({ ...formData, url: e.target.value })}
placeholder="https://example.com/mcp"
data-testid="mcp-server-url-input"
/>
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={handleCloseDialog}>
Cancel
</Button>
<Button onClick={handleSave} data-testid="mcp-server-save-button">
{editingServer ? 'Save Changes' : 'Add Server'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Delete Confirmation Dialog */}
<Dialog open={!!deleteConfirmId} onOpenChange={() => setDeleteConfirmId(null)}>
<DialogContent data-testid="mcp-server-delete-dialog">
<DialogHeader>
<DialogTitle>Delete MCP Server</DialogTitle>
<DialogDescription>
Are you sure you want to delete this MCP server? This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setDeleteConfirmId(null)}>
Cancel
</Button>
<Button
variant="destructive"
onClick={() => deleteConfirmId && handleDelete(deleteConfirmId)}
data-testid="mcp-server-confirm-delete-button"
>
Delete
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Import JSON Dialog */}
<Dialog open={isImportDialogOpen} onOpenChange={setIsImportDialogOpen}>
<DialogContent className="max-w-2xl" data-testid="mcp-import-dialog">
<DialogHeader>
<DialogTitle>Import MCP Servers</DialogTitle>
<DialogDescription>
Paste JSON configuration in Claude Code format. Servers with duplicate names will be
skipped.
</DialogDescription>
</DialogHeader>
<div className="py-4">
<Textarea
value={importJson}
onChange={(e) => setImportJson(e.target.value)}
placeholder={`{
"mcpServers": {
"server-name": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-name"],
"type": "stdio"
}
}
}`}
className="font-mono text-sm h-64"
data-testid="mcp-import-textarea"
/>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => {
setIsImportDialogOpen(false);
setImportJson('');
}}
>
Cancel
</Button>
<Button
onClick={handleImportJson}
disabled={!importJson.trim()}
data-testid="mcp-import-button"
>
<FileJson className="w-4 h-4 mr-2" />
Import
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@@ -21,6 +21,7 @@ import { useEffect, useState, useRef } from 'react';
import { getHttpApiClient } from '@/lib/http-api-client';
import { isElectron } from '@/lib/electron';
import { getItem, removeItem } from '@/lib/storage';
import { useAppStore } from '@/store/app-store';
/**
* State returned by useSettingsMigration hook
@@ -227,6 +228,9 @@ export async function syncSettingsToServer(): Promise<boolean> {
enableSandboxMode: state.enableSandboxMode,
keyboardShortcuts: state.keyboardShortcuts,
aiProfiles: state.aiProfiles,
mcpServers: state.mcpServers,
mcpAutoApproveTools: state.mcpAutoApproveTools,
mcpUnrestrictedTools: state.mcpUnrestrictedTools,
projects: state.projects,
trashedProjects: state.trashedProjects,
projectHistory: state.projectHistory,
@@ -316,3 +320,42 @@ export async function syncProjectSettingsToServer(
return false;
}
}
/**
* Load MCP servers from server settings file into the store
*
* Fetches the global settings from the server and updates the store's
* mcpServers state. Useful when settings were modified externally
* (e.g., by editing the settings.json file directly).
*
* Only functions in Electron mode. Returns false if not in Electron or on error.
*
* @returns Promise resolving to true if load succeeded, false otherwise
*/
export async function loadMCPServersFromServer(): Promise<boolean> {
if (!isElectron()) return false;
try {
const api = getHttpApiClient();
const result = await api.settings.getGlobal();
if (!result.success || !result.settings) {
console.error('[Settings Load] Failed to load settings:', result.error);
return false;
}
const mcpServers = result.settings.mcpServers || [];
const mcpAutoApproveTools = result.settings.mcpAutoApproveTools ?? true;
const mcpUnrestrictedTools = result.settings.mcpUnrestrictedTools ?? true;
// Clear existing and add all from server
// We need to update the store directly since we can't use hooks here
useAppStore.setState({ mcpServers, mcpAutoApproveTools, mcpUnrestrictedTools });
console.log(`[Settings Load] Loaded ${mcpServers.length} MCP servers from server`);
return true;
} catch (error) {
console.error('[Settings Load] Failed to load MCP servers:', error);
return false;
}
}

View File

@@ -926,6 +926,20 @@ export class HttpApiClient implements ElectronAPI {
recentFolders: string[];
worktreePanelCollapsed: boolean;
lastSelectedSessionByProject: Record<string, string>;
mcpServers?: Array<{
id: string;
name: string;
description?: string;
type?: 'stdio' | 'sse' | 'http';
command?: string;
args?: string[];
env?: Record<string, string>;
url?: string;
headers?: Record<string, string>;
enabled?: boolean;
}>;
mcpAutoApproveTools?: boolean;
mcpUnrestrictedTools?: boolean;
};
error?: string;
}> => this.get('/api/settings/global'),

View File

@@ -7,6 +7,7 @@ import type {
AgentModel,
PlanningMode,
AIProfile,
MCPServerConfig,
} from '@automaker/types';
// Re-export ThemeMode for convenience
@@ -482,6 +483,11 @@ export interface AppState {
autoLoadClaudeMd: boolean; // Auto-load CLAUDE.md files using SDK's settingSources option
enableSandboxMode: boolean; // Enable sandbox mode for bash commands (may cause issues on some systems)
// MCP Servers
mcpServers: MCPServerConfig[]; // List of configured MCP servers for agent use
mcpAutoApproveTools: boolean; // Auto-approve MCP tool calls without permission prompts
mcpUnrestrictedTools: boolean; // Allow unrestricted tools when MCP servers are enabled
// Project Analysis
projectAnalysis: ProjectAnalysis | null;
isAnalyzing: boolean;
@@ -758,6 +764,8 @@ export interface AppActions {
// Claude Agent SDK Settings actions
setAutoLoadClaudeMd: (enabled: boolean) => Promise<void>;
setEnableSandboxMode: (enabled: boolean) => Promise<void>;
setMcpAutoApproveTools: (enabled: boolean) => Promise<void>;
setMcpUnrestrictedTools: (enabled: boolean) => Promise<void>;
// AI Profile actions
addAIProfile: (profile: Omit<AIProfile, 'id'>) => void;
@@ -766,6 +774,12 @@ export interface AppActions {
reorderAIProfiles: (oldIndex: number, newIndex: number) => void;
resetAIProfiles: () => void;
// MCP Server actions
addMCPServer: (server: Omit<MCPServerConfig, 'id'>) => void;
updateMCPServer: (id: string, updates: Partial<MCPServerConfig>) => void;
removeMCPServer: (id: string) => void;
reorderMCPServers: (oldIndex: number, newIndex: number) => void;
// Project Analysis actions
setProjectAnalysis: (analysis: ProjectAnalysis | null) => void;
setIsAnalyzing: (analyzing: boolean) => void;
@@ -932,6 +946,9 @@ const initialState: AppState = {
validationModel: 'opus', // Default to opus for GitHub issue validation
autoLoadClaudeMd: false, // Default to disabled (user must opt-in)
enableSandboxMode: true, // Default to enabled for security (can be disabled if issues occur)
mcpServers: [], // No MCP servers configured by default
mcpAutoApproveTools: true, // Default to enabled - bypass permission prompts for MCP tools
mcpUnrestrictedTools: true, // Default to enabled - don't filter allowedTools when MCP enabled
aiProfiles: DEFAULT_AI_PROFILES,
projectAnalysis: null,
isAnalyzing: false,
@@ -1570,6 +1587,18 @@ export const useAppStore = create<AppState & AppActions>()(
const { syncSettingsToServer } = await import('@/hooks/use-settings-migration');
await syncSettingsToServer();
},
setMcpAutoApproveTools: async (enabled) => {
set({ mcpAutoApproveTools: enabled });
// Sync to server settings file
const { syncSettingsToServer } = await import('@/hooks/use-settings-migration');
await syncSettingsToServer();
},
setMcpUnrestrictedTools: async (enabled) => {
set({ mcpUnrestrictedTools: enabled });
// Sync to server settings file
const { syncSettingsToServer } = await import('@/hooks/use-settings-migration');
await syncSettingsToServer();
},
// AI Profile actions
addAIProfile: (profile) => {
@@ -1611,6 +1640,29 @@ export const useAppStore = create<AppState & AppActions>()(
set({ aiProfiles: [...DEFAULT_AI_PROFILES, ...userProfiles] });
},
// MCP Server actions
addMCPServer: (server) => {
const id = `mcp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
set({ mcpServers: [...get().mcpServers, { ...server, id, enabled: true }] });
},
updateMCPServer: (id, updates) => {
set({
mcpServers: get().mcpServers.map((s) => (s.id === id ? { ...s, ...updates } : s)),
});
},
removeMCPServer: (id) => {
set({ mcpServers: get().mcpServers.filter((s) => s.id !== id) });
},
reorderMCPServers: (oldIndex, newIndex) => {
const servers = [...get().mcpServers];
const [movedServer] = servers.splice(oldIndex, 1);
servers.splice(newIndex, 0, movedServer);
set({ mcpServers: servers });
},
// Project Analysis actions
setProjectAnalysis: (analysis) => set({ projectAnalysis: analysis }),
setIsAnalyzing: (analyzing) => set({ isAnalyzing: analyzing }),
@@ -2716,6 +2768,10 @@ export const useAppStore = create<AppState & AppActions>()(
validationModel: state.validationModel,
autoLoadClaudeMd: state.autoLoadClaudeMd,
enableSandboxMode: state.enableSandboxMode,
// MCP settings
mcpServers: state.mcpServers,
mcpAutoApproveTools: state.mcpAutoApproveTools,
mcpUnrestrictedTools: state.mcpUnrestrictedTools,
// Profiles and sessions
aiProfiles: state.aiProfiles,
chatSessions: state.chatSessions,

View File

@@ -13,6 +13,10 @@ export type {
InstallationStatus,
ValidationResult,
ModelDefinition,
McpServerConfig,
McpStdioServerConfig,
McpSSEServerConfig,
McpHttpServerConfig,
} from './provider.js';
// Feature types
@@ -54,6 +58,8 @@ export type {
ModelProvider,
KeyboardShortcuts,
AIProfile,
MCPToolInfo,
MCPServerConfig,
ProjectRef,
TrashedProjectRef,
ChatSessionRef,

View File

@@ -28,6 +28,38 @@ export interface SystemPromptPreset {
append?: string;
}
/**
* MCP server configuration types for SDK options
* Matches the Claude Agent SDK's McpServerConfig types
*/
export type McpServerConfig = McpStdioServerConfig | McpSSEServerConfig | McpHttpServerConfig;
/**
* Stdio-based MCP server (subprocess)
* Note: `type` is optional and defaults to 'stdio' to match SDK behavior
* and allow simpler configs like { command: "node", args: ["server.js"] }
*/
export interface McpStdioServerConfig {
type?: 'stdio';
command: string;
args?: string[];
env?: Record<string, string>;
}
/** SSE-based MCP server */
export interface McpSSEServerConfig {
type: 'sse';
url: string;
headers?: Record<string, string>;
}
/** HTTP-based MCP server */
export interface McpHttpServerConfig {
type: 'http';
url: string;
headers?: Record<string, string>;
}
/**
* Options for executing a query via a provider
*/
@@ -38,7 +70,9 @@ export interface ExecuteOptions {
systemPrompt?: string | SystemPromptPreset;
maxTurns?: number;
allowedTools?: string[];
mcpServers?: Record<string, unknown>;
mcpServers?: Record<string, McpServerConfig>;
mcpAutoApproveTools?: boolean; // Auto-approve MCP tool calls without permission prompts
mcpUnrestrictedTools?: boolean; // Allow unrestricted tools when MCP servers are enabled
abortController?: AbortController;
conversationHistory?: ConversationMessage[]; // Previous messages for context
sdkSessionId?: string; // Claude SDK session ID for resuming conversations

View File

@@ -163,6 +163,53 @@ export interface AIProfile {
icon?: string;
}
/**
* MCPToolInfo - Information about a tool provided by an MCP server
*
* Contains the tool's name, description, and whether it's enabled for use.
*/
export interface MCPToolInfo {
/** Tool name as exposed by the MCP server */
name: string;
/** Description of what the tool does */
description?: string;
/** Whether this tool is enabled for use (defaults to true) */
enabled: boolean;
}
/**
* MCPServerConfig - Configuration for an MCP (Model Context Protocol) server
*
* MCP servers provide additional tools and capabilities to AI agents.
* Supports stdio (subprocess), SSE, and HTTP transport types.
*/
export interface MCPServerConfig {
/** Unique identifier for the server config */
id: string;
/** Display name for the server */
name: string;
/** User-friendly description of what this server provides */
description?: string;
/** Transport type: stdio (default), sse, or http */
type?: 'stdio' | 'sse' | 'http';
/** For stdio: command to execute (e.g., 'node', 'python', 'npx') */
command?: string;
/** For stdio: arguments to pass to the command */
args?: string[];
/** For stdio: environment variables to set */
env?: Record<string, string>;
/** For sse/http: URL endpoint */
url?: string;
/** For sse/http: headers to include in requests */
headers?: Record<string, string>;
/** Whether this server is enabled */
enabled?: boolean;
/** Tools discovered from this server with their enabled states */
tools?: MCPToolInfo[];
/** Timestamp when tools were last fetched */
toolsLastFetched?: string;
}
/**
* ProjectRef - Minimal reference to a project stored in global settings
*
@@ -303,6 +350,14 @@ export interface GlobalSettings {
autoLoadClaudeMd?: boolean;
/** Enable sandbox mode for bash commands (default: true, disable if issues occur) */
enableSandboxMode?: boolean;
// MCP Server Configuration
/** List of configured MCP servers for agent use */
mcpServers: MCPServerConfig[];
/** Auto-approve MCP tool calls without permission prompts (uses bypassPermissions mode) */
mcpAutoApproveTools?: boolean;
/** Allow unrestricted tools when MCP servers are enabled (don't filter allowedTools) */
mcpUnrestrictedTools?: boolean;
}
/**
@@ -462,6 +517,9 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
lastSelectedSessionByProject: {},
autoLoadClaudeMd: false,
enableSandboxMode: true,
mcpServers: [],
mcpAutoApproveTools: true,
mcpUnrestrictedTools: true,
};
/** Default credentials (empty strings - user must provide API keys) */

219
package-lock.json generated
View File

@@ -36,6 +36,7 @@
"@automaker/prompts": "^1.0.0",
"@automaker/types": "^1.0.0",
"@automaker/utils": "^1.0.0",
"@modelcontextprotocol/sdk": "^1.25.1",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.2.1",
@@ -2647,6 +2648,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/@hono/node-server": {
"version": "1.19.7",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.7.tgz",
"integrity": "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==",
"license": "MIT",
"engines": {
"node": ">=18.14.1"
},
"peerDependencies": {
"hono": "^4"
}
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -3473,6 +3486,67 @@
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
"license": "MIT"
},
"node_modules/@modelcontextprotocol/sdk": {
"version": "1.25.1",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz",
"integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==",
"license": "MIT",
"dependencies": {
"@hono/node-server": "^1.19.7",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"content-type": "^1.0.5",
"cors": "^2.8.5",
"cross-spawn": "^7.0.5",
"eventsource": "^3.0.2",
"eventsource-parser": "^3.0.0",
"express": "^5.0.1",
"express-rate-limit": "^7.5.0",
"jose": "^6.1.1",
"json-schema-typed": "^8.0.2",
"pkce-challenge": "^5.0.0",
"raw-body": "^3.0.0",
"zod": "^3.25 || ^4.0",
"zod-to-json-schema": "^3.25.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@cfworker/json-schema": "^4.1.1",
"zod": "^3.25 || ^4.0"
},
"peerDependenciesMeta": {
"@cfworker/json-schema": {
"optional": true
},
"zod": {
"optional": false
}
}
},
"node_modules/@modelcontextprotocol/sdk/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
},
"node_modules/@next/env": {
"version": "16.0.10",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.10.tgz",
@@ -6802,6 +6876,45 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv-formats": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
"integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
"license": "MIT",
"dependencies": {
"ajv": "^8.0.0"
},
"peerDependencies": {
"ajv": "^8.0.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
}
},
"node_modules/ajv-formats/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv-formats/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
},
"node_modules/ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
@@ -9436,6 +9549,27 @@
"dev": true,
"license": "MIT"
},
"node_modules/eventsource": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
"integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
"license": "MIT",
"dependencies": {
"eventsource-parser": "^3.0.1"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/eventsource-parser": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
"integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/expect-type": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
@@ -9458,6 +9592,7 @@
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
@@ -9496,6 +9631,21 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/express-rate-limit": {
"version": "7.5.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz",
"integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/express-rate-limit"
},
"peerDependencies": {
"express": ">= 4.11"
}
},
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -9538,7 +9688,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-json-stable-stringify": {
@@ -9555,6 +9704,22 @@
"dev": true,
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "BSD-3-Clause"
},
"node_modules/fd-slicer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
@@ -10320,6 +10485,16 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/hono": {
"version": "4.11.3",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.3.tgz",
"integrity": "sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=16.9.0"
}
},
"node_modules/hosted-git-info": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
@@ -10911,6 +11086,15 @@
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/jose": {
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz",
"integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -10958,6 +11142,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/json-schema-typed": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
"integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
"license": "BSD-2-Clause"
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
@@ -13468,6 +13658,15 @@
"node": ">=0.10"
}
},
"node_modules/pkce-challenge": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
"integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==",
"license": "MIT",
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/playwright": {
"version": "1.57.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz",
@@ -14034,6 +14233,15 @@
"node": ">=0.10.0"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/resedit": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz",
@@ -16549,6 +16757,15 @@
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zod-to-json-schema": {
"version": "3.25.1",
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz",
"integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==",
"license": "ISC",
"peerDependencies": {
"zod": "^3.25 || ^4"
}
},
"node_modules/zustand": {
"version": "5.0.9",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.9.tgz",