Files
goodbuddy/src/main/agent/mcp-tool-utils.ts
T
mesalogo cbbe30896e feat: expand multi-runtime workflows for 0.10.0
GoodBuddy previously exposed Runtime capabilities, MCP assignments, plugin controls, context compaction, usage reporting, and task-completion behavior through incomplete or inconsistent paths. This release unifies managed OpenCode, Continue, and DeepSeek Harness controls; adds DSH plugin and image workflows; strengthens MCP and Runtime lifecycle bounds; fixes Windows notification activation; validates cross-architecture packages; and presents the approved bilingual four-section release notes.

The DSH plugin marketplace remains a default-off preview whose trusted third-party code runs with current-user permissions. Ask remains read-only, Execute keeps approval controls, and context compaction may use the selected model without deleting GoodBuddy chat history.

Release note: GoodBuddy 0.10.0 重点完善多 Runtime 工作流,统一 OpenCode、Continue 与 DeepSeek Harness 的能力、MCP、插件和上下文管理,并提升长对话、多会话与任务通知的连贯性。
2026-08-17 12:57:12 +08:00

135 lines
3.7 KiB
TypeScript

import { createHash } from 'node:crypto'
import type { Client } from '@modelcontextprotocol/sdk/client/index.js'
const MAXIMUM_MCP_TOOL_SCHEMA_BYTES = 32 * 1024
const DEFAULT_MAXIMUM_MCP_TOOL_PAGES = 100
type ListedMcpTool =
Awaited<ReturnType<Client['listTools']>>['tools'][number]
export async function listAllMcpTools(
client: Pick<Client, 'listTools'>,
serverName: string,
signal: AbortSignal,
options: {
maximumTools: number
pageTimeoutMs: number
totalTimeoutMs: number
maximumPages?: number
}
): Promise<ListedMcpTool[]> {
const tools: ListedMcpTool[] = []
const toolNames = new Set<string>()
const cursors = new Set<string>()
const deadline = Date.now() + options.totalTimeoutMs
const maximumPages =
options.maximumPages ?? DEFAULT_MAXIMUM_MCP_TOOL_PAGES
let cursor: string | undefined
for (let page = 0; page < maximumPages; page += 1) {
signal.throwIfAborted()
const remainingMs = deadline - Date.now()
if (remainingMs <= 0) {
throw new Error(
`MCP Server「${serverName}」的工具分页超过总超时`
)
}
const result = await client.listTools(
cursor !== undefined ? { cursor } : undefined,
{
timeout: Math.max(
1,
Math.min(options.pageTimeoutMs, remainingMs)
),
signal
}
)
for (const tool of result.tools) {
if (toolNames.has(tool.name)) {
throw new Error(
`MCP Server「${serverName}」返回了重复工具「${tool.name}」`
)
}
toolNames.add(tool.name)
tools.push(tool)
if (tools.length > options.maximumTools) {
throw new Error(
`MCP Server「${serverName}」提供的工具数量超过安全限制`
)
}
}
if (result.nextCursor === undefined) {
return tools
}
if (cursors.has(result.nextCursor)) {
throw new Error(
`MCP Server「${serverName}」的工具分页游标发生循环`
)
}
cursors.add(result.nextCursor)
cursor = result.nextCursor
}
throw new Error(
`MCP Server「${serverName}」的工具分页超过安全限制`
)
}
export function isValidMcpToolName(value: unknown): value is string {
return (
typeof value === 'string' &&
value.length > 0 &&
value.length <= 128 &&
![...value].some((character) => {
const code = character.charCodeAt(0)
return code <= 31 || code === 127
})
)
}
export function createMcpToolName(
serverId: string,
originalName: string
): string {
const serverHash = createHash('sha256')
.update(serverId)
.digest('hex')
.slice(0, 8)
const toolHash = createHash('sha256')
.update(originalName)
.digest('hex')
.slice(0, 8)
const readable =
originalName
.replace(/[^a-zA-Z0-9_-]+/gu, '_')
.replace(/^_+|_+$/gu, '')
.slice(0, 36) || 'tool'
return `mcp_${serverHash}_${toolHash}_${readable}`.slice(0, 64)
}
export function normalizeMcpToolSchema(
value: unknown
): Record<string, unknown> & { type: 'object' } {
let serialized: string
try {
serialized = JSON.stringify(value)
} catch (error) {
throw new Error('MCP 工具参数结构无效', { cause: error })
}
if (
!serialized ||
Buffer.byteLength(serialized) >
MAXIMUM_MCP_TOOL_SCHEMA_BYTES
) {
throw new Error('MCP 工具参数结构超过 32KB 安全限制')
}
const schema = JSON.parse(serialized) as unknown
if (
!schema ||
typeof schema !== 'object' ||
Array.isArray(schema) ||
(schema as Record<string, unknown>).type !== 'object'
) {
throw new Error('MCP 工具参数必须使用 object JSON Schema')
}
return schema as Record<string, unknown> & { type: 'object' }
}