尝试给Lookin 支持 MCP

89 阅读6分钟

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

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

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

path-image-1e9c6001a4ab420da34cb981b0080ae1.png

image.png

image.png

Lookin MCP 支持的所有方法

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

实现细节

1. 手写 MCP 协议

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

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?
}

2. HTTP Server (LKMCPServer)

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

@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

3. 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列出 VCGET /viewcontrollers
reload刷新层级数据POST /reload

4. 构建和部署

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

# 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

{
  "mcp": {
    "lookin": {
        "type": "local",
        "command": "/Applications/Lookin.app/Contents/Helpers/lookin-mcp"
    }
  }
}

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

1040g3g831t60a6umla06gl60tgb1aifea95vsf8.jpg

第一版的问题

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

  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<br/>:47199/mcp]
        MCP[MCP Server<br/>官方 Swift SDK]
        DS[LKStaticHierarchyDataSource]
        Apps[LKAppsManager]
    end
    
    subgraph "iOS App"
        LookinServer[LookinServer SDK]
    end
    
    AI <-->|HTTP<br/>MCP Protocol| HTTP
    HTTP --> MCP
    MCP --> DS
    MCP --> Apps
    Apps <-->|USB/WiFi| LookinServer

对比一下两个版本:

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

SDK 选型

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

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

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

实现细节

1. LookinMCP Package 结构

重构后的 Package 变得更简洁:

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

Package.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:

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:

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:

if ([LKPreferenceManager mainManager].enableMCPServer) {
    [[MCPServerManager shared] start];
}

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

@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:

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

对于 Claude Code:

claude mcp add --transport http lookin http://127.0.0.1:47199/mcp

然后就可以直接使用了:

image.png

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

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