尝试给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. 手写 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 |
列出 VC | GET /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"
}
}
}
运行试了一下,可以拿到对应节点的视图信息。
![]()
第一版的问题
虽然能用,但有几个不太满意的地方:
- 双进程架构复杂 - 需要维护两套代码,调试也麻烦
- 手写协议不可靠 - MCP 协议还在演进,手写实现容易出 bug
- 部署麻烦 - 需要在 Build Phase 中复制二进制文件
- 配置繁琐 - 用户需要手动修改 JSON 配置文件
第二版
MCP 是有 Swift 版本官方 SDK 的:
既然有官方 SDK,为什么还要自己手写协议呢?而且第一版的双进程架构也有点复杂。于是我决定重构,目标是:
- 使用官方 Swift SDK 替换手写的 MCP 协议实现
- 将 MCP Server 内嵌到 Lookin 主应用,去掉独立进程
- 使用 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 bundle | SPM 本地 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
然后就可以直接使用了:
![]()
现在可以愉快地让 AI 帮我看布局、找问题了!
代码放在 fork 的仓库里:github.com/FeliksLv01/…