记录MCP开发表单

1. 背景介绍

公司最近调研Dify的智能体功能,在交互步骤中,表单是至关重要的,目前Dify支持的表单形式不像其它智能体平台有可视化的拖拉拽形式,自己在调研过程及查看源码中发现了Dify支持代码形式直接渲染表单,只要创建的代码符合Diyf能够渲染的代码形式即可。但问题是领导不希望用户使用时需要写代码,即有成本又麻烦,于是我根据Dify目前已有形式,提出了通过配置工作流发布为工具,工作流中通过大模型理解用户直接输入的表单需求,转换为Dify能够识别的代码形式。这也是目前我们依然在采用的方案。但目前依然有问题,由于大模型提示词配置或者说是通过大模型转换本身就是有不可控因素,于是组长让我使用MCP封装表单形式。

2. MCP调研

mcp的介绍和定义这里就不赘述了,本身我理解的也够抽象的一个定义,自己的简单理解就是转换器吧。老实说,最开始是想要找找市面上有没有合适的MCP表单可直接使用,但很快就意识到问题了,MCP提供的应该是通用的能力,目前想在Dify渲染出表单,是要符合Dify能够渲染的特定的代码规范,这是冲突的啊,我理解,MCP不应该提供这样的能力,所以如果要开发MCP表单,就是自定义开发特定的MCP服务。

3. 工作开始

遇事不决就找ai,看了很多网上已有的关于MCP服务的文章以及不断询问ai,最终发现了就是目前关于这方面的知识很少,甚至我在问一些不良ai时,都不知道MCP是什么。。所以只能借鉴市面开源的已有MCP服务,最终找了Dify上集成飞书api的mcp源码,在此基础上改动。

理解代码后(问懂ai后),自己新建了一个单独的项目,模版借鉴于上面源码,开发专门生成form表单代码的。

项目结构

  1. adapters目录下fastify-sse代码中直接copy下来使用,主要功能用做封装服务端后向我们的客户端推送事件,是实现sse通信的关键。
  2. logger目录实现了一个基于 Winston 的日志系统,用于 MCP 服务器,主要使用了winston库实现我们整个服务的日志记录。
  3. tools目录,这个目录就是主要用来实现我们需要的代码功能以及注册。我在form.ts中封装了一个转换接受客户端传入的字符串,用来转换为符合Dify代码的,这里详细解释下关键,在Dify那边依然配置的是工具,并通过大模型生成符合我们这边MCP接受的字符串代码,但关键是我们这边处理后保证返回的只有代码内容了,不会有任何大模型yy出来的内容。大概流程如下:
graph TD A[用户输入需求] --> B[Dify工具接收表单需求] B --> C[大模型理解用户需求] C --> D[转换为符合MCP服务的代码结构] D --> E[MCP服务接收字符串] E --> F[MCP过滤无用信息] F --> G[生成符合Dify可渲染的表单代码] G --> H[Dify渲染最终表单]
  1. mcp-server.ts及根目录下index.ts就是注册我们的Mcp服务以及启动相关的代码。这块代码应该对大家来说才是注册Mcp的核心,我直接粘贴出来,大家理解后自己使用。
mcpserver.ts 复制代码
import fastifyCors from '@fastify/cors';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import { FastifySSETransport } from './adapters/fastify-sse.js';
import logger from '@/logger/index.js';
import fastify from 'fastify';
import { z } from 'zod';
import { registerAllTools } from "./tools/index.js"
export class McpFormServer {
    /** MCP server instance */
    private server: McpServer;
    /** SSE transport instance for HTTP mode */
    private sseTransport: FastifySSETransport | null = null;
    /** Server version */
    private readonly version = '0.0.1';

    constructor() {
        this.server = new McpServer(
            {
                name: 'form-mcp-server',
                version: this.version,
            },
            {
                capabilities: {
                    logging: {},
                    tools: {},
                },
            },
        );

        // Register all tools
        this.registerTools();
    }

    /**
     * Register all MCP tools
     */
    private registerTools(): void {
        registerAllTools({
            server: this.server,
            logger: logger,
        });
    }

    /**
     * Connect to a transport
     *
     * @param transport - Transport instance
     */
    async connect(transport: any): Promise<void> {
        await this.server.connect(transport);
        logger.info('Server connected and ready to process requests');
    }

    /**
     * Start HTTP server
     *
     * @param port - Server port
     */
    async startHttpServer(port: number): Promise<void> {
        const app = fastify({
            logger: true,
            disableRequestLogging: true, // Disable default request logging as we use custom logging
        });

        await app.register(fastifyCors);
        await this.configureFastifyServer(app);

        try {
            await app.listen({ port, host: '0.0.0.0' });
            logger.info(`HTTP server listening on port ${port}`);
            logger.info(`SSE endpoint available at http://localhost:${port}/sse`);
            logger.info(
                `Message endpoint available at http://localhost:${port}/messages`,
            );
        } catch (err) {
            logger.error('Error starting server:', err);
            process.exit(1);
        }
    }

    /**
     * Configure Fastify server
     *
     * @param app - Fastify instance
     */
    private async configureFastifyServer(app: FastifyInstance): Promise<void> {
        // SSE endpoint
        app.get('/sse', async (request: FastifyRequest, reply: FastifyReply) => {
            try {
                logger.info('New SSE connection established');
                this.sseTransport = new FastifySSETransport('/messages', reply);
                await this.server.connect(this.sseTransport);
                await this.sseTransport.initializeSSE();
                request.raw.on('close', () => {
                    logger.info('SSE connection closed');
                    this.sseTransport?.close();
                    this.sseTransport = null;
                });
            } catch (err) {
                logger.error('Error establishing SSE connection:', err);
                reply.code(500).send({ error: 'Internal Server Error' });
            }
        });
        // Check health
        app.get("/", async (request: FastifyRequest, reply: FastifyReply) => {
            try {
                reply.code(200).send({
                    name: "form-mcp-server",
                    version: this.version,
                    status: "ok",
                    endpoints: {
                        sse: "/sse",
                        messages: "/messages"
                    }
                });
            } catch (err) {
                logger.error('Error handling health check:', err);
                reply.code(500).send({ error: 'Internal Server Error' });
            }
        })
        // Message handling endpoint
        app.post(
            '/messages',
            async (request: FastifyRequest, reply: FastifyReply) => {
                try {
                    if (!this.sseTransport) {
                        reply.code(400).send({ error: 'No active SSE connection' });
                        return;
                    }
                    await this.sseTransport.handleFastifyRequest(request, reply);
                } catch (err) {
                    logger.error('Error handling message:', err);
                    reply.code(500).send({ error: 'Internal Server Error' });
                }
            },
        );
    }
}
index.ts 复制代码
#!/usr/bin/env node

import { McpFormServer } from './mcp-server.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';

/**
 * Initialize and start the server
 *
 * Determines which mode to use based on environment variables or command-line arguments:
 * - stdio mode: Used in CLI environments, communicates via standard input/output
 * - HTTP mode: Launches a web server exposing API endpoints
 */
async function startServer() {
    // Determine if stdio mode should be used
    const isStdioMode =
        process.env.NODE_ENV === 'cli' || process.argv.includes('--stdio');
    // Instantiate the server
    const server = new McpFormServer();
    const port = Number(process.env.PORT) || 3344;
    if (isStdioMode) {
        // stdio mode: Communication via standard input/output
        const transport = new StdioServerTransport();
        await server.connect(transport);
    } else {
        // HTTP mode: Launch web server
        console.log(
            `Initializing MCP Server (HTTP mode) on port ${port}...`,
        );
        await server.startHttpServer(port);
    }
}

// Launch server 
startServer().catch((error) => {
    console.error('Failed to start server:', error);
    process.exit(1);
});

结尾

以上就是自己从调研Dify平台到最终开发Mcp表单服务的过程,仅是记录,特殊性比较强,大家如果有相同需求,可以直接联系我,我可以提供Dify配置的工作流内容以及该源码。

相关推荐
顾辰逸you几秒前
uniapp--咸虾米壁纸(三)
前端·微信小程序
北鸟南游4 分钟前
用现有bootstrap的模板,改造成nuxt3项目
前端·bootstrap·nuxt.js
前端老鹰6 分钟前
JavaScript Intl.RelativeTimeFormat:自动生成 “3 分钟前” 的国际化工具
前端·javascript
梦想CAD控件6 分钟前
(在线CAD插件)网页CAD实现图纸表格智能提取
前端·javascript·全栈
木子雨廷24 分钟前
Flutter 开发一个plugin
前端·flutter
重生之我是一名前端程序员26 分钟前
websocket + xterm 前端实现网页版终端
前端·websocket
sorryhc28 分钟前
【AI解读源码系列】ant design mobile——Space间距
前端·javascript·react.js
uhakadotcom42 分钟前
NPM与NPX的区别是什么?
前端·面试·github
GAMC1 小时前
如何修改node_modules的组件不被install替换?可以使用patch-package
前端
页面仔Dony1 小时前
webpack 与 Vite 深度对比
前端·前端工程化