feat: add session-scoped client tool waits

This commit is contained in:
Xin Wang
2026-07-31 23:53:47 +08:00
parent f155f98e6e
commit ad5ff061bb
10 changed files with 339 additions and 26 deletions

View File

@@ -54,6 +54,7 @@ import { Textarea } from "@/components/ui/textarea";
import {
mcpServersApi,
toolsApi,
type ClientToolResponseWaitMode,
type HttpToolDefinition,
type McpServer,
type Tool,
@@ -94,6 +95,7 @@ type ToolForm = {
allowInterruptions: boolean;
executionMode: ToolExecutionMode;
waitForResponse: boolean;
responseWaitMode: ClientToolResponseWaitMode;
headers: string;
secretHeaders: string;
parameters: string;
@@ -122,6 +124,7 @@ function blankForm(): ToolForm {
allowInterruptions: true,
executionMode: "immediate",
waitForResponse: true,
responseWaitMode: "timeout",
headers: EMPTY_OBJECT,
secretHeaders: EMPTY_OBJECT,
parameters: EMPTY_ARRAY,
@@ -156,6 +159,8 @@ function formFromTool(tool: Tool): ToolForm {
tool.definition.config.allowInterruptions ?? true;
base.executionMode = tool.definition.config.executionMode ?? "async";
base.waitForResponse = tool.definition.config.waitForResponse ?? true;
base.responseWaitMode =
tool.definition.config.responseWaitMode ?? "timeout";
base.timeoutSeconds = String(tool.definition.config.timeoutSeconds);
base.parameters = pretty(tool.definition.config.parameters, EMPTY_ARRAY);
base.dynamicVariableAssignments = pretty(
@@ -249,14 +254,19 @@ function payloadFromForm(form: ToolForm): ToolUpsert {
};
}
if (form.type === "client") {
const timeoutSeconds = Number(form.timeoutSeconds);
const parsedTimeoutSeconds = Number(form.timeoutSeconds);
const hasValidTimeout =
Number.isInteger(parsedTimeoutSeconds) &&
parsedTimeoutSeconds >= 1 &&
parsedTimeoutSeconds <= 30;
if (
!Number.isInteger(timeoutSeconds) ||
timeoutSeconds < 1 ||
timeoutSeconds > 30
form.waitForResponse &&
form.responseWaitMode === "timeout" &&
!hasValidTimeout
) {
throw new Error("Client Tool 超时时间必须是 1 到 30 秒之间的整数");
}
const timeoutSeconds = hasValidTimeout ? parsedTimeoutSeconds : 15;
const dynamicVariableAssignments = form.updateDynamicVariables
? parseObject(form.dynamicVariableAssignments, "变量赋值")
: {};
@@ -272,6 +282,7 @@ function payloadFromForm(form: ToolForm): ToolUpsert {
allowInterruptions: form.allowInterruptions,
executionMode: form.executionMode,
waitForResponse: form.waitForResponse,
responseWaitMode: form.responseWaitMode,
parameters: parseParameters(form.parameters),
timeoutSeconds,
dynamicVariableAssignments:
@@ -759,7 +770,11 @@ export function ComponentsToolsPage() {
...current,
type,
...(type === "client"
? { timeoutSeconds: "3", executionMode: "async" as const }
? {
timeoutSeconds: "3",
executionMode: "async" as const,
responseWaitMode: "timeout" as const,
}
: type === "http"
? { timeoutSeconds: "15", executionMode: "immediate" as const }
: {}),
@@ -926,20 +941,45 @@ function ClientToolFields({
/>
</div>
{form.waitForResponse && (
<Field label="响应超时(秒)">
<Input
type="number"
min={1}
max={30}
value={form.timeoutSeconds}
onChange={(event) =>
setForm((current) => ({
...current,
timeoutSeconds: event.target.value,
}))
}
/>
</Field>
<>
<Field label="响应等待方式">
<Select
value={form.responseWaitMode}
onValueChange={(responseWaitMode: ClientToolResponseWaitMode) =>
setForm((current) => ({ ...current, responseWaitMode }))
}
>
<SelectTrigger className="w-full border-hairline-strong bg-background">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="timeout"></SelectItem>
<SelectItem value="session"></SelectItem>
</SelectContent>
</Select>
<span className="block text-xs leading-5 text-muted-foreground">
{form.responseWaitMode === "timeout"
? "超过设定时间后,将本次调用按失败处理。"
: "持续等待客户端返回,或直到本次会话结束。"}
</span>
</Field>
{form.responseWaitMode === "timeout" && (
<Field label="响应超时(秒)">
<Input
type="number"
min={1}
max={30}
value={form.timeoutSeconds}
onChange={(event) =>
setForm((current) => ({
...current,
timeoutSeconds: event.target.value,
}))
}
/>
</Field>
)}
</>
)}
<JsonField
label="参数定义"

View File

@@ -316,6 +316,7 @@ export const conversationsApi = {
// ---------- 工具 ----------
export type ToolStatus = "active" | "archived" | "draft";
export type ToolExecutionMode = "immediate" | "async";
export type ClientToolResponseWaitMode = "timeout" | "session";
export type ToolParameter = {
name: string;
type: "string" | "number" | "integer" | "boolean" | "object" | "array";
@@ -357,6 +358,7 @@ export type ClientToolDefinition = {
allowInterruptions: boolean;
executionMode: ToolExecutionMode;
waitForResponse: boolean;
responseWaitMode?: ClientToolResponseWaitMode;
parameters: ToolParameter[];
timeoutSeconds: number;
dynamicVariableAssignments: Record<string, string>;