V4.9.6 feature (#4565)

* Dashboard submenu (#4545)

* add app submenu (#4452)

* add app submenu

* fix

* width & i18n

* optimize submenu code (#4515)

* optimize submenu code

* fix

* fix

* fix

* fix ts

* perf: dashboard sub menu

* doc

---------

Co-authored-by: heheer <heheer@sealos.io>

* feat: value format test

* doc

* Mcp export (#4555)

* feat: mcp server

* feat: mcp server

* feat: mcp server build

* update doc

* perf: path selector (#4556)

* perf: path selector

* fix: docker file path

* perf: add image endpoint to dataset search (#4557)

* perf: add image endpoint to dataset search

* fix: mcp_server url

* human in loop (#4558)

* Support interactive nodes for loops, and enhance the function of merging nested and loop node history messages. (#4552)

* feat: add LoopInteractive definition

* feat: Support LoopInteractive type and update related logic

* fix: Refactor loop handling logic and improve output value initialization

* feat: Add mergeSignId to dispatchLoop and dispatchRunAppNode responses

* feat: Enhance mergeChatResponseData to recursively merge plugin details and improve response handling

* refactor: Remove redundant comments in mergeChatResponseData for clarity

* perf: loop interactive

* perf: human in loop

---------

Co-authored-by: Theresa <63280168+sd0ric4@users.noreply.github.com>

* mcp server ui

* integrate mcp (#4549)

* integrate mcp

* delete unused code

* fix ts

* bug fix

* fix

* support whole mcp tools

* add try catch

* fix

* fix

* fix ts

* fix test

* fix ts

* fix: interactive in v1 completions

* doc

* fix: router path

* fix mcp integrate (#4563)

* fix mcp integrate

* fix ui

* fix: mcp ux

* feat: mcp call title

* remove repeat loading

* fix mcp tools avatar (#4564)

* fix

* fix avatar

* fix update version

* update doc

* fix: value format

* close server and remove cache

* perf: avatar

---------

Co-authored-by: heheer <heheer@sealos.io>
Co-authored-by: Theresa <63280168+sd0ric4@users.noreply.github.com>
This commit is contained in:
Archer
2025-04-16 22:18:51 +08:00
committed by GitHub
parent ab799e13cd
commit 952412f648
166 changed files with 6318 additions and 1263 deletions

View File

@@ -0,0 +1 @@
FASTGPT_ENDPOINT=http://localhost:3000

View File

@@ -0,0 +1,43 @@
# --------- Install -----------
FROM node:20.14.0-alpine AS install
WORKDIR /app
RUN npm install -g pnpm@9.4.0
# 复制package.json
COPY pnpm-lock.yaml pnpm-workspace.yaml ./
COPY projects/mcp_server/package.json ./projects/mcp_server/package.json
RUN apk add --no-cache\
curl ca-certificates\
&& update-ca-certificates
# 安装依赖
RUN [ -f pnpm-lock.yaml ] || (echo "Lockfile not found." && exit 1)
RUN pnpm i
# --------- builder -----------
FROM node:20.14.0-alpine AS builder
WORKDIR /app
COPY package.json pnpm-workspace.yaml /app/
COPY --from=install /app/node_modules /app/node_modules
COPY ./projects/mcp_server /app/projects/mcp_server
COPY --from=install /app/projects/mcp_server /app/projects/mcp_server
RUN npm install -g pnpm@9.4.0
RUN pnpm --filter=mcp_server build
# runner
FROM node:20.14.0-alpine AS runner
WORKDIR /app
RUN apk add --no-cache libffi libffi-dev strace bash
COPY --from=builder /app/node_modules /app/node_modules
COPY --from=builder /app/projects/mcp_server /app/projects/mcp_server
ENV NODE_ENV=production
ENV PORT=3000
ENTRYPOINT ["sh","-c","node projects/mcp_server/dist/index.js"]

View File

@@ -0,0 +1,30 @@
{
"name": "fastgpt-mcp-server",
"version": "0.1",
"keywords": [],
"author": "fastgpt",
"files": [
"dist"
],
"type": "module",
"scripts": {
"build": "tsc && shx chmod +x dist/*.js",
"dev": "nodemon --watch src --ext ts,json --exec \"npm run dev:run\"",
"dev:run": "tsc && node dist/index.js",
"mcp_test": "npx @modelcontextprotocol/inspector"
},
"dependencies": {
"@modelcontextprotocol/sdk": "1.9.0",
"axios": "^1.8.2",
"chalk": "^5.3.0",
"dayjs": "^1.11.7",
"dotenv": "^16.5.0",
"express": "^4.21.2"
},
"devDependencies": {
"@types/express": "^5.0.1",
"nodemon": "^3.1.9",
"shx": "^0.3.4",
"typescript": "^5.6.2"
}
}

View File

@@ -0,0 +1,7 @@
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { GET, POST } from './request.js';
export const getTools = (key: string) => GET<Tool[]>('/support/mcp/server/toolList', { key });
export const callTool = (data: { key: string; toolName: string; inputs: Record<string, any> }) =>
POST('/support/mcp/server/toolCall', data);

View File

@@ -0,0 +1,111 @@
import axios, { Method, InternalAxiosRequestConfig, AxiosResponse } from 'axios';
type ConfigType = {};
type ResponseDataType = {
code: number;
message: string;
data: any;
};
/**
* 请求开始
*/
function startInterceptors(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig {
if (config.headers) {
}
return config;
}
/**
* 请求成功,检查请求头
*/
function responseSuccess(response: AxiosResponse<ResponseDataType>) {
return response;
}
/**
* 响应数据检查
*/
function checkRes(data: ResponseDataType) {
if (data === undefined) {
console.log('error->', data, 'data is empty');
return Promise.reject('服务器异常');
} else if (data.code < 200 || data.code >= 400) {
return Promise.reject(data);
}
return data.data;
}
/**
* 响应错误
*/
function responseError(err: any) {
console.log('error->', '请求错误', err);
const data = err?.response?.data || err;
if (!err) {
return Promise.reject({ message: '未知错误' });
}
if (typeof err === 'string') {
return Promise.reject({ message: err });
}
if (typeof data === 'string') {
return Promise.reject(data);
}
}
/* 创建请求实例 */
const instance = axios.create({
baseURL: `${process.env.FASTGPT_ENDPOINT}/api`,
timeout: 600000, // 超时时间
headers: {
'content-type': 'application/json'
}
});
/* 请求拦截 */
instance.interceptors.request.use(startInterceptors, (err) => Promise.reject(err));
/* 响应拦截 */
instance.interceptors.response.use(responseSuccess, (err) => Promise.reject(err));
function request(url: string, data: any, config: ConfigType, method: Method): any {
/* 去空 */
for (const key in data) {
if (data[key] === undefined) {
delete data[key];
}
}
return instance
.request({
url,
method,
data: ['POST', 'PUT'].includes(method) ? data : undefined,
params: !['POST', 'PUT'].includes(method) ? data : undefined
})
.then((res) => checkRes(res.data))
.catch((err) => responseError(err));
}
/**
* api请求方式
* @param {String} url
* @param {Any} params
* @param {Object} config
* @returns
*/
export function GET<T = undefined>(url: string, params = {}, config: ConfigType = {}): Promise<T> {
return request(url, params, config, 'GET');
}
export function POST<T = undefined>(url: string, data = {}, config: ConfigType = {}): Promise<T> {
return request(url, data, config, 'POST');
}
export function PUT<T = undefined>(url: string, data = {}, config: ConfigType = {}): Promise<T> {
return request(url, data, config, 'PUT');
}
export function DELETE<T = undefined>(url: string, data = {}, config: ConfigType = {}): Promise<T> {
return request(url, data, config, 'DELETE');
}

View File

@@ -0,0 +1,108 @@
#!/usr/bin/env node
import './init.js';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
CallToolResult
} from '@modelcontextprotocol/sdk/types.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import express from 'express';
import { callTool, getTools } from './api/fastgpt.js';
import { addLog } from './utils/log.js';
import { getErrText } from './utils/error.js';
const app = express();
const transportMap: Record<string, SSEServerTransport> = {};
app.get('/:key/sse', async (req, res) => {
const { key } = req.params;
const transport = new SSEServerTransport(`/${key}/messages`, res);
transportMap[transport.sessionId] = transport;
// Create server
const server = new Server(
{
name: 'fastgpt-mcp-server',
version: '1.0.0'
},
{
capabilities: {
tools: {}
}
}
);
transport.onclose = () => {
addLog.info(`Transport ${transport.sessionId} closed`);
delete transportMap[transport.sessionId];
};
transport.onerror = (err) => {
addLog.error(`Transport ${transport.sessionId} error`, err);
};
server.onclose = () => {
addLog.info(`Server ${transport.sessionId} closed`);
delete transportMap[transport.sessionId];
};
server.onerror = (err) => {
addLog.error(`Server ${transport.sessionId} error`, err);
};
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: await getTools(key)
}));
const handleToolCall = async (
name: string,
args: Record<string, any>
): Promise<CallToolResult> => {
try {
addLog.info(`Call tool: ${name} with args: ${JSON.stringify(args)}`);
const result = await callTool({ key, toolName: name, inputs: args });
return {
content: [
{
type: 'text',
text: typeof result === 'string' ? result : JSON.stringify(result)
}
],
isError: false
};
} catch (error) {
return {
message: getErrText(error),
content: [],
isError: true
};
}
};
server.setRequestHandler(CallToolRequestSchema, async (request) =>
handleToolCall(request.params.name, request.params.arguments ?? {})
);
await server.connect(transport);
addLog.info(`Server connected: ${transport.sessionId}`);
});
app.post('/:key/messages', (req, res) => {
const { sessionId } = req.query as { sessionId: string };
const transport = transportMap[sessionId];
if (transport) {
transport.handlePostMessage(req, res);
}
});
const PORT = process.env.PORT || 3000;
app
.listen(PORT, () => {
addLog.info(`Server is running on port ${PORT}`);
})
.on('error', (err) => {
addLog.error(`Server error`, err);
});

View File

@@ -0,0 +1,3 @@
import * as dotenv from 'dotenv';
dotenv.config();
dotenv.config({ path: '.env.local' });

5
projects/mcp_server/src/type.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare namespace NodeJS {
interface ProcessEnv {
FASTGPT_ENDPOINT: string;
}
}

View File

@@ -0,0 +1,10 @@
import { replaceSensitiveText } from './string.js';
export const getErrText = (err: any, def = ''): any => {
const msg: string =
typeof err === 'string'
? err
: err?.response?.data?.message || err?.response?.message || err?.message || def;
// msg && console.log('error =>', msg);
return replaceSensitiveText(msg);
};

View File

@@ -0,0 +1,68 @@
import chalk from 'chalk';
import dayjs from 'dayjs';
export enum LogLevelEnum {
debug = 0,
info = 1,
warn = 2,
error = 3
}
const logMap = {
[LogLevelEnum.debug]: {
levelLog: chalk.green('[Debug]')
},
[LogLevelEnum.info]: {
levelLog: chalk.blue('[Info]')
},
[LogLevelEnum.warn]: {
levelLog: chalk.yellow('[Warn]')
},
[LogLevelEnum.error]: {
levelLog: chalk.red('[Error]')
}
};
/* add logger */
export const addLog = {
log(level: LogLevelEnum, msg: string, obj: Record<string, any> = {}) {
const stringifyObj = JSON.stringify(obj);
const isEmpty = Object.keys(obj).length === 0;
console.log(
`${logMap[level].levelLog} ${dayjs().format('YYYY-MM-DD HH:mm:ss')} ${msg} ${
level !== LogLevelEnum.error && !isEmpty ? stringifyObj : ''
}`
);
level === LogLevelEnum.error && console.error(obj);
},
debug(msg: string, obj?: Record<string, any>) {
this.log(LogLevelEnum.debug, msg, obj);
},
info(msg: string, obj?: Record<string, any>) {
this.log(LogLevelEnum.info, msg, obj);
},
warn(msg: string, obj?: Record<string, any>) {
this.log(LogLevelEnum.warn, msg, obj);
},
error(msg: string, error?: any) {
this.log(LogLevelEnum.error, msg, {
message: error?.message || error,
stack: error?.stack,
...(error?.config && {
config: {
headers: error.config.headers,
url: error.config.url,
data: error.config.data
}
}),
...(error?.response && {
response: {
status: error.response.status,
statusText: error.response.statusText
}
})
});
}
};

View File

@@ -0,0 +1,8 @@
export const replaceSensitiveText = (text: string) => {
// 1. http link
text = text.replace(/(?<=https?:\/\/)[^\s]+/g, 'xxx');
// 2. nx-xxx 全部替换成xxx
text = text.replace(/ns-[\w-]+/g, 'xxx');
return text;
};

View File

@@ -0,0 +1,17 @@
{
"include": ["src"],
"exclude": ["node_modules"],
"compilerOptions": {
"target": "ES2022",
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"outDir": "./dist",
"baseUrl": "./src",
"lib": ["ES2015", "DOM"]
}
}