MCP 开发实战-如何使用 MCP 真正加速 UE 项目开发

用说人话的方式讲解 MCP

目前各种 MCP 的文章和实际例子以及开源工具层出不穷,本文试图用最简单的方式解释下 MCP 解决什么问题和 MCP 怎么写的问题。

为啥要用 MCP

MCP 是一项专为 LLM 工具化操作设计的轻量化标准协议,其核心目标是构建 LLM 与异构软件系统间的通用指令交互框架。与传统的单一功能调用机制不同,MCP 通过三层架构创新解决工具扩展性问题:

【协议定位】

作为中间协议层,MCP 抽象出独立于具体 LLM 和业务系统的接口描述层,允许开发者在不同维度(功能权限、输入格式、执行环境)对工具接口进行灵活管控,避免传统方案中接口爆炸带来的维护难题。

【技术架构】

  1. 接口描述层:采用声明式 DSL 定义工具元数据,包括功能语义、入参 Schema、权限策略和执行上下文
  2. 代理控制层:内置动态路由引擎和权限验证模块,支持热插拔式工具注册与版本管理
  3. 协议适配层:提供跨平台 SDK,自动生成 OpenAPI/Swagger 等标准接口文档

【核心优势】

  • 双向解耦:前端 LLM 无需感知具体工具实现,后端系统可独立迭代
  • 权限纵深:细粒度控制工具可见性(开发者/用户/模型层级)
  • 执行沙箱:支持 Docker/WASM 等多重运行时隔离方案
  • 生态兼容:自带 LangChain/LLamaIndex 等主流框架的适配器

综合上述的专业表述,说人话就是,只要你的 LLM 有 Prompt 遵循能力,那么不管你是 qwen,llama,DeepSeek 还是 claude ,都可以连接同样的 MCP Server 并且让你的 LLM 能够真正的调用工具,因此大大加速了 LLM 工具使用的开发速度。

为什么最近 MCP 爆发了?

最近大量 MCP 的爆发依赖于 LLM 本身两个能力的大幅度提升: 1.结构化输出能力 2.指令遵循能力。特别是 claude3.7 sonnets 之后的进展,使得工具的使用成功率大幅提升。对于 LLM 本身的能力进展来说,通过工具使用的方式积累真实世界的数据,并且进行后训练,也会成为 LLM 的垂直能力和 LLM 工作准确率进一步提升的关键。

MCP Server 开发实战

有了基础概念之后,我们就可以直接开始一个 MCP Server 的开发了,目前 MCP 官方提供四种语言的开发 SDK,包括 Python,typescript,java 和 kotlin。我们以 IEG 最常用的 typescript 为例构建工程。

在开始前我们先明确一些概念,通常,我们编写的 MCP 是一个 MCP Server,在 Server 中我们通常会定义一系列我们所需要的工具。使用各种 LLM 的客户端只要能连接上 Server,就可以使用我们的 MCP 的各种工具调用能力了。

在 UE 开发中,UE 废物一样的文档和天量的代码经常让人头大,那么能不能让 LLM 帮我来分析代码呢?结合 Emacs 常用的 tree-sitter 语法分析库和 MCP,我们就可以用 LLM 来做这件事。

(本工程基于 github:github.com/ayeletstu... 进行修改得来,由于原工程已经无法配置运行,我已经将修改后的代码传至 git.woa.com/IEG-RED-...)

首先,我们和普通配置 NodeJs 工程一样,在 Package.json 中添加相应依赖:

"dependencies": {
    "@modelcontextprotocol/sdk": "0.6.0",
    "glob": "^8.1.0",
    "tree-sitter": "^0.20.1",
    "tree-sitter-cpp": "^0.20.0"
  },
  "devDependencies": {
    "@types/glob": "^8.1.0",
    "@types/jest": "^29.5.14",
    "@types/node": "^18.15.11",
    "jest": "^29.7.0",
    "ts-jest": "^29.2.5",
    "typescript": "^5.0.4"
  }

可以看到我们所需要的 modelcontextprotocol sdk 和 tree-sitter 等都可以直接从 npm 下载配置,我们按照常理执行 npm install 等步骤。接下来和通常的 NodeJS 程序一样,我们编写 index.ts 文件,先导入 mcp 相关的接口:

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ErrorCode,
  ListToolsRequestSchema,
  McpError,
} from '@modelcontextprotocol/sdk/types.js';

这里我们可以看到 MCP 的几个关键概念:

  • Server:我们的 MCP 服务器,也就是处理一类任务的工具集合
  • stdioServertransport:MCP 默认用的通讯格式。
  • RequestSchema:使用 MCP 时需要提供的参数,名字等等信息。

首先,我们需要定义一个 Server class

class UnrealAnalyzerServer {
  private server: Server;
  private analyzer: UnrealCodeAnalyzer;
 ......
  }

  public async start() {
    try {
      // Setup handlers first
      this.setupToolHandlers();
      
      // Connect to stdio transport
      const transport = new StdioServerTransport();
      await this.server.connect(transport);
      
      console.log('Unreal Analyzer Server started successfully');
    } catch (error) {
      console.error('Failed to initialize server:', error);
      process.exit(1);
    }
  }

这里定义了我们的 server 的一些最常用的初始化流程和工具定义过程,因为我们是希望用 MCP 来分析代码,因此我们的 CodeAnalyzer 也属于我们的 Server Class Member。

要定义工具,我们首先需要结合 ListToolsRequestSchema 来绑定我们的 tools,参考下面的代码:

private setupToolHandlers() {
    this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
      tools: [
        {
          ......
        {
          name: 'analyze_class',
          description: 'Get detailed information about a C++ class',
          inputSchema: {
            type: 'object',
            properties: {
              className: {
                type: 'string',
                description: 'Name of the class to analyze',
              },
            },
            required: ['className'],
          },
        },
  .....

我们定义工具的名字,和工具所需要的输入,并将其绑定到 server。这些信息会让 MCP 识别到我们需要调用到什么工具,并且在调用工具时,需要提供什么样的参数。

有了工具的名字和参数,MCP 需要知道具体如何去执行我们想要的操作,比如分析 C++类,搜索代码等等,这里就要用到 callToolRequestSchema 结构体:

this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
      // Only check for initialization for analysis tools
      const analysisTools = ['analyze_class', 'find_class_hierarchy', 'find_references', 'search_code', 'analyze_subsystem', 'query_api'];
      if (analysisTools.includes(request.params.name) && !this.analyzer.isInitialized() && 
          request.params.name !== 'set_unreal_path' && request.params.name !== 'set_custom_codebase') {
        throw new Error('No codebase initialized. Use set_unreal_path or set_custom_codebase first.');
      }

      switch (request.params.name) {
       .....
        case 'search_code':
          return this.handleSearchCode(request.params.arguments);
        case 'analyze_subsystem':
          return this.handleAnalyzeSubsystem(request.params.arguments);
        case 'query_api':
          return this.handleQueryApi(request.params.arguments);
        default:
          throw new Error(`Unknown tool: ${request.params.name}`);
      }
    });
  }

很明显,callToolRequestSchema 会将 tools 的名字和参数传给工具真正的执行者。在我们的 Server 内部定义的工具函数中,会调用 tree-sitter cpp 库 去进行真正的分析,然后将结果返回给我们的 LLM 进行总结。

来总结下, MCP 的编写本身是非常简单的,我们需要实现的是定义工具的名字,参数(从 LLM 中自然语言的方式获取),以及用代码描述的真正执行工具的流程,并且将这些都绑定到我们的 Server 上,我们只需要关心我们在调用什么工具和我们需要什么数据就行了,至于给大模型的提示词,多轮对话暂存,格式化输出验证等需要考虑到问题,MCP 的 SDK 都能帮我们搞定。

接下来我们用 tsc 编译我们的 Nodejs 程序,我们的 Server 就做好了。

使用 MCP

读到这里细心的读者肯定会发现。我们的 LLM 在哪里?这就是 MCP 更重要的一个好处,它的 Server 是 LLM 无关的,只要客户端使用的 LLM 看得懂提示词,那么它就能使用同一个 MCP Server。

接下来我们配置客户端来使用我们的 MCP Server,目前很多软件,包括 Claude desktop,dify 等都支持了 MCP,这里我们选择 VSCode 的 Cline 插件作为客户端(因为他开源),安装和配置 Cline 的过程在此不再赘述,打开 Cline 的 Setting,点击 MCP Servers 的按钮,我们会在下方看到一个 Configure MCP Server 的按钮,点击我们就可以打开我们的 MCP 设置 Json:

image.png

前文提到过,MCP 支持多种语言开发,包括 Python,Typescript 等,因此可以看到我们的 MCP 配置中也支持多种入口,一个 MCP Server 的入口,可以是 Python 脚本,可以是 bat 批处理,也可以是 nodejs 程序的入口,对于我们的 server 来说,我们要配置的是一个 nodejs 的入口程序,如下面的代码:

"unreal-analyzer": {
      "command": "node",
      "args": [
        "C:/Users/admin/Documents/Cline/MCP/unreal-analyzer-mcp/build/index.js"
      ],
      "env": {},
      "disabled": false,
      "autoApprove": [],
      "timeout": 3600    }

我们将入口指向我们编译好的 JavaScript 文件,保存好之后,Cline 就会自动去执行这个 index.js,如果有运行错误,那么 Cline 的设置窗口中会报错,当出现下面的绿色按钮时,则证明我们的 MCP Server 连接成功了:

image.png

接下来,我们就可以用自然语言的方式快速分析 UE 代码了,我们在 Cline 的对话框中切换到 Act Mode(只有 Actmode 可以调用 MCP Server),然后按照我们日常和同事交流说话的口吻打字:先告诉他我们的 UE 代码在哪里:

image.png

直接说:我想分析下 UMaterialExpressionPanner 这个类,LLM 会分析你的需求,自己去调用工具:

image.png

同时,LLM 也会根据自己的思考去继续调用工具,比如我的这个问题,它会继续调用工具,去搜索代码:

image.png

当他发现代码非常多的时候,它会考虑到:OK,我可能需要过滤一下代码,于是它会调用 SearchWithContext 的工具:

image.png

来个稍微复杂点的任务,让它帮我找找 lumen 里 AO 相关的类:

image.png

image.png

通过这个很简单的例子工程,我们可以总结出 MCP 的特点,MCP 通过工程化的方法和统一的协议,给 LLM 装上了使用工具的手,这样我们的 AI 就可以真正的替我们干活。

真正的 UnrealMCP 实现

理解了 MCP 的工作逻辑和原理之后, 再开发垂直领域的 MCP 工具就会相对简单。接下来我们来分析下真正的能干活的 UnrealMCP(github.com/kvick-gam...)是怎么工作的。同时也展示下 Python SDK 下的 MCP 工作流。

UnrealMCP 由两部分组成,一部分是 MCP Server 的 Python 代码,一部分是 UE5 的插件,其中 UE5 的插件主要负责对接我们操作 UE 需要的一些 C++逻辑。这部分的安装逻辑和通常的 UE 插件完全一样。

而 MCP 本身的部分,我们希望能够在 Cline 插件中调用,由于这个 UnrealMCP 只能在 Claude 中工作,而 Claude 在国内使用非常麻烦,因此在 Cline 中配置本 MCP 时,我们需要对仓库上说明的配置文件稍微进行些修改。 在运行安装 python, 启动 venv 等工作之后,我们在配置 Cline MCP 的 json 时,需要按照如下代码配置:

"unreal": {
      "command": "cmd.exe",
      "args": [
        "/c", 
        "F:\\UnrealEngine\\Engine\\Plugins\\UnrealMCP\\MCP\\run_unreal_mcp.bat"
      ],
      "env": {
        "PYTHONPATH": "F:\\UnrealEngine\\Engine\\Plugins\\UnrealMCP\\MCP\\python_modules",
        "PATH": "${PATH};F:\\UnrealEngine\\Engine\\Plugins\\UnrealMCP\\MCP\\python_env\\Scripts"
      },
      "disabled": false,
      "autoApprove": [],
      "cwd": "F:\\UnrealEngine\\Engine\\Plugins\\UnrealMCP\\MCP"
    },

这样就可以在 Cline 中使用 UnrealMCP 了。

使用上,UnrealMCP 也是非常简单的,打开 UE,启动 UnrealMCP 插件之后,我们告诉 LLM 我们的需求:在场景中创建一个迷宫关卡:

image.png

MCP 会将相关信息包装成 Python 调用脚本,发给 UnrealMCP Server

image.png

我们就可以得到最终结果:

image.png

和之前的例子一样,开发 UnrealMCP 还是遵循定义工具名字,参数,定义行为的这几个步骤,在 UnrealMCP 中,它采用了:

  • Python MCP 服务
  • Python-C++ 桥接层
  • C++ Unreal Engine 插件

的三层架构来实现(因为 UE 的 EditorPython 并不是很完善,所以需要通过 C++插件来实现命令的解析和工作)。以最简单的 CreateObject 为例子: 首先,依然是注册工具和参数,当然,使用 Python SDK,这个过程会更加直接简单,通过 Python 的注解语法来进行:

@mcp.tool()
def create_object(ctx: Context, type: str, location: list = None, label: str = None) -> str:
    params = {"type": type}
    if location:
        params["location"] = location
    if label:
        params["label"] = label
    response = send_command("create_object", params)

我们需要告诉 UE 我要创建的 location 和物体类型,接下来,用 Python 的 socket 通信封装一下 LLM 产生的数据,传给 UE:

def send_command(command_type, params=None, timeout=DEFAULT_TIMEOUT):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.connect(("localhost", DEFAULT_PORT))
        command = {"type": command_type, "params": params or {}}
        s.sendall(json.dumps(command).encode('utf-8'))
        response_data = receive_response(s)
        return json.loads(response_data.decode('utf-8'))

最后 UE 在 C++插件中进行接受消息,去调用 NewActor 函数:

TSharedPtr<FJsonObject> FMCPCreateObjectHandler::Execute(const TSharedPtr<FJsonObject> &Params, FSocket *ClientSocket)
{
    // 从参数中提取数据
    FString Type;
    Params->TryGetStringField(FStringView(TEXT("type")), Type);
    
    // 执行 Unreal Engine 操作
    AStaticMeshActor *NewActor = World->SpawnActor<AStaticMeshActor>(...);
    
    // 返回响应
    TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
    ResultObj->SetStringField("name", NewActor->GetName());
    return CreateSuccessResponse(ResultObj);
}

虽然整体流程复杂了些,但是可以看到,它依然遵循了 MCP 的设计范式,既通过 Tools 来扩展能力,告诉 LLM,你可以去干什么事,然后用各种方法,将 LLM 分析自然语言后得出的指令转为工具调用,去做真正的工作。

MCP 的局限性

MCP 虽然非常简洁明了,大大方便了 LLM Tool use 的开发成本,但是从本质上来说,MCP 只是解决了工具使用的可能性这一个主题,要想让 AI 真正干活,可以说 MCP 只是干活的那只手,我们同样需要大脑(规划 Agent),记忆力(数据库,记事本)来共同辅助完成自动化的工作。

此外,对于真正的专业软件来说,每一个接口/功能对应 MCP 可能也是一个工程量不小的工作,MCP 结合真正靠谱的 Agent 编程框架才有可能完成真正复杂的任务。

总结

本文通过两个案例,展示了 MCP 的整体开发逻辑和能力。通过 MCP,大模型可以真正干活。但同时,MCP 不应该被过度神话,它只是解决了工具调用这一系列的问题。要想让大模型彻底重塑日常的游戏开发工作流,还需要在流程,记忆力,以及模型本身的后训练上持续工作。

END

作者:hanzo
文章来源:腾讯技术工程

推荐阅读

更多腾讯 AI 相关技术干货,请关注专栏腾讯技术工程 欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。
推荐阅读
关注数
8163
内容数
248
腾讯AI,物联网等相关技术干货,欢迎关注
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息