mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 05:12:39 +00:00
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:
1
projects/mcp_server/.env.template
Normal file
1
projects/mcp_server/.env.template
Normal file
@@ -0,0 +1 @@
|
||||
FASTGPT_ENDPOINT=http://localhost:3000
|
43
projects/mcp_server/Dockerfile
Normal file
43
projects/mcp_server/Dockerfile
Normal 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"]
|
30
projects/mcp_server/package.json
Normal file
30
projects/mcp_server/package.json
Normal 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"
|
||||
}
|
||||
}
|
7
projects/mcp_server/src/api/fastgpt.ts
Normal file
7
projects/mcp_server/src/api/fastgpt.ts
Normal 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);
|
111
projects/mcp_server/src/api/request.ts
Normal file
111
projects/mcp_server/src/api/request.ts
Normal 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');
|
||||
}
|
108
projects/mcp_server/src/index.ts
Normal file
108
projects/mcp_server/src/index.ts
Normal 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);
|
||||
});
|
3
projects/mcp_server/src/init.ts
Normal file
3
projects/mcp_server/src/init.ts
Normal 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
5
projects/mcp_server/src/type.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
FASTGPT_ENDPOINT: string;
|
||||
}
|
||||
}
|
10
projects/mcp_server/src/utils/error.ts
Normal file
10
projects/mcp_server/src/utils/error.ts
Normal 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);
|
||||
};
|
68
projects/mcp_server/src/utils/log.ts
Normal file
68
projects/mcp_server/src/utils/log.ts
Normal 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
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
};
|
8
projects/mcp_server/src/utils/string.ts
Normal file
8
projects/mcp_server/src/utils/string.ts
Normal 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;
|
||||
};
|
17
projects/mcp_server/tsconfig.json
Normal file
17
projects/mcp_server/tsconfig.json
Normal 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"]
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user