mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-27 15:54:48 +08:00
feat: implement MCP UIs
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { env } from "~/env";
|
||||
|
||||
import type { MCPServerMetadata } from "../mcp";
|
||||
import { fetchStream } from "../sse";
|
||||
import { sleep } from "../utils";
|
||||
|
||||
import { resolveServiceURL } from "./resolve-service-url";
|
||||
import type { ChatEvent } from "./types";
|
||||
|
||||
export function chatStream(
|
||||
@@ -15,23 +15,29 @@ export function chatStream(
|
||||
max_plan_iterations: number;
|
||||
max_step_num: number;
|
||||
interrupt_feedback?: string;
|
||||
mcp_settings?: {
|
||||
servers: Record<
|
||||
string,
|
||||
MCPServerMetadata & {
|
||||
enabled_tools: string[];
|
||||
add_to_agents: string[];
|
||||
}
|
||||
>;
|
||||
};
|
||||
},
|
||||
options: { abortSignal?: AbortSignal } = {},
|
||||
) {
|
||||
if (location.search.includes("mock")) {
|
||||
return chatStreamMock(userMessage, params, options);
|
||||
}
|
||||
return fetchStream<ChatEvent>(
|
||||
(env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000/api") + "/chat/stream",
|
||||
{
|
||||
body: JSON.stringify({
|
||||
messages: [{ role: "user", content: userMessage }],
|
||||
auto_accepted_plan: false,
|
||||
...params,
|
||||
}),
|
||||
signal: options.abortSignal,
|
||||
},
|
||||
);
|
||||
return fetchStream<ChatEvent>(resolveServiceURL("chat/stream"), {
|
||||
body: JSON.stringify({
|
||||
messages: [{ role: "user", content: userMessage }],
|
||||
auto_accepted_plan: false,
|
||||
...params,
|
||||
}),
|
||||
signal: options.abortSignal,
|
||||
});
|
||||
}
|
||||
|
||||
async function* chatStreamMock(
|
||||
|
||||
@@ -2,4 +2,6 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
export * from "./chat";
|
||||
export * from "./mcp";
|
||||
export * from "./podcast";
|
||||
export * from "./types";
|
||||
|
||||
20
web/src/core/api/mcp.ts
Normal file
20
web/src/core/api/mcp.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import type { SimpleMCPServerMetadata } from "../mcp";
|
||||
|
||||
import { resolveServiceURL } from "./resolve-service-url";
|
||||
|
||||
export async function queryMCPServerMetadata(config: SimpleMCPServerMetadata) {
|
||||
const response = await fetch(resolveServiceURL("mcp/server/metadata"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(config),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
@@ -1,20 +1,16 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { env } from "~/env";
|
||||
import { resolveServiceURL } from "./resolve-service-url";
|
||||
|
||||
export async function generatePodcast(content: string) {
|
||||
const response = await fetch(
|
||||
(env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000/api") +
|
||||
"/podcast/generate",
|
||||
{
|
||||
method: "post",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ content }),
|
||||
const response = await fetch(resolveServiceURL("podcast/generate"), {
|
||||
method: "post",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
);
|
||||
body: JSON.stringify({ content }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
12
web/src/core/api/resolve-service-url.ts
Normal file
12
web/src/core/api/resolve-service-url.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { env } from "~/env";
|
||||
|
||||
export function resolveServiceURL(path: string) {
|
||||
let BASE_URL = env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000/api/";
|
||||
if (!BASE_URL.endsWith("/")) {
|
||||
BASE_URL += "/";
|
||||
}
|
||||
return new URL(path, BASE_URL).toString();
|
||||
}
|
||||
6
web/src/core/mcp/index.ts
Normal file
6
web/src/core/mcp/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
export * from "./schema";
|
||||
export * from "./types";
|
||||
export * from "./utils";
|
||||
57
web/src/core/mcp/schema.ts
Normal file
57
web/src/core/mcp/schema.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
export const MCPConfigSchema = z.object({
|
||||
mcpServers: z.record(
|
||||
z.union(
|
||||
[
|
||||
z.object({
|
||||
command: z.string({
|
||||
message: "`command` must be a string",
|
||||
}),
|
||||
args: z
|
||||
.array(z.string(), {
|
||||
message: "`args` must be an array of strings",
|
||||
})
|
||||
.optional(),
|
||||
env: z
|
||||
.record(z.string(), {
|
||||
message: "`env` must be an object of key-value pairs",
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
z.object({
|
||||
url: z
|
||||
.string({
|
||||
message:
|
||||
"`url` must be a valid URL starting with http:// or https://",
|
||||
})
|
||||
.refine(
|
||||
(value) => {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return url.protocol === "http:" || url.protocol === "https:";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{
|
||||
message:
|
||||
"`url` must be a valid URL starting with http:// or https://",
|
||||
},
|
||||
),
|
||||
env: z
|
||||
.record(z.string(), {
|
||||
message: "`env` must be an object of key-value pairs",
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
],
|
||||
{
|
||||
message: "Invalid server type",
|
||||
},
|
||||
),
|
||||
),
|
||||
});
|
||||
43
web/src/core/mcp/types.ts
Normal file
43
web/src/core/mcp/types.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
export interface MCPToolMetadata {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface GenericMCPServerMetadata<T extends string> {
|
||||
name: string;
|
||||
transport: T;
|
||||
enabled: boolean;
|
||||
env?: Record<string, string>;
|
||||
tools: MCPToolMetadata[];
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
export interface StdioMCPServerMetadata
|
||||
extends GenericMCPServerMetadata<"stdio"> {
|
||||
transport: "stdio";
|
||||
command: string;
|
||||
args?: string[];
|
||||
}
|
||||
export type SimpleStdioMCPServerMetadata = Omit<
|
||||
StdioMCPServerMetadata,
|
||||
"enabled" | "tools" | "createdAt" | "updatedAt"
|
||||
>;
|
||||
|
||||
export interface SSEMCPServerMetadata extends GenericMCPServerMetadata<"sse"> {
|
||||
transport: "sse";
|
||||
url: string;
|
||||
}
|
||||
export type SimpleSSEMCPServerMetadata = Omit<
|
||||
SSEMCPServerMetadata,
|
||||
"enabled" | "tools" | "createdAt" | "updatedAt"
|
||||
>;
|
||||
|
||||
export type MCPServerMetadata = StdioMCPServerMetadata | SSEMCPServerMetadata;
|
||||
export type SimpleMCPServerMetadata =
|
||||
| SimpleStdioMCPServerMetadata
|
||||
| SimpleSSEMCPServerMetadata;
|
||||
16
web/src/core/mcp/utils.ts
Normal file
16
web/src/core/mcp/utils.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { useSettingsStore } from "../store";
|
||||
|
||||
export function findMCPTool(name: string) {
|
||||
const mcpServers = useSettingsStore.getState().mcp.servers;
|
||||
for (const server of mcpServers) {
|
||||
for (const tool of server.tools) {
|
||||
if (tool.name === name) {
|
||||
return tool;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { create } from "zustand";
|
||||
|
||||
import type { MCPServerMetadata } from "../mcp";
|
||||
|
||||
const SETTINGS_KEY = "deerflow.settings";
|
||||
|
||||
const DEFAULT_SETTINGS: SettingsState = {
|
||||
@@ -7,6 +12,34 @@ const DEFAULT_SETTINGS: SettingsState = {
|
||||
maxPlanIterations: 1,
|
||||
maxStepNum: 3,
|
||||
},
|
||||
mcp: {
|
||||
servers: [
|
||||
{
|
||||
enabled: true,
|
||||
name: "Zapier",
|
||||
transport: "sse",
|
||||
url: "https://actions.zapier.com/mcp/sk-ak-OnJ4kVKzxcLjpvpLChkT7RCYuh/sse",
|
||||
env: { API_KEY: "123" },
|
||||
createdAt: new Date("2025-04-20").valueOf(),
|
||||
updatedAt: new Date("2025-04-20").valueOf(),
|
||||
tools: [
|
||||
{
|
||||
name: "youtube_get_report",
|
||||
description:
|
||||
"Creates a report on specified data from your owned and managed channels.",
|
||||
},
|
||||
{
|
||||
name: "edit_actions",
|
||||
description: "Edit your existing MCP provider actions",
|
||||
},
|
||||
{
|
||||
name: "add_actions",
|
||||
description: "Add new actions to your MCP provider",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export type SettingsState = {
|
||||
@@ -14,6 +47,9 @@ export type SettingsState = {
|
||||
maxPlanIterations: number;
|
||||
maxStepNum: number;
|
||||
};
|
||||
mcp: {
|
||||
servers: MCPServerMetadata[];
|
||||
};
|
||||
};
|
||||
|
||||
export const useSettingsStore = create<SettingsState>(() => ({
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { create } from "zustand";
|
||||
|
||||
import { chatStream } from "../api";
|
||||
import { generatePodcast } from "../api/podcast";
|
||||
import { chatStream, generatePodcast } from "../api";
|
||||
import type { Message } from "../messages";
|
||||
import { mergeMessage } from "../messages";
|
||||
import { parseJSON } from "../utils";
|
||||
|
||||
import { useSettingsStore } from "./settings-store";
|
||||
import type { MCPServerMetadata, SimpleMCPServerMetadata } from "../mcp";
|
||||
|
||||
const THREAD_ID = nanoid();
|
||||
|
||||
@@ -57,7 +57,52 @@ export async function sendMessage(
|
||||
|
||||
setResponding(true);
|
||||
try {
|
||||
const generalSettings = useSettingsStore.getState().general;
|
||||
const settings = useSettingsStore.getState();
|
||||
const generalSettings = settings.general;
|
||||
const mcpServers = settings.mcp.servers.filter((server) => server.enabled);
|
||||
let mcpSettings:
|
||||
| {
|
||||
servers: Record<
|
||||
string,
|
||||
MCPServerMetadata & {
|
||||
enabled_tools: string[];
|
||||
add_to_agents: string[];
|
||||
}
|
||||
>;
|
||||
}
|
||||
| undefined = undefined;
|
||||
if (mcpServers.length > 0) {
|
||||
mcpSettings = {
|
||||
servers: mcpServers.reduce((acc, cur) => {
|
||||
const { transport, env } = cur;
|
||||
let server: SimpleMCPServerMetadata;
|
||||
if (transport === "stdio") {
|
||||
server = {
|
||||
name: cur.name,
|
||||
transport,
|
||||
env,
|
||||
command: cur.command,
|
||||
args: cur.args,
|
||||
};
|
||||
} else {
|
||||
server = {
|
||||
name: cur.name,
|
||||
transport,
|
||||
env,
|
||||
url: cur.url,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...acc,
|
||||
[cur.name]: {
|
||||
...server,
|
||||
enabled_tools: cur.tools.map((tool) => tool.name),
|
||||
add_to_agents: ["researcher"],
|
||||
},
|
||||
};
|
||||
}, {}),
|
||||
};
|
||||
}
|
||||
const stream = chatStream(
|
||||
content,
|
||||
{
|
||||
@@ -65,6 +110,7 @@ export async function sendMessage(
|
||||
max_plan_iterations: generalSettings.maxPlanIterations,
|
||||
max_step_num: generalSettings.maxStepNum,
|
||||
interrupt_feedback: interruptFeedback,
|
||||
mcp_settings: mcpSettings,
|
||||
},
|
||||
options,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user