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表单代码的。
项目结构
- adapters目录下fastify-sse代码中直接copy下来使用,主要功能用做封装服务端后向我们的客户端推送事件,是实现sse通信的关键。
- logger目录实现了一个基于 Winston 的日志系统,用于 MCP 服务器,主要使用了winston库实现我们整个服务的日志记录。
- tools目录,这个目录就是主要用来实现我们需要的代码功能以及注册。我在form.ts中封装了一个转换接受客户端传入的字符串,用来转换为符合Dify代码的,这里详细解释下关键,在Dify那边依然配置的是工具,并通过大模型生成符合我们这边MCP接受的字符串代码,但关键是我们这边处理后保证返回的只有代码内容了,不会有任何大模型yy出来的内容。大概流程如下:
- 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配置的工作流内容以及该源码。