feat: configure built-in MCP access
Built-in MCP servers were always granted to supported runtimes without per-server controls. They now have persistent enablement and runtime assignments for direct models, managed OpenCode, and Continue, while DeepSeek Harness remains visibly unsupported and cannot be assigned. MCP settings are reorganized into Built-in MCP, Direct model, Custom MCP, and Computer control tabs. Direct-model web search now uses the same accessible collapsible inventory pattern as other tool groups. Release note: 内置 MCP 现在可分别启停并分配给直连模型、OpenCode 和 Continue;MCP 设置分类与直连模型工具列表也更清晰,DeepSeek Harness 会明确显示为暂不支持。
This commit is contained in:
@@ -236,6 +236,70 @@ describe('CapabilityService', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('persists built-in MCP enablement and supported runtime assignments', async () => {
|
||||
const { filePath, builtinRoot, importedRoot, service } =
|
||||
await createService()
|
||||
|
||||
await expect(service.getSnapshot()).resolves.toMatchObject({
|
||||
builtinMcpServers: [
|
||||
{
|
||||
id: 'knowledge-base',
|
||||
enabled: true,
|
||||
assignments: ['model', 'opencode', 'continue']
|
||||
},
|
||||
{
|
||||
id: 'magic-notes',
|
||||
enabled: true,
|
||||
assignments: ['model', 'opencode', 'continue']
|
||||
},
|
||||
{
|
||||
id: 'goodbuddy-config',
|
||||
enabled: true,
|
||||
assignments: ['model', 'opencode', 'continue']
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
await service.setBuiltinMcpServerEnabled('magic-notes', false)
|
||||
await service.setBuiltinMcpServerAssignments('knowledge-base', [
|
||||
'model'
|
||||
])
|
||||
expect(() =>
|
||||
service.setBuiltinMcpServerAssignments('knowledge-base', [
|
||||
'deepseek-harness'
|
||||
])
|
||||
).toThrow('DeepSeek Harness 当前不支持内置 MCP')
|
||||
|
||||
await expect(
|
||||
service.getEnabledBuiltinMcpServerIds('model')
|
||||
).resolves.toEqual(['knowledge-base', 'goodbuddy-config'])
|
||||
await expect(
|
||||
service.getEnabledBuiltinMcpServerIds('opencode')
|
||||
).resolves.toEqual(['goodbuddy-config'])
|
||||
await expect(
|
||||
service.getEnabledBuiltinMcpServerIds('deepseek-harness')
|
||||
).resolves.toEqual([])
|
||||
|
||||
const reloaded = new CapabilityService(
|
||||
filePath,
|
||||
builtinRoot,
|
||||
importedRoot,
|
||||
cipher
|
||||
)
|
||||
await expect(reloaded.getSnapshot()).resolves.toMatchObject({
|
||||
builtinMcpServers: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'knowledge-base',
|
||||
assignments: ['model']
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'magic-notes',
|
||||
enabled: false
|
||||
})
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
it('discovers built-in skills and persists enablement and assignments', async () => {
|
||||
const { filePath, builtinRoot, importedRoot, service } =
|
||||
await createService()
|
||||
@@ -723,7 +787,7 @@ describe('CapabilityService', () => {
|
||||
await expect(service.getResolvedMcpServers('model')).resolves.toEqual([])
|
||||
})
|
||||
|
||||
it('migrates v1 to v3 without losing skills, MCP configuration, or encrypted secrets', async () => {
|
||||
it('migrates v1 to v5 without losing skills, MCP configuration, or encrypted secrets', async () => {
|
||||
const { filePath, builtinRoot, importedRoot } = await createService()
|
||||
const credential = Buffer.from(
|
||||
'encrypted:{"version":1,"serverId":"d2ef774b-146c-4467-a909-6feb112a9c2c","secret":"preserved-secret"}'
|
||||
@@ -803,7 +867,7 @@ describe('CapabilityService', () => {
|
||||
}
|
||||
})
|
||||
const persisted = await readFile(filePath, 'utf8')
|
||||
expect(persisted).toContain('"version": 4')
|
||||
expect(persisted).toContain('"version": 5')
|
||||
expect(persisted).toContain(credential)
|
||||
expect(persisted).not.toContain('preserved-secret')
|
||||
})
|
||||
@@ -839,7 +903,7 @@ describe('CapabilityService', () => {
|
||||
await expect(service.getSnapshot()).resolves.toMatchObject({
|
||||
webSearch: { enabled: true }
|
||||
})
|
||||
expect(await readFile(filePath, 'utf8')).toContain('"version": 4')
|
||||
expect(await readFile(filePath, 'utf8')).toContain('"version": 5')
|
||||
})
|
||||
|
||||
it('migrates v3 MCP servers with dynamic tools disabled', async () => {
|
||||
@@ -889,10 +953,51 @@ describe('CapabilityService', () => {
|
||||
]
|
||||
})
|
||||
const persisted = await readFile(filePath, 'utf8')
|
||||
expect(persisted).toContain('"version": 4')
|
||||
expect(persisted).toContain('"version": 5')
|
||||
expect(persisted).toContain('"allowDynamicTools": false')
|
||||
})
|
||||
|
||||
it('migrates v4 capabilities with built-in MCP enabled for supported runtimes', async () => {
|
||||
const { filePath, builtinRoot, importedRoot } = await createService()
|
||||
await writeFile(
|
||||
filePath,
|
||||
JSON.stringify({
|
||||
version: 4,
|
||||
skills: {},
|
||||
mcpServers: [],
|
||||
webSearch: { enabled: true },
|
||||
computerCapabilities: {
|
||||
'host-browser-control': {
|
||||
enabled: false,
|
||||
browserProfileId: null
|
||||
},
|
||||
'linux-desktop-control': {
|
||||
enabled: false,
|
||||
browserProfileId: null
|
||||
}
|
||||
}
|
||||
}),
|
||||
'utf8'
|
||||
)
|
||||
const service = new CapabilityService(
|
||||
filePath,
|
||||
builtinRoot,
|
||||
importedRoot,
|
||||
cipher
|
||||
)
|
||||
|
||||
await expect(service.getSnapshot()).resolves.toMatchObject({
|
||||
builtinMcpServers: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'knowledge-base',
|
||||
enabled: true,
|
||||
assignments: ['model', 'opencode', 'continue']
|
||||
})
|
||||
])
|
||||
})
|
||||
expect(await readFile(filePath, 'utf8')).toContain('"version": 5')
|
||||
})
|
||||
|
||||
it('preserves capabilities created by a newer unsupported version', async () => {
|
||||
const { directory, filePath, builtinRoot, importedRoot } =
|
||||
await createService()
|
||||
|
||||
@@ -18,6 +18,9 @@ import {
|
||||
browserProfileIdSchema,
|
||||
browserProfileNameSchema,
|
||||
browserProfilesSummarySchema,
|
||||
builtinMcpAssignmentsSchema,
|
||||
builtinMcpServerIdSchema,
|
||||
builtinMcpServerStateSummarySchema,
|
||||
capabilityDiagnosticReportSchema,
|
||||
capabilityAssignmentsSchema,
|
||||
computerCapabilityConfigSummarySchema,
|
||||
@@ -25,6 +28,7 @@ import {
|
||||
mcpServerIdSchema,
|
||||
mcpServerInputSchema,
|
||||
mcpServerSummarySchema,
|
||||
runtimeTargetSchema,
|
||||
skillIdSchema,
|
||||
skillSummarySchema,
|
||||
webSearchCapabilitySchema,
|
||||
@@ -32,6 +36,7 @@ import {
|
||||
type CapabilityDiagnosticReport,
|
||||
type CapabilitySnapshot,
|
||||
type BrowserProfilesSummary,
|
||||
type BuiltinMcpServerId,
|
||||
type ComputerCapabilityId,
|
||||
type McpServerInput,
|
||||
type McpServerSummary,
|
||||
@@ -105,6 +110,21 @@ const skillStateSchema = z
|
||||
})
|
||||
.strict()
|
||||
|
||||
const builtinMcpServerStateSchema = z
|
||||
.object({
|
||||
enabled: z.boolean(),
|
||||
assignments: builtinMcpAssignmentsSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
const builtinMcpServerStatesSchema = z
|
||||
.object({
|
||||
'knowledge-base': builtinMcpServerStateSchema,
|
||||
'magic-notes': builtinMcpServerStateSchema,
|
||||
'goodbuddy-config': builtinMcpServerStateSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
const encryptedSecretSchema =
|
||||
encryptedSettingsCredentialSchema.optional()
|
||||
|
||||
@@ -193,10 +213,15 @@ const storedCapabilitiesV3Schema = z
|
||||
})
|
||||
.strict()
|
||||
|
||||
const storedCapabilitiesSchema = storedCapabilitiesV3Schema.extend({
|
||||
const storedCapabilitiesV4Schema = storedCapabilitiesV3Schema.extend({
|
||||
version: z.literal(4)
|
||||
})
|
||||
|
||||
const storedCapabilitiesSchema = storedCapabilitiesV4Schema.extend({
|
||||
version: z.literal(5),
|
||||
builtinMcpServers: builtinMcpServerStatesSchema
|
||||
})
|
||||
|
||||
type StoredCapabilitiesV1 = z.infer<typeof storedCapabilitiesV1Schema>
|
||||
type StoredCapabilities = z.infer<typeof storedCapabilitiesSchema>
|
||||
type StoredMcpServer = z.infer<typeof storedMcpServerSchema>
|
||||
@@ -254,12 +279,25 @@ function defaultComputerCapabilityStates(): StoredCapabilities['computerCapabili
|
||||
}
|
||||
}
|
||||
|
||||
function defaultBuiltinMcpServerStates(): StoredCapabilities['builtinMcpServers'] {
|
||||
const defaultState = (): z.infer<typeof builtinMcpServerStateSchema> => ({
|
||||
enabled: true,
|
||||
assignments: ['model', 'opencode', 'continue']
|
||||
})
|
||||
return {
|
||||
'knowledge-base': defaultState(),
|
||||
'magic-notes': defaultState(),
|
||||
'goodbuddy-config': defaultState()
|
||||
}
|
||||
}
|
||||
|
||||
function emptyStoredCapabilities(
|
||||
webSearchEnabled = true
|
||||
): StoredCapabilities {
|
||||
return {
|
||||
version: 4,
|
||||
version: 5,
|
||||
skills: {},
|
||||
builtinMcpServers: defaultBuiltinMcpServerStates(),
|
||||
mcpServers: [],
|
||||
webSearch: { enabled: webSearchEnabled },
|
||||
computerCapabilities: defaultComputerCapabilityStates()
|
||||
@@ -728,7 +766,7 @@ export class CapabilityService {
|
||||
let shouldPersist = false
|
||||
try {
|
||||
const raw = JSON.parse(await readFile(this.filePath, 'utf8')) as unknown
|
||||
assertSupportedSettingsVersion(raw, 4, (version) =>
|
||||
assertSupportedSettingsVersion(raw, 5, (version) =>
|
||||
`当前 GoodBuddy 不支持能力设置版本 ${version},请升级应用后重试`
|
||||
)
|
||||
const version = z
|
||||
@@ -737,7 +775,8 @@ export class CapabilityService {
|
||||
z.literal(1),
|
||||
z.literal(2),
|
||||
z.literal(3),
|
||||
z.literal(4)
|
||||
z.literal(4),
|
||||
z.literal(5)
|
||||
])
|
||||
})
|
||||
.passthrough()
|
||||
@@ -746,8 +785,9 @@ export class CapabilityService {
|
||||
const legacy: StoredCapabilitiesV1 =
|
||||
storedCapabilitiesV1Schema.parse(raw)
|
||||
loaded = {
|
||||
version: 4,
|
||||
version: 5,
|
||||
skills: legacy.skills,
|
||||
builtinMcpServers: defaultBuiltinMcpServerStates(),
|
||||
mcpServers: legacy.mcpServers,
|
||||
webSearch: { enabled: true },
|
||||
computerCapabilities: defaultComputerCapabilityStates()
|
||||
@@ -757,7 +797,8 @@ export class CapabilityService {
|
||||
const legacy = storedCapabilitiesV2Schema.parse(raw)
|
||||
loaded = {
|
||||
...legacy,
|
||||
version: 4,
|
||||
version: 5,
|
||||
builtinMcpServers: defaultBuiltinMcpServerStates(),
|
||||
webSearch: { enabled: true }
|
||||
}
|
||||
shouldPersist = true
|
||||
@@ -765,7 +806,16 @@ export class CapabilityService {
|
||||
const legacy = storedCapabilitiesV3Schema.parse(raw)
|
||||
loaded = {
|
||||
...legacy,
|
||||
version: 4
|
||||
version: 5,
|
||||
builtinMcpServers: defaultBuiltinMcpServerStates()
|
||||
}
|
||||
shouldPersist = true
|
||||
} else if (version === 4) {
|
||||
const legacy = storedCapabilitiesV4Schema.parse(raw)
|
||||
loaded = {
|
||||
...legacy,
|
||||
version: 5,
|
||||
builtinMcpServers: defaultBuiltinMcpServerStates()
|
||||
}
|
||||
shouldPersist = true
|
||||
} else {
|
||||
@@ -891,6 +941,12 @@ export class CapabilityService {
|
||||
? -1
|
||||
: 1
|
||||
),
|
||||
builtinMcpServers: builtinMcpServerIdSchema.options.map((id) =>
|
||||
builtinMcpServerStateSummarySchema.parse({
|
||||
id,
|
||||
...state.builtinMcpServers[id]
|
||||
})
|
||||
),
|
||||
mcpServers: state.mcpServers.map((server) =>
|
||||
this.toMcpSummary(server)
|
||||
),
|
||||
@@ -1564,6 +1620,43 @@ export class CapabilityService {
|
||||
})
|
||||
}
|
||||
|
||||
setBuiltinMcpServerEnabled(
|
||||
serverId: BuiltinMcpServerId,
|
||||
enabled: boolean
|
||||
): Promise<CapabilitySnapshot> {
|
||||
return this.updateBuiltinMcpServerState(serverId, { enabled })
|
||||
}
|
||||
|
||||
setBuiltinMcpServerAssignments(
|
||||
serverId: BuiltinMcpServerId,
|
||||
assignments: CapabilityAssignments
|
||||
): Promise<CapabilitySnapshot> {
|
||||
return this.updateBuiltinMcpServerState(serverId, {
|
||||
assignments: builtinMcpAssignmentsSchema.parse(assignments)
|
||||
})
|
||||
}
|
||||
|
||||
private updateBuiltinMcpServerState(
|
||||
serverId: BuiltinMcpServerId,
|
||||
update: Partial<z.infer<typeof builtinMcpServerStateSchema>>
|
||||
): Promise<CapabilitySnapshot> {
|
||||
return this.queue(async () => {
|
||||
const id = builtinMcpServerIdSchema.parse(serverId)
|
||||
const state = await this.load()
|
||||
await this.persistUserChange({
|
||||
...state,
|
||||
builtinMcpServers: {
|
||||
...state.builtinMcpServers,
|
||||
[id]: {
|
||||
...state.builtinMcpServers[id],
|
||||
...update
|
||||
}
|
||||
}
|
||||
})
|
||||
return this.getSnapshot()
|
||||
})
|
||||
}
|
||||
|
||||
private updateSkillState(
|
||||
skillId: string,
|
||||
update: Partial<z.infer<typeof skillStateSchema>>
|
||||
@@ -1801,4 +1894,18 @@ export class CapabilityService {
|
||||
assigned.map((server) => this.getResolvedMcpServer(server.id))
|
||||
)
|
||||
}
|
||||
|
||||
async getEnabledBuiltinMcpServerIds(
|
||||
target: RuntimeTarget
|
||||
): Promise<BuiltinMcpServerId[]> {
|
||||
const runtime = runtimeTargetSchema.parse(target)
|
||||
if (runtime === 'deepseek-harness') {
|
||||
return []
|
||||
}
|
||||
const state = await this.load()
|
||||
return builtinMcpServerIdSchema.options.filter((id) => {
|
||||
const server = state.builtinMcpServers[id]
|
||||
return server.enabled && server.assignments.includes(runtime)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+83
-3
@@ -118,6 +118,8 @@ describe('registerIpcHandlers computer capabilities', () => {
|
||||
}
|
||||
const capabilityService = {
|
||||
importSkill: vi.fn(async () => snapshot),
|
||||
setBuiltinMcpServerEnabled: vi.fn(async () => snapshot),
|
||||
setBuiltinMcpServerAssignments: vi.fn(async () => snapshot),
|
||||
setComputerCapabilityEnabled: vi.fn(async () => snapshot),
|
||||
setWebSearchEnabled: vi.fn(async () => snapshot),
|
||||
createBrowserProfile: vi.fn(async () => snapshot),
|
||||
@@ -215,6 +217,31 @@ describe('registerIpcHandlers computer capabilities', () => {
|
||||
expect(capabilityService.setWebSearchEnabled).toHaveBeenCalledWith(false)
|
||||
expect(onRuntimeSettingsChanged).toHaveBeenCalledTimes(2)
|
||||
|
||||
await expect(
|
||||
electronMocks.handlers.get(
|
||||
ipcChannels.capabilitiesToggleBuiltinMcp
|
||||
)?.(event, {
|
||||
serverId: 'knowledge-base',
|
||||
enabled: false
|
||||
})
|
||||
).resolves.toEqual(snapshot)
|
||||
expect(
|
||||
capabilityService.setBuiltinMcpServerEnabled
|
||||
).toHaveBeenCalledWith('knowledge-base', false)
|
||||
|
||||
await expect(
|
||||
electronMocks.handlers.get(
|
||||
ipcChannels.capabilitiesAssignBuiltinMcp
|
||||
)?.(event, {
|
||||
serverId: 'knowledge-base',
|
||||
assignments: ['model', 'continue']
|
||||
})
|
||||
).resolves.toEqual(snapshot)
|
||||
expect(
|
||||
capabilityService.setBuiltinMcpServerAssignments
|
||||
).toHaveBeenCalledWith('knowledge-base', ['model', 'continue'])
|
||||
expect(onRuntimeSettingsChanged).toHaveBeenCalledTimes(4)
|
||||
|
||||
electronMocks.showOpenDialog.mockResolvedValueOnce({
|
||||
canceled: false,
|
||||
filePaths: ['C:\\meeting-helper.zip']
|
||||
@@ -290,7 +317,7 @@ describe('registerIpcHandlers computer capabilities', () => {
|
||||
expect(capabilityService.createBrowserProfile).toHaveBeenCalledWith(
|
||||
'工作配置'
|
||||
)
|
||||
expect(onRuntimeSettingsChanged).toHaveBeenCalledTimes(3)
|
||||
expect(onRuntimeSettingsChanged).toHaveBeenCalledTimes(5)
|
||||
|
||||
expect(() =>
|
||||
electronMocks.handlers.get(
|
||||
@@ -2290,7 +2317,8 @@ describe('registerIpcHandlers agent terminal state', () => {
|
||||
knowledgeServiceOverride?: Record<string, unknown>,
|
||||
knowledgeGateway?: Record<string, unknown>,
|
||||
magicNotesEnabled = false,
|
||||
goodbuddyConfigService?: Record<string, unknown>
|
||||
goodbuddyConfigService?: Record<string, unknown>,
|
||||
capabilityServiceOverride?: Record<string, unknown>
|
||||
) {
|
||||
const assistantDatabase = {
|
||||
claimDueSchedules: vi.fn(() => []),
|
||||
@@ -2394,7 +2422,7 @@ describe('registerIpcHandlers agent terminal state', () => {
|
||||
getPolicySettings,
|
||||
getResolvedSettings
|
||||
} as never,
|
||||
{} as never,
|
||||
(capabilityServiceOverride ?? {}) as never,
|
||||
contextManager as never,
|
||||
(knowledgeServiceOverride ?? {
|
||||
database: { listKnowledgeBases: vi.fn(() => []) }
|
||||
@@ -2697,6 +2725,58 @@ describe('registerIpcHandlers agent terminal state', () => {
|
||||
await harness.dispose()
|
||||
})
|
||||
|
||||
it('does not grant a built-in MCP that is disabled or unassigned for the runtime', async () => {
|
||||
const runtime = {
|
||||
runtimeId: 'model',
|
||||
capability: 'chat',
|
||||
supportsToolExecution: true,
|
||||
async *run(request: { requestId: string }) {
|
||||
yield { requestId: request.requestId, type: 'done' }
|
||||
}
|
||||
}
|
||||
const knowledgeGateway = {
|
||||
grant: vi.fn(() => 'must-not-be-granted'),
|
||||
getAvailableToolNames: vi.fn(() => ['note_list']),
|
||||
drainReferences: vi.fn(() => []),
|
||||
revoke: vi.fn()
|
||||
}
|
||||
const getEnabledBuiltinMcpServerIds = vi.fn(async () => [
|
||||
'knowledge-base'
|
||||
])
|
||||
const harness = createHarness(
|
||||
runtime,
|
||||
undefined,
|
||||
'always',
|
||||
undefined,
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
knowledgeGateway,
|
||||
true,
|
||||
undefined,
|
||||
{ getEnabledBuiltinMcpServerIds }
|
||||
)
|
||||
const requestId = '00000000-0000-4000-8000-000000000025'
|
||||
|
||||
await harness.handler?.(trustedEvent(harness.webContents), {
|
||||
requestId,
|
||||
conversationId: 'disabled-notes',
|
||||
prompt: '读取笔记',
|
||||
workMode: 'ask',
|
||||
knowledgeLibraryIds: []
|
||||
})
|
||||
await vi.waitFor(() =>
|
||||
expect(harness.assistantDatabase.updateTaskStatus).toHaveBeenCalledWith(
|
||||
requestId,
|
||||
'completed'
|
||||
)
|
||||
)
|
||||
|
||||
expect(getEnabledBuiltinMcpServerIds).toHaveBeenCalledWith('model')
|
||||
expect(knowledgeGateway.grant).not.toHaveBeenCalled()
|
||||
await harness.dispose()
|
||||
})
|
||||
|
||||
it.each(['ask', 'execute'] as const)(
|
||||
'does not grant or advertise scoped data tools to external OpenCode in %s mode',
|
||||
async (workMode) => {
|
||||
|
||||
+84
-9
@@ -81,6 +81,9 @@ import {
|
||||
browserProfileCreateInputSchema,
|
||||
browserProfileRenameInputSchema,
|
||||
browserProfileSelectionInputSchema,
|
||||
builtinMcpServerAssignmentsInputSchema,
|
||||
builtinMcpServerIdSchema,
|
||||
builtinMcpServerToggleInputSchema,
|
||||
computerCapabilityConfigInputSchema,
|
||||
computerCapabilityIdSchema,
|
||||
computerCapabilityToggleInputSchema,
|
||||
@@ -90,6 +93,8 @@ import {
|
||||
skillIdSchema,
|
||||
skillImportKindSchema,
|
||||
skillToggleInputSchema,
|
||||
runtimeTargetSchema,
|
||||
type BuiltinMcpServerId,
|
||||
type CapabilitySnapshot,
|
||||
type CapabilityDiagnosticReport,
|
||||
type McpServerTestResult,
|
||||
@@ -316,6 +321,19 @@ function isAgentRuntime(runtime: AgentRuntime): boolean {
|
||||
)
|
||||
}
|
||||
|
||||
function runtimeTargetFor(
|
||||
runtime: AgentRuntime
|
||||
): ReturnType<typeof runtimeTargetSchema.parse> | undefined {
|
||||
const target = runtimeTargetSchema.safeParse(runtime.runtimeId)
|
||||
if (target.success) {
|
||||
return target.data
|
||||
}
|
||||
return runtime.runtimeId === undefined &&
|
||||
runtime.supportsScopedDataTools !== false
|
||||
? 'model'
|
||||
: undefined
|
||||
}
|
||||
|
||||
type ScopedDataCapability = {
|
||||
token?: string
|
||||
toolNames: readonly string[]
|
||||
@@ -324,6 +342,7 @@ type ScopedDataCapability = {
|
||||
function grantScopedDataCapability(input: {
|
||||
gateway?: KnowledgeMcpGateway
|
||||
runtime: AgentRuntime
|
||||
enabledServers: readonly BuiltinMcpServerId[]
|
||||
requestId: string
|
||||
libraryIds: readonly string[]
|
||||
magicNotesAccess: MagicNotesCapabilityAccess
|
||||
@@ -335,11 +354,21 @@ function grantScopedDataCapability(input: {
|
||||
) => Promise<boolean>
|
||||
signal: AbortSignal
|
||||
}): ScopedDataCapability {
|
||||
const enabledServers = new Set(input.enabledServers)
|
||||
const libraryIds = enabledServers.has('knowledge-base')
|
||||
? input.libraryIds
|
||||
: []
|
||||
const magicNotesAccess = enabledServers.has('magic-notes')
|
||||
? input.magicNotesAccess
|
||||
: 'none'
|
||||
const configAccess = enabledServers.has('goodbuddy-config')
|
||||
? input.configAccess ?? 'none'
|
||||
: 'none'
|
||||
if (
|
||||
input.runtime.supportsScopedDataTools === false ||
|
||||
(input.libraryIds.length === 0 &&
|
||||
input.magicNotesAccess === 'none' &&
|
||||
(input.configAccess ?? 'none') === 'none')
|
||||
(libraryIds.length === 0 &&
|
||||
magicNotesAccess === 'none' &&
|
||||
configAccess === 'none')
|
||||
) {
|
||||
return { toolNames: [] }
|
||||
}
|
||||
@@ -347,9 +376,9 @@ function grantScopedDataCapability(input: {
|
||||
throw new Error('内置数据工具服务不可用')
|
||||
}
|
||||
const config =
|
||||
input.configAccess && input.configAccess !== 'none' && input.workspacePath
|
||||
configAccess !== 'none' && input.workspacePath
|
||||
? {
|
||||
access: input.configAccess,
|
||||
access: configAccess,
|
||||
workspacePath: input.workspacePath,
|
||||
authorizeApply: input.authorizeConfigApply
|
||||
}
|
||||
@@ -357,16 +386,16 @@ function grantScopedDataCapability(input: {
|
||||
const token = config
|
||||
? input.gateway.grant(
|
||||
input.requestId,
|
||||
input.libraryIds,
|
||||
libraryIds,
|
||||
input.signal,
|
||||
input.magicNotesAccess,
|
||||
magicNotesAccess,
|
||||
config
|
||||
)
|
||||
: input.gateway.grant(
|
||||
input.requestId,
|
||||
input.libraryIds,
|
||||
libraryIds,
|
||||
input.signal,
|
||||
input.magicNotesAccess
|
||||
magicNotesAccess
|
||||
)
|
||||
return {
|
||||
token,
|
||||
@@ -1301,9 +1330,18 @@ export function registerIpcHandlers(
|
||||
origin === 'channel' &&
|
||||
((await applicationSettingsStore?.get())?.magicNotesEnabled ??
|
||||
false)
|
||||
const requestRuntimeTarget = runtimeTargetFor(requestRuntime)
|
||||
const enabledBuiltinMcpServers = requestRuntimeTarget
|
||||
? capabilityService.getEnabledBuiltinMcpServerIds
|
||||
? await capabilityService.getEnabledBuiltinMcpServerIds(
|
||||
requestRuntimeTarget
|
||||
)
|
||||
: [...builtinMcpServerIdSchema.options]
|
||||
: []
|
||||
const notesCapability = grantScopedDataCapability({
|
||||
gateway: knowledgeGateway,
|
||||
runtime: requestRuntime,
|
||||
enabledServers: enabledBuiltinMcpServers,
|
||||
requestId,
|
||||
libraryIds: [],
|
||||
magicNotesAccess: magicNotesToolEnabled
|
||||
@@ -2303,9 +2341,18 @@ export function registerIpcHandlers(
|
||||
: enrichedRequest.projectId
|
||||
? assistantDatabase.getProject(enrichedRequest.projectId).rootPath
|
||||
: (await settingsStore.getResolvedSettings()).workspacePath
|
||||
const selectedRuntimeTarget = runtimeTargetFor(selectedRuntime)
|
||||
const enabledBuiltinMcpServers = selectedRuntimeTarget
|
||||
? capabilityService.getEnabledBuiltinMcpServerIds
|
||||
? await capabilityService.getEnabledBuiltinMcpServerIds(
|
||||
selectedRuntimeTarget
|
||||
)
|
||||
: [...builtinMcpServerIdSchema.options]
|
||||
: []
|
||||
const scopedCapability = grantScopedDataCapability({
|
||||
gateway: knowledgeGateway,
|
||||
runtime: selectedRuntime,
|
||||
enabledServers: enabledBuiltinMcpServers,
|
||||
requestId: enrichedRequest.requestId,
|
||||
libraryIds: hasKnowledgeScope ? knowledgeLibraryIds : [],
|
||||
magicNotesAccess: magicNotesToolEnabled
|
||||
@@ -4397,6 +4444,34 @@ export function registerIpcHandlers(
|
||||
}
|
||||
)
|
||||
|
||||
registerHandler(
|
||||
ipcChannels.capabilitiesToggleBuiltinMcp,
|
||||
(event, input: unknown): Promise<CapabilitySnapshot> => {
|
||||
assertTrustedSender(event, window)
|
||||
const value = builtinMcpServerToggleInputSchema.parse(input)
|
||||
return refreshCapabilities(
|
||||
capabilityService.setBuiltinMcpServerEnabled(
|
||||
value.serverId,
|
||||
value.enabled
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
registerHandler(
|
||||
ipcChannels.capabilitiesAssignBuiltinMcp,
|
||||
(event, input: unknown): Promise<CapabilitySnapshot> => {
|
||||
assertTrustedSender(event, window)
|
||||
const value = builtinMcpServerAssignmentsInputSchema.parse(input)
|
||||
return refreshCapabilities(
|
||||
capabilityService.setBuiltinMcpServerAssignments(
|
||||
value.serverId,
|
||||
value.assignments
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
registerHandler(
|
||||
ipcChannels.capabilitiesSaveMcp,
|
||||
(event, input: unknown): Promise<CapabilitySnapshot> => {
|
||||
|
||||
@@ -30,6 +30,7 @@ import { ipcChannels } from '../shared/ipc-channels'
|
||||
import type {
|
||||
BrowserProfileCreateInput,
|
||||
BrowserProfileRenameInput,
|
||||
BuiltinMcpServerId,
|
||||
CapabilityDiagnosticReport,
|
||||
CapabilitySnapshot,
|
||||
ComputerCapabilityId,
|
||||
@@ -811,6 +812,22 @@ const desktopApi: DesktopApi = {
|
||||
skillId,
|
||||
assignments
|
||||
}) as Promise<CapabilitySnapshot>,
|
||||
setBuiltinMcpServerEnabled: (
|
||||
serverId: BuiltinMcpServerId,
|
||||
enabled: boolean
|
||||
) =>
|
||||
ipcRenderer.invoke(ipcChannels.capabilitiesToggleBuiltinMcp, {
|
||||
serverId,
|
||||
enabled
|
||||
}) as Promise<CapabilitySnapshot>,
|
||||
setBuiltinMcpServerAssignments: (
|
||||
serverId: BuiltinMcpServerId,
|
||||
assignments
|
||||
) =>
|
||||
ipcRenderer.invoke(ipcChannels.capabilitiesAssignBuiltinMcp, {
|
||||
serverId,
|
||||
assignments
|
||||
}) as Promise<CapabilitySnapshot>,
|
||||
saveMcpServer: (serverId, input) =>
|
||||
ipcRenderer.invoke(ipcChannels.capabilitiesSaveMcp, {
|
||||
serverId,
|
||||
|
||||
@@ -474,6 +474,14 @@ const api: DesktopApi = {
|
||||
skills: [],
|
||||
mcpServers: []
|
||||
})),
|
||||
setBuiltinMcpServerEnabled: vi.fn(async () => ({
|
||||
skills: [],
|
||||
mcpServers: []
|
||||
})),
|
||||
setBuiltinMcpServerAssignments: vi.fn(async () => ({
|
||||
skills: [],
|
||||
mcpServers: []
|
||||
})),
|
||||
saveMcpServer: vi.fn(async () => ({
|
||||
skills: [],
|
||||
mcpServers: []
|
||||
|
||||
@@ -19,6 +19,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { builtinMcpServers } from '../../shared/builtin-mcp-servers'
|
||||
import { builtinModelToolGroups } from '../../shared/builtin-model-tools'
|
||||
import type {
|
||||
BuiltinMcpServerId,
|
||||
CapabilityDiagnosticReport,
|
||||
CapabilityAssignments,
|
||||
CapabilitySnapshot,
|
||||
@@ -43,7 +44,11 @@ const configurableMcpTargets: RuntimeTarget[] = [
|
||||
'continue',
|
||||
'deepseek-harness'
|
||||
]
|
||||
type McpSettingsTab = 'builtin' | 'computer' | 'custom'
|
||||
type McpSettingsTab =
|
||||
| 'builtin'
|
||||
| 'custom'
|
||||
| 'model-tools'
|
||||
| 'computer'
|
||||
|
||||
type McpEditor = {
|
||||
id?: string
|
||||
@@ -342,6 +347,23 @@ export function McpSettingsSection({
|
||||
})
|
||||
}
|
||||
|
||||
const updateBuiltinAssignment = (
|
||||
serverId: BuiltinMcpServerId,
|
||||
assignments: CapabilityAssignments,
|
||||
target: RuntimeTarget,
|
||||
checked: boolean
|
||||
): void => {
|
||||
const next = checked
|
||||
? [...assignments, target]
|
||||
: assignments.filter((item) => item !== target)
|
||||
void run(`builtin:assign:${serverId}`, () =>
|
||||
window.goodbuddy.capabilities.setBuiltinMcpServerAssignments(
|
||||
serverId,
|
||||
next
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const openEditor = (
|
||||
nextEditor: McpEditor,
|
||||
trigger: HTMLButtonElement
|
||||
@@ -374,11 +396,8 @@ export function McpSettingsSection({
|
||||
}
|
||||
|
||||
const computerCapabilities = snapshot?.computerCapabilities ?? []
|
||||
const visibleComputerCapabilities = computerCapabilities.filter(
|
||||
(capability) =>
|
||||
activeTab === 'builtin'
|
||||
? capability.id === 'host-browser-control'
|
||||
: capability.id !== 'host-browser-control'
|
||||
const builtinMcpStates = new Map(
|
||||
snapshot?.builtinMcpServers?.map((server) => [server.id, server])
|
||||
)
|
||||
const browserProfiles = snapshot?.browserProfiles ?? {
|
||||
profiles: [],
|
||||
@@ -414,24 +433,43 @@ export function McpSettingsSection({
|
||||
headingId="mcp-settings-heading"
|
||||
/>
|
||||
<SettingsWarningList warnings={snapshot?.warnings} />
|
||||
<PageTabs
|
||||
ariaLabel={t('mcp.tabs.ariaLabel')}
|
||||
idPrefix="mcp-settings"
|
||||
onChange={(tab) => {
|
||||
setError(undefined)
|
||||
setActiveTab(tab)
|
||||
}}
|
||||
tabs={[
|
||||
{ id: 'builtin', label: t('mcp.tabs.builtin') },
|
||||
{ id: 'computer', label: t('mcp.tabs.computer') },
|
||||
{ id: 'custom', label: t('mcp.tabs.custom') }
|
||||
]}
|
||||
value={activeTab}
|
||||
variant="segmented"
|
||||
/>
|
||||
<div className="mcp-settings__tabs">
|
||||
<PageTabs
|
||||
ariaLabel={t('mcp.tabs.ariaLabel')}
|
||||
idPrefix="mcp-settings"
|
||||
onChange={(tab) => {
|
||||
setError(undefined)
|
||||
setActiveTab(tab)
|
||||
}}
|
||||
tabs={[
|
||||
{
|
||||
id: 'builtin',
|
||||
icon: <Database size={14} />,
|
||||
label: t('mcp.tabs.builtin')
|
||||
},
|
||||
{
|
||||
id: 'model-tools',
|
||||
icon: <Wrench size={14} />,
|
||||
label: t('mcp.tabs.modelTools')
|
||||
},
|
||||
{
|
||||
id: 'custom',
|
||||
icon: <Network size={14} />,
|
||||
label: t('mcp.tabs.custom')
|
||||
},
|
||||
{
|
||||
id: 'computer',
|
||||
icon: <MonitorCog size={14} />,
|
||||
label: t('mcp.tabs.computer')
|
||||
}
|
||||
]}
|
||||
value={activeTab}
|
||||
variant="segmented"
|
||||
/>
|
||||
</div>
|
||||
<section
|
||||
aria-labelledby={`mcp-settings-tab-${activeTab}`}
|
||||
className="settings-section"
|
||||
className="settings-section mcp-settings__panel"
|
||||
id={`mcp-settings-panel-${activeTab}`}
|
||||
role="tabpanel"
|
||||
>
|
||||
@@ -446,36 +484,24 @@ export function McpSettingsSection({
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{activeTab !== 'custom' && (
|
||||
{activeTab === 'computer' && (
|
||||
<section
|
||||
aria-labelledby="computer-capabilities-heading"
|
||||
className="mcp-tool-section"
|
||||
>
|
||||
<div className="mcp-subsection-heading">
|
||||
<div>
|
||||
{activeTab === 'builtin' ? (
|
||||
<Globe2 size={15} />
|
||||
) : (
|
||||
<MonitorCog size={15} />
|
||||
)}
|
||||
<MonitorCog size={15} />
|
||||
<strong id="computer-capabilities-heading">
|
||||
{t(
|
||||
activeTab === 'builtin'
|
||||
? 'mcp.computer.browserTitle'
|
||||
: 'mcp.computer.title'
|
||||
)}
|
||||
{t('mcp.computer.title')}
|
||||
</strong>
|
||||
</div>
|
||||
<small>
|
||||
{t(
|
||||
activeTab === 'builtin'
|
||||
? 'mcp.computer.browserSubtitle'
|
||||
: 'mcp.computer.subtitle'
|
||||
)}
|
||||
{t('mcp.computer.subtitle')}
|
||||
</small>
|
||||
</div>
|
||||
<div className="capability-list">
|
||||
{visibleComputerCapabilities.map((capability) => {
|
||||
{computerCapabilities.map((capability) => {
|
||||
const report = diagnostics[capability.id]
|
||||
return (
|
||||
<article className="capability-card" key={capability.id}>
|
||||
@@ -601,8 +627,7 @@ export function McpSettingsSection({
|
||||
</section>
|
||||
)}
|
||||
|
||||
{activeTab === 'builtin' && (
|
||||
<>
|
||||
{activeTab === 'computer' && (
|
||||
<section
|
||||
aria-labelledby="browser-profiles-heading"
|
||||
className="mcp-tool-section"
|
||||
@@ -748,7 +773,9 @@ export function McpSettingsSection({
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{activeTab === 'builtin' && (
|
||||
<section
|
||||
aria-labelledby="builtin-mcp-heading"
|
||||
className="mcp-tool-section"
|
||||
@@ -777,16 +804,126 @@ export function McpSettingsSection({
|
||||
const expansionId = `builtin:${server.id}`
|
||||
const expanded = expandedItemIds.has(expansionId)
|
||||
const panelId = `mcp-server-tools-${server.id}`
|
||||
const enabled =
|
||||
const state = builtinMcpStates.get(server.id) ?? {
|
||||
id: server.id,
|
||||
enabled: true,
|
||||
assignments:
|
||||
[...server.supportedAssignments] as CapabilityAssignments
|
||||
}
|
||||
const featureAvailable =
|
||||
!('requiresFeature' in server) ||
|
||||
magicNotesEnabled === true
|
||||
return (
|
||||
<article
|
||||
className={`mcp-server-card${
|
||||
enabled ? '' : ' mcp-server-card--disabled'
|
||||
className={`capability-card builtin-mcp-card${
|
||||
state.enabled && featureAvailable
|
||||
? ''
|
||||
: ' capability-card--disabled'
|
||||
}`}
|
||||
key={server.id}
|
||||
>
|
||||
<div className="capability-card__header">
|
||||
<div>
|
||||
<strong>{server.name}</strong>
|
||||
<small>
|
||||
{!state.enabled
|
||||
? t('mcp.builtin.disabled')
|
||||
: !featureAvailable
|
||||
? t('mcp.builtin.serverSummaryDisabled')
|
||||
: server.access === 'mixed'
|
||||
? t('mcp.builtin.serverSummaryMixed')
|
||||
: t('mcp.builtin.serverSummaryReadOnly')}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<label className="toggle-row">
|
||||
<input
|
||||
aria-label={t('mcp.builtin.enableAriaLabel', {
|
||||
name: server.name
|
||||
})}
|
||||
checked={state.enabled}
|
||||
disabled={Boolean(busy)}
|
||||
onChange={(event) =>
|
||||
void run(`builtin:toggle:${server.id}`, () =>
|
||||
window.goodbuddy.capabilities.setBuiltinMcpServerEnabled(
|
||||
server.id,
|
||||
event.target.checked
|
||||
)
|
||||
)
|
||||
}
|
||||
role="switch"
|
||||
type="checkbox"
|
||||
/>
|
||||
<span>
|
||||
{state.enabled
|
||||
? t('mcp.builtin.enabled')
|
||||
: t('mcp.builtin.disabled')}
|
||||
</span>
|
||||
</label>
|
||||
<p>{server.description}</p>
|
||||
{!featureAvailable && (
|
||||
<p className="computer-capability-risk">
|
||||
<CircleAlert aria-hidden="true" size={13} />
|
||||
{t('mcp.builtin.featureDisabled')}
|
||||
</p>
|
||||
)}
|
||||
<div className="runtime-assignments">
|
||||
<small>{t('mcp.builtin.assignedTo')}</small>
|
||||
{configurableMcpTargets.map((target) => {
|
||||
const unsupported =
|
||||
!server.supportedAssignments.some(
|
||||
(supportedTarget) =>
|
||||
supportedTarget === target
|
||||
)
|
||||
return (
|
||||
<label
|
||||
className={
|
||||
unsupported
|
||||
? 'runtime-assignment--unsupported'
|
||||
: undefined
|
||||
}
|
||||
key={target}
|
||||
title={
|
||||
unsupported
|
||||
? t('mcp.builtin.runtimeUnsupported')
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<input
|
||||
aria-label={
|
||||
unsupported
|
||||
? t(
|
||||
'mcp.builtin.runtimeAssignmentUnsupportedAriaLabel',
|
||||
{
|
||||
name: server.name,
|
||||
runtime: runtimeLabels[target]
|
||||
}
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
checked={
|
||||
!unsupported &&
|
||||
state.assignments.includes(target)
|
||||
}
|
||||
disabled={Boolean(busy) || unsupported}
|
||||
onChange={(event) =>
|
||||
updateBuiltinAssignment(
|
||||
server.id,
|
||||
state.assignments,
|
||||
target,
|
||||
event.target.checked
|
||||
)
|
||||
}
|
||||
type="checkbox"
|
||||
/>
|
||||
{runtimeLabels[target]}
|
||||
{unsupported
|
||||
? t('mcp.builtin.unsupportedSuffix')
|
||||
: ''}
|
||||
</label>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<button
|
||||
aria-controls={panelId}
|
||||
aria-expanded={expanded}
|
||||
@@ -796,43 +933,28 @@ export function McpSettingsSection({
|
||||
: 'mcp.builtin.expandServer',
|
||||
{ name: server.name }
|
||||
)}
|
||||
className="mcp-server-card__toggle"
|
||||
className="secondary-button builtin-mcp-card__details"
|
||||
onClick={() => toggleItem(expansionId)}
|
||||
type="button"
|
||||
>
|
||||
<div>
|
||||
<strong>{server.name}</strong>
|
||||
<small>
|
||||
{!enabled
|
||||
? t('mcp.builtin.serverSummaryDisabled')
|
||||
: server.access === 'mixed'
|
||||
? t('mcp.builtin.serverSummaryMixed')
|
||||
: t('mcp.builtin.serverSummaryReadOnly')}
|
||||
</small>
|
||||
</div>
|
||||
<span className="mcp-server-card__summary">
|
||||
{t('mcp.builtin.toolCount', {
|
||||
count: server.tools.length
|
||||
})}
|
||||
<ChevronDown
|
||||
aria-hidden="true"
|
||||
className={
|
||||
expanded
|
||||
? 'mcp-server-card__chevron mcp-server-card__chevron--expanded'
|
||||
: 'mcp-server-card__chevron'
|
||||
}
|
||||
size={15}
|
||||
/>
|
||||
</span>
|
||||
{t('mcp.builtin.toolCount', {
|
||||
count: server.tools.length
|
||||
})}
|
||||
<ChevronDown
|
||||
aria-hidden="true"
|
||||
className={
|
||||
expanded
|
||||
? 'mcp-server-card__chevron mcp-server-card__chevron--expanded'
|
||||
: 'mcp-server-card__chevron'
|
||||
}
|
||||
size={15}
|
||||
/>
|
||||
</button>
|
||||
{expanded && (
|
||||
<div className="mcp-server-card__body" id={panelId}>
|
||||
{!enabled && (
|
||||
<p className="mcp-server-card__disabled-notice">
|
||||
{t('mcp.builtin.featureDisabled')}
|
||||
</p>
|
||||
)}
|
||||
<p>{server.description}</p>
|
||||
<div
|
||||
className="builtin-mcp-card__tools"
|
||||
id={panelId}
|
||||
>
|
||||
<section
|
||||
aria-label={t('mcp.builtin.toolsAriaLabel', {
|
||||
name: server.name
|
||||
@@ -870,7 +992,9 @@ export function McpSettingsSection({
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{activeTab === 'model-tools' && (
|
||||
<div className="mcp-tool-section">
|
||||
<div className="mcp-subsection-heading">
|
||||
<div>
|
||||
@@ -884,89 +1008,142 @@ export function McpSettingsSection({
|
||||
</small>
|
||||
</div>
|
||||
<div className="mcp-server-list">
|
||||
<article className="capability-card">
|
||||
<div className="capability-card__header">
|
||||
<div>
|
||||
<strong>{t('mcp.webSearch.title')}</strong>
|
||||
<small>{t('mcp.webSearch.subtitle')}</small>
|
||||
</div>
|
||||
</div>
|
||||
<label className="toggle-row">
|
||||
<input
|
||||
aria-label={t('mcp.webSearch.enableAriaLabel')}
|
||||
checked={webSearch.enabled}
|
||||
disabled={Boolean(busy)}
|
||||
onChange={(event) =>
|
||||
void run('web-search:toggle', () =>
|
||||
window.goodbuddy.capabilities.setWebSearchEnabled?.(
|
||||
event.target.checked
|
||||
) ??
|
||||
Promise.reject(
|
||||
new Error(t('mcp.webSearch.unsupported'))
|
||||
)
|
||||
)
|
||||
}
|
||||
role="switch"
|
||||
type="checkbox"
|
||||
/>
|
||||
<span>
|
||||
{webSearch.enabled
|
||||
? t('mcp.webSearch.enabled')
|
||||
: t('mcp.webSearch.disabled')}
|
||||
</span>
|
||||
</label>
|
||||
<p>{t('mcp.webSearch.description')}</p>
|
||||
<p className="computer-capability-risk">
|
||||
<CircleAlert aria-hidden="true" size={13} />
|
||||
{t('mcp.webSearch.privacy')}
|
||||
</p>
|
||||
<div className="capability-card__actions">
|
||||
<button
|
||||
className="secondary-button"
|
||||
disabled={Boolean(busy)}
|
||||
onClick={() => void testDirectModelWebSearch()}
|
||||
type="button"
|
||||
>
|
||||
<FlaskConical aria-hidden="true" size={13} />
|
||||
{busy === 'test:web-search'
|
||||
? t('mcp.webSearch.testing')
|
||||
: t('mcp.webSearch.test')}
|
||||
</button>
|
||||
</div>
|
||||
{webSearchTestResult && (
|
||||
<div
|
||||
aria-label={t('mcp.webSearch.resultAriaLabel')}
|
||||
className="capability-diagnostic__result"
|
||||
>
|
||||
<strong>
|
||||
{t('mcp.webSearch.result', {
|
||||
duration: webSearchTestResult.durationMs
|
||||
})}
|
||||
</strong>
|
||||
<p>{webSearchTestResult.preview}</p>
|
||||
</div>
|
||||
)}
|
||||
<section
|
||||
aria-label={t('mcp.webSearch.toolsAriaLabel')}
|
||||
className="mcp-server-tools"
|
||||
>
|
||||
<ul>
|
||||
{builtinModelToolGroups
|
||||
.find((group) => group.id === 'web')
|
||||
?.tools.map((tool) => (
|
||||
<li key={tool.name}>
|
||||
<div>
|
||||
<code>{tool.name}</code>
|
||||
<span className="builtin-tool-badge">
|
||||
{t('mcp.builtin.readOnly')}
|
||||
{builtinModelToolGroups
|
||||
.filter((group) => group.id === 'web')
|
||||
.map((group) => {
|
||||
const expansionId = `model-tools:${group.id}`
|
||||
const expanded = expandedItemIds.has(expansionId)
|
||||
const panelId = `model-tool-group-${group.id}`
|
||||
return (
|
||||
<article className="mcp-server-card" key={group.id}>
|
||||
<button
|
||||
aria-controls={panelId}
|
||||
aria-expanded={expanded}
|
||||
aria-label={t(
|
||||
expanded
|
||||
? 'mcp.modelTools.collapseGroup'
|
||||
: 'mcp.modelTools.expandGroup',
|
||||
{ name: t('mcp.webSearch.title') }
|
||||
)}
|
||||
className="mcp-server-card__toggle"
|
||||
onClick={() => toggleItem(expansionId)}
|
||||
type="button"
|
||||
>
|
||||
<div>
|
||||
<strong>{t('mcp.webSearch.title')}</strong>
|
||||
<small>{t('mcp.webSearch.subtitle')}</small>
|
||||
</div>
|
||||
<span className="mcp-server-card__summary">
|
||||
{t('mcp.builtin.toolCount', {
|
||||
count: group.tools.length
|
||||
})}
|
||||
<ChevronDown
|
||||
aria-hidden="true"
|
||||
className={
|
||||
expanded
|
||||
? 'mcp-server-card__chevron mcp-server-card__chevron--expanded'
|
||||
: 'mcp-server-card__chevron'
|
||||
}
|
||||
size={15}
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
{expanded && (
|
||||
<div className="mcp-server-card__body" id={panelId}>
|
||||
<label className="toggle-row">
|
||||
<input
|
||||
aria-label={t('mcp.webSearch.enableAriaLabel')}
|
||||
checked={webSearch.enabled}
|
||||
disabled={Boolean(busy)}
|
||||
onChange={(event) =>
|
||||
void run('web-search:toggle', () =>
|
||||
window.goodbuddy.capabilities.setWebSearchEnabled?.(
|
||||
event.target.checked
|
||||
) ??
|
||||
Promise.reject(
|
||||
new Error(t('mcp.webSearch.unsupported'))
|
||||
)
|
||||
)
|
||||
}
|
||||
role="switch"
|
||||
type="checkbox"
|
||||
/>
|
||||
<span>
|
||||
{webSearch.enabled
|
||||
? t('mcp.webSearch.enabled')
|
||||
: t('mcp.webSearch.disabled')}
|
||||
</span>
|
||||
</label>
|
||||
<p>{t('mcp.webSearch.description')}</p>
|
||||
<p className="computer-capability-risk">
|
||||
<CircleAlert aria-hidden="true" size={13} />
|
||||
{t('mcp.webSearch.privacy')}
|
||||
</p>
|
||||
<div className="capability-card__actions">
|
||||
<button
|
||||
className="secondary-button"
|
||||
disabled={Boolean(busy)}
|
||||
onClick={() => void testDirectModelWebSearch()}
|
||||
type="button"
|
||||
>
|
||||
<FlaskConical aria-hidden="true" size={13} />
|
||||
{busy === 'test:web-search'
|
||||
? t('mcp.webSearch.testing')
|
||||
: t('mcp.webSearch.test')}
|
||||
</button>
|
||||
</div>
|
||||
<p>{tool.description}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
</article>
|
||||
{webSearchTestResult && (
|
||||
<div
|
||||
aria-label={t(
|
||||
'mcp.webSearch.resultAriaLabel'
|
||||
)}
|
||||
className="capability-diagnostic__result"
|
||||
>
|
||||
<strong>
|
||||
{t('mcp.webSearch.result', {
|
||||
duration:
|
||||
webSearchTestResult.durationMs
|
||||
})}
|
||||
</strong>
|
||||
<p>{webSearchTestResult.preview}</p>
|
||||
</div>
|
||||
)}
|
||||
<section
|
||||
aria-label={t(
|
||||
'mcp.webSearch.toolsAriaLabel'
|
||||
)}
|
||||
className="mcp-server-tools"
|
||||
>
|
||||
<div className="mcp-server-tools__heading">
|
||||
<strong>{t('mcp.builtin.tools')}</strong>
|
||||
<small>
|
||||
{t('mcp.profiles.count', {
|
||||
count: group.tools.length
|
||||
})}
|
||||
</small>
|
||||
</div>
|
||||
<ul>
|
||||
{group.tools.map((tool) => (
|
||||
<li key={tool.name}>
|
||||
<div>
|
||||
<span className="mcp-server-tool__identity">
|
||||
<strong>{tool.displayName}</strong>
|
||||
<code>{tool.name}</code>
|
||||
</span>
|
||||
<span className="builtin-tool-badge">
|
||||
{t('mcp.builtin.readOnly')}
|
||||
</span>
|
||||
</div>
|
||||
<p>{tool.description}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
)
|
||||
})}
|
||||
{builtinModelToolGroups
|
||||
.filter((group) => group.id !== 'web')
|
||||
.map((group) => {
|
||||
@@ -1050,7 +1227,6 @@ export function McpSettingsSection({
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{editor &&
|
||||
|
||||
@@ -15,7 +15,10 @@ import type {
|
||||
DesktopApi,
|
||||
RuntimeSettings
|
||||
} from '../../shared/contracts'
|
||||
import type { CapabilitySnapshot } from '../../shared/capability-contracts'
|
||||
import type {
|
||||
CapabilityAssignments,
|
||||
CapabilitySnapshot
|
||||
} from '../../shared/capability-contracts'
|
||||
import type { ApplicationSettings } from '../../shared/application-settings-contracts'
|
||||
import type {
|
||||
EmbeddingDiagnosticResult,
|
||||
@@ -191,6 +194,35 @@ const capabilitySnapshot = {
|
||||
)[]
|
||||
}
|
||||
],
|
||||
builtinMcpServers: [
|
||||
{
|
||||
id: 'knowledge-base' as const,
|
||||
enabled: true,
|
||||
assignments: ['model', 'opencode', 'continue'] as (
|
||||
| 'model'
|
||||
| 'opencode'
|
||||
| 'continue'
|
||||
)[]
|
||||
},
|
||||
{
|
||||
id: 'magic-notes' as const,
|
||||
enabled: true,
|
||||
assignments: ['model', 'opencode', 'continue'] as (
|
||||
| 'model'
|
||||
| 'opencode'
|
||||
| 'continue'
|
||||
)[]
|
||||
},
|
||||
{
|
||||
id: 'goodbuddy-config' as const,
|
||||
enabled: true,
|
||||
assignments: ['model', 'opencode', 'continue'] as (
|
||||
| 'model'
|
||||
| 'opencode'
|
||||
| 'continue'
|
||||
)[]
|
||||
}
|
||||
],
|
||||
mcpServers: [] as CapabilitySnapshot['mcpServers'],
|
||||
webSearch: {
|
||||
provider: 'exa' as const,
|
||||
@@ -254,6 +286,24 @@ const setSkillEnabled = vi.fn(async (_skillId: string, enabled: boolean) => ({
|
||||
enabled
|
||||
}))
|
||||
}))
|
||||
const setBuiltinMcpServerEnabled = vi.fn(
|
||||
async (serverId: string, enabled: boolean) => ({
|
||||
...capabilitySnapshot,
|
||||
builtinMcpServers: capabilitySnapshot.builtinMcpServers.map(
|
||||
(server) =>
|
||||
server.id === serverId ? { ...server, enabled } : server
|
||||
)
|
||||
})
|
||||
)
|
||||
const setBuiltinMcpServerAssignments = vi.fn(
|
||||
async (serverId: string, assignments: CapabilityAssignments) => ({
|
||||
...capabilitySnapshot,
|
||||
builtinMcpServers: capabilitySnapshot.builtinMcpServers.map(
|
||||
(server) =>
|
||||
server.id === serverId ? { ...server, assignments } : server
|
||||
)
|
||||
})
|
||||
)
|
||||
const setComputerCapabilityEnabled = vi.fn(
|
||||
async (_capabilityId: string, enabled: boolean) => ({
|
||||
...capabilitySnapshot,
|
||||
@@ -539,6 +589,8 @@ describe('SettingsPanel runtime files', () => {
|
||||
removeSkill: vi.fn(async () => capabilitySnapshot),
|
||||
setSkillEnabled,
|
||||
setSkillAssignments: vi.fn(async () => capabilitySnapshot),
|
||||
setBuiltinMcpServerEnabled,
|
||||
setBuiltinMcpServerAssignments,
|
||||
saveMcpServer,
|
||||
removeMcpServer: vi.fn(async () => capabilitySnapshot),
|
||||
testMcpServer: vi.fn(async () => ({
|
||||
@@ -1042,7 +1094,7 @@ describe('SettingsPanel runtime files', () => {
|
||||
name: '展开服务器 笔记'
|
||||
})
|
||||
expect(noteServerToggle.closest('article')).toHaveClass(
|
||||
'mcp-server-card--disabled'
|
||||
'capability-card--disabled'
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: '平台功能' }))
|
||||
@@ -1063,7 +1115,7 @@ describe('SettingsPanel runtime files', () => {
|
||||
screen
|
||||
.getByRole('button', { name: '展开服务器 笔记' })
|
||||
.closest('article')
|
||||
).not.toHaveClass('mcp-server-card--disabled')
|
||||
).not.toHaveClass('capability-card--disabled')
|
||||
)
|
||||
expect(
|
||||
screen.getAllByText('内置 MCP Server · 按模式读写 · 按对话授权')
|
||||
@@ -1089,6 +1141,9 @@ describe('SettingsPanel runtime files', () => {
|
||||
expect(navigation.parentElement).toHaveClass('settings-panel__body')
|
||||
expect(content.parentElement).toBe(navigation.parentElement)
|
||||
expect(content).toHaveClass('settings-panel__content')
|
||||
expect(
|
||||
screen.getByRole('tab', { name: 'Agent Runtime' })
|
||||
).toHaveTextContent('配置 Agent Runtime、默认工作区与原生能力')
|
||||
})
|
||||
|
||||
it('omits the redundant close-only footer on passive settings pages', () => {
|
||||
@@ -3470,12 +3525,11 @@ describe('SettingsPanel runtime files', () => {
|
||||
within(mcpTabs)
|
||||
.getAllByRole('tab')
|
||||
.map((tab) => tab.textContent)
|
||||
).toEqual(['内置能力', '电脑控制', '自定义 MCP'])
|
||||
).toEqual(['内置 MCP', '直连模型', '自定义 MCP', '电脑控制'])
|
||||
expect(
|
||||
within(mcpTabs).getByRole('tab', { name: '内置能力' })
|
||||
within(mcpTabs).getByRole('tab', { name: '内置 MCP' })
|
||||
).toHaveAttribute('aria-selected', 'true')
|
||||
expect(await screen.findByText('浏览器能力')).toBeInTheDocument()
|
||||
expect(screen.getAllByText('托管浏览器配置').length).toBeGreaterThan(0)
|
||||
expect(await screen.findByText('GoodBuddy 内置 MCP')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByRole('switch', {
|
||||
name: '启用 Linux 桌面控制'
|
||||
@@ -3484,6 +3538,43 @@ describe('SettingsPanel runtime files', () => {
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /添加 Server/ })
|
||||
).not.toBeInTheDocument()
|
||||
const knowledgeBuiltinCard = screen
|
||||
.getByRole('switch', { name: '启用 知识库 内置 MCP' })
|
||||
.closest('article')
|
||||
expect(knowledgeBuiltinCard).not.toBeNull()
|
||||
expect(
|
||||
within(knowledgeBuiltinCard!).getByLabelText(
|
||||
/知识库 无法分配给 DeepSeek Harness/
|
||||
)
|
||||
).toBeDisabled()
|
||||
expect(
|
||||
within(knowledgeBuiltinCard!).getByLabelText(
|
||||
/知识库 无法分配给 DeepSeek Harness/
|
||||
)
|
||||
).not.toBeChecked()
|
||||
fireEvent.click(
|
||||
within(knowledgeBuiltinCard!).getByLabelText('Continue')
|
||||
)
|
||||
await waitFor(() =>
|
||||
expect(setBuiltinMcpServerAssignments).toHaveBeenCalledWith(
|
||||
'knowledge-base',
|
||||
['model', 'opencode']
|
||||
)
|
||||
)
|
||||
fireEvent.click(
|
||||
screen.getByRole('switch', { name: '启用 知识库 内置 MCP' })
|
||||
)
|
||||
await waitFor(() =>
|
||||
expect(setBuiltinMcpServerEnabled).toHaveBeenCalledWith(
|
||||
'knowledge-base',
|
||||
false
|
||||
)
|
||||
)
|
||||
fireEvent.click(
|
||||
within(mcpTabs).getByRole('tab', { name: '电脑控制' })
|
||||
)
|
||||
expect(await screen.findByText('电脑控制能力')).toBeInTheDocument()
|
||||
expect(screen.getAllByText('托管浏览器配置').length).toBeGreaterThan(0)
|
||||
fireEvent.click(
|
||||
screen.getByRole('switch', { name: '启用 浏览器控制' })
|
||||
)
|
||||
@@ -3529,24 +3620,28 @@ describe('SettingsPanel runtime files', () => {
|
||||
await waitFor(() =>
|
||||
expect(removeBrowserProfile).toHaveBeenCalledWith(browserProfileId)
|
||||
)
|
||||
fireEvent.click(
|
||||
within(mcpTabs).getByRole('tab', { name: '电脑控制' })
|
||||
)
|
||||
expect(await screen.findByText('电脑控制能力')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('switch', {
|
||||
name: '启用 Linux 桌面控制'
|
||||
})
|
||||
).toBeDisabled()
|
||||
expect(
|
||||
screen.queryByRole('switch', { name: '启用 浏览器控制' })
|
||||
).not.toBeInTheDocument()
|
||||
screen.getByRole('switch', { name: '启用 浏览器控制' })
|
||||
).toBeInTheDocument()
|
||||
fireEvent.click(
|
||||
within(mcpTabs).getByRole('tab', { name: '内置能力' })
|
||||
within(mcpTabs).getByRole('tab', { name: '直连模型' })
|
||||
)
|
||||
expect(await screen.findByText('文件系统操作')).toBeInTheDocument()
|
||||
expect(screen.getByText('浏览器操作')).toBeInTheDocument()
|
||||
expect(screen.getByText('联网搜索')).toBeInTheDocument()
|
||||
expect(screen.queryByText('web_search')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('web_fetch')).not.toBeInTheDocument()
|
||||
const webSearchToggle = screen.getByRole('button', {
|
||||
name: '展开工具组 联网搜索'
|
||||
})
|
||||
expect(webSearchToggle).toHaveAttribute('aria-expanded', 'false')
|
||||
fireEvent.click(webSearchToggle)
|
||||
expect(webSearchToggle).toHaveAttribute('aria-expanded', 'true')
|
||||
expect(screen.getByText('web_search')).toBeInTheDocument()
|
||||
expect(screen.getByText('web_fetch')).toBeInTheDocument()
|
||||
expect(
|
||||
@@ -3569,6 +3664,9 @@ describe('SettingsPanel runtime files', () => {
|
||||
expect(screen.getByText('GoodBuddy search result')).toBeInTheDocument()
|
||||
expect(testWebSearch).toHaveBeenCalledOnce()
|
||||
expect(screen.queryByText('读取工作区文本')).not.toBeInTheDocument()
|
||||
fireEvent.click(
|
||||
within(mcpTabs).getByRole('tab', { name: '内置 MCP' })
|
||||
)
|
||||
expect(screen.getByText('知识库')).toBeInTheDocument()
|
||||
expect(screen.queryByText('knowledge_list')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('knowledge_search')).not.toBeInTheDocument()
|
||||
@@ -3599,7 +3697,7 @@ describe('SettingsPanel runtime files', () => {
|
||||
)
|
||||
).toBeInTheDocument()
|
||||
expect(noteServerToggle.closest('article')).toHaveClass(
|
||||
'mcp-server-card--disabled'
|
||||
'capability-card--disabled'
|
||||
)
|
||||
fireEvent.click(noteServerToggle)
|
||||
expect(
|
||||
@@ -3614,11 +3712,14 @@ describe('SettingsPanel runtime files', () => {
|
||||
})
|
||||
).toHaveLength(builtinMcpServers.length)
|
||||
expect(
|
||||
screen.getByText('可用于:模型、OpenCode、Continue')
|
||||
screen.getByText(/按请求提供,可分别控制启停与 Runtime 分配/)
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByText(/不公开服务地址或凭据/)
|
||||
).toBeInTheDocument()
|
||||
fireEvent.click(
|
||||
within(mcpTabs).getByRole('tab', { name: '直连模型' })
|
||||
)
|
||||
const filesystemToggle = screen.getByRole('button', {
|
||||
name: '展开工具组 文件系统操作'
|
||||
})
|
||||
@@ -3633,9 +3734,7 @@ describe('SettingsPanel runtime files', () => {
|
||||
expect(screen.getByText('浏览器导航')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getAllByRole('button', { name: /工具组/u })
|
||||
).toHaveLength(
|
||||
builtinModelToolGroups.filter((group) => group.id !== 'web').length
|
||||
)
|
||||
).toHaveLength(builtinModelToolGroups.length)
|
||||
fireEvent.click(
|
||||
within(mcpTabs).getByRole('tab', { name: '自定义 MCP' })
|
||||
)
|
||||
|
||||
@@ -174,7 +174,8 @@ export const integrations = {
|
||||
sectionAriaLabel: 'MCP configuration',
|
||||
tabs: {
|
||||
ariaLabel: 'MCP settings categories',
|
||||
builtin: 'Built-in capabilities',
|
||||
builtin: 'Built-in MCP',
|
||||
modelTools: 'Direct model',
|
||||
computer: 'Computer control',
|
||||
custom: 'Custom MCP'
|
||||
},
|
||||
@@ -222,9 +223,19 @@ export const integrations = {
|
||||
},
|
||||
builtin: {
|
||||
title: 'Built-in GoodBuddy MCP',
|
||||
availableTo: 'Available to: Model, OpenCode, Continue',
|
||||
availableTo:
|
||||
'Provided per request with independent enablement and runtime assignment',
|
||||
notice:
|
||||
'GoodBuddy grants built-in MCP short-lived permissions for the current conversation in the main process without exposing service addresses or credentials.',
|
||||
'GoodBuddy grants built-in MCP short-lived permissions for the current conversation in the main process without exposing service addresses or credentials. Changes apply to subsequent requests, while Ask and Execute access boundaries remain enforced by the system.',
|
||||
enabled: 'Enabled',
|
||||
disabled: 'Disabled',
|
||||
enableAriaLabel: 'Enable the built-in {{name}} MCP',
|
||||
assignedTo: 'Assign to',
|
||||
runtimeUnsupported:
|
||||
'This runtime does not currently support request-scoped built-in MCP',
|
||||
runtimeAssignmentUnsupportedAriaLabel:
|
||||
'{{name}} cannot be assigned to {{runtime}} because this runtime does not support built-in MCP',
|
||||
unsupportedSuffix: ' (not supported yet)',
|
||||
serverSummaryMixed:
|
||||
'Built-in MCP server · Access depends on mode · Authorized per conversation',
|
||||
serverSummaryReadOnly:
|
||||
|
||||
@@ -43,9 +43,9 @@ export const settings = {
|
||||
runtime: {
|
||||
label: 'Agent Runtime',
|
||||
navigationDescription:
|
||||
'OpenCode, Continue, DeepSeek Harness, and workspace settings',
|
||||
'Configure Agent Runtimes, the default workspace, and native capabilities',
|
||||
description:
|
||||
'OpenCode, Continue, DeepSeek Harness, and workspace settings'
|
||||
'Configure Agent Runtimes, the default workspace, and native capabilities'
|
||||
},
|
||||
security: {
|
||||
label: 'Security and data',
|
||||
|
||||
@@ -161,7 +161,8 @@ export const integrations = {
|
||||
sectionAriaLabel: 'MCP 配置',
|
||||
tabs: {
|
||||
ariaLabel: 'MCP 设置分类',
|
||||
builtin: '内置能力',
|
||||
builtin: '内置 MCP',
|
||||
modelTools: '直连模型',
|
||||
computer: '电脑控制',
|
||||
custom: '自定义 MCP'
|
||||
},
|
||||
@@ -209,9 +210,17 @@ export const integrations = {
|
||||
},
|
||||
builtin: {
|
||||
title: 'GoodBuddy 内置 MCP',
|
||||
availableTo: '可用于:模型、OpenCode、Continue',
|
||||
availableTo: '按请求提供,可分别控制启停与 Runtime 分配',
|
||||
notice:
|
||||
'内置 MCP 由 GoodBuddy 在主进程按当前对话签发短期权限,不公开服务地址或凭据。',
|
||||
'内置 MCP 由 GoodBuddy 在主进程按当前对话签发短期权限,不公开服务地址或凭据。设置更改会应用到后续请求,Ask / Execute 的读写边界仍由系统强制。',
|
||||
enabled: '已启用',
|
||||
disabled: '已停用',
|
||||
enableAriaLabel: '启用 {{name}} 内置 MCP',
|
||||
assignedTo: '分配给',
|
||||
runtimeUnsupported: '当前 Runtime 不支持请求级内置 MCP',
|
||||
runtimeAssignmentUnsupportedAriaLabel:
|
||||
'{{name}} 无法分配给 {{runtime}},当前 Runtime 不支持内置 MCP',
|
||||
unsupportedSuffix: '(暂不支持)',
|
||||
serverSummaryMixed: '内置 MCP Server · 按模式读写 · 按对话授权',
|
||||
serverSummaryReadOnly: '内置 MCP Server · 只读 · 按对话授权',
|
||||
serverSummaryDisabled:
|
||||
|
||||
@@ -35,8 +35,8 @@ export const settings = {
|
||||
},
|
||||
runtime: {
|
||||
label: 'Agent Runtime',
|
||||
navigationDescription: 'OpenCode、Continue、DeepSeek Harness 与工作区',
|
||||
description: 'OpenCode、Continue、DeepSeek Harness 与工作区'
|
||||
navigationDescription: '配置 Agent Runtime、默认工作区与原生能力',
|
||||
description: '配置 Agent Runtime、默认工作区与原生能力'
|
||||
},
|
||||
security: {
|
||||
label: '安全与数据',
|
||||
|
||||
@@ -6812,6 +6812,27 @@ details.settings-section > :not(summary) + :not(summary) {
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.mcp-settings__tabs {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.mcp-settings__tabs > .page-tabs {
|
||||
width: 100%;
|
||||
min-width: max-content;
|
||||
}
|
||||
|
||||
.mcp-settings__tabs .page-tabs__tab {
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mcp-settings__panel {
|
||||
min-width: 0;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.mcp-server-list {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
@@ -6833,6 +6854,32 @@ details.settings-section > :not(summary) + :not(summary) {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.capability-card--disabled {
|
||||
border-style: dashed;
|
||||
background: var(--surface-muted);
|
||||
}
|
||||
|
||||
.builtin-mcp-card__details {
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.builtin-mcp-card__tools {
|
||||
display: grid;
|
||||
padding-top: var(--space-3);
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.runtime-assignment--unsupported {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.runtime-assignment--unsupported input {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.mcp-server-card__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { RuntimeTarget } from './capability-contracts'
|
||||
import type {
|
||||
BuiltinMcpServerId,
|
||||
RuntimeTarget
|
||||
} from './capability-contracts'
|
||||
|
||||
import {
|
||||
knowledgeScopedDataTools,
|
||||
@@ -7,7 +10,7 @@ import {
|
||||
import { goodbuddyConfigTools } from './goodbuddy-config-tools'
|
||||
|
||||
export type BuiltinMcpServerSummary = {
|
||||
id: string
|
||||
id: BuiltinMcpServerId
|
||||
name: string
|
||||
description: string
|
||||
tools: readonly {
|
||||
@@ -15,7 +18,7 @@ export type BuiltinMcpServerSummary = {
|
||||
description: string
|
||||
access: 'read' | 'write'
|
||||
}[]
|
||||
assignments: readonly RuntimeTarget[]
|
||||
supportedAssignments: readonly RuntimeTarget[]
|
||||
access: 'read' | 'mixed'
|
||||
authorization: 'conversation-scoped'
|
||||
requiresFeature?: 'magic-notes'
|
||||
@@ -32,7 +35,7 @@ export const builtinMcpServers = [
|
||||
description: summary,
|
||||
access
|
||||
})),
|
||||
assignments: ['model', 'opencode', 'continue'],
|
||||
supportedAssignments: ['model', 'opencode', 'continue'],
|
||||
access: 'read',
|
||||
authorization: 'conversation-scoped'
|
||||
},
|
||||
@@ -46,7 +49,7 @@ export const builtinMcpServers = [
|
||||
description: summary,
|
||||
access
|
||||
})),
|
||||
assignments: ['model', 'opencode', 'continue'],
|
||||
supportedAssignments: ['model', 'opencode', 'continue'],
|
||||
access: 'mixed',
|
||||
authorization: 'conversation-scoped',
|
||||
requiresFeature: 'magic-notes'
|
||||
@@ -61,7 +64,7 @@ export const builtinMcpServers = [
|
||||
description: summary,
|
||||
access
|
||||
})),
|
||||
assignments: ['model', 'opencode', 'continue'],
|
||||
supportedAssignments: ['model', 'opencode', 'continue'],
|
||||
access: 'mixed',
|
||||
authorization: 'conversation-scoped'
|
||||
}
|
||||
|
||||
@@ -35,6 +35,46 @@ export type CapabilityAssignments = z.infer<
|
||||
typeof capabilityAssignmentsSchema
|
||||
>
|
||||
|
||||
export const builtinMcpServerIdSchema = z.enum([
|
||||
'knowledge-base',
|
||||
'magic-notes',
|
||||
'goodbuddy-config'
|
||||
])
|
||||
export type BuiltinMcpServerId = z.infer<
|
||||
typeof builtinMcpServerIdSchema
|
||||
>
|
||||
|
||||
export const builtinMcpAssignmentsSchema =
|
||||
capabilityAssignmentsSchema.refine(
|
||||
(assignments) => !assignments.includes('deepseek-harness'),
|
||||
'DeepSeek Harness 当前不支持内置 MCP'
|
||||
)
|
||||
|
||||
export const builtinMcpServerToggleInputSchema = z
|
||||
.object({
|
||||
serverId: builtinMcpServerIdSchema,
|
||||
enabled: z.boolean()
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const builtinMcpServerAssignmentsInputSchema = z
|
||||
.object({
|
||||
serverId: builtinMcpServerIdSchema,
|
||||
assignments: builtinMcpAssignmentsSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const builtinMcpServerStateSummarySchema = z
|
||||
.object({
|
||||
id: builtinMcpServerIdSchema,
|
||||
enabled: z.boolean(),
|
||||
assignments: builtinMcpAssignmentsSchema
|
||||
})
|
||||
.strict()
|
||||
export type BuiltinMcpServerStateSummary = z.infer<
|
||||
typeof builtinMcpServerStateSummarySchema
|
||||
>
|
||||
|
||||
export const secretActionSchema = z.discriminatedUnion('action', [
|
||||
z.object({ action: z.literal('keep') }).strict(),
|
||||
z
|
||||
@@ -327,6 +367,10 @@ export type WebSearchCapability = z.infer<
|
||||
export const capabilitySnapshotSchema = z
|
||||
.object({
|
||||
skills: z.array(skillSummarySchema).max(256),
|
||||
builtinMcpServers: z
|
||||
.array(builtinMcpServerStateSummarySchema)
|
||||
.max(3)
|
||||
.optional(),
|
||||
mcpServers: z.array(mcpServerSummarySchema).max(64),
|
||||
webSearch: webSearchCapabilitySchema.optional(),
|
||||
computerCapabilities: z
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import type {
|
||||
BrowserProfileCreateInput,
|
||||
BrowserProfileRenameInput,
|
||||
BuiltinMcpServerId,
|
||||
CapabilityDiagnosticReport,
|
||||
CapabilityAssignments,
|
||||
CapabilitySnapshot,
|
||||
@@ -1532,6 +1533,14 @@ export type DesktopApi = {
|
||||
skillId: string,
|
||||
assignments: CapabilityAssignments
|
||||
) => Promise<CapabilitySnapshot>
|
||||
setBuiltinMcpServerEnabled: (
|
||||
serverId: BuiltinMcpServerId,
|
||||
enabled: boolean
|
||||
) => Promise<CapabilitySnapshot>
|
||||
setBuiltinMcpServerAssignments: (
|
||||
serverId: BuiltinMcpServerId,
|
||||
assignments: CapabilityAssignments
|
||||
) => Promise<CapabilitySnapshot>
|
||||
saveMcpServer: (
|
||||
serverId: string | undefined,
|
||||
input: McpServerInput
|
||||
|
||||
@@ -128,6 +128,8 @@ export const ipcChannels = {
|
||||
capabilitiesRemoveSkill: 'capabilities:skill:remove',
|
||||
capabilitiesToggleSkill: 'capabilities:skill:toggle',
|
||||
capabilitiesAssignSkill: 'capabilities:skill:assign',
|
||||
capabilitiesToggleBuiltinMcp: 'capabilities:builtin-mcp:toggle',
|
||||
capabilitiesAssignBuiltinMcp: 'capabilities:builtin-mcp:assign',
|
||||
capabilitiesSaveMcp: 'capabilities:mcp:save',
|
||||
capabilitiesRemoveMcp: 'capabilities:mcp:remove',
|
||||
capabilitiesTestMcp: 'capabilities:mcp:test',
|
||||
|
||||
Reference in New Issue
Block a user