Files
goodbuddy/src/shared/capability-contracts.ts
T
mesalogo 792d80e67c 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 会明确显示为暂不支持。
2026-08-17 00:25:46 +08:00

458 lines
12 KiB
TypeScript

import { z } from 'zod'
import { settingsWarningsSchema } from './settings-warning-contracts'
const controlCharacterFreeString = (maximumLength: number) =>
z
.string()
.trim()
.min(1)
.max(maximumLength)
.refine(
(value) =>
[...value].every((character) => {
const code = character.charCodeAt(0)
return code > 31 && code !== 127
}),
'值包含控制字符'
)
export const runtimeTargetSchema = z.enum([
'model',
'opencode',
'continue',
'deepseek-harness'
])
export type RuntimeTarget = z.infer<typeof runtimeTargetSchema>
export const capabilityAssignmentsSchema = z
.array(runtimeTargetSchema)
.max(4)
.refine(
(assignments) => new Set(assignments).size === assignments.length,
'Runtime 分配不能重复'
)
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
.object({
action: z.literal('replace'),
value: controlCharacterFreeString(8_192)
})
.strict(),
z.object({ action: z.literal('clear') }).strict()
])
export type SecretAction = z.infer<typeof secretActionSchema>
export const skillIdSchema = z
.string()
.min(1)
.max(128)
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/)
export const skillImportKindSchema = z.enum(['directory', 'zip'])
export type SkillImportKind = z.infer<typeof skillImportKindSchema>
export const skillToggleInputSchema = z
.object({
skillId: skillIdSchema,
enabled: z.boolean()
})
.strict()
export const skillAssignmentsInputSchema = z
.object({
skillId: skillIdSchema,
assignments: capabilityAssignmentsSchema
})
.strict()
export const skillSummarySchema = z
.object({
id: skillIdSchema,
name: z.string().min(1).max(80),
description: z.string().min(1).max(500),
version: z.string().max(32).optional(),
tags: z.array(z.string().min(1).max(32)).max(12),
source: z.enum(['builtin', 'imported']),
digest: z.string().regex(/^[a-f0-9]{64}$/),
enabled: z.boolean(),
assignments: capabilityAssignmentsSchema
})
.strict()
export type SkillSummary = z.infer<typeof skillSummarySchema>
export const computerCapabilityIdSchema = z.enum([
'host-browser-control',
'linux-desktop-control'
])
export type ComputerCapabilityId = z.infer<
typeof computerCapabilityIdSchema
>
export const browserProfileIdSchema = z.string().uuid()
export const browserProfileNameSchema = controlCharacterFreeString(80)
export const browserProfileCreateInputSchema = z
.object({
name: browserProfileNameSchema
})
.strict()
export type BrowserProfileCreateInput = z.infer<
typeof browserProfileCreateInputSchema
>
export const browserProfileRenameInputSchema = z
.object({
profileId: browserProfileIdSchema,
name: browserProfileNameSchema
})
.strict()
export type BrowserProfileRenameInput = z.infer<
typeof browserProfileRenameInputSchema
>
export const browserProfileSelectionInputSchema = z
.object({
profileId: browserProfileIdSchema
})
.strict()
export const browserProfileSummarySchema = z
.object({
id: browserProfileIdSchema,
name: browserProfileNameSchema,
mode: z.literal('managed-isolated')
})
.strict()
export type BrowserProfileSummary = z.infer<
typeof browserProfileSummarySchema
>
export const browserProfilesSummarySchema = z
.object({
profiles: z.array(browserProfileSummarySchema).max(32),
defaultProfileId: browserProfileIdSchema.nullable()
})
.strict()
export type BrowserProfilesSummary = z.infer<
typeof browserProfilesSummarySchema
>
export const computerCapabilityToggleInputSchema = z
.object({
capabilityId: computerCapabilityIdSchema,
enabled: z.boolean()
})
.strict()
export const computerCapabilityConfigInputSchema = z
.object({
capabilityId: computerCapabilityIdSchema,
browserProfileId: browserProfileIdSchema.nullable()
})
.strict()
export const computerCapabilityConfigSummarySchema = z
.object({
id: computerCapabilityIdSchema,
name: z.string().min(1).max(80),
description: z.string().min(1).max(500),
enabled: z.boolean(),
supported: z.boolean(),
browserProfileId: browserProfileIdSchema.nullable(),
riskSummary: z.string().min(1).max(500)
})
.strict()
export type ComputerCapabilityConfigSummary = z.infer<
typeof computerCapabilityConfigSummarySchema
>
export const capabilityDiagnosticStatusSchema = z.enum([
'available',
'degraded',
'unavailable',
'disabled'
])
export type CapabilityDiagnosticStatus = z.infer<
typeof capabilityDiagnosticStatusSchema
>
export const capabilityDiagnosticCheckStatusSchema =
capabilityDiagnosticStatusSchema.exclude(['disabled'])
export const capabilityDiagnosticCheckSchema = z
.object({
id: z
.string()
.min(1)
.max(80)
.regex(/^[a-z][a-z0-9-]*$/u),
status: capabilityDiagnosticCheckStatusSchema,
summary: z.string().min(1).max(240),
remedy: z.string().min(1).max(400).optional()
})
.strict()
export const capabilityDiagnosticReportSchema = z
.object({
capabilityId: computerCapabilityIdSchema,
status: capabilityDiagnosticStatusSchema,
checkedAt: z.string().datetime(),
checks: z.array(capabilityDiagnosticCheckSchema).max(16)
})
.strict()
export type CapabilityDiagnosticReport = z.infer<
typeof capabilityDiagnosticReportSchema
>
export const mcpTransportSchema = z.enum(['stdio', 'http', 'sse'])
export type McpTransport = z.infer<typeof mcpTransportSchema>
export const mcpServerIdSchema = z.string().uuid()
const mcpServerNameSchema = controlCharacterFreeString(80)
const mcpServerDescriptionSchema = z.string().trim().max(500)
const mcpCommandSchema = controlCharacterFreeString(4_096)
const mcpArgumentSchema = controlCharacterFreeString(4_096)
const mcpRemoteUrlSchema = z
.string()
.url()
.max(2_048)
.superRefine((value, context) => {
if (!['http:', 'https:'].includes(new URL(value).protocol)) {
context.addIssue({
code: 'custom',
message: 'MCP URL 必须使用 HTTP 或 HTTPS'
})
}
})
const mcpCommonInputShape = {
name: mcpServerNameSchema,
description: mcpServerDescriptionSchema,
enabled: z.boolean(),
allowDynamicTools: z.boolean(),
assignments: capabilityAssignmentsSchema,
secret: secretActionSchema
}
export const mcpServerInputSchema = z.discriminatedUnion('transport', [
z
.object({
...mcpCommonInputShape,
transport: z.literal('stdio'),
command: mcpCommandSchema,
args: z.array(mcpArgumentSchema).max(64)
})
.strict(),
z
.object({
...mcpCommonInputShape,
transport: z.literal('http'),
url: mcpRemoteUrlSchema
})
.strict(),
z
.object({
...mcpCommonInputShape,
transport: z.literal('sse'),
url: mcpRemoteUrlSchema
})
.strict()
])
export type McpServerInput = z.infer<typeof mcpServerInputSchema>
export const mcpServerSummarySchema = z.discriminatedUnion('transport', [
z
.object({
id: mcpServerIdSchema,
name: mcpServerNameSchema,
description: mcpServerDescriptionSchema,
enabled: z.boolean(),
allowDynamicTools: z.boolean(),
assignments: capabilityAssignmentsSchema,
secretConfigured: z.boolean(),
transport: z.literal('stdio'),
command: mcpCommandSchema,
args: z.array(mcpArgumentSchema).max(64)
})
.strict(),
z
.object({
id: mcpServerIdSchema,
name: mcpServerNameSchema,
description: mcpServerDescriptionSchema,
enabled: z.boolean(),
allowDynamicTools: z.boolean(),
assignments: capabilityAssignmentsSchema,
secretConfigured: z.boolean(),
transport: z.literal('http'),
url: mcpRemoteUrlSchema
})
.strict(),
z
.object({
id: mcpServerIdSchema,
name: mcpServerNameSchema,
description: mcpServerDescriptionSchema,
enabled: z.boolean(),
allowDynamicTools: z.boolean(),
assignments: capabilityAssignmentsSchema,
secretConfigured: z.boolean(),
transport: z.literal('sse'),
url: mcpRemoteUrlSchema
})
.strict()
])
export type McpServerSummary = z.infer<typeof mcpServerSummarySchema>
export const webSearchCapabilitySchema = z
.object({
provider: z.literal('exa'),
enabled: z.boolean(),
availableIn: z.tuple([z.literal('ask'), z.literal('execute')]),
tools: z.tuple([
z.literal('web_search'),
z.literal('web_fetch')
])
})
.strict()
export type WebSearchCapability = z.infer<
typeof webSearchCapabilitySchema
>
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
.array(computerCapabilityConfigSummarySchema)
.max(2)
.optional(),
browserProfiles: browserProfilesSummarySchema.optional(),
warnings: settingsWarningsSchema.optional()
})
.strict()
export type CapabilitySnapshot = z.infer<typeof capabilitySnapshotSchema>
export const mcpServerTestResultSchema = z
.object({
serverName: z.string().min(1).max(120).optional(),
serverVersion: z.string().min(1).max(64).optional(),
dynamicToolsSupported: z.boolean(),
toolCount: z.number().int().min(0).max(10_000),
tools: z
.array(
z
.object({
name: z.string().min(1).max(128),
description: z.string().max(500).optional()
})
.strict()
)
.max(100),
promptCount: z.number().int().min(0).max(10_000).optional(),
prompts: z
.array(
z
.object({
name: z.string().min(1).max(128),
description: z.string().max(500).optional(),
arguments: z
.array(
z
.object({
name: z.string().min(1).max(128),
description: z.string().max(500).optional(),
required: z.boolean()
})
.strict()
)
.max(32)
})
.strict()
)
.max(100)
.optional(),
resourceCount: z.number().int().min(0).max(10_000).optional(),
resources: z
.array(
z
.object({
name: z.string().min(1).max(200),
uri: z.string().min(1).max(2_048),
description: z.string().max(500).optional(),
mimeType: z.string().min(1).max(200).optional()
})
.strict()
)
.max(100)
.optional(),
promptsSupported: z.boolean().optional(),
resourcesSupported: z.boolean().optional()
})
.strict()
export type McpServerTestResult = z.infer<
typeof mcpServerTestResultSchema
>
export const webSearchTestResultSchema = z
.object({
provider: z.literal('exa'),
query: z.string().min(1).max(120),
durationMs: z.number().int().min(0),
preview: z.string().min(1).max(500)
})
.strict()
export type WebSearchTestResult = z.infer<
typeof webSearchTestResultSchema
>