feat: add MCP tool integration
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user