用 AI 开发 AI:原汤化原食的 MCP 桌面客户端

24年底,Anthropic 发布的模型上下文协议(Model Context Protocol)促使我开始边学边撰写相关专栏:

MCP(Model Context Protocol)专栏https://blog.youkuaiyun.com/aiqlcom/category_12851864.html

为了深入理解协议细节并确保技术专栏内容的准确性,我尽可能实现了协议定义的客户端能力,通过实战验证了代码可行性。在两星期内就基于当时的 SDK 跑通了大部分流程,并且写了两篇对应的实战文:

MCP(Model Context Protocol)模型上下文协议 实战篇1_mcp协议实现-优快云博客

MCP(Model Context Protocol)模型上下文协议 实战篇2_mcp 实践-优快云博客

鉴于项目开发所需的技术框架已趋于成熟,我由此萌生了一个实验性构想:完全从零开始,高度依赖 AI 辅助构建完整的桌面客户端。这一尝试涵盖架构设计、代码实现、文档撰写以及 UI 生成,全程以 AI 的建议为核心参考。

先说结论,现有商用AI模型(如GPT系列、Deepseek、Claude等)能够理解项目整体架构并提供基础实现方案。但在以下场景中,AI 的表现存在局限性:

  • 数据结构定义:模型对复杂类型(如嵌套枚举、泛型约束)的逻辑一致性处理较弱。
  • UI设计:生成的布局缺乏系统性审美考量,需人工调整组件层级与交互逻辑。
  • 代码风格统一性:不同模块的代码风格差异显著(例如命名规则、异常处理范式),导致可读性降低。

想要快速逃课的同学,可直接访问Github链接获取实现细节:

Tool Unitary User Interfacehttps://github.com/AI-QL/tuui或者跳转文章末尾获得更多相关链接和结论。

代码完全开源,可以自己配置接入任意语言模型,或者搭配使用不同的 MCP server。

下面开始正文。


项目概述

TUUI 是一个基于MCP(Model Context Protocol)的 LLM 桌面应用程序,它能够集成 AI 工具并对接不同供应商的 LLM API。该项目代表了一个大胆的实验,尝试使用 AI 创建完整项目。 

主要功能

  • 通过 MCP 加速 AI 工具集成
  • 通过动态配置编排跨供应商 LLM API
  • 自动化 Playwright 应用测试支持
  • TypeScript 支持
  • 多语言支持(目前支持英文和简体中文)
  • 基本布局管理器
  • 通过 Pinia 实现全局状态管理
  • Github 社区开源和官方文档

此外,TUUI还提供:

  • MCP 调用可视化跟踪
  • 特定 Tool 选择功能
  • LLM API 设置界面
  • Sampling 采样功能
  • 基于 Electron 的跨平台桌面应用打包

系统架构

总览

Electron

Electron主进程 作为TUUI应用的后端核心,负责系统资源管理、窗口控制及外部集成,运行于具备完整系统访问权限的Node.js环境中。

主进程组件构成

组件模块文件路径核心功能
应用入口src/main/index.ts应用生命周期管理、异常处理、IPC通信初始化
窗口管理器src/main/MainRunner.ts主窗口创建(createMainWindow())、错误窗口创建(createErrorWindow()
系统托盘src/main/tray.ts托盘图标创建(createTray())、窗口定位控制
IPC通信枢纽src/main/IPCs.ts进程间通信桥梁搭建
MCP集成模块src/main/mcp/外部服务器通信交互

前端架构

TUUI使用Vue 3作为前端框架,结合Electron实现跨平台桌面应用。整个应用基于以下主要技术构建:

  1. UI框架: 使用Vuetify提供现代化UI组件

  2. 状态管理: 采用Pinia进行全局状态管理,实现持久化本地存储

  3. 路由系统: 使用Vue Router管理应用的不同页面

  4. 国际化: 通过Vue I18n实现多语言支持


主体页面

应用包含四个主要页面,通过路由系统进行管理:

  • MCP主界面("/"):管理 Model Context Protocol 相关功能
  • 聊天界面("/chat"):提供与 LLM 的对话功能
  • 代理界面("/agent"):管理 AI 智能体配置
  • 设置界面("/setting"):LLM API 设置

每个页面使用组件化设计,包含centralStage(中心区域)、sideDrawer(侧边抽屉)、sideDock(侧边底部配置)和bottomConsole(中心底部交互控制台)等部分。


主要功能实现

状态管理

TUUI使用多个Pinia store管理不同方面的状态:

主要 Store
  1. MCP Store: 处理MCP工具相关配置功能,包括列出、获取和调用工具

  2. Chatbot Store: 管理聊天机器人配置,支持多种LLM,如Qwen、OpenAI等

  3. Message Store: 处理消息发送、接收

  4. History Store: 管理对话历史,用indexedDB持久化

  5. Layout Store: 管理应用布局状态

次要 Store
  1. Locale Store: 处理 i18n 语言设置

  2. Agent Store: 管理智能体配置,MCP tool 相关配置

  3. Resource Store: 管理 MCP resource 相关配置

  4. Prompt Store: 管理 MCP prompt 相关配置

LLM 集成

TUUI 支持多种语言模型,默认配置包括:

  • Qwen系列(qwen-turbo, qwen-plus, qwen-max)
  • OpenAI系列(gpt-4-turbo, gpt-4.1, gpt-4o, o1)
  • DeepInfra API
  • ...

每个语言模型配置支持自定义API端点、模型选择、认证方式等参数。所以理论上可以支持各种 OpenAI API 兼容的 LLM 后端模型。同时,可以在 Electron 配置里关闭 webSecurity 来解除 CORS 跨域限制,从而支持一些尚在开发中的私有后端 API。

MCP 客户端

主架构

通过 MCP Typescript SDK 实现 MCP 调用,支持工具列表获取、调用和结果处理。用户也可以利用 Cloudflare 推荐的 mcp-remote 来配置远程 MCP 服务器。

MCP 客户端运行于Electron主进程,作为外部MCP服务器与TUUI前端之间的通信桥接层。该系统包含以下核心功能模块:

  • 服务器发现与连接管理: 动态探测可用MCP服务器并维护稳定连接,包含自动重连机制与心跳检测功能。
  • 能力注册机制: 提供标准化接口供功能模块注册服务能力,支持动态能力更新与权限校验。
  • 请求路由分发: 通过消息队列实现请求/响应异步处理,确保高并发场景下的消息有序传递。

能力管理

MCP服务器提供三种能力(工具、提示、资源),TUUI系统会动态将其注册为IPC处理器。通过capabilitySchemas 对象,可将各能力类型与其对应的结果模式进行映射关联。

采样

MCP客户端通过处理来自MCP服务器的采样请求实现双向通信。当服务器请求创建消息时,客户端会将该请求转发至渲染器进程,并等待响应返回。

主要代码实现

Electron 主进程

主进程入口

主进程负责应用程序的生命周期管理,包括窗口创建、应用启动和退出处理。

app.on('ready', async () => {
  mainWindow = await createMainWindow()
})

app.on('activate', async () => {
  if (!mainWindow) {
    mainWindow = await createMainWindow()
  }
})

app.on('window-all-closed', () => {
  mainWindow = null
  errorWindow = null

  if (!Constants.IS_MAC) {
    app.quit()
  }
})
IPC通信层

通过IPC(进程间通信)实现主进程与渲染进程的数据交换,支持MCP服务器初始化、工具调用等核心功能。

export default class IPCs {
  static clients: ClientObj[] = []
  static currentFeatures: any[] = []

  static initialize(): void {
    // Get application version
    ipcMain.handle('msgRequestGetVersion', () => {
      return Constants.APP_VERSION
    })

    ipcMain.handle('msgInitAllMcpServers', async (event: IpcMainEvent, config: ConfigObj) => {
      this.clients.forEach((client: ClientObj) => {
        if (client.connection?.transport) {
          disconnect(client.connection.transport)
        }
      })

      IPCs.removeAllHandlers()

      try {
        const newClients = await initClients(config)
        const features = newClients.map((params) => {
          return registerIpcHandlers(params)
        })

        IPCs.updateMCP(features)
        this.clients = newClients
        return features
      } catch (error) {
        const configs = await loadConfig()

        const features = configs.map((params) => {
          return registerIpcHandlers(params)
        })

        IPCs.updateMCP(features)

        return {
          status: 'error',
          error: error
        }
      }
    })

MCP 客户端集成

MCP 客户端初始化

系统通过StdioClientTransport建立与MCP服务器的连接,支持多服务器并发管理。

async function initializeStdioClient(
  name: String,
  config: ServerConfig
): Promise<McpClientTransport> {
  const transport = new StdioClientTransport({
    ...config,
    env: {
      ...process.env,
      ...(config.env || {})
    },
    stderr: 'pipe'
  })
  const clientName = `${name}-client`
  const client = new Client(
    {
      name: clientName,
      version: Constants.APP_VERSION
    },
    {
      capabilities: {
        sampling: {}
      }
    }
  )

  if (transport.stderr) {
    transport.stderr.on('data', (chunk) => {
      console.error('stderr:', chunk.toString())
    })
  }

  await connect(client, transport)
  console.log(`${clientName} connected.`)

  client.setRequestHandler(CreateMessageRequestSchema, async (request) => {
    console.log('Sampling request received:\n', request)
    const response = await sendToRenderer('renderListenSampling', request)
    console.log(response)
    return response
  })

  return { client, transport }
}
工具调用管理

实现了完整的MCP工具调用流程,包括请求处理和响应管理。

export async function manageRequests(client: Client, method: string, schema: any, params?: any) {
  const requestObject = {
    method,
    ...(params && { params })
  }

  const result = await client.request(requestObject, schema)
  console.log(result)
  return result
}

Vue3 渲染进程

应用程序初始化

渲染进程使用Vue3 + Pinia + Vue Router构建,集成了国际化和状态持久化功能。

const app = createApp(App)
const pinia = createPinia()
pinia.use(createStatePersistence())

app.use(vuetify).use(i18n).use(router).use(pinia).use(Vue3Lottie)
app.mount('#app')
布局系统

采用命名视图的路由架构,支持多区域组件布局(中央舞台、侧边栏、底部控制台等)。

      <SidebarLayout v-if="hasComponent('sideDrawer').value" class="position-fixed">
        <router-view name="sideDrawer" />
        <template #append>
          <router-view v-if="hasComponent('sideDock').value" name="sideDock" />
        </template>
      </SidebarLayout>
      <HeaderLayout class="position-fixed" />
      <v-main class="d-flex justify-center">
        <v-container>
          <router-view name="centralStage" />
          <router-view />
        </v-container>
      </v-main>
      <FooterLayout v-if="hasComponent('bottomConsole').value" class="position-fixed">
        <router-view name="bottomConsole" />
      </FooterLayout>

状态管理

聊天机器人配置管理

通过Pinia Store管理多个聊天机器人的配置,支持动态切换和配置更新。

export const useChatbotStore = defineStore('chatbotStore', {
  state: (): ChatbotStoreState => ({
    chatbots: [
      { ...CHATBOT_DEFAULTS, name: 'Chatbot Default', mcp: false },
      {
        ...CHATBOT_DEFAULTS,
        ...CHATBOT_QWEN
      },
      {
        ...CHATBOT_DEFAULTS,
        ...CHATBOT_OPENAI
      },
      {
        ...CHATBOT_DEFAULTS,
        ...CHATBOT_DEEPINFRA
      }
    ],
    currentChatbotId: 0, // points to first chatbot by default
    selectedChatbotId: 0
  }),

  persist: {
    exclude: ['currentChatbotId']
  },
MCP状态管理

管理MCP服务器连接状态、工具列表和服务器选择逻辑。

export const useMcpStore = defineStore('mcpStore', {
  state: (): any => ({
    version: 1,
    serverTools: [],
    loading: true,
    selected: undefined as string[] | undefined,
    selectedChips: {} // { key : 0 | 1 | 2}
  }),
消息状态管理

处理聊天会话、消息发送等核心业务逻辑。

export const useMessageStore = defineStore('messageStore', {
  state: (): any => ({
    userMessage: '',
    conversation: [],
    images: [],
    base64: '',
    generating: false
  }),

消息处理流程

消息发送逻辑

用户消息发送后触发推理流程,支持图片和文本混合输入。

    sendMessage() {
      if (this.userMessage) {

        const imageBase64 = this.base64

        this.conversation.push({
          content: imageBase64
            ? [
                { type: 'image_url', image_url: { url: imageBase64 } },
                { type: 'text', text: this.userMessage }
              ]
            : this.userMessage,
          role: 'user'
        })

        if (this.conversation.length === 1) {
          this.syncHistory()
        }

        this.startInference()
      }
    },
工具调用后处理

自动检测助手回复中的工具调用请求,执行工具并将结果反馈给模型。

   postToolCall: async function () {
      const mcpStore = useMcpStore()
      const last = this.conversation.at(-1)
      if (!last || !last.tool_calls) {
        return
      }
      if (isEmptyTools(last.tool_calls)) {
        delete last.tool_calls
      } else {
        let toolCalled = false
        console.log(last.tool_calls)

        const callNextTool = async (toolCalls, index) => {
          if (index >= toolCalls.length) {
            return
          }

          const toolCall = toolCalls[index]

          let result

          try {
            result = await mcpStore.callTool(toolCall.function.name, toolCall.function.arguments)
            console.log(result)
          } catch (error) {
            result = mcpStore.packReturn(`Error calling tool: ${error}`)
          }

          if (result.content) {
            this.contentConvert(result.content, toolCall.id).forEach((item) => {
              this.conversation.push(item)
            })
            toolCalled = true
          }
        }

        await callNextTool(last.tool_calls, 0)

        if (toolCalled) {
          this.startInference()
        }
      }
    },

路由与页面

多页面路由设计

采用命名组件路由,支持MCP管理、聊天、代理配置和设置等多个功能模块。

export default createRouter({
  history: createWebHashHistory(),
  routes: [
    {
      path: '/',
      components: McpScreen,
      meta: {
        titleKey: 'title.main'
      }
    },
    {
      path: '/chat',
      components: ChatScreen,
      meta: {
        titleKey: 'title.chat'
      }
    },
    {
      path: '/agent',
      components: AgentScreen,
      meta: {
        titleKey: 'title.agent'
      }
    },
    {
      path: '/setting',
      components: SettingScreen,
      meta: {
        titleKey: 'title.setting'
      }
    },
屏幕组件组织

每个功能模块包含多个子组件,通过命名视图实现复杂布局。 

export const ChatScreen: ScreenType = {
  centralStage: ChatMainScreen,
  sideDrawer: ChatHistoryScreen,
  sideDock: ChatEndScreen,
  bottomConsole: ChatInputScreen
}

技术栈组成

  "dependencies": {
    "@mdi/font": "^7.4.47",
    "@modelcontextprotocol/sdk": "^1.12.1",
    "highlight.js": "^11.11.1",
    "iconify-icon": "^3.0.0",
    "katex": "^0.16.22",
    "localforage": "^1.10.0",
    "md-editor-v3": "^5.6.0",
    "mermaid": "^11.6.0",
    "pinia": "^3.0.2",
    "pinia-plugin-state-persistence": "^1.10.2",
    "uuid": "^11.1.0",
    "vue": "^3.5.16",
    "vue-i18n": "^11.1.5",
    "vue-router": "^4.5.1",
    "vue3-lottie": "^3.3.1",
    "vuetify": "^3.8.7"
  },
  "devDependencies": {
    "electron": "^36.3.2",
    "electron-builder": "^26.0.15",
    "eslint": "^9.28.0",
    "prettier": "^3.5.3",
    "typescript": "^5.8.3",
    "vite": "^6.3.5",
    ...
  },

包括Electron、Vue3、TypeScript、Vuetify、Pinia等,并集成了 MCP SDK 用于协议支持。

总结

TUUI 是一个尝试性项目,几乎所有组件通过 AI 生成和转换。(包括这篇文章中的所有架构和交互图也是 AI 分析生成的)

因此,项目采用严格的语法检查和命名规范,使用ESLint和Prettier尽可能保持代码质量和风格。

从我的实际体验来看,AI 生成的代码质量往往呈现出一种矛盾的特质——就像经验丰富的匠人生硬地做着新手活计(有点"面向测试开发"的那个味道)。这意味着绝大多数AI生成的组件都需要经过深度重构,必须后期精心打磨。仅靠静态代码检查这类表面功夫,根本不足以确保最终产出质量。

即便在前端开发部分,AI仍存在明显局限,例如在UI设计层面,其生成的界面往往呈现过于生硬的极简风格,缺乏人性化考量。

本项目代码已放出,采用非常宽松的Apache 2.0开源协议,支持二次开发和商业使用。

最后完整提供一下所有链接:

项目地址:https://github.com/AI-QL/tuui

项目官网:https://www.tuui.com/

项目全架构英文解析(支持 AI 对话提问):AI-QL/tuui | DeepWiki

      评论
      添加红包

      请填写红包祝福语或标题

      红包个数最小为10个

      红包金额最低5元

      当前余额3.43前往充值 >
      需支付:10.00
      成就一亿技术人!
      领取后你会自动成为博主和红包主的粉丝 规则
      hope_wisdom
      发出的红包
      实付
      使用余额支付
      点击重新获取
      扫码支付
      钱包余额 0

      抵扣说明:

      1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
      2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

      余额充值