摘要:MCP 火了大半年,但大多数人做的都是服务端工具。Google 在 I/O 2026 放出的 AppFunctions(Android MCP)把这条路搬到了设备上——你的 App 可以像 MCP server 一样向 Gemini 等智能体暴露函数,而且全部本地执行、不走网络。本文用官方 API 从零实现一个笔记 App 的 AppFunction,讲清楚它和标准 MCP 的架构差异、测试方法,以及我踩过的 6 个坑。

1. 背景与痛点

如果你跟我一样,这半年被 MCP 折腾得不轻,应该能理解这种感觉:服务端 MCP server 确实强,但每次调工具都要走一遍网络往返,而且它完全够不到你手机里的数据。

我做过一个 MCP server 去查日历,部署在云上,手机端 agent 调它要经过「手机 → 云 → 日历 API → 云 → 手机」一整条链路。延迟能接受,但有个本质问题:它碰不到本机应用的状态。你的笔记、相册、任务清单,全躺在 App 的沙盒里,服务端 MCP 只能干瞪眼。

Google 在 2026 年 5 月的 I/O 大会上给出了自己的答案:AppFunctions,官方别名就叫 Android MCP。核心思路一句话:让 Android App 像一个「设备端 MCP server」,把自己暴露成一个个可被智能体发现和调用的函数。

这篇文章写给三类人:

  • 已经玩过 MCP、想搞清楚「设备端」和「服务端」到底差在哪的开发者
  • Android 开发者,想提前给 App 接上 agent 生态(Gemini 的私测预览 2026 年 5 月已经开放给可信测试者了)
  • 好奇下一代人机交互长什么样的技术爱好者

读完你会得到:AppFunctions 的完整架构拆解、一份可跑通的笔记 App 示例代码、adb 实测命令,以及 6 个真实的坑。

先给结论:这玩意儿现在还很不成熟,但方向是对的。如果你正在做 Android App,现在花一个周末摸一遍,等生态成熟的时候你就已经在前排了。

2. 技术原理:AppFunctions 到底是什么

2.1 一句话定位

AppFunctions 是 Android 16(API 36)的平台 API + 一套 Jetpack 库androidx.appfunctions)。它让你的 App 像 MCP server 一样向外提供「函数」,调用方(agent App、Gemini 这类助理)可以发现并执行这些函数。

关键机制:

  • 提供方:给函数方法加 @AppFunction 注解,注解处理器自动生成 schema 元数据,注册进系统
  • 调用方:持有 EXECUTE_APP_FUNCTIONS 权限(注意,这是个 protected 权限,后面踩坑细说),用 AppFunctionManager 发现和执行
  • 传输:基于 AIDL 的 IPC,进程隔离,不是随便哪个 App 都能调你的函数
flowchart LR
    subgraph 设备端
        A[用户自然语言指令] --> B[Gemini / Agent App]
        B --> C[AppFunctionManager]
        C --> D[系统 AppFunctions 注册表]
        D --> E[你的 App<br/>@AppFunction 函数]
        E --> F[本地数据/服务]
    end
    G[云上 MCP Server] -.网络往返.-> H[远程工具]
    B -.可选.-> G

2.2 和标准 MCP 的对比

这是很多人第一眼会问的问题:都是给 agent 提供工具,AppFunctions 和 MCP 有什么区别?

对比维度 标准 MCP Server Android AppFunctions
运行位置 云端/服务器 设备本地
网络依赖 每次调用都要网络往返 无,本地 IPC
平台绑定 平台无关 Android 16+ 专属
数据可达性 只能访问服务端数据 直接访问 App 本地状态
安全模型 服务端鉴权 AIDL IPC + EXECUTE_APP_FUNCTIONS 权限
开发者工作量 搭服务、部署、维护 加注解 + 一个 Service 入口类
生态状态 成熟,工具海量 实验性,私测预览阶段

官方 FAQ 里原话是:「AppFunctions 是 Android 独有的内置操作系统级钩子,可在本地执行;标准 MCP 服务器依赖云执行和网络往返。」说人话就是:MCP 解决的是「agent 怎么连服务器」,AppFunctions 解决的是「agent 怎么用你手机里的 App」

还有个有趣的类比:Chrome 有个 WebMCP(EPP)项目,思路也是把浏览器能力暴露给 agent。Google 这边是铁了心要把「设备即工具」这条路铺开。

目前最典型的落地案例是三星:Galaxy S26 系列上,Gemini 已经能通过 AppFunctions 调起 Samsung Gallery。用户直接说「帮我找我家猫的照片」,Gemini 识别意图、触发对应的查询函数、把相册里的照片直接呈现在对话里,全程不用离开 Gemini 应用,还能把结果接着发消息给朋友。这个场景官方演示用的是语音输入,多模态链路已经跑通了。日历、笔记、任务这三类应用的能力,Gemini 在多家厂商的设备上也已经能用 AppFunctions 自动化了。

也就是说,这条路不是 Google 画饼:头部厂商已经在真机上演过了。剩下的问题是生态什么时候铺开——而铺开之前,就是开发者提前卡位的窗口。

2.3 版本和依赖

以我写这篇文章时的最新稳定 alpha 版本为准:

  • 平台要求:Android 16(API 36)或更高
  • Jetpack 库:androidx.appfunctions:appfunctions:1.0.0-alpha10
  • 编译器:androidx.appfunctions:appfunctions-compiler:1.0.0-alpha10(走 KSP)
  • 可选:androidx.appfunctions:appfunctions-service(封装 Service 入口的脚手架)

注意:这是 alpha 版本,API 一直在变。官方甚至专门出了个 migration skill 帮你从 alpha09 迁到 alpha10 的 @AppFunctionServiceEntryPoint 架构——后面踩坑部分细说。

3. 环境准备

写之前先确认三件事:

# 1. 确认 Android Studio 版本(需要支持 API 36 的 SDK)
# 2. 确认设备的系统版本
adb shell getprop ro.build.version.sdk
# 应该输出 36 或更高,低于 36 直接放弃

# 3. 确认 AppFunctions 系统服务存在
adb shell cmd app_function help
# 能打出帮助页就说明系统服务在;报 "Can't find service: app_function" 就是设备太老或 ROM 不支持

我用的环境:Android Studio Otter(2026.1)+ Kotlin 2.1 + KSP 2.1 + Gradle 8.14,测试设备是 Pixel 9 Pro(Android 16 QPR2,API 36)。

然后给模块的 build.gradle.kts 加依赖:

dependencies {
    implementation("androidx.appfunctions:appfunctions:1.0.0-alpha10")
    // 项目里有 Kotlin 源码就必须加 KSP 编译器
    ksp("androidx.appfunctions:appfunctions-compiler:1.0.0-alpha10")
}

如果编译器报「找不到 KSP 插件」,先确认根项目 build.gradle.kts 里挂了 com.google.devtools.ksp 插件,版本和 Kotlin 对齐。

4. 实战实现:给笔记 App 加一个 AppFunction

我拿一个简化版笔记 App 演示。目标:暴露两个函数给智能体——listNotes(列出笔记)和 createNote(新建笔记)。

4.1 定义序列化参数和返回值

AppFunctions 的参数和返回值必须是可序列化的 data class,用 @AppFunctionSerializable 标注:

@AppFunctionSerializable(isDescribedByKDoc = true)
data class Note(
    /** 笔记的唯一 ID */
    val id: String,
    /** 笔记标题 */
    val title: String,
    /** 笔记正文 */
    val content: String,
    /** 创建时间戳(毫秒) */
    val createdAt: Long,
)

@AppFunctionSerializable(isDescribedByKDoc = true)
data class CreateNoteParams(
    /** 新笔记的标题 */
    val title: String,
    /** 新笔记的正文 */
    val content: String,
)

4.2 实现函数逻辑

核心逻辑类,方法加 @AppFunction 注解:

class NotesAppFunctions(
    private val repository: NotesRepository,
) {
    /**
     * 列出当前用户的所有笔记。
     *
     * @param appFunctionContext 由 AppFunctions 运行时提供的执行上下文
     * @return 笔记列表,按创建时间倒序
     */
    @AppFunction(isDescribedByKDoc = true)
    suspend fun listNotes(
        appFunctionContext: AppFunctionContext,
    ): List<Note> = repository.getAllNotes()
        .sortedByDescending { it.createdAt }
        .map { Note(it.id, it.title, it.content, it.createdAt) }

    /**
     * 创建一条新笔记。
     *
     * @param appFunctionContext 由 AppFunctions 运行时提供的执行上下文
     * @param params 包含标题和正文
     * @return 创建成功后的完整笔记对象
     */
    @AppFunction(isDescribedByKDoc = true)
    suspend fun createNote(
        appFunctionContext: AppFunctionContext,
        params: CreateNoteParams,
    ): Note {
        val note = repository.create(
            id = UUID.randomUUID().toString(),
            title = params.title,
            content = params.content,
        )
        return Note(note.id, note.title, note.content, note.createdAt)
    }
}

isDescribedByKDoc = true 这个参数千万别省。编译器会把 KDoc 折进生成的 schema 元数据里,agent 靠这个理解参数语义。我一开始没写 KDoc,结果 agent 把 title 当成「要搜索的关键词」用了,折腾半天才发现是描述缺失。

4.3 Service 入口类

AppFunctions 是无前台执行的——App 不在前台,系统也要能找到并实例化你的函数。所以需要一个静态入口 Service,用 @AppFunctionServiceEntryPoint 标注:

@AppFunctionServiceEntryPoint(
    serviceName = "NotesAppFunctionService",
    appFunctionXmlFileName = "notes_app_function_service",
)
abstract class NotesAppFunctionService : AppFunctionService() {

    override fun createAppFunctionInstance(
        appFunctionContext: AppFunctionContext,
    ): AppFunction {
        val repository = (applicationContext as NotesApplication).repository
        return NotesAppFunctions(repository)
    }
}

appFunctionXmlFileName 对应 res/xml/notes_app_function_service.xml,声明这个 Service 暴露哪些函数:

<?xml version="1.0" encoding="utf-8"?>
<app-function-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:name=".NotesAppFunctionService"
    android:exported="true">
    <app-function android:name="listNotes" />
    <app-function android:name="createNote" />
</app-function-service>

最后别忘了在 AndroidManifest.xml 注册 Service:

<service
    android:name=".NotesAppFunctionService"
    android:exported="true"
    android:permission="android.permission.EXECUTE_APP_FUNCTIONS">
    <intent-filter>
        <action android:name="android.app.appfunctions.action.EXECUTE_APP_FUNCTIONS" />
    </intent-filter>
</service>

4.4 调用方:Agent App 怎么调

如果你写的是调用方(agent 端),核心 API 是 AppFunctionManager

class NotesAgent(
    private val appFunctionManager: AppFunctionManager,
) {
    suspend fun listNotesFromNotesApp(): List<Note> {
        // 先确认目标 App 的某个函数是否可用
        val enabled = appFunctionManager.isAppFunctionEnabled(
            packageName = "com.example.notes",
            functionId = NotesAppFunctionsIds.listNotes, // 编译器生成的 Ids 类
        )
        if (!enabled) return emptyList()

        val request = ExecuteAppFunctionRequest.Builder(
            appFunctionId = NotesAppFunctionsIds.listNotes,
            parameters = AppFunctionSerializableParams.Builder()
                .setSerializable(EmptyParams())
                .build(),
        ).build()

        val response = appFunctionManager.executeAppFunction(request)
        return response.result.getSerializable(Note::class.java)
    }
}

注意 NotesAppFunctionsIds 这个类:每个带 @AppFunction 的类,编译器都会生成一个以 Ids 结尾的类,里面是每个函数的常量 ID。别自己拼字符串,直接用它。

4.5 动态启停函数(重要)

不是所有函数都适合一直开着。比如「需要登录才能用的函数」「需要特定账号状态的函数」,如果默认就暴露给智能体,agent 可能在你没登录时就尝试调用,返回一堆错误。

官方推荐的两步流程:

第 1 步:默认停用。@AppFunction 注解的 isEnabled 参数设为 false

@AppFunction(isEnabled = false, isDescribedByKDoc = true)
suspend fun syncNotes(
    appFunctionContext: AppFunctionContext,
): SyncResult { ... }

第 2 步:运行时动态启用。 用编译器生成的 Ids 常量和 AppFunctionManagerCompat 在合适的时机打开:

class NotesAppFunctionsController(
    private val appFunctionManager: AppFunctionManagerCompat,
) {
    fun onUserLoggedIn() {
        appFunctionManager.setAppFunctionEnabled(
            appFunctionId = NotesAppFunctionsIds.syncNotes,
            enabled = true,
        )
    }

    fun onUserLoggedOut() {
        appFunctionManager.setAppFunctionEnabled(
            appFunctionId = NotesAppFunctionsIds.syncNotes,
            enabled = false,
        )
    }
}

这样智能体在任何时刻看到的函数集合,都是「用户当前真正能用」的集合。这比把所有函数一股脑暴露出去安全得多——agent 会尝试调用它能发现的一切,别给它发现用不了的东西

5. 效果验证

装到设备上之后,用 adb 验证注册和执行。这一步比写代码还重要——能跑通 adb,就说明你的函数已经被系统索引了

# 1. 列出设备上所有已注册的 AppFunctions,过滤你自己的包名
adb shell cmd app_function list-app-functions | grep -A 10 com.example.notes

# 2. 直接命令行执行 listNotes
adb shell cmd app_function execute-app-function \
  --package com.example.notes \
  --function 'com.example.notes.NotesAppFunctionService#listNotes' \
  --parameters '{}'

# 3. 执行 createNote,传入参数
adb shell cmd app_function execute-app-function \
  --package com.example.notes \
  --function 'com.example.notes.NotesAppFunctionService#createNote' \
  --parameters '{"createNoteParams": {"title": "买牛奶", "content": "记得买脱脂的"}}'

我实测 listNotes 的输出(节选):

AppFunctionId: com.example.notes.NotesAppFunctions#listNotes
  Display name: listNotes
  Description: 列出当前用户的所有笔记。
  ...
  Result: OK

函数执行返回 OK,说明整条链路通了。如果想跑端到端,Google 还提供了一个官方 AppFunctions testing agent 示例 App(GitHub 上 android/appfunctions 仓库),装到设备上就能用自然语言指挥 agent 调你的函数。

测试时还有两个更细的命令值得记下来:

# 按包名过滤,看某个 App 暴露了哪些函数
adb shell cmd app_function list-app-functions --package com.example.notes

# 调试阶段想临时停用某个函数,验证动态启停逻辑
adb shell cmd app_function set-app-function-enabled \
  --package com.example.notes \
  --function 'com.example.notes.NotesAppFunctions#syncNotes' \
  --enabled false

另外,如果你习惯在 JVM 层跑单元测试,Jetpack 提供了 AppFunctionTestRule,配合 Robolectric 环境可以脱离真机验证 AppFunctionManager@AppFunction 逻辑。不过 Google 官方建议优先做真机系统级验证——adb 这套命令就是为此准备的,比模拟环境靠谱。

6. 踩坑记录

六个坑,按杀伤力排序。

坑 1:EXECUTE_APP_FUNCTIONS 是 protected 权限,普通 App 拿不到

这是最大的坑。文档写「调用方必须有 EXECUTE_APP_FUNCTIONS 权限」,但这个权限的 protection level 是 privileged,默认只授予系统应用。在 Android 16 r4 及更早的 ROM 上,你写个普通 agent App 去调 AppFunctionManager,会直接 SecurityException

解法:测试阶段用官方 testing agent(它通过特殊渠道拿到权限),或者自己刷带权限的 system image。生产环境想当调用方,得等 Google 放开,别跟系统权限死磕。

坑 2:execute-app-function 强制要求 --parameters

我第一次执行 listNotes 没带 --parameters,直接报参数缺失。这个函数明明一个参数都没有。试了半天发现必须传 {} 占位。社区里也有人遇到同样的问题。记住:没参数也要传 --parameters '{}'

坑 3:alpha09 → alpha10 换了 Service 架构

alpha09 时代的写法(AppFunctionService 基类 + 手动处理)在 alpha10 里被 @AppFunctionServiceEntryPoint 注解架构取代了。如果你照着老教程写,编译能过但运行期找不到入口。官方出了 migration 文档,迁移要点就是本文 4.3 节那套:注解 + 抽象 Service 类 + createAppFunctionInstance 工厂方法。

坑 4:isDescribedByKDoc = true 但没写 KDoc,等于没开

这个我前面提过。编译器只认 KDoc 里的 @param 描述,你光把参数开成 true 但方法上没写文档块,schema 里就是空的。agent 拿到空描述只能瞎猜参数语义,表现就是「答非所问」。要么写完整 KDoc,要么别开这个开关

坑 5:Schema 解析失败,查 aflags

如果你 list-app-functions 能看到函数但执行时 schema 解析报错,检查设备上的 feature flag:

adb shell aflags list | grep enable_app_functions_schema_parser

部分 ROM 上这个 flag 默认没开,导致系统解析不了你打包进 APK 的 app_functions.xml / app_functions_v2.xml。验证元数据是否真的打包进去了:

unzip -l app/build/outputs/apk/debug/app-debug.apk | grep app_functions

坑 6:Gemini 集成是私测预览,你的 App 不一定能被系统 agent 访问

这是最容易被忽略的「预期管理」坑。AppFunctions API 本身现在就能用、能测,但「Gemini 真的来调你的函数」目前只对可信测试者开放。官方 FAQ 原话:为了评估质量,只有有限数量的应用和系统级智能体可以访问整个流水线。所以别期待装上 App 明天 Gemini 就能用你的笔记——先把 adb 链路跑通,申请 EAP 等通知。

7. 总结与展望

AppFunctions 把 MCP 的思想从云端搬到了设备上,解决了服务端工具「够不到本地数据」的根本问题。当前状态:API 可用、生态未熟——Android 16+ 限定、alpha 版本 API 还在变、Gemini 集成私测中,但方向已经很清楚:未来 agent 操作你的手机,靠的不再是「模拟点击」这种脆弱的 UI 自动化,而是 App 主动暴露的结构化函数。

几个值得关注的时间点:

  • Android 17 会把这个能力铺到更多设备(官方博客已预告)
  • Samsung Gallery × Gemini 已经在 Galaxy S26 上跑通了「帮我找猫的照片」这种多模态案例
  • 如果你在做 Android App,现在动手的窗口期正好——等生态成熟了你已经踩完坑了

我的建议是三步走:先按本文把 adb 链路跑通,再申请 EAP 抢占端到端体验,最后盯着 androidx.appfunctions 的 release note 随时跟版本。

最后留个开放问题:你觉得「设备端 MCP」和「云端 MCP」是互补关系还是替代关系?你的 App 会优先暴露哪些函数给 agent?评论区聊聊你的想法——我很好奇大家的第一反应是兴奋还是焦虑。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐