基于 MCP 协议从零开发 AI 辅助代码生成插件

本文介绍了如何使用 MCP 协议从零开始开发 AI 辅助代码生成插件,将设计稿 AST 转换为 React 代码,提升开发效率。

原文标题:从零开始的 MCP 开发

原文作者:阿里云开发者

冷月清谈:

本文记录了作者从零开始,利用 AI 和 MCP 协议开发 OneDay Web 端代码生成插件的实践过程。MCP 协议旨在为 LLM 提供统一的外部工具集成标准,解决 AI 模型与外部数据源和工具交互的碎片化问题。文章详细介绍了 MCP 的核心价值和关键能力,包括统一集成标准、实时数据更新、自动工具发现和隐私保护。作者通过学习开源工具 Roo 和 MCP Typescript SDK,深入了解了 MCP 的运作方式,并逐步实现了从 ZIP 文件解析到 React 代码生成的整个流程。文章还分享了在实际开发中遇到的问题和解决方案,例如文件选择器的实现和任务之间的衔接,并对未来改进方向进行了展望,包括优化 LLM 调用方式、增强移动端适配以及缩短 D2C 图生码时间。

怜星夜思:

1、MCP 协议被称为 AI 时代的 "USB-C",你认为这个比喻是否准确?MCP 在实际应用中还存在哪些局限性?
2、文章中提到使用 LLM 调用 MCP 接口,接口内部又调用 LLM,作者觉得不够优雅。你有什么更好的方案,可以优化这个调用链路?
3、作者在开发过程中遇到了文件选择器的问题,最终使用了一个脚本解决。你认为这个方案是否优雅?有没有更通用的解决方案?

原文内容

阿里妹导读


这篇文章主要记录了作者在开发 MCP 插件的过程中的学习路径,以及是如何从零用 AI 开发一个小插件的。

前言:我们迎来万能插头?

在 AI 提效上,我们小组的每个人都有自己的独特方式,作为一个沉醉在业务开发+业务样式改版的终端开发,再加上我的 CSS 功底基本上样式就是靠试,每次在 UI 还原部分都是很是痛苦。这样,在团队内部同学完成了 Done 插件转React 代码并完成 OneDay Web 端落地后,我就在想,是否可以在插件端实现一样的能力,就这样 MCP 的能力自然就进入我的视野了。

先看一下效果:

这个小玩具是通过 MCP 协议进行开发的,并且集成在了 OneDay插件中。这篇文章,主要记录了自己在开发 MCP 插件的过程中的学习路径,以及是如何从零用 AI 开发一个小插件的。最后,也是趁着业务大改版的机会,将这个插件结合在我的开发流程中。

MCP 协议简介:AI 的"万能插头"

2024 年 11 月,Anthropic 推出了 Model Context Protocol (MCP),这一开放协议旨在解决 LLM 与外部工具集成的标准化问题。MCP 提供了一种统一的方式,使 AI 模型能够与各种数据源和工具进行交互,被官方形象地称为 AI 应用的"USB-C 端口"。 

MCP 的本质与价值

MCP的核心价值在于提供一种标准化的方式,让 AI 模型与外部世界进行交互。在 MCP 出现之前,开发者需要为每个 AI 集成创建定制化的解决方案,这导致了严重的碎片化问题。 

MCP 解决了这些问题,它提供了以下关键价值: 

  • 统一集成标准一个协议对接所有集成,降低开发难度  

  • 实时数据更新支持动态数据交互而非静态连接  

  • 自动工具发现支持动态工具发现和上下文处理  

  • 隐私保护数据和工具不需上传远端,保护数据隐私  

  • 开发效率显著减少开发时间,提高系统可靠性

核心能力

根据 MCP 协议规范,服务器可以提供三种核心对象: 

支持程度

目前 MCP 这一概念的火热也让众多 IDE 和框架积极投身在这一领域,其中Claude桌面应用和Continue提供了最全面的MCP支持,包括资源、提示模板和工具集成,使其能够深度整合本地工具和数据源。众多代码编辑器和IDE(如Cursor、Zed、Windsurf Editor和Theia IDE)通过MCP增强了开发工作流程,提供如智能代码生成、AI辅助编码等功能。

在官网的示例中(https://modelcontextprotocol.io/examples),可以发现,越来越多的公司、组织开始积极拥抱 MCP,目前通过 MCP 可以进行本地文件、云端文件的修改Git 相关仓库的阅读与更改基于Puppeteer 进行浏览器自动化和网页抓取,甚至通过EverArt的相关服务可以进行图像生成

一些更抽象的 MCP 服务可以在这里看一看(https://github.com/punkpeye/awesome-mcp-servers

真的是大一统么?

文档上说的很好,MCP 是AI 届的USB-C,使用了 MCP 就意味着你的协议可以在所有的 AI 应用上使用了。

但是,强如 USB-C 现在也没有办法做到真正的大一统,不同厂商之间还是存在着不同。

所以,“MCP 可能统一,但是 MCP统一不太可能”。

现在针对不同的 AI 终端每个 MCP 支持的能力也是不尽相同的,本文说的只是在 OneDay VSC 插件上的开发体验;





前置学习一下

看完前面的 MCP 具体协议相关的文档之后,理解能力比较强的老师可能已经知道 MCP 是在干啥了,像我这种 AI 知识早就还给 CV、ML 老师了的同学来说,还是不是很清楚 MCP 具体是咋被调用的。

为了搞清楚MCP 的运作方式,我准备学习一下开源的工具以及 SDK 是如何运作的,作为一个练习时长2 坤年的终端开发,我选择的开源仓库是 Roo 和 MCP Typescript 的 SDK。

如何使用MCP TS进行开发

在 MCP 官网上,赫然写着Building MCP with LLMshttps://modelcontextprotocol.io/tutorials/building-mcp-with-llms),但是本着尊重 AI 的劳动成果的原则还是要学习一下里面具体的内容的。

这部分不太详细展开,具体的 MCP 开发还是参考官网的文档好了。

Client

负责与 MCP 服务器建立连接并发送请求,主要的方法有:

  • connect(transport):连接到服务器

  • request(request, schema, options):发送请求并等待响应

  • close():关闭连接

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
McpServer

提供一个高级 API 来创建 MCP 服务器,主要的方法有:

  • tool(name, schema, handler):注册一个工具

  • resource(name, template, handler):注册一个资源

  • connect(transport):连接到传输层

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const server = new McpServer({
  name: "我的MCP服务",
  version: "1.0.0"
});
Server

一个低级类,也是本文采用的一个类,低级开发用低级类(bushi

  • setRequestHandler(schema, handler):为特定请求类型设置处理程序

  • connect(transport):连接到传输层

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
传输接口(Transport)

MCP 支持多种传输方式,用于与客户端通信,主要是通过:stdio 传输(命令行应用)和SSE 传输(Web服务器)。

import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const transport = new StdioServerTransport();
await server.connect(transport);

import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";

const app = express();

app.get(“/sse”, async (req, res) => {
  const transport = new SSEServerTransport(“/messages”, res);
  await server.connect(transport);
});

app.post(“/messages”, async (req, res) => {
  await transport.handlePostMessage(req, res);
});

app.listen(3000);

具体的开发流程如下





Roo 如何调用MCP

Roo 是谁,Cline 优化版罢了

大体上了解了 MCP SDK中的使用方式,那么问题又来了: MCP 集成在客户端上,客户端是如何判断是否需要调用 MCP 以及使用哪个 MCP 的?

打开 Roo 的源码,AI 总结启动...





可以看出来主要流程有意图识别、工具识别、工具调用这三个主要的步骤。

意图识别

Roo Code使用大型语言模型(LLM)来理解用户的自然语言输入并识别用户的意图。当用户提出一个请求时,LLM会分析请求并决定使用哪些工具来完成任务。

系统提示构建:通过 generatePrompt 函数构建完整的系统提示,包括 MCP 服务器和工具信息。

这使 LLM 能够了解可用的 MCP 服务器及其功能。

这使 LLM 能够了解可用的 MCP 服务器及其功能// src/core/prompts/system.ts
// 通过 generatePrompt 函数构建完整的系统提示,包括 MCP 服务器和工具信息。
// 这使 LLM 能够了解可用的 MCP 服务器及其功能
async function generatePrompt(
    context: vscode.ExtensionContext,
    cwd: string,
    supportsComputerUse: boolean,
    mode: Mode,
    mcpHub?: McpHub,  // MCP 集线器实例,负责管理所有 MCP 服务器连接
    diffStrategy?: DiffStrategy,
    browserViewportSize?: string,
    // ... 其他参数
): Promise<string> {
    // ... 前面的代码
    
    // 异步获取两个部分:模式部分和 MCP 服务器部分
    const [modesSection, mcpServersSection] = await Promise.all([
        getModesSection(context),
        // 仅当当前模式包含 mcp 组时才加载 MCP 服务器部分
        modeConfig.groups.some((groupEntry) => getGroupName(groupEntry) === "mcp")
            ? getMcpServersSection(mcpHub, effectiveDiffStrategy, enableMcpServerCreation)
            : Promise.resolve(""),
    ])

    // 构建完整的系统提示,包括多个部分
    const basePrompt = `${roleDefinition}

${getSharedToolUseSection()}

${getToolDescriptionsForMode(  // 这里会包含 MCP 相关工具的描述
    mode,
    cwd,
    supportsComputerUse,
    effectiveDiffStrategy,
    browserViewportSize,
    mcpHub,
    customModeConfigs,
    experiments,
)}

${getToolUseGuidelinesSection()}

${mcpServersSection}  // 这部分包含所有可用的 MCP 服务器及其工具信息

// … 其他部分
    `

    return basePrompt
}

MCP 服务器信息生成getMcpServersSection 方法收集并格式化已连接的 MCP 服务器信息:提供服务器名称、可用工具及其参数架构,让 LLM 知道如何使用它们。

// src/core/prompts/sections/mcp-servers.ts
export async function getMcpServersSection(
    mcpHub?: McpHub,
    diffStrategy?: DiffStrategy,
    enableMcpServerCreation?: boolean,
): Promise<string> {
    if (!mcpHub) {
        return ""
    }

    // 构建已连接服务器的信息字符串
    const connectedServers =
        mcpHub.getServers().length > 0
            ? ${mcpHub &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .getServers() &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .filter((server) =&gt; server.status ===&nbsp;"connected") &nbsp;// 只显示已连接的服务器 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .map((server) =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // 为每个服务器生成其工具列表信息 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;const&nbsp;tools = server.tools &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ?.map((tool) =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // 为每个工具包含输入模式(如果有) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;const&nbsp;schemaStr = tool.inputSchema &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ?&nbsp;    Input Schema:
    ${JSON.stringify(tool.inputSchema, null, 2).split(“\n”).join("\n    ")}`
                                    : “”

                                return -&nbsp;${tool.name}:&nbsp;${tool.description}\n${schemaStr}
                            })
                            .join(“\n\n”)

                        // … 生成资源模板和直接资源信息 …
                        
                        // 解析服务器配置以显示命令信息
                        const config = JSON.parse(server.config)

                        // 返回完整的服务器描述,包括工具、资源模板和直接资源
                        return (
                            ##&nbsp;${server.name}&nbsp;(\${config.command}${config.args ? &nbsp;${config.args.join(" ")} : “”}`)&nbsp;+ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (tools ?&nbsp;\n\n### Available Tools\n${tools}&nbsp;:&nbsp;"") + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (templates ?&nbsp;\n\n### Resource Templates\n${templates}&nbsp;:&nbsp;"") + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (resources ?&nbsp;\n\n### Direct Resources\n${resources}&nbsp;:&nbsp;"") &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .join("\n\n")}
            : “(No MCP servers currently connected)”  // 如果没有连接服务器,显示此消息

    // … 返回完整部分,包括 MCP 服务器介绍和创建指南 …
}

工具描述提供getUseMcpToolDescription 函数定义了 MCP 工具的使用方法和参数格式。包含使用示例,帮助 LLM 生成正确格式的工具调用。

// src/core/prompts/tools/use-mcp-tool.ts
export function getUseMcpToolDescription(args: ToolArgs): string | undefined {
    // 如果没有 MCP 集线器,不需要此工具描述
    if (!args.mcpHub) {
        return undefined
    }
    
    // 返回标准化的工具描述,包括参数说明和使用示例
    return `## use_mcp_tool
Description: Request to use a tool provided by a connected MCP server. Each MCP server can provide multiple tools with different capabilities. Tools have defined input schemas that specify required and optional parameters.
Parameters:
- server_name: (required) The name of the MCP server providing the tool
- tool_name: (required) The name of the tool to execute
- arguments: (required) A JSON object containing the tool's input parameters, following the tool's input schema
Usage:
<use_mcp_tool>
<server_name>server name here</server_name>
<tool_name>tool name here</tool_name>
<arguments>
{
  "param1": "value1",
  "param2": "value2"
}
</arguments>
</use_mcp_tool>

Example: Requesting to use an MCP tool

<use_mcp_tool>
<server_name>weather-server</server_name>
<tool_name>get_forecast</tool_name>
<arguments>
{
  “city”: “San Francisco”,
  “days”: 5
}
</arguments>
</use_mcp_tool>`
}

工具识别&调用

首先通过 use_mcp_tool 工具来解析 LLM 返回的工具调用并验证参数。

// src/core/Cline.ts
async presentAssistantMessage() {
    // ... 前面的代码
    
    case "use_mcp_tool": {
        const server_name: string | undefined = block.params.server_name
        const tool_name: string | undefined = block.params.tool_name
        const mcp_arguments: string | undefined = block.params.arguments
        try {
            // 处理部分工具调用 - 这是处理未完成的工具调用的机制
            if (block.partial) {
                const partialMessage = JSON.stringify({
                    type: "use_mcp_tool",
                    serverName: removeClosingTag("server_name", server_name),
                    toolName: removeClosingTag("tool_name", tool_name),
                    arguments: removeClosingTag("arguments", mcp_arguments),
                } satisfies ClineAskUseMcpServer)
                await this.ask("use_mcp_server", partialMessage, block.partial).catch(() => {})
                break
            } else {
                // 验证必要参数是否存在
                if (!server_name) {
                    this.consecutiveMistakeCount++
                    pushToolResult(
                        await this.sayAndCreateMissingParamError("use_mcp_tool", "server_name"),
                    )
                    break
                }
                if (!tool_name) {
                    this.consecutiveMistakeCount++
                    pushToolResult(
                        await this.sayAndCreateMissingParamError("use_mcp_tool", "tool_name"),
                    )
                    break
                }
                
                // 解析 JSON 参数(如果提供)
                let parsedArguments: Record<string, unknown> | undefined
                if (mcp_arguments) {
                    try {
                        parsedArguments = JSON.parse(mcp_arguments)
                    } catch (error) {
                        // 处理 JSON 解析错误
                        this.consecutiveMistakeCount++
                        await this.say(
                            "error",
                            `Roo tried to use ${tool_name} with an invalid JSON argument. Retrying...`,
                        )
                        pushToolResult(
                            formatResponse.toolError(
                                formatResponse.invalidMcpToolArgumentError(server_name, tool_name),
                            ),
                        )
                        break
                    }
                }

 然后通过McpHub.callTool方法来实现 MCP 工具的调用。

// src/core/Cline.ts - 继续上面的代码
await this.say("mcp_server_request_started")
const toolResult = await this.providerRef
    .deref()
    ?.getMcpHub()
    ?.callTool(server_name, tool_name, parsedArguments)
// src/services/mcp/McpHub.ts
async callTool(
    serverName: string,
    toolName: string,
    toolArguments?: Record<string, unknown>,
): Promise<McpToolCallResponse> {
    // 查找对应的服务器连接
    const connection = this.connections.find((conn) => conn.server.name === serverName)
    if (!connection) {
        throw new Error(
            `No connection found for server: ${serverName}. Please make sure to use MCP servers available under 'Connected MCP Servers'.`,
        )
    }
    // 检查服务器是否被禁用
    if (connection.server.disabled) {
        throw new Error(`Server "${serverName}" is disabled and cannot be used`)
    }

    // 从服务器配置中获取超时设置
    let timeout: number
    try {
        const parsedConfig = ServerConfigSchema.parse(JSON.parse(connection.server.config))
        timeout = (parsedConfig.timeout ?? 60)  1000  // 默认 60 秒
    } catch (error) {
        console.error(“Failed to parse server config for timeout:”, error)
        // 解析失败时使用默认值
        timeout = 60 
 1000
    }

    // 使用 MCP SDK 的 Client 接口发送请求
    return await connection.client.request(
        {
            method: “tools/call”,
            params: {
                name: toolName,
                arguments: toolArguments,
            },
        },
        CallToolResultSchema,  // 用于验证响应的模式
        {
            timeout,  // 应用从配置获取的超时值
        },
    )
}

callTool 里面最后的调用还是落在我们创建 Server 时使用的 connection。

connection.client.request(
        {
            method: "tools/call",
            params: {
                name: toolName,
                arguments: toolArguments,
            },
        },
        CallToolResultSchema,
        {
            timeout,
        },
    )

问题的答案

讲到这里我们终于可以给之前疑问画上一个句号了,具体的 MCP被调用的链路如下:

1.初始化连接:

  • McpHub 实例化各种 McpConnection

  • 每个连接包含 Client 和 StdioClientTransport/SSEClientTransport

2.工具调用:

  • McpHub.callTool 找到合适的 McpConnection

  • 使用 connection.client.request 发送请求

  • 请求通过 transport 发送到 MCP 服务器

3.服务端处理:

  • McpServer 接收请求

  • 找到对应的工具处理函数

  • 验证参数并执行处理函数

  • 返回结果

4.结果处理:

  • 结果通过 transport 返回

  • Client 解析响应并将其返回给 McpHub

  • McpHub 处理结果并返回给调用者

接下来又到了 AI 画图时间,具体的关系如下:





MCP-Pixelator 设计

场景分析

回到当前的 MCP 场景,目前图生码的链路已经打通,现在需要解决的问题就很是清晰了,如何把图生码的结果应用在本地 IDE 上。

流程如下: 

1.用户通过 OneDay VSC 等支持 MCP 的 AI 客户端上传 ZIP 文件 

2.MCP 服务器解析 ZIP 文件,提取 AST 数据 

3.服务器调用 AST 转码 API,将 AST 转换为 React 代码 

4.根据用户选择,生成新项目或将组件添加到现有项目 

5.返回生成的代码给用户

这一流程可以通过以下图表直观展示:

 



架构设计

基于 MCP 协议,我们的系统架构如下:

 



系统模块设计

McpPixelator 系统包含以下核心模块: 

1.MCP 服务器模块负责与 AI 客户端通信,处理请求和响应  

2.工具注册模块注册和管理 MCP 工具  

3.文件处理模块解析 ZIP 文件,提取 AST 数据  

4.API 通信模块与 AST 转码 API 进行交互  

5.错误处理模块处理各种异常情况

这些模块之间的关系如下图所示: 





通过这样的系统设计,我们构建了一个基于 MCP 协议的、能够将设计稿 AST 转换为 React 代码的服务。接下来,SHOW ME THE CODE!

核心代码实现

代码核心实现大部分基于 AI 实现,MCP 插件通过合理的 Prompt 调试+拆分任务维度,很容易就可以实现了。

基本结构与初始化

McpPixelatorServer 类是Ï整个系统的核心,负责初始化 MCP 服务器、设置工具处理器、处理请求等。以下是其基本结构和初始化逻辑: 

class McpPixelatorServer {  
  private server: Server;  
  private zipHandler: ZipHandler;  
  private apiToken: string = "";  
  private apiEndpoint: string = "fake";  
  private userId: string = "MCP_PIXELATOR" + "_" + process.env.USER_ID;  
  private from: string = process.env.FROM || "unknown";  

  constructor() {  
    // 初始化token  
    this.fetchToken()  
      .then((token) => {  
        this.apiToken = token;  
        console.log(“Token已更新”);  
      })  
      .catch((error) => {  
        console.error(“初始化token失败:”, error);  
      });  

    // 初始化MCP服务器  
    this.server = new Server(  
      {  
        name: “mcp-pixelator”,  
        version: “0.1.0”,  
      },  
      {  
        capabilities: {  
          tools: {},  
        },  
      },  
    );  

    this.zipHandler = new ZipHandler();  
    this.setupToolHandlers();  

    // 错误处理  
    this.server.onerror = (error: Error) => console.error(“[MCP Error]”, error);  
    process.on(“SIGINT”, async () => {  
      await this.server.close();  
      process.exit(0);  
    });  
  }  
  
  // 其他方法…  
}  

在构造函数中,我们首先初始化 API Token,然后创建 MCP 服务器实例,设置基本配置和能力。接着初始化 ZIP 文件处理器,并设置工具处理器。最后,我们配置错误处理逻辑和进程退出处理。 

工具注册与处理

MCP 协议的核心是工具(Tools)的注册和处理。以下是我们注册工具的代码: 

private setupToolHandlers() {  
  // 注册工具列表  
  this.server.setRequestHandler(ListToolsRequestSchema, async () => ({  
    tools: [  
      {  
        name: "process_done_zip_and_generate",  
        description: "读取 Done Zip文件并直接生成 React 代码",  
        inputSchema: {  
          type: "object",  
          properties: {  
            options: {  
              type: "object",  
              properties: {  
                type: {  
                  type: "string",  
                  enum: ["create", "add"],  
                  description: "生成代码的类型:create - 创建新项目,add - 添加到现有项目",  
                  default: "create",  
                },  
                projectPath: {  
                  type: "string",  
                  description: "当 type 为 add 时,需要提供项目路径",  
                },  
              },  
              required: ["type"],  
            },  
          },  
          required: ["options"],  
        },  
      },  
    ],  
  }));  
  
  // 处理工具调用请求  
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {  
    try {  
      console.log("收到工具调用请求:", {  
        工具名称: request.params.name,  
        参数: request.params.arguments,  
      });  

      if (request.params.name === “process_done_zip_and_generate”) {  
        // 处理逻辑…  
      } else {  
        throw new McpError(  
          ErrorCode.MethodNotFound,  
          未知工具: ${request.params.name},  
        );  
      }  
    } catch (error) {  
      // 错误处理…  
    }  
  });  

这段代码首先注册了一个名为 process_done_zip_and_generate 的工具,用于读取 ZIP 文件并生成 React 代码。该工具接受一个 options 参数,包含 type(创建新项目或添加到现有项目)和可选的 projectPath(当 type 为 add 时的项目路径)。

然后,我们设置了处理工具调用的逻辑,根据工具名称执行相应的操作。如果请求的是未知工具,则抛出 MethodNotFound 错误。  

ZIP 文件处理与 AST 提取

当接收到工具调用请求后,我们需要处理 ZIP 文件并提取 AST 数据: 

// 在 CallToolRequestSchema 处理函数中  
if (request.params.name === "process_done_zip_and_generate") {  
  // 获取参数  
  const { options } = request.params.arguments as {  
    options: CodeGenerationOptions;  
  };  

  // 打开文件选择器  
  const zipFilePath = await this.zipHandler.selectFile();  

  console.log(处理ZIP文件:&nbsp;${zipFilePath});  

  // 读取ZIP文件内容  
  const contents = await this.zipHandler.readZipFile(zipFilePath);  

  // 提取AST数据  
  const astData = this.extractAstData(contents);  

  console.log(“已提取AST数据,开始生成代码”);  

  // 直接调用API生成代码  
  const result = await this.generateReactCode(astData, options);  

  // 处理结果…  
}  

AST 数据提取的具体实现如下: 

private extractAstData(contents: ZipContents): AstData {  
  // 首先尝试找到 AST 文件  
  const astFile = contents.files.find(  
    (f: { name: string; type: string }) =>  
      (f.name === "ast.json" ||  
        f.name === "ast.txt" ||  
        f.name.endsWith(".ast")) &&  
      (f.type === "json" || f.type === "text"),  
  );  

  if (!astFile || typeof astFile.content !== “string”) {  
    throw new Error(“未找到有效的 AST 文件”);  
  }  

  try {  
    // 尝试解析 JSON  
    const astContent = JSON.parse(astFile.content);  
    return { ast: astContent };  
  } catch (e) {  
    // 如果解析失败,直接使用原始字符串  
    console.log(“AST 文件解析为 JSON 失败,使用原始字符串”);  
    return { ast: astFile.content };  
  }  
}  

这段代码首先在 ZIP 内容中查找 AST 文件(名为 "ast.json"、"ast.txt" 或以 ".ast" 结尾),然后尝试将其解析为 JSON 对象。如果解析失败,则使用原始字符串。 

调用 AST 转码 API

提取 AST 数据后,我们调用外部 API 将其转换为 React 代码: 

private async generateReactCode(  
  astData: AstData,  
  options: CodeGenerationOptions,  
): Promise<ApiResponse> {  
  // 确保有可用的token  
  if (!this.apiToken) {  
    try {  
      this.apiToken = await this.fetchToken();  
    } catch (error) {  
      throw new Error("无法获取API Token: " + error);  
    }  
  }  

  console.log(“开始生成 React 代码,输入参数:”, JSON.stringify(astData));  
  try {  
    console.log(“发送请求到 API…”);  
    const response = await fetch(this.apiEndpoint, {  
      method: “POST”,  
      headers: {  
        Authorization: Bearer&nbsp;${this.apiToken},  
        “Content-Type”: “application/json”,  
      },  
      body: JSON.stringify({  
        inputs: {  
          from: this.from,  
          options, // 添加 options 到请求中  
        },  
        query: astData.ast,  
        response_mode: “blocking”,  
        conversation_id: “”,  
        user: this.userId,  
      }),  
    });  

    if (!response.ok) {  
      throw new Error(  
        API 请求失败:&nbsp;${response.status}&nbsp;${response.statusText},  
      );  
    }  

    const data = (await response.json()) as ApiServerResponse;  
    console.log(“API 响应数据:”, JSON.stringify(data, null, 2));  
    return {  
      answer: data?.answer || undefined,  
      error: data?.error || data?.message,  
    };  
  } catch (error) {  
    console.error(“请求失败:”, error);  
    throw error;  
  }  
}  

这段代码首先确保有可用的 API Token,然后向 AST 转码 API 发送请求,将 AST 数据、用户选项等信息包含在请求体中。如果请求成功,则解析响应数据并返回;如果失败,则抛出相应的错误。 

返回结果处理

最后,我们需要处理 API 响应并将结果返回给 AI 客户端: 

// 在 CallToolRequestSchema 处理函数中  
const result = await this.generateReactCode(astData, options);  

if (result.error) {  
  return {  
    content: [  
      {  
        type: “text”,  
        text: 处理ZIP并生成代码失败:&nbsp;${result.error},  
      },  
    ],  
    isError: true,  
  };  
}  

// 返回结果  
const responseData = result.answer  
  ? (JSON.parse(result.answer) as ApiResponseData)  
  : {};  
return {  
  content: [  
    {  
      type: “text”,  
      text: 已成功处理ZIP文件并生成React&nbsp;${ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; options.type&nbsp;===&nbsp;"create"&nbsp;?&nbsp;"项目"&nbsp;:&nbsp;"组件"&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; }代码:\n\n${responseData.text ||&nbsp;""},  
    },  
  ],  
};  

这段代码检查 API 响应中是否有错误。如果有,则返回错误信息;如果没有,则解析响应数据并返回生成的 React 代码。 

服务器启动与运行

最后,我们需要启动 MCP 服务器,开始监听请求: 

async run() {  
  const transport = new StdioServerTransport();  
  await this.server.connect(transport);  
  console.error("MCP Pixelator server 正在运行...");  
}  

// 主程序  
const server = new McpPixelatorServer();  
server.run().catch(console.error);  

这段代码创建了一个标准输入/输出传输器(StdioServerTransport),并使用它连接 MCP 服务器。服务器成功连接后,输出运行消息,等待 AI 客户端的请求。 

至此,我们已经完成了 McpPixelatorServer 的核心实现。 集成在插件内的整体流程如下:





实战一下

这是在本次在开发逛逛视频沉浸流的新版本场景为例,将 UI 和逻辑完全拆分后(这部分具体的前端开发理念暂且不提),直接开发 UI 相关的部分。

选中对应设计稿区域:

使用 Pixelator 插件生成 AST





在插件中输入





选择对应的 Zip 文件





等待生成(一般耗时 60s 左右)





生成完成





代码被插在了一个现有的React Demo 仓库中并找到了对应的位置





运行一下

待改进

1.现在还是比较粗暴的方式,用一个 LLM 去调用 MCP,然后用 MCP 调用相关接口(但是这个接口里面也是一个 LLM),再把返回的代码结构吐回来,再来判断是否要新增还是补全;这个有点不够优雅,后续会考虑直接融合在一个对话内。

2.现在还是全 Web 相关代码,在移动端上的适配还是有点弱,针对插件侧的产出,可以和移动端、大屏、Weex相关的内容结合,这样需要二次创作的东西会少一点。

3.现在 D2C 图生码的时间会比较长,可能会存在超时被插件强制超时处理的情况(Roo 的默认超时时间是60s可以配置,OneDay VSC 默认还是 60s 但是并不能配置),这个后面考虑和插件开发同学一道,完成。

一些槽点记录

文件选择器

在直接用 LLM 生成我的诉求的时候,初始的诉求是打开一个文件选择器,并且可以支持用户选择 Zip 文件,LLM 一直在尝试安装 electron 来实现这个文件选择器,矫正了几次都是一直在使用这个,同时还试了一下 inquiry 这个社区包,但是也是没能实现,后面发现了个神奇的脚本,解决了这个问题,也算是有所收获。

osascript -e 'choose file with prompt "选择 ZIP 文件" of type {"ZIP"}' 2>/dev/null

任务之间的衔接

一个大的任务拆成多个子任务时,比如说代码生成任务拆成选择 Zip 并解析和生成 Code 两步后,负责衔接两步任务的是 LLM,因此遇到了很多次,JSON 数据要么 String 了一下 要么直接给我套上了个双引号,总之质疑了自己好几次,最后还是选择了将两个任务合二为一了...

调试

调试是非常之困难...学习了一下官方提供的 inspector,但是对于这个使用骚操作拉起来选择框的情况,是没有办法直接使用 inspector 的只能再魔改...

同时如果想要看 mcp 插件具体返回了什么,还要 console 打印/写文件里面,又是一堆 token 浪费...

参考:

1.mcp 文档:https://modelcontextprotocol.io/introduction

2.Roo 源码:https://github.com/RooVetGit/Roo-Code?tab=readme-ov-file

3.MCP Typescript SDK:https://github.com/modelcontextprotocol/typescript-sdk

4.https://www.linkedin.com/pulse/ai-data-connection-why-anthropics-mcp-matters-chandrakumar-r-pillai-lvepe

5.https://blog.bytebytego.com/p/ep154-what-is-mcp

6.https://www.ali213.net/news/html/2025-1/896671.html

7.Sider DeepResearch

无代理ECS数据备份与高效环境搭建


基于快照提供数据保护和环境搭建,实现无代理且有效可靠的数据备份,同时可以快速克隆部署开发测试环境。    


点击阅读原文查看详情。

问题:文章中提到使用 LLM 调用 MCP 接口,接口内部又调用 LLM,作者觉得不够优雅。你有什么更好的方案,可以优化这个调用链路?

我来抖个机灵:直接把两个 LLM 训练成一个!:dog_face: 开玩笑啦。

言归正传,我觉得可以从这几个方面入手:

* Prompt Engineering:优化给 LLM 的指令,明确告诉它如何调用 MCP 接口,并期望得到什么样的结果。好的 Prompt 可以减少 LLM 的试错,提高效率。
* 中间层优化:在 LLM 和 MCP 接口之间加一个中间层,负责数据转换、错误处理、以及一些简单的逻辑判断。这样可以减轻 LLM 的负担,让它更专注于代码生成。
* Task Decomposition:把复杂的任务分解成更小的、更具体的子任务,然后分配给不同的模块处理。例如,一个模块负责解析设计稿,一个模块负责生成代码,一个模块负责测试和部署。

问题:作者在开发过程中遇到了文件选择器的问题,最终使用了一个脚本解决。你认为这个方案是否优雅?有没有更通用的解决方案?

用脚本解决文件选择器问题,感觉有点 “曲线救国”。虽然能用,但不够通用,可移植性也差。如果换个环境,脚本可能就失效了。

更通用的解决方案:
1. VS Code API:既然是在 VS Code 插件里开发,应该优先使用 VS Code 提供的 API。VS Code 应该有文件选择器的 API,可以查一下文档。
2. Web 标准 API:如果需要在 Web 环境中使用文件选择器,可以考虑使用 HTML 的 <input type="file"> 元素。虽然样式可能不够美观,但兼容性好,而且可以通过 JavaScript 进行自定义。

问题:MCP 协议被称为 AI 时代的 “USB-C”,你认为这个比喻是否准确?MCP 在实际应用中还存在哪些局限性?

USB-C 的比喻挺形象的,MCP 确实想解决 AI 工具和模型之间的兼容性问题。但我觉得用 “万能插头” 来形容可能有点早。USB-C 好歹发展了好几年,才逐渐普及,而且现在也没完全统一所有设备。

MCP 的局限性:
1. 支持程度不一:就像文章说的,不同 AI 平台对 MCP 的支持程度不一样,有的可能只支持部分功能。
2. 标准演进:MCP 还在发展中,标准可能会不断变化,开发者需要持续关注。
3. 安全性:统一接口也可能带来安全风险,需要考虑如何保护数据和工具的安全。

问题:MCP 协议被称为 AI 时代的 “USB-C”,你认为这个比喻是否准确?MCP 在实际应用中还存在哪些局限性?

这个比喻我给80分!USB-C解决了物理接口的统一,MCP则是尝试解决AI模型和工具之间的“逻辑接口”统一问题。但是!USB-C的统一也经历了一个漫长的过程,而且至今不同厂商的充电协议、数据传输速度仍然存在差异。MCP也一样,虽然定义了标准,但实际应用中,不同平台的兼容性、性能表现,甚至安全策略都可能存在差异。

至于局限性,我觉得最大的挑战在于生态的构建。需要足够多的工具和服务都支持MCP协议,才能真正发挥它的价值。另外,安全问题也是一个潜在的风险点,统一的接口意味着一旦出现漏洞,攻击面也会更大。

问题:作者在开发过程中遇到了文件选择器的问题,最终使用了一个脚本解决。你认为这个方案是否优雅?有没有更通用的解决方案?

脚本方案,简单粗暴有效!但在我眼里,优雅永不过时!:smiling_face_with_sunglasses:

更通用的方案,我认为要分情况讨论:

* VS Code 插件:必须用 VS Code 提供的 API!这是最正宗、最安全、最稳定的方式。可以搜索 vscode.window.showOpenDialog API。
* Web 应用:用 <input type="file">,然后用 JavaScript 控制样式和行为。虽然不如原生应用那么炫酷,但兼容性好,适用范围广。
* 桌面应用(Electron):用 Electron 的 dialog 模块,方便快捷,跨平台!

问题:作者在开发过程中遇到了文件选择器的问题,最终使用了一个脚本解决。你认为这个方案是否优雅?有没有更通用的解决方案?

说实话,直接用osascript感觉有点黑科技,不够优雅,而且依赖 macOS。移植性肯定不好。

更通用的方案:

1. Electron (如果适用):如果插件是基于 Electron 开发的,那直接用 Electron 提供的 dialog 模块,可以跨平台。

2. Web 标准 + 权限:如果插件运行在 Web 环境,可以尝试用 <input type='file'> 元素,但这需要用户授权访问文件系统,可能需要一些安全配置。

3. 原生插件:如果对性能和用户体验要求很高,可以考虑用 C++ 或其他原生语言写一个文件选择器插件,然后通过 Node.js 的 Native Addons 机制集成到 VS Code 插件里。

问题:MCP 协议被称为 AI 时代的 “USB-C”,你认为这个比喻是否准确?MCP 在实际应用中还存在哪些局限性?

我觉得把 MCP 比作 USB-C 有一定道理,都是为了统一接口、简化连接。但是,USB-C 发展这么多年,各家厂商的实现还是有差异,快充协议都不统一。MCP 现在也面临这个问题,虽然协议统一了,但不同 AI 终端的支持力度和能力肯定不一样,很难做到真正的 “即插即用”。

局限性方面,我觉得除了文章里提到的,可能还有像安全性问题。统一接口意味着一旦出现漏洞,影响范围也会更大。另外,MCP 的普及也依赖于整个 AI 领域的生态建设,需要更多工具和服务支持这个协议。

问题:文章中提到使用 LLM 调用 MCP 接口,接口内部又调用 LLM,作者觉得不够优雅。你有什么更好的方案,可以优化这个调用链路?

这个问题的确很关键!两次LLM调用确实增加了延迟和成本,听起来就不是最优解。我的想法是:

1. 模型微调(Fine-tuning):如果条件允许,可以考虑用特定领域的数据(比如UI设计稿相关的AST数据和React代码)对LLM进行微调。这样,LLM就能更直接地理解设计稿和代码之间的关系,减少对外部API的依赖。

2. 知识蒸馏(Knowledge Distillation):如果微调成本太高,可以考虑用“知识蒸馏”的方法,用一个更小的、更快的模型去学习大型LLM的知识。这样可以在保证一定效果的前提下,降低延迟。

3. 缓存机制:对于常见的转换任务,可以建立一个缓存机制。如果用户上传的设计稿和之前的某个设计稿很相似,可以直接使用之前的转换结果,避免重复计算。

问题:文章中提到使用 LLM 调用 MCP 接口,接口内部又调用 LLM,作者觉得不够优雅。你有什么更好的方案,可以优化这个调用链路?

我觉得可以尝试把两个 LLM 的能力融合到一个对话流程里。现在是分开调用,信息传递有损耗,而且上下文理解可能不连贯。

可以考虑这样做:
1. Prompt 优化:设计更完善的 Prompt,让一个 LLM 既能理解用户的意图,又能调用 MCP 接口,并根据接口返回的结果生成代码。
2. Function Calling:利用 LLM 的 Function Calling 能力,让 LLM 知道有哪些 MCP 工具可用,以及每个工具的参数和作用。这样 LLM 就能更智能地选择合适的工具,并生成正确的参数。
3. 知识库:建立一个知识库,存储常用的代码片段和组件。LLM 可以先从知识库中查找,如果找不到再调用 MCP 接口生成代码。