refactor: enhance MCP tool management with risk policies and confirmation handling

This commit is contained in:
mlogclub
2026-07-28 11:33:15 +08:00
parent ed109047d3
commit 07370fe9a2
17 changed files with 448 additions and 117 deletions
@@ -69,3 +69,11 @@ test("saving a published AI Agent keeps the active revision online", () => {
assert.match(configWorkbenchSource, /已发布版本正在生效;再次发布后应用当前配置/)
assert.doesNotMatch(saveFunction, /loadData\(\)/)
})
test("trusted MCP tools use backend risk metadata and cannot be edited", () => {
assert.match(adminApiSource, /riskEditable: boolean/)
assert.match(configWorkbenchSource, /riskLevel: tool\.riskLevel/)
assert.match(configWorkbenchSource, /requireConfirmation: tool\.requireConfirmation/)
assert.match(configWorkbenchSource, /只读(系统定义)/)
assert.match(configWorkbenchSource, /!catalogTool\.riskEditable/)
})
@@ -73,6 +73,23 @@ type MCPToolOption = {
meta: MCPToolItem
}
function normalizeMCPToolsWithCatalog(
tools: MCPToolItem[],
catalog: MCPToolCatalogItem[],
) {
return tools.map((tool) => {
const catalogTool = catalog.find((item) => item.toolCode === tool.toolCode)
if (!catalogTool || catalogTool.riskEditable) return tool
return {
...tool,
title: catalogTool.title || tool.title,
description: catalogTool.description || tool.description,
riskLevel: catalogTool.riskLevel,
requireConfirmation: catalogTool.requireConfirmation,
}
})
}
function toText(value: string | number | undefined | null) {
if (value === undefined || value === null || value === 0) return ""
return String(value)
@@ -192,7 +209,7 @@ export function AIAgentConfigWorkbench({
setSelectedTeamIds((detail.teams ?? []).map((team) => team.id))
setSelectedSkillIds(detail.skillIds ?? [])
setSelectedKnowledgeBaseIds(detail.knowledgeBaseIds ?? [])
setMCPTools(detail.mcpTools ?? [])
setMCPTools(normalizeMCPToolsWithCatalog(detail.mcpTools ?? [], catalog ?? []))
setWorkflowBindings(
(detail.workflowBindings ?? []).map(
({ workflowVersionId, toolName, triggerInstruction, priority, enabled }) => ({
@@ -294,8 +311,8 @@ export function AIAgentConfigWorkbench({
toolName: tool.toolName,
title: tool.title || tool.toolName,
description: tool.description || "",
riskLevel: "read",
requireConfirmation: false,
riskLevel: tool.riskLevel,
requireConfirmation: tool.requireConfirmation,
arguments: undefined,
},
})),
@@ -693,63 +710,84 @@ export function AIAgentConfigWorkbench({
/>
{mcpTools.length > 0 ? (
<div className="space-y-2">
{mcpTools.map((tool) => (
<div
key={tool.toolCode}
className="flex flex-wrap items-center justify-between gap-3 rounded-lg border px-3 py-2"
>
<div className="min-w-0">
<div className="truncate text-sm font-medium">
{tool.title || tool.toolCode}
{mcpTools.map((tool) => {
const catalogTool = toolCatalog.find(
(item) => item.toolCode === tool.toolCode,
)
return (
<div
key={tool.toolCode}
className="flex flex-wrap items-center justify-between gap-3 rounded-lg border px-3 py-2"
>
<div className="min-w-0">
<div className="truncate text-sm font-medium">
{tool.title || tool.toolCode}
</div>
<div className="truncate font-mono text-xs text-muted-foreground">
{tool.toolCode}
</div>
</div>
<div className="truncate font-mono text-xs text-muted-foreground">
{tool.toolCode}
<div className="flex items-center gap-2">
{catalogTool && !catalogTool.riskEditable ? (
<Badge variant="secondary">
{tool.riskLevel === "read"
? "只读(系统定义)"
: "写操作(系统定义)"}
</Badge>
) : (
<>
<Button
type="button"
size="sm"
variant={
tool.riskLevel === "read" ? "default" : "outline"
}
onClick={() =>
setMCPTools((items) =>
items.map((item) =>
item.toolCode === tool.toolCode
? {
...item,
riskLevel: "read",
requireConfirmation: false,
}
: item,
),
)
}
>
</Button>
<Button
type="button"
size="sm"
variant={
tool.riskLevel === "write"
? "destructive"
: "outline"
}
onClick={() =>
setMCPTools((items) =>
items.map((item) =>
item.toolCode === tool.toolCode
? {
...item,
riskLevel: "write",
requireConfirmation: true,
}
: item,
),
)
}
>
</Button>
</>
)}
</div>
</div>
<div className="flex items-center gap-2">
<Button
type="button"
size="sm"
variant={tool.riskLevel === "read" ? "default" : "outline"}
onClick={() =>
setMCPTools((items) =>
items.map((item) =>
item.toolCode === tool.toolCode
? {
...item,
riskLevel: "read",
requireConfirmation: false,
}
: item,
),
)
}
>
</Button>
<Button
type="button"
size="sm"
variant={tool.riskLevel === "write" ? "destructive" : "outline"}
onClick={() =>
setMCPTools((items) =>
items.map((item) =>
item.toolCode === tool.toolCode
? {
...item,
riskLevel: "write",
requireConfirmation: true,
}
: item,
),
)
}
>
</Button>
</div>
</div>
))}
)
})}
</div>
) : null}
</FormSection>
+3
View File
@@ -578,6 +578,9 @@ export type MCPToolCatalogItem = {
description: string
inputSchema: unknown
outputSchema?: unknown
riskLevel: "read" | "write"
requireConfirmation: boolean
riskEditable: boolean
}
export type MCPToolResultContent = {