Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 81 additions & 4 deletions src/main/presenter/configPresenter/acpConfHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
AcpCustomAgent,
AcpStoreData
} from '@shared/presenter'
import { McpConfHelper } from './mcpConfHelper'

const ACP_STORE_VERSION = '2'
const DEFAULT_PROFILE_NAME = 'Default'
Expand Down Expand Up @@ -64,8 +65,10 @@ const deepClone = <T>(value: T): T => {

export class AcpConfHelper {
private store: ElectronStore<InternalStore>
private readonly mcpConfHelper: McpConfHelper

constructor() {
constructor(options?: { mcpConfHelper?: McpConfHelper }) {
this.mcpConfHelper = options?.mcpConfHelper ?? new McpConfHelper()
this.store = new ElectronStore<InternalStore>({
name: 'acp_agents',
defaults: {
Expand Down Expand Up @@ -149,6 +152,57 @@ export class AcpConfHelper {
return deepClone(this.getData().customs)
}

async getAgentMcpSelections(agentId: string, isBuiltin?: boolean): Promise<string[]> {
const builtin = typeof isBuiltin === 'boolean' ? isBuiltin : this.isBuiltinAgent(agentId)
if (builtin) {
const agent = this.getBuiltins().find((item) => item.id === agentId)
return this.normalizeMcpSelections(agent?.mcpSelections) ?? []
}

const agent = this.getCustoms().find((item) => item.id === agentId)
return this.normalizeMcpSelections(agent?.mcpSelections) ?? []
}

async setAgentMcpSelections(
agentId: string,
isBuiltin: boolean,
mcpIds: string[]
): Promise<void> {
const normalized = this.normalizeMcpSelections(mcpIds) ?? []
const validated = await this.validateMcpSelections(normalized)

if (isBuiltin) {
this.mutateBuiltins((builtins) => {
const target = builtins.find((agent) => agent.id === agentId)
if (!target) {
throw new Error(`ACP builtin agent not found: ${agentId}`)
}
target.mcpSelections = validated
})
return
}

this.mutateCustoms((customs) => {
const target = customs.find((agent) => agent.id === agentId)
if (!target) {
throw new Error(`ACP custom agent not found: ${agentId}`)
}
target.mcpSelections = validated
})
}

async addMcpToAgent(agentId: string, isBuiltin: boolean, mcpId: string): Promise<void> {
const current = await this.getAgentMcpSelections(agentId, isBuiltin)
const next = Array.from(new Set([...current, mcpId]))
await this.setAgentMcpSelections(agentId, isBuiltin, next)
}

async removeMcpFromAgent(agentId: string, isBuiltin: boolean, mcpId: string): Promise<void> {
const current = await this.getAgentMcpSelections(agentId, isBuiltin)
const next = current.filter((id) => id !== mcpId)
await this.setAgentMcpSelections(agentId, isBuiltin, next)
}

addBuiltinProfile(
agentId: AcpBuiltinAgentId,
profile: Omit<AcpAgentProfile, 'id'>,
Expand Down Expand Up @@ -585,7 +639,8 @@ export class AcpConfHelper {
name: BUILTIN_TEMPLATES[id].name,
enabled: false,
activeProfileId: profile.id,
profiles: [profile]
profiles: [profile],
mcpSelections: undefined
}
}

Expand Down Expand Up @@ -620,7 +675,8 @@ export class AcpConfHelper {
name: template.name,
enabled: Boolean(agent.enabled),
activeProfileId,
profiles
profiles,
mcpSelections: this.normalizeMcpSelections(agent.mcpSelections)
}
}

Expand Down Expand Up @@ -682,7 +738,8 @@ export class AcpConfHelper {
command,
args: this.normalizeArgs(agent.args),
env: this.normalizeEnv(agent.env),
enabled
enabled,
mcpSelections: this.normalizeMcpSelections(agent.mcpSelections)
}
}

Expand Down Expand Up @@ -722,6 +779,26 @@ export class AcpConfHelper {
return Object.fromEntries(entries)
}

private normalizeMcpSelections(value: unknown): string[] | undefined {
if (!Array.isArray(value)) return undefined
const cleaned = value
.map((item) => (typeof item === 'string' ? item.trim() : String(item).trim()))
.filter((item) => item.length > 0)
if (!cleaned.length) return undefined
return Array.from(new Set(cleaned))
}

private async validateMcpSelections(selections: string[]): Promise<string[]> {
if (!selections.length) return []
const servers = await this.mcpConfHelper.getMcpServers()
const valid = new Set(
Object.entries(servers)
.filter(([, config]) => config?.type !== 'inmemory')
.map(([name]) => name)
)
return selections.filter((name) => valid.has(name))
}

private isBuiltinAgent(id: string): id is AcpBuiltinAgentId {
return BUILTIN_ORDER.includes(id as AcpBuiltinAgentId)
}
Expand Down
31 changes: 27 additions & 4 deletions src/main/presenter/configPresenter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,13 +190,13 @@ export class ConfigPresenter implements IConfigPresenter {
setSetting: this.setSetting.bind(this)
})

this.acpConfHelper = new AcpConfHelper()
this.syncAcpProviderEnabled(this.acpConfHelper.getGlobalEnabled())
this.setupIpcHandlers()

// Initialize MCP configuration helper
this.mcpConfHelper = new McpConfHelper()

this.acpConfHelper = new AcpConfHelper({ mcpConfHelper: this.mcpConfHelper })
this.syncAcpProviderEnabled(this.acpConfHelper.getGlobalEnabled())
this.setupIpcHandlers()

// Initialize model configuration helper
this.modelConfigHelper = new ModelConfigHelper(this.currentAppVersion)

Expand Down Expand Up @@ -1184,6 +1184,29 @@ export class ConfigPresenter implements IConfigPresenter {
this.handleAcpAgentsMutated([agentId])
}

async getAgentMcpSelections(agentId: string, isBuiltin?: boolean): Promise<string[]> {
return await this.acpConfHelper.getAgentMcpSelections(agentId, isBuiltin)
}

async setAgentMcpSelections(
agentId: string,
isBuiltin: boolean,
mcpIds: string[]
): Promise<void> {
await this.acpConfHelper.setAgentMcpSelections(agentId, isBuiltin, mcpIds)
this.handleAcpAgentsMutated([agentId])
}

async addMcpToAgent(agentId: string, isBuiltin: boolean, mcpId: string): Promise<void> {
await this.acpConfHelper.addMcpToAgent(agentId, isBuiltin, mcpId)
this.handleAcpAgentsMutated([agentId])
}

async removeMcpFromAgent(agentId: string, isBuiltin: boolean, mcpId: string): Promise<void> {
await this.acpConfHelper.removeMcpFromAgent(agentId, isBuiltin, mcpId)
this.handleAcpAgentsMutated([agentId])
}

private handleAcpAgentsMutated(agentIds?: string[]) {
this.clearProviderModelStatusCache('acp')
this.notifyAcpAgentsChanged()
Expand Down
2 changes: 1 addition & 1 deletion src/main/presenter/configPresenter/systemPromptHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ type SetSetting = <T>(key: string, value: T) => void
export const DEFAULT_SYSTEM_PROMPT = `You are DeepChat, a highly capable AI assistant. Your goal is to fully complete the user’s requested task before handing the conversation back to them. Keep working autonomously until the task is fully resolved.
Be thorough in gathering information. Before replying, make sure you have all the details necessary to provide a complete solution. Use additional tools or ask clarifying questions when needed, but if you can find the answer on your own, avoid asking the user for help.
When using tools, briefly describe your intended steps first—for example, which tool you’ll use and for what purpose.
Adhere to this in all languages.Always respond in the same language as the user's query.`
Adhere to this in all languages.Respond in the same language as the user's query.`

type GetSetting = <T>(key: string) => T | undefined

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface AcpProcessHandle extends AgentProcessHandle {
workdir: string
availableModes?: Array<{ id: string; name: string; description: string }>
currentModeId?: string
mcpCapabilities?: schema.McpCapabilities
}

interface AcpProcessManagerOptions {
Expand Down Expand Up @@ -524,6 +525,14 @@ export class AcpProcessManager implements AgentProcessManager<AcpProcessHandle,
availableModes?: Array<{ id: string }>
currentModeId?: string
}
agentCapabilities?: {
mcpCapabilities?: schema.McpCapabilities
}
}

if (resultData.agentCapabilities?.mcpCapabilities) {
handleSeed.mcpCapabilities = resultData.agentCapabilities.mcpCapabilities
console.info('[ACP] MCP capabilities:', resultData.agentCapabilities.mcpCapabilities)
}

if (resultData.sessionId) {
Expand Down Expand Up @@ -580,7 +589,8 @@ export class AcpProcessManager implements AgentProcessManager<AcpProcessHandle,
boundConversationId: undefined,
workdir,
availableModes: handleSeed.availableModes,
currentModeId: handleSeed.currentModeId
currentModeId: handleSeed.currentModeId,
mcpCapabilities: handleSeed.mcpCapabilities
}

child.on('exit', (code, signal) => {
Expand Down
54 changes: 52 additions & 2 deletions src/main/presenter/llmProviderPresenter/agent/acpSessionManager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { app } from 'electron'
import type { AcpAgentConfig } from '@shared/presenter'
import type { AcpAgentConfig, IConfigPresenter } from '@shared/presenter'
import type { AgentSessionState } from './types'
import type {
AcpProcessManager,
Expand All @@ -9,11 +9,15 @@ import type {
} from './acpProcessManager'
import type { ClientSideConnection as ClientSideConnectionType } from '@agentclientprotocol/sdk'
import { AcpSessionPersistence } from './acpSessionPersistence'
import { convertMcpConfigToAcpFormat } from './mcpConfigConverter'
import { filterMcpServersByTransportSupport } from './mcpTransportFilter'
import type * as schema from '@agentclientprotocol/sdk/dist/schema.js'

interface AcpSessionManagerOptions {
providerId: string
processManager: AcpProcessManager
sessionPersistence: AcpSessionPersistence
configPresenter: IConfigPresenter
}

interface SessionHooks {
Expand All @@ -34,6 +38,7 @@ export class AcpSessionManager {
private readonly providerId: string
private readonly processManager: AcpProcessManager
private readonly sessionPersistence: AcpSessionPersistence
private readonly configPresenter: IConfigPresenter
private readonly sessionsByConversation = new Map<string, AcpSessionRecord>()
private readonly sessionsById = new Map<string, AcpSessionRecord>()
private readonly pendingSessions = new Map<string, Promise<AcpSessionRecord>>()
Expand All @@ -42,6 +47,7 @@ export class AcpSessionManager {
this.providerId = options.providerId
this.processManager = options.processManager
this.sessionPersistence = options.sessionPersistence
this.configPresenter = options.configPresenter

app.on('before-quit', () => {
void this.clearAllSessions()
Expand Down Expand Up @@ -267,9 +273,53 @@ export class AcpSessionManager {
currentModeId?: string
}> {
try {
let mcpServers: schema.McpServer[] = []
try {
const selections = await this.configPresenter.getAgentMcpSelections(agent.id)
if (selections.length > 0) {
const serverConfigs = await this.configPresenter.getMcpServers()
const converted = selections
.map((name) => {
const cfg = serverConfigs[name]
if (!cfg) return null
return convertMcpConfigToAcpFormat(name, cfg)
})
.filter((item): item is schema.McpServer => Boolean(item))

mcpServers = filterMcpServersByTransportSupport(converted, handle.mcpCapabilities)

if (converted.length !== mcpServers.length) {
console.info(`[ACP] Filtered MCP servers by transport support for agent ${agent.id}:`, {
selected: selections,
converted: converted.map((s) =>
'type' in s ? `${s.name}:${s.type}` : `${s.name}:stdio`
),
passed: mcpServers.map((s) =>
'type' in s ? `${s.name}:${s.type}` : `${s.name}:stdio`
)
})
} else {
console.info(`[ACP] Passing MCP servers to agent ${agent.id}:`, {
selected: selections,
passed: mcpServers.map((s) =>
'type' in s ? `${s.name}:${s.type}` : `${s.name}:stdio`
)
})
}
} else {
console.info(`[ACP] No MCP selections for agent ${agent.id}; passing none.`)
}
} catch (error) {
console.warn(
`[ACP] Failed to resolve MCP servers for agent ${agent.id}; passing none.`,
error
)
mcpServers = []
}

const response = await handle.connection.newSession({
cwd: workdir,
mcpServers: []
mcpServers
})

// Extract modes from response if available
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type * as schema from '@agentclientprotocol/sdk/dist/schema.js'
import type { MCPServerConfig } from '@shared/presenter'

const normalizeStringRecordToArray = (
record: Record<string, unknown> | undefined | null
): Array<{ name: string; value: string }> => {
if (!record || typeof record !== 'object') return []
return Object.entries(record)
.map(([name, value]) => ({
name: name?.toString().trim(),
value: typeof value === 'string' ? value : String(value ?? '')
}))
.filter((entry) => entry.name.length > 0)
}

const normalizeHeaders = (
record: Record<string, string> | undefined | null
): Array<{ name: string; value: string }> => {
if (!record || typeof record !== 'object') return []
return Object.entries(record)
.map(([name, value]) => ({
name: name?.toString().trim(),
value: value?.toString() ?? ''
}))
.filter((entry) => entry.name.length > 0)
}

export function convertMcpConfigToAcpFormat(
serverName: string,
config: MCPServerConfig
): schema.McpServer | null {
if (!config || !serverName) return null

if (config.type === 'inmemory') {
return null
}

if (config.type === 'stdio') {
return {
name: serverName,
command: config.command,
args: Array.isArray(config.args) ? config.args : [],
env: normalizeStringRecordToArray(config.env)
}
}

if (config.type === 'http' || config.type === 'sse') {
const url = config.baseUrl?.toString().trim()
if (!url) return null
return {
type: config.type,
name: serverName,
url,
headers: normalizeHeaders(config.customHeaders)
}
}

return null
}
Loading