尝试给Lookin 支持 MCP

不知道大家在 Vibe Coding 的时候,是否经常遇到这样的情况,让 AI 修改一个复杂页面,改完之后发现布局乱了,只能通过文字描述让 AI 去改,还经常改不对。

在日常开发中,我们经常会使用 Lookin 来查看布局,我想能否给 Lookin 支持 MCP 查看布局+刷新。这样是不是就不用我们自己给 AI 描述问题了。

于是我开始了这个集成工作,用本文记录下整个过程。先放下最终效果:

Lookin MCP 支持的所有方法

# 方法名 描述 参数
1 get_status 获取 Lookin 服务器状态和连接状态,返回是否有 iOS 应用连接以及是否有层级数据
2 list_apps 列出所有连接的 iOS 应用及其基本信息
3 get_hierarchy 获取已连接 iOS 应用的完整视图层级,返回所有视图及其属性、frame 和关系 flat: bool (可选) - 是否返回扁平数组 maxDepth: int (可选) - 最大遍历深度
4 get_view 通过 oid 获取指定视图的详细信息,包括 frame、bounds、类继承链等 oid: int (必需) - 视图对象 ID
5 get_screenshot 获取指定视图的截图,返回 base64 编码的 PNG 图片 oid: int (必需) - 视图对象 ID
6 search_views 按类名、文字内容或 oid 搜索视图 query: string (必需) - 搜索关键词 type: enum (可选) - 搜索类型: "class"/"text"/"oid"
7 list_viewcontrollers 列出应用中所有的 ViewController,包括类名、内存地址和关联的视图 oid
8 get_app_info 获取已连接 iOS 应用的详细信息,包括应用名、Bundle ID、设备名、OS 版本、屏幕尺寸
9 reload_hierarchy 重新加载视图层级数据,用于 UI 变化后刷新数据
10 get_view_attributes 获取视图的完整属性详情,包括所有属性组(Layout、AutoLayout、UILabel、UIScrollView 等)、事件处理器(手势、target-action)和 AutoLayout 约束 oid: int (必需) - 视图对象 ID

第一版

为什么需要双进程?

MCP 协议要求 Server 通过 stdio(标准输入/输出)与 Client 通信,这对于 GUI 应用来说是个问题:

  • macOS GUI 应用没有 stdin/stdout
  • GUI 应用不适合作为子进程被其他应用启动
  • Lookin 需要保持独立运行以维护与 iOS 设备的连接

所以第一版采用双进程架构:一个独立的命令行工具 lookin-mcp 处理 MCP 协议,通过 HTTP 与 Lookin 主应用通信。

sequenceDiagram participant AI as AI 工具 participant MCP as lookin-mcp participant HTTP as LKMCPServer participant iOS as iOS App Note over AI,MCP: stdio (JSON-RPC) Note over MCP,HTTP: HTTP (REST) Note over HTTP,iOS: Peertalk/Bonjour AI->>MCP: tools/call (get_hierarchy) MCP->>HTTP: GET /hierarchy HTTP->>iOS: 获取视图数据 iOS-->>HTTP: LookinHierarchyInfo HTTP-->>MCP: JSON Response MCP-->>AI: Tool Result
组件 角色 通信方式
lookin-mcp MCP Server stdio (JSON-RPC)
LKMCPServer HTTP Server HTTP REST API
Lookin.app 主应用 内嵌 HTTP Server

实现细节

1. LookinMCP Package 结构

bash 复制代码
LookinMCP/
├── Package.swift
├── Sources/
│   ├── LookinMCP/
│   │   └── main.swift              # MCP Server 入口
│   ├── LookinBridge/
│   │   ├── LookinBridge.swift      # Swift 封装
│   │   └── LookinHTTPClient.swift  # HTTP 客户端
│   └── MCPServer/
│       ├── MCPServer.swift         # MCP 协议实现
│       ├── MCPTypes.swift          # MCP 类型定义
│       └── JSONRPCTypes.swift      # JSON-RPC 类型

2. 手写 MCP 协议

MCP 协议基于 JSON-RPC 2.0,需要实现请求/响应的解析和序列化:

swift 复制代码
struct JSONRPCRequest: Codable {
    let jsonrpc: String
    let id: RequestId?
    let method: String
    let params: AnyCodable?
}

struct JSONRPCResponse: Codable {
    let jsonrpc: String
    let id: RequestId?
    let result: AnyCodable?
    let error: JSONRPCError?
}

3. HTTP Server (LKMCPServer)

在 Lookin 主应用中内嵌一个轻量级 HTTP Server,监听 127.0.0.1:47199:

objc 复制代码
@implementation LKMCPServer

- (void)start {
    self.server = [[GCDWebServer alloc] init];
    
    // GET /status - 检查连接状态
    [self.server addHandlerForMethod:@"GET" path:@"/status" 
        requestClass:[GCDWebServerRequest class]
        processBlock:^GCDWebServerResponse *(GCDWebServerRequest *request) {
            return [self handleStatusRequest];
        }];
    
    // GET /hierarchy - 获取视图层级
    [self.server addHandlerForMethod:@"GET" path:@"/hierarchy" ...];
    
    // POST /reload - 刷新数据
    [self.server addHandlerForMethod:@"POST" path:@"/reload" ...];
    
    [self.server startWithPort:47199 bonjourName:nil];
}

@end

4. MCP Tools 定义

第一版实现了 8 个 Tools:

Tool 描述 HTTP 映射
status 检查连接状态 GET /status
get_hierarchy 获取视图层级 GET /hierarchy
get_view 获取视图详情 GET /view/:oid
search 搜索视图 GET /search?q=&type=
get_screenshot 获取视图截图 GET /screenshot/:oid
get_app_info 获取应用信息 GET /app-info
list_view_controllers 列出 VC GET /viewcontrollers
reload 刷新层级数据 POST /reload

5. 构建和部署

需要在 Xcode Build Phase 中添加脚本,将 lookin-mcp 复制到 app bundle:

bash 复制代码
# Scripts/build_mcp.sh
cd "${SRCROOT}/LookinMCP"
swift build -c release

mkdir -p "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/Contents/Helpers"
cp ".build/release/lookin-mcp" "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/Contents/Helpers/"

通信流程

启动流程:

sequenceDiagram participant User as 用户 participant Lookin as Lookin.app participant Pref as LKPreferenceManager participant HTTP as LKMCPServer User->>Lookin: 启动应用 Lookin->>Pref: 读取 enableMCPServer alt MCP Server 已启用 Pref-->>Lookin: YES Lookin->>HTTP: start() HTTP-->>Lookin: 监听 127.0.0.1:47199 else MCP Server 已禁用 Pref-->>Lookin: NO Note over HTTP: 不启动 end

查询流程:

sequenceDiagram participant AI as AI 工具 participant MCP as lookin-mcp participant HTTP as LKMCPServer participant DS as DataSource AI->>MCP: {"method": "tools/call", "params": {"name": "get_hierarchy"}} MCP->>HTTP: GET http://127.0.0.1:47199/hierarchy HTTP->>DS: flatItems DS-->>HTTP: [LookinDisplayItem] HTTP-->>MCP: {"views": [...], "total": 150} MCP-->>AI: {"content": [{"type": "text", "text": "..."}]}

使用方式

以OpenCode为例,修改 ~/.config/opencode/opencode.json

json 复制代码
{
  "mcp": {
    "lookin": {
        "type": "local",
        "command": "/Applications/Lookin.app/Contents/Helpers/lookin-mcp"
    }
  }
}

运行试了一下,可以拿到对应节点的视图信息。

第一版的问题

虽然能用,但有几个不太满意的地方:

  1. 双进程架构复杂 - 需要维护两套代码,调试也麻烦
  2. 手写协议不可靠 - MCP 协议还在演进,手写实现容易出 bug
  3. 部署麻烦 - 需要在 Build Phase 中复制二进制文件
  4. 配置繁琐 - 用户需要手动修改 JSON 配置文件

第二版

MCP 是有 Swift 版本官方 SDK 的:

github.com/modelcontex...

既然有官方 SDK,为什么还要自己手写协议呢?而且第一版的双进程架构也有点复杂。于是我决定重构,目标是:

  1. 使用官方 Swift SDK 替换手写的 MCP 协议实现
  2. 将 MCP Server 内嵌到 Lookin 主应用,去掉独立进程
  3. 使用 HTTP Transport,简化用户配置

新架构

graph TB subgraph "AI 工具" AI[OpenCode / Claude / Cursor ...] end subgraph "Lookin.app" HTTP[NIO HTTP Server
:47199/mcp] MCP[MCP Server
官方 Swift SDK] DS[LKStaticHierarchyDataSource] Apps[LKAppsManager] end subgraph "iOS App" LookinServer[LookinServer SDK] end AI <-->|HTTP
MCP Protocol| HTTP HTTP --> MCP MCP --> DS MCP --> Apps Apps <-->|USB/WiFi| LookinServer

对比一下两个版本:

对比项 第一版 第二版
协议实现 手写 JSON-RPC 官方 Swift SDK
进程模型 双进程 (stdio + HTTP) 单进程 (内嵌 HTTP)
通信方式 stdio → HTTP → 数据源 HTTP → 数据源
配置方式 修改配置文件 一行命令
依赖管理 复制二进制到 app bundle SPM 本地 Package

SDK 选型

官方 SDK 版本 0.11.0 支持多种 Transport:

  • StdioServerTransport: 传统的 stdio 方式
  • StreamableHTTPServerTransport: 有状态的 HTTP 流式传输
  • StatelessHTTPServerTransport: 无状态 HTTP,适合简单场景

我选择了 StatelessHTTPServerTransport,因为 Lookin 的场景不需要维护会话状态,每次请求都是独立的查询。

实现细节

1. LookinMCP Package 结构

重构后的 Package 变得更简洁:

bash 复制代码
LookinMCP/
├── Package.swift                    # SPM 配置,依赖官方 SDK
└── Sources/LookinMCP/
    ├── LookinMCPDataSource.swift    # 数据源协议 + 模型定义
    ├── LookinMCPServer.swift        # HTTP Server + MCP Server
    └── LookinMCPToolHandler.swift   # 9 个 Tools 的注册和处理

Package.swift 配置:

swift 复制代码
// swift-tools-version: 6.1
import PackageDescription

let package = Package(
    name: "LookinMCP",
    platforms: [.macOS(.v13)],
    products: [
        .library(name: "LookinMCP", targets: ["LookinMCP"]),
    ],
    dependencies: [
        .package(url: "https://github.com/modelcontextprotocol/swift-sdk.git", from: "0.11.0"),
    ],
    targets: [
        .target(
            name: "LookinMCP",
            dependencies: [
                .product(name: "MCP", package: "swift-sdk"),
            ]
        ),
    ]
)

2. HTTP Server 实现

SDK 提供了 StatelessHTTPServerTransport,但需要自己搭建 HTTP Server。参考 SDK 示例,使用 swift-nio:

swift 复制代码
public func start() async throws {
    let bootstrap = ServerBootstrap(group: eventLoopGroup)
        .serverChannelOption(.backlog, value: 256)
        .childChannelInitializer { channel in
            channel.pipeline.configureHTTPServerPipeline().flatMap {
                channel.pipeline.addHandler(HTTPHandler(transport: self.transport))
            }
        }
    
    let channel = try await bootstrap.bind(host: host, port: port).get()
    // Server started on http://127.0.0.1:47199/mcp
}

3. Tool 注册

使用 SDK 的 withMethodHandler API 注册 Tools:

swift 复制代码
await server.withMethodHandler(ListTools.self) { _ in
    ListToolsResult(tools: [
        Tool(name: "status", description: "Check Lookin connection status"),
        Tool(name: "get_hierarchy", description: "Get view hierarchy", inputSchema: ...),
        // ... 更多 tools
    ])
}

await server.withMethodHandler(CallTool.self) { params in
    switch params.name {
    case "status":
        let status = await dataSource.getStatus()
        return CallToolResult(content: [.text(status.toJSON())])
    case "get_hierarchy":
        // ...
    }
}

4. 主应用集成

AppDelegate.m 中启动 MCP Server:

objc 复制代码
if ([LKPreferenceManager mainManager].enableMCPServer) {
    [[MCPServerManager shared] start];
}

MCPServerManager 是一个 Swift 类,提供 @objc 接口供 Obj-C 调用:

swift 复制代码
@objc(MCPServerManager)
@MainActor
final class MCPServerManager: NSObject {
    @objc static let shared = MCPServerManager()
    
    @objc func start() {
        Task {
            let server = try await LookinMCPServer(dataSource: dataProvider, port: 47199)
            try await server.start()
        }
    }
}

使用方式

对于 OpenCode:

json 复制代码
// .config/opencode/opencode.json
{
    // ...
  "mcp": {
    "lookin": {
      "type": "remote",
      "url": "http://127.0.0.1:47199/mcp",
      "enabled": true
    }
  }
}

对于 Claude Code:

bash 复制代码
claude mcp add --transport http lookin http://127.0.0.1:47199/mcp

然后就可以直接使用了:

现在可以愉快地让 AI 帮我看布局、找问题了!

代码放在 fork 的仓库里:github.com/FeliksLv01/...

相关推荐
tangweiguo030519871 天前
SwiftUI布局完全指南:从入门到精通
ios·swift
T1an-11 天前
最右IOS岗一面
ios
坏小虎1 天前
Expo 快速创建 Android/iOS 应用开发指南
android·ios·rn·expo
光影少年1 天前
Android和iOS原生开发的基础知识对RN开发的重要性,RN打包发布时原生端需要做哪些配置?
android·前端·react native·react.js·ios
北京自在科技1 天前
Find My 修复定位 BUG,AirTag 安全再升级
ios·findmy·airtag
Digitally1 天前
如何不用 USB 线将 iPhone 照片传到电脑?
ios·电脑·iphone
Sim14802 天前
iPhone将内置本地大模型,手机端AI实现0 token成本时代来临?
人工智能·ios·智能手机·iphone
Digitally2 天前
如何将 iPad 上的照片传输到 U 盘(4 种解决方案)
ios·ipad
报错小能手2 天前
ios开发方向——swift并发进阶核心 @MainActor 与 DispatchQueue.main 解析
开发语言·ios·swift
LcGero2 天前
Cocos Creator 业务与原生通信详解
android·ios·cocos creator·游戏开发·jsb