feat: add MCP tool integration

This commit is contained in:
Xin Wang
2026-07-18 00:00:06 +08:00
parent bdf3d3dd9c
commit e39bb48ba8
21 changed files with 1641 additions and 62 deletions

View File

@@ -12,6 +12,7 @@ import {
Trash2,
} from "lucide-react";
import { McpServersSection } from "@/components/tools/McpServersSection";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { DataList, type DataListColumn } from "@/components/ui/data-list";
@@ -52,12 +53,12 @@ import {
type ToolUpsert,
} from "@/lib/api";
type ToolKind = "end_call" | "http";
type ToolKind = "end_call" | "http" | "mcp";
type HttpMethod = HttpToolDefinition["config"]["method"];
type ToolFilter = "全部" | "End Call" | "HTTP";
type ToolFilter = "全部" | "End Call" | "HTTP" | "MCP";
type SortOrder = "newest" | "oldest";
const toolFilters: readonly ToolFilter[] = ["全部", "End Call", "HTTP"];
const toolFilters: readonly ToolFilter[] = ["全部", "End Call", "HTTP", "MCP"];
type ToolForm = {
name: string;
@@ -77,6 +78,10 @@ type ToolForm = {
body: string;
dynamicVariableAssignments: string;
secretDynamicVariables: string;
mcpServerId: string;
remoteToolName: string;
inputSchema: string;
schemaHash: string;
};
const EMPTY_OBJECT = "{}";
@@ -101,6 +106,10 @@ function blankForm(): ToolForm {
body: EMPTY_OBJECT,
dynamicVariableAssignments: EMPTY_OBJECT,
secretDynamicVariables: EMPTY_OBJECT,
mcpServerId: "",
remoteToolName: "",
inputSchema: EMPTY_OBJECT,
schemaHash: "",
};
}
@@ -119,6 +128,17 @@ function formFromTool(tool: Tool): ToolForm {
base.captureReason = tool.definition.config.captureReason;
return base;
}
if (tool.definition.type === "mcp") {
base.mcpServerId = tool.mcpServerId ?? "";
base.remoteToolName = tool.definition.config.remoteToolName;
base.inputSchema = pretty(tool.definition.config.inputSchema, EMPTY_OBJECT);
base.schemaHash = tool.definition.config.schemaHash;
base.dynamicVariableAssignments = pretty(
tool.definition.config.dynamicVariableAssignments ?? {},
EMPTY_OBJECT,
);
return base;
}
base.method = tool.definition.config.method;
base.url = tool.definition.config.url;
base.timeoutSeconds = String(tool.definition.config.timeoutSeconds);
@@ -198,6 +218,29 @@ function payloadFromForm(form: ToolForm): ToolUpsert {
},
};
}
if (form.type === "mcp") {
return {
name: form.name.trim(),
functionName: form.functionName,
description: form.description.trim(),
status: form.status,
mcpServerId: form.mcpServerId,
secrets: {},
definition: {
schemaVersion: 1,
type: "mcp",
config: {
remoteToolName: form.remoteToolName,
inputSchema: parseObject(form.inputSchema, "MCP Input Schema"),
schemaHash: form.schemaHash,
dynamicVariableAssignments: parseObject(
form.dynamicVariableAssignments,
"变量赋值",
) as Record<string, string>,
},
},
};
}
const timeoutSeconds = Number(form.timeoutSeconds);
if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > 120) {
@@ -295,7 +338,8 @@ export function ComponentsToolsPage() {
return tools.filter((tool) =>
(filter === "全部" ||
(filter === "End Call" && tool.type === "end_call") ||
(filter === "HTTP" && tool.type === "http")) &&
(filter === "HTTP" && tool.type === "http") ||
(filter === "MCP" && tool.type === "mcp")) &&
(!query ||
[tool.name, tool.functionName, tool.description].some((value) =>
value.toLowerCase().includes(query),
@@ -411,7 +455,11 @@ export function ComponentsToolsPage() {
variant="secondary"
className="h-6 bg-surface-strong px-3 text-muted-foreground"
>
{tool.type === "end_call" ? "End Call" : "HTTP"}
{tool.type === "end_call"
? "End Call"
: tool.type === "mcp"
? "MCP"
: "HTTP"}
</Badge>
),
},
@@ -478,7 +526,7 @@ export function ComponentsToolsPage() {
>
<DropdownMenuItem
className="rounded-lg"
disabled={duplicatingId === tool.id}
disabled={duplicatingId === tool.id || tool.type === "mcp"}
onSelect={(event) => {
event.preventDefault();
void duplicateTool(tool.id);
@@ -527,6 +575,8 @@ export function ComponentsToolsPage() {
}
/>
<McpServersSection onToolsChanged={loadTools} />
<section className="rounded-2xl border border-hairline bg-card p-6 shadow-sm">
<ListToolbar
filters={
@@ -607,6 +657,7 @@ export function ComponentsToolsPage() {
<Field label="工具类型" required>
<Select
value={form.type}
disabled={form.type === "mcp"}
onValueChange={(type: ToolKind) =>
setForm((current) => ({ ...current, type }))
}
@@ -615,6 +666,9 @@ export function ComponentsToolsPage() {
<SelectContent>
<SelectItem value="end_call">End Call</SelectItem>
<SelectItem value="http">HTTP</SelectItem>
<SelectItem value="mcp" disabled={!editing || form.type !== "mcp"}>
MCP Server
</SelectItem>
</SelectContent>
</Select>
</Field>
@@ -647,6 +701,8 @@ export function ComponentsToolsPage() {
<FieldSection title="参数配置" scrollable tall>
{form.type === "end_call" ? (
<EndCallFields form={form} setForm={setForm} />
) : form.type === "mcp" ? (
<McpFields form={form} setForm={setForm} />
) : (
<HttpFields form={form} setForm={setForm} />
)}
@@ -720,6 +776,42 @@ function EndCallFields({
);
}
function McpFields({
form,
setForm,
}: {
form: ToolForm;
setForm: React.Dispatch<React.SetStateAction<ToolForm>>;
}) {
return (
<div className="space-y-4">
<div className="rounded-xl border border-hairline bg-canvas-soft px-4 py-3 text-sm text-muted-foreground">
Schema MCP Server
</div>
<Field label="远端工具名称">
<Input value={form.remoteToolName} disabled className="font-mono" />
</Field>
<Field label="MCP Server ID">
<Input value={form.mcpServerId} disabled className="font-mono" />
</Field>
<JsonField
label="Input Schema只读"
value={form.inputSchema}
onChange={() => undefined}
rows={10}
disabled
/>
<JsonField
label="响应变量赋值"
value={form.dynamicVariableAssignments}
onChange={(dynamicVariableAssignments) =>
setForm((current) => ({ ...current, dynamicVariableAssignments }))
}
/>
</div>
);
}
function HttpFields({
form,
setForm,
@@ -845,11 +937,13 @@ function JsonField({
value,
onChange,
rows = 4,
disabled = false,
}: {
label: string;
value: string;
onChange: (value: string) => void;
rows?: number;
disabled?: boolean;
}) {
return (
<Field label={label}>
@@ -857,6 +951,7 @@ function JsonField({
value={value}
onChange={(event) => onChange(event.target.value)}
rows={rows}
disabled={disabled}
className="font-mono text-xs"
spellCheck={false}
/>