| import { createHash } from 'crypto' |
| import { join } from 'path' |
| import { getIsNonInteractiveSession } from '../../bootstrap/state.js' |
| import type { Command } from '../../commands.js' |
| import type { AgentMcpServerInfo } from '../../components/mcp/types.js' |
| import type { Tool } from '../../Tool.js' |
| import type { AgentDefinition } from '../../tools/AgentTool/loadAgentsDir.js' |
| import { getCwd } from '../../utils/cwd.js' |
| import { getGlobalClaudeFile } from '../../utils/env.js' |
| import { isSettingSourceEnabled } from '../../utils/settings/constants.js' |
| import { |
| getSettings_DEPRECATED, |
| hasSkipDangerousModePermissionPrompt, |
| } from '../../utils/settings/settings.js' |
| import { jsonStringify } from '../../utils/slowOperations.js' |
| import { getEnterpriseMcpFilePath, getMcpConfigByName } from './config.js' |
| import { mcpInfoFromString } from './mcpStringUtils.js' |
| import { normalizeNameForMCP } from './normalization.js' |
| import { |
| type ConfigScope, |
| ConfigScopeSchema, |
| type MCPServerConnection, |
| type McpHTTPServerConfig, |
| type McpServerConfig, |
| type McpSSEServerConfig, |
| type McpStdioServerConfig, |
| type McpWebSocketServerConfig, |
| type ScopedMcpServerConfig, |
| type ServerResource, |
| } from './types.js' |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function filterToolsByServer(tools: Tool[], serverName: string): Tool[] { |
| const prefix = `mcp__${normalizeNameForMCP(serverName)}__` |
| return tools.filter(tool => tool.name?.startsWith(prefix)) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function commandBelongsToServer( |
| command: Command, |
| serverName: string, |
| ): boolean { |
| const normalized = normalizeNameForMCP(serverName) |
| const name = command.name |
| if (!name) return false |
| return ( |
| name.startsWith(`mcp__${normalized}__`) || name.startsWith(`${normalized}:`) |
| ) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function filterCommandsByServer( |
| commands: Command[], |
| serverName: string, |
| ): Command[] { |
| return commands.filter(c => commandBelongsToServer(c, serverName)) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function filterMcpPromptsByServer( |
| commands: Command[], |
| serverName: string, |
| ): Command[] { |
| return commands.filter( |
| c => |
| commandBelongsToServer(c, serverName) && |
| !(c.type === 'prompt' && c.loadedFrom === 'mcp'), |
| ) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function filterResourcesByServer( |
| resources: ServerResource[], |
| serverName: string, |
| ): ServerResource[] { |
| return resources.filter(resource => resource.server === serverName) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function excludeToolsByServer( |
| tools: Tool[], |
| serverName: string, |
| ): Tool[] { |
| const prefix = `mcp__${normalizeNameForMCP(serverName)}__` |
| return tools.filter(tool => !tool.name?.startsWith(prefix)) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function excludeCommandsByServer( |
| commands: Command[], |
| serverName: string, |
| ): Command[] { |
| return commands.filter(c => !commandBelongsToServer(c, serverName)) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function excludeResourcesByServer( |
| resources: Record<string, ServerResource[]>, |
| serverName: string, |
| ): Record<string, ServerResource[]> { |
| const result = { ...resources } |
| delete result[serverName] |
| return result |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function hashMcpConfig(config: ScopedMcpServerConfig): string { |
| const { scope: _scope, ...rest } = config |
| const stable = jsonStringify(rest, (_k, v: unknown) => { |
| if (v && typeof v === 'object' && !Array.isArray(v)) { |
| const obj = v as Record<string, unknown> |
| const sorted: Record<string, unknown> = {} |
| for (const k of Object.keys(obj).sort()) sorted[k] = obj[k] |
| return sorted |
| } |
| return v |
| }) |
| return createHash('sha256').update(stable).digest('hex').slice(0, 16) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function excludeStalePluginClients( |
| mcp: { |
| clients: MCPServerConnection[] |
| tools: Tool[] |
| commands: Command[] |
| resources: Record<string, ServerResource[]> |
| }, |
| configs: Record<string, ScopedMcpServerConfig>, |
| ): { |
| clients: MCPServerConnection[] |
| tools: Tool[] |
| commands: Command[] |
| resources: Record<string, ServerResource[]> |
| stale: MCPServerConnection[] |
| } { |
| const stale = mcp.clients.filter(c => { |
| const fresh = configs[c.name] |
| if (!fresh) return c.config.scope === 'dynamic' |
| return hashMcpConfig(c.config) !== hashMcpConfig(fresh) |
| }) |
| if (stale.length === 0) { |
| return { ...mcp, stale: [] } |
| } |
|
|
| let { tools, commands, resources } = mcp |
| for (const s of stale) { |
| tools = excludeToolsByServer(tools, s.name) |
| commands = excludeCommandsByServer(commands, s.name) |
| resources = excludeResourcesByServer(resources, s.name) |
| } |
| const staleNames = new Set(stale.map(c => c.name)) |
|
|
| return { |
| clients: mcp.clients.filter(c => !staleNames.has(c.name)), |
| tools, |
| commands, |
| resources, |
| stale, |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function isToolFromMcpServer( |
| toolName: string, |
| serverName: string, |
| ): boolean { |
| const info = mcpInfoFromString(toolName) |
| return info?.serverName === serverName |
| } |
|
|
| |
| |
| |
| |
| |
| export function isMcpTool(tool: Tool): boolean { |
| return tool.name?.startsWith('mcp__') || tool.isMcp === true |
| } |
|
|
| |
| |
| |
| |
| |
| export function isMcpCommand(command: Command): boolean { |
| return command.name?.startsWith('mcp__') || command.isMcp === true |
| } |
|
|
| |
| |
| |
| |
| |
| export function describeMcpConfigFilePath(scope: ConfigScope): string { |
| switch (scope) { |
| case 'user': |
| return getGlobalClaudeFile() |
| case 'project': |
| return join(getCwd(), '.mcp.json') |
| case 'local': |
| return `${getGlobalClaudeFile()} [project: ${getCwd()}]` |
| case 'dynamic': |
| return 'Dynamically configured' |
| case 'enterprise': |
| return getEnterpriseMcpFilePath() |
| case 'claudeai': |
| return 'claude.ai' |
| default: |
| return scope |
| } |
| } |
|
|
| export function getScopeLabel(scope: ConfigScope): string { |
| switch (scope) { |
| case 'local': |
| return 'Local config (private to you in this project)' |
| case 'project': |
| return 'Project config (shared via .mcp.json)' |
| case 'user': |
| return 'User config (available in all your projects)' |
| case 'dynamic': |
| return 'Dynamic config (from command line)' |
| case 'enterprise': |
| return 'Enterprise config (managed by your organization)' |
| case 'claudeai': |
| return 'claude.ai config' |
| default: |
| return scope |
| } |
| } |
|
|
| export function ensureConfigScope(scope?: string): ConfigScope { |
| if (!scope) return 'local' |
|
|
| if (!ConfigScopeSchema().options.includes(scope as ConfigScope)) { |
| throw new Error( |
| `Invalid scope: ${scope}. Must be one of: ${ConfigScopeSchema().options.join(', ')}`, |
| ) |
| } |
|
|
| return scope as ConfigScope |
| } |
|
|
| export function ensureTransport(type?: string): 'stdio' | 'sse' | 'http' { |
| if (!type) return 'stdio' |
|
|
| if (type !== 'stdio' && type !== 'sse' && type !== 'http') { |
| throw new Error( |
| `Invalid transport type: ${type}. Must be one of: stdio, sse, http`, |
| ) |
| } |
|
|
| return type as 'stdio' | 'sse' | 'http' |
| } |
|
|
| export function parseHeaders(headerArray: string[]): Record<string, string> { |
| const headers: Record<string, string> = {} |
|
|
| for (const header of headerArray) { |
| const colonIndex = header.indexOf(':') |
| if (colonIndex === -1) { |
| throw new Error( |
| `Invalid header format: "${header}". Expected format: "Header-Name: value"`, |
| ) |
| } |
|
|
| const key = header.substring(0, colonIndex).trim() |
| const value = header.substring(colonIndex + 1).trim() |
|
|
| if (!key) { |
| throw new Error( |
| `Invalid header: "${header}". Header name cannot be empty.`, |
| ) |
| } |
|
|
| headers[key] = value |
| } |
|
|
| return headers |
| } |
|
|
| export function getProjectMcpServerStatus( |
| serverName: string, |
| ): 'approved' | 'rejected' | 'pending' { |
| const settings = getSettings_DEPRECATED() |
| const normalizedName = normalizeNameForMCP(serverName) |
|
|
| |
| |
| if ( |
| settings?.disabledMcpjsonServers?.some( |
| name => normalizeNameForMCP(name) === normalizedName, |
| ) |
| ) { |
| return 'rejected' |
| } |
|
|
| if ( |
| settings?.enabledMcpjsonServers?.some( |
| name => normalizeNameForMCP(name) === normalizedName, |
| ) || |
| settings?.enableAllProjectMcpServers |
| ) { |
| return 'approved' |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| if ( |
| hasSkipDangerousModePermissionPrompt() && |
| isSettingSourceEnabled('projectSettings') |
| ) { |
| return 'approved' |
| } |
|
|
| |
| |
| |
| |
| |
| if ( |
| getIsNonInteractiveSession() && |
| isSettingSourceEnabled('projectSettings') |
| ) { |
| return 'approved' |
| } |
|
|
| return 'pending' |
| } |
|
|
| |
| |
| |
| |
| |
| export function getMcpServerScopeFromToolName( |
| toolName: string, |
| ): ConfigScope | null { |
| if (!isMcpTool({ name: toolName } as Tool)) { |
| return null |
| } |
|
|
| |
| const mcpInfo = mcpInfoFromString(toolName) |
| if (!mcpInfo) { |
| return null |
| } |
|
|
| |
| const serverConfig = getMcpConfigByName(mcpInfo.serverName) |
|
|
| |
| |
| if (!serverConfig && mcpInfo.serverName.startsWith('claude_ai_')) { |
| return 'claudeai' |
| } |
|
|
| return serverConfig?.scope ?? null |
| } |
|
|
| |
| function isStdioConfig( |
| config: McpServerConfig, |
| ): config is McpStdioServerConfig { |
| return config.type === 'stdio' || config.type === undefined |
| } |
|
|
| function isSSEConfig(config: McpServerConfig): config is McpSSEServerConfig { |
| return config.type === 'sse' |
| } |
|
|
| function isHTTPConfig(config: McpServerConfig): config is McpHTTPServerConfig { |
| return config.type === 'http' |
| } |
|
|
| function isWebSocketConfig( |
| config: McpServerConfig, |
| ): config is McpWebSocketServerConfig { |
| return config.type === 'ws' |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function extractAgentMcpServers( |
| agents: AgentDefinition[], |
| ): AgentMcpServerInfo[] { |
| |
| const serverMap = new Map< |
| string, |
| { |
| config: McpServerConfig & { name: string } |
| sourceAgents: string[] |
| } |
| >() |
|
|
| for (const agent of agents) { |
| if (!agent.mcpServers?.length) continue |
|
|
| for (const spec of agent.mcpServers) { |
| |
| if (typeof spec === 'string') continue |
|
|
| |
| const entries = Object.entries(spec) |
| if (entries.length !== 1) continue |
|
|
| const [serverName, serverConfig] = entries[0]! |
| const existing = serverMap.get(serverName) |
|
|
| if (existing) { |
| |
| if (!existing.sourceAgents.includes(agent.agentType)) { |
| existing.sourceAgents.push(agent.agentType) |
| } |
| } else { |
| |
| serverMap.set(serverName, { |
| config: { ...serverConfig, name: serverName } as McpServerConfig & { |
| name: string |
| }, |
| sourceAgents: [agent.agentType], |
| }) |
| } |
| } |
| } |
|
|
| |
| |
| const result: AgentMcpServerInfo[] = [] |
| for (const [name, { config, sourceAgents }] of serverMap) { |
| |
| |
| if (isStdioConfig(config)) { |
| result.push({ |
| name, |
| sourceAgents, |
| transport: 'stdio', |
| command: config.command, |
| needsAuth: false, |
| }) |
| } else if (isSSEConfig(config)) { |
| result.push({ |
| name, |
| sourceAgents, |
| transport: 'sse', |
| url: config.url, |
| needsAuth: true, |
| }) |
| } else if (isHTTPConfig(config)) { |
| result.push({ |
| name, |
| sourceAgents, |
| transport: 'http', |
| url: config.url, |
| needsAuth: true, |
| }) |
| } else if (isWebSocketConfig(config)) { |
| result.push({ |
| name, |
| sourceAgents, |
| transport: 'ws', |
| url: config.url, |
| needsAuth: false, |
| }) |
| } |
| |
| |
| } |
|
|
| return result.sort((a, b) => a.name.localeCompare(b.name)) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function getLoggingSafeMcpBaseUrl( |
| config: McpServerConfig, |
| ): string | undefined { |
| if (!('url' in config) || typeof config.url !== 'string') { |
| return undefined |
| } |
|
|
| try { |
| const url = new URL(config.url) |
| url.search = '' |
| return url.toString().replace(/\/$/, '') |
| } catch { |
| return undefined |
| } |
| } |
|
|