mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 20:43:36 +00:00
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:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user