)
1. 为什么要把 Android 手机变成 MCP Server先说清楚 MCP 是什么。MCPModel Context Protocol是一套让 AI 客户端调用外部工具的协议你可以把它理解成「AI 世界的 USB 接口」只要你的服务端按协议暴露工具清单任何支持 MCP 的客户端Cursor、Claude Desktop、Cline 等都能直接调用不用为每个客户端单独写适配。而 Android 手机恰好是一台塞满了摄像头、GPS、传感器、短信、通讯录的边缘设备把它做成 MCP Server等于给 AI 装上了一套「移动感官」。这个场景适合谁三类人值得动手一是 Android 开发者想把自己的 App 能力开放给 AI 工作流二是折腾 AI Agent 的玩家希望电脑上的 AI 能直接读手机验证码、拍桌面照片、取当前坐标三是做智能硬件的同学想验证「端侧能力 云端模型」的混合架构。我试过把这套跑通之后最直观的感受是以前要「拿起手机→截图→传到电脑→粘贴给 AI」的四步操作现在一句话就完成了。但 Android 上跑 MCP 有几个绕不开的坎。官方 SDK 目前主要覆盖 TypeScript 和 Python默认走 stdio标准输入输出通信而 Android 应用跑在 ART 虚拟机上没有简单的 stdio 管道直连电脑。所以我们的方案是用 HTTP SSE 作为传输层手机当服务端电脑端 AI 当客户端通过局域网或 ADB 端口转发建立连接。同时所有对模型的请求统一走 TaoToken 的 API 通道Key 和 Base URL 只配一次省得在多个客户端之间来回改配置。这一篇的目标很明确交付可复制的 Kotlin 工具注册代码、MCP 清单配置以及一次端到端的调用验证。读完你应该能让 AI 稳定调用设备能力而不是停在「理论上可行」。2. TaoToken 前置准备统一 Key 与 API 通道在写 Kotlin 之前先把「AI 侧」的通道打通。为什么这一步不能省因为你的 Android MCP Server 只是「工具提供方」真正发起调用的是电脑端的 AI 客户端。如果每个客户端都单独配 Key、单独记 Base URL调试阶段会非常痛苦。TaoToken 在这里扮演的是统一入口一个 Key、一个 API 地址模型对话、编码 Agent、工具调用都走同一条通道。你需要准备的东西只有三样一个可用的 API Key、Base URL、以及你要用的 Model ID。这三件套在后面的 MCP 配置里会反复出现建议先记下来。第一步打开官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册并登录。登录后进入控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 。在控制台里你能看到账户余额、用量统计和 Key 管理入口。第二步创建 API Key。进入 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 点新建复制生成的 Key。注意这个 Key 只在创建时完整显示一次务必先存到安全的地方。Key 的格式通常是一串以特定前缀开头的长字符串别把它硬编码进 Android 工程里提交到 Git。第三步确认 Base URL。API 的基础地址是 https://taotoken.net/api 注意这个地址不带任何查询参数配置时原样填入即可。很多客户端要求 Base URL 以/v1结尾具体看客户端文档但根地址就是上面这个。第四步选 Model ID。在模型对话页面 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 可以先试跑一下确认你要用的模型能正常返回。把模型名记下来比如常见的编码类、通用对话类模型后面写进配置。如果你打算长期跑编码 Agent 或让 AI 频繁调用工具建议看一下 Coding Plan 页面 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 按用量选套餐比按次付费更划算。接入细节和参数说明在文档里 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 遇到字段不确定时优先查文档。这里有个容易踩的坑不要把 TaoToken 理解成「中转」或「代理」它就是一个标准的 API 服务入口你按官方文档填 Base URL 和 Key 即可。另外Android 端和电脑端要连同一个局域网否则 SSE 长连接建不起来。调试阶段最稳的方式是用 ADB 端口转发把手机的 8080 端口映射到电脑本地这样连 IP 都不用查。3. 可复制配置Kotlin MCP Server 与清单文件这一节是全文的核心给你能直接抄的代码和配置。整体结构分三块Gradle 依赖、Kotlin 服务端逻辑、MCP 工具清单。先看依赖。在app/build.gradle.kts里加入 Ktor 和序列化库dependencies { implementation(io.ktor:ktor-server-core:2.3.12) implementation(io.ktor:ktor-server-netty:2.3.12) implementation(io.ktor:ktor-server-sse:2.3.12) implementation(io.ktor:ktor-server-content-negotiation:2.3.12) implementation(io.ktor:ktor-serialization-kotlinx-json:2.3.12) implementation(org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3) }别忘了在plugins块里加上kotlin(plugin.serialization)否则Serializable不生效。接着定义 MCP 的请求/响应数据结构。MCP 基于 JSON-RPC 2.0核心字段是jsonrpc、id、method、paramsSerializable data class McpRequest( val jsonrpc: String 2.0, val id: JsonElement? null, val method: String, val params: JsonObject? null ) Serializable data class McpResponse( val jsonrpc: String 2.0, val id: JsonElement? null, val result: JsonElement? null, val error: McpError? null ) Serializable data class McpError(val code: Int, val message: String)然后是服务端主体。用前台 Service 启动 Ktor避免被系统杀掉class AndroidMcpService : Service() { private val server by lazy { embeddedServer(Netty, port 8080) { install(SSE) install(ContentNegotiation) { json() } routing { get(/mcp/sse) { send(id init, data {mcp_version:2024-11-05}) } post(/mcp/message) { val req call.receiveMcpRequest() call.respond(handleMcpLogic(req)) } } } } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { startForeground(1, buildNotification()) server.start(wait false) return START_STICKY } override fun onBind(intent: Intent?) null }工具注册逻辑集中在handleMcpLogic按 method 分发private fun handleMcpLogic(req: McpRequest): McpResponse { return when (req.method) { tools/list - McpResponse(id req.id, result buildToolList()) tools/call - { val name req.params?.get(name)?.jsonPrimitive?.content val args req.params?.get(arguments) McpResponse(id req.id, result executeTool(name, args)) } else - McpResponse(id req.id, error McpError(-32601, Method not found)) } }工具清单用 JSON 描述这是 AI 客户端「看到」的能力列表private fun buildToolList(): JsonElement buildJsonObject { put(tools, buildJsonArray { addJsonObject { put(name, read_last_sms) put(description, 读取最近一条短信内容用于获取验证码) put(inputSchema, buildJsonObject { put(type, object) put(properties, buildJsonObject {}) }) } addJsonObject { put(name, get_location) put(description, 获取当前 GPS 坐标) put(inputSchema, buildJsonObject { put(type, object) put(properties, buildJsonObject {}) }) } }) }对应的工具实现以读短信为例private fun readLastSms(): String { val cursor contentResolver.query( Telephony.Sms.CONTENT_URI, null, null, null, date DESC ) cursor?.use { if (it.moveToFirst()) { return it.getString(it.getColumnIndexOrThrow(Telephony.Sms.BODY)) } } return 未找到短信 }最后是电脑端 AI 客户端的 MCP 配置。以常见的settings.json或mcp.json为例把三件套填进去{ mcpServers: { android-bridge: { url: http://127.0.0.1:8080/mcp/sse, env: { TAOTOKEN_BASE_URL: https://taotoken.net/api, TAOTOKEN_API_KEY: 你的Key, TAOTOKEN_MODEL: 你的ModelID } } } }如果你用的是 Cline 或 Claude Code 这类支持 MCP 的客户端配置结构大同小异关键是 Base URL、Key、Model ID 三件套要写全。ADB 端口转发命令是adb forward tcp:8080 tcp:8080执行后电脑访问127.0.0.1:8080就等于访问手机。4. 验证请求一次端到端调用配置写完必须验证。分两步先确认 MCP Server 活着再确认 AI 能真正调用工具。第一步验证 SSE 连接。在电脑终端执行curl -N http://127.0.0.1:8080/mcp/sse正常的话你会看到一行data: {mcp_version:2024-11-05}然后连接保持不断。如果卡住没输出说明手机端 Service 没起来或端口没转发成功。第二步验证工具清单。发一个tools/list请求curl -X POST http://127.0.0.1:8080/mcp/message \ -H Content-Type: application/json \ -d {jsonrpc:2.0,id:1,method:tools/list}预期返回里能看到read_last_sms和get_location两个工具名。这一步过了说明协议握手没问题。第三步验证工具调用。发一个tools/callcurl -X POST http://127.0.0.1:8080/mcp/message \ -H Content-Type: application/json \ -d {jsonrpc:2.0,id:2,method:tools/call,params:{name:read_last_sms,arguments:{}}}如果手机里有短信返回的result里就是短信正文。到这一步端到端链路就通了。第四步在 AI 客户端里实测。打开你的 MCP 客户端确认android-bridge已连接然后直接问「帮我读一下手机最近一条短信」。AI 会先调用tools/list拿到工具再调tools/call执行read_last_sms最后把结果组织成自然语言返回。整个过程你能在客户端日志里看到两次 JSON-RPC 往返。实测下来最容易出问题的是权限。READ_SMS是危险权限必须在运行时动态申请而且要在AndroidManifest.xml里声明uses-permission android:nameandroid.permission.READ_SMS/ uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION/ uses-permission android:nameandroid.permission.FOREGROUND_SERVICE/前台服务的通知渠道也要建好否则 Android 12 以上会直接抛异常。验证通过后建议把工具调用日志打到 Logcat方便排查 AI 到底传了什么参数进来。5. 本篇常见错误排查这一节按真实报错来遇到问题直接对号入座。报错一401 Unauthorized。这是 Key 没配对。检查三处MCP 配置里的TAOTOKEN_API_KEY是否完整复制有没有漏字符或带空格Key 是否已过期或被删除Base URL 是否写成了带/v1的变体而客户端不认。解决方式是回到 API Keys 页面重新生成一个粘贴时注意别带换行。报错二local proxy failed或连接被拒绝。这通常不是 TaoToken 的问题而是本地网络。检查 ADB 转发是否还在adb forward --list手机和电脑是否在同一网段防火墙是否拦了 8080。如果是真机调试用adb reverse有时比adb forward更稳。报错三reading choices相关错误。这个报错说明请求发出去了但返回结构里没有choices字段。常见原因是 Model ID 写错或者客户端把非对话接口当对话接口调了。核对 Model ID 是否和模型对话页面里一致Base URL 是否指向https://taotoken.net/api。报错四OAuth 相关报错。部分客户端默认走 OAuth 流程但 MCP 的 HTTP 传输不需要 OAuth。检查客户端配置里是否误开了 OAuth 选项关掉它改用 API Key 直连。报错五工具列表为空。AI 客户端连上了但tools/list返回空数组。检查buildToolList()里的 JSON 结构tools必须是数组每个元素要有name、description、inputSchema三个字段。少一个客户端就可能忽略。报错六Service 被系统杀掉。表现为过一会儿 AI 就调不通了。这是 Android 后台限制。解决方式是前台服务 唤醒锁并在电池优化里把 App 设为「不优化」。别指望普通后台 Service 能长期存活。报错七短信读取返回空。权限申请了但没生效或者查询 URI 不对。确认Telephony.Sms.CONTENT_URI拼写正确且运行时权限回调里确实拿到了PERMISSION_GRANTED。排查顺序建议固定先 curl 测 SSE再 curl 测 tools/list最后才进 AI 客户端。这样能把「服务端问题」和「客户端问题」分开省一半时间。6. 把设备能力交给 AI 的下一步代码跑通之后你会发现真正的难点不在协议而在「边界设计」。把READ_SMS直接暴露给 AI 是很危险的正确做法是在 MCP Server 层加过滤比如只允许读取包含「验证码」字样的短信且每次调用都要在手机上弹窗确认。同理定位可以只返回模糊坐标相册可以只开放最近一张。这些过滤逻辑写在executeTool里AI 客户端完全无感。另一个实用技巧是把工具按风险分级。低风险工具读传感器、取时间直接放行中风险工具读短信、读通讯录加用户确认高风险工具发短信、删文件默认禁用需要手动开启。这样即使 AI 判断失误也不会造成不可逆的后果。如果你想让 AI 长期稳定地调用这些能力建议把 MCP Server 做成常驻前台服务并在电脑端用 Coding Plan 跑一个长期 Agent让它按需调用手机工具。接入文档里有完整的参数说明遇到字段不确定时优先查文档而不是猜。最后留个思考当手机变成 MCP ServerAndroid 开发者的角色其实变了——从「写界面给用户看」变成「写接口给 AI 调」。这个转变里权限沙箱和用户确认机制才是真正的护城河。下一篇会专门聊安全边界但在此之前先把这一篇的端到端链路跑通比什么都实在。