Home/Insights/AppFunctions Tutorial: Building Android MCP Integrations
Engineering

AppFunctions Tutorial: Building Android MCP Integrations

With AppFunctions — Android's Model Context Protocol — your app's capabilities become tools that system AI agents like Gemini can discover and invoke. This hands-on tutorial covers project setup, writing agent-ready functions, service registration, the security wiring that is not optional, and testing from the ADB shell.

N
NetConsulate Engineering Team
📅 8 July 2026⏱ 8 min read

AppFunctions Tutorial: Building Android MCP Integrations

Android's transition into an App Intelligence Platform changes what it means to build a "feature." With AppFunctions — Android's equivalent of the Model Context Protocol (MCP) — your app's capabilities become tools that system AI agents like Gemini can discover and invoke on the user's behalf. When a user says "find my package confirmation and add a reminder to my tracker," the OS orchestrates that across apps — but only across apps that have exposed their capabilities properly.

This tutorial walks through building your first AppFunctions integration end to end: project setup, writing and annotating functions, service registration, security, and testing from the ADB shell — with the practical details that determine whether the system agent actually understands your tools. For the strategic overview of the platform shift, see our companion article: Gemini Nano 4 on Android: What Developers Need to Know.


What You Are Building — The Mental Model

An AppFunctions integration has three parts:

  • Functions — Kotlin suspend functions exposing discrete capabilities (create a task, search orders, send a message), annotated so the compiler generates tool metadata
  • A service — the discoverable endpoint the Android system binds to when an agent wants to invoke your functions
  • Metadata — descriptions compiled from your KDoc comments that tell the system LLM when and how to call each function
Think of each function as a tool listing in a catalogue the OS maintains. The system agent reads your descriptions, matches them against user intent, and calls your function with structured parameters. Your descriptions are effectively prompts — write them for an LLM reader, not just a human one.

Prerequisites and Project Setup

Requirements:

  • API level 36+ (Android 16 preview) as your target
  • Kotlin Symbol Processing (KSP) toolchain — the AppFunctions compiler generates inventory and routing code at build time
Add to your module-level build.gradle.kts:

kotlin
plugins {
    id("com.google.devtools.ksp") version "2.0.0-1.0.22" // match your Kotlin version
}

dependencies {
    implementation("androidx.appfunctions:appfunctions:1.0.0-alpha02")
    implementation("androidx.appfunctions:appfunctions-service:1.0.0-alpha02")
    ksp("androidx.appfunctions:appfunctions-compiler:1.0.0-alpha02")
}

The library is alpha — expect API evolution between releases and pin versions deliberately.


Writing Your First AppFunction

Functions are standard Kotlin suspend functions inside a class, annotated with @AppFunction. Parameters and return types must be primitives or data classes annotated with @AppFunctionSerializable.

kotlin
import androidx.appfunctions.AppFunction
import androidx.appfunctions.AppFunctionContext
import androidx.appfunctions.AppFunctionSerializable

@AppFunctionSerializable
data class TaskResponse(val taskId: String, val status: String)

class ProductivityAppFunctions {

    /**
     * Create a new task or reminder with a title, due time, and location.
     * Use when the user wants to remember to do something, set a reminder,
     * or add an item to their task list.
     */
    @AppFunction(isDescribedByKDoc = true)
    suspend fun createTask(
        context: AppFunctionContext,
        title: String,
        dueDateTime: String? = null,
        location: String? = null
    ): TaskResponse {
        val created = taskRepository.insertTask(title, dueDateTime, location)
        return TaskResponse(taskId = created.id, status = "SUCCESS")
    }
}

The details that matter:

isDescribedByKDoc = true instructs the KSP compiler to compile your KDoc — the function description and parameter documentation — directly into the tool metadata. The system LLM reads exactly this text when deciding whether your function matches the user's intent. This makes KDoc quality a functional requirement, not documentation hygiene. Writing agent-ready descriptions. Apply prompt-engineering discipline:
  • State what the function does and when to use it — including phrasings users might employ
  • Describe each parameter's format explicitly ("ISO-8601 date-time string"), since the agent must construct valid arguments
  • Keep one function to one capability — agents select better among several narrow tools than one broad one
AppFunctionContext is the system-supplied first parameter carrying invocation context. Your remaining parameters define the tool's schema — keep them primitive-typed or simple serialisable data classes; optional parameters with sensible defaults make your tool callable from sparser user requests. Return structured results. Agents chain function calls — the output of createTask may feed the next step in a multi-app workflow. Return meaningful structured data (IDs, statuses), not booleans.

Registering the Service

Capabilities become discoverable through an AppFunctionService implementation. The KSP compiler generates the inventory class that routes invocations to your annotated functions — you wire the service into the manifest:

xml
<service
    android:name=".MyAppFunctionService"
    android:permission="android.permission.BIND_APP_FUNCTION_SERVICE"
    android:exported="true">
    <intent-filter>
        <action android:name="androidx.appfunctions.action.APP_FUNCTION_SERVICE" />
    </intent-filter>
</service>

The security line that is not optional: android:permission="android.permission.BIND_APP_FUNCTION_SERVICE" restricts binding to the core Android operating system. Omitting it exposes your functions to invocation by arbitrary third-party apps — turning your convenient tool surface into an attack surface. Treat this as a mandatory checklist item in code review.

Defence-in-depth still applies inside each function: validate parameters as untrusted input, enforce your app's own authorisation state (a deleteAccount function should verify a signed-in, confirmed user regardless of who is calling), and return errors as structured results rather than exceptions where possible.


Testing from the ADB Shell

You do not need to wait for a full system-agent integration to validate your work. Android 16 platform tools include direct debugging entry points:

bash
# 1. List all registered app functions for your package
adb shell cmd app_function list-app-functions --package com.example.productivity

# 2. Invoke a function directly with a JSON payload
adb shell cmd app_function execute-app-function \
  --package com.example.productivity \
  --function com.example.productivity.ProductivityAppFunctions#createTask \
  --parameters '{"title":"Pick up cargo layout","location":"Warehouse 4"}'

A practical testing sequence:

  • List — confirm your functions registered and inspect the compiled metadata; if a function is missing, the KSP step or manifest wiring is the usual culprit
  • Invoke happy paths — verify parameter deserialisation and results
  • Invoke hostile inputs — missing optionals, malformed dates, oversized strings; your functions should fail gracefully with structured errors
  • Verify side effects — confirm database writes and app state changes behave identically to the same action taken through your UI

Design Guidelines for a Production Tool Surface

Expose verbs, not screens. Audit your app for core user outcomes — search, create, send, book, list — and expose those. Do not mirror your navigation graph; agents compose capabilities, they do not click through flows. Mind the destructive operations. Deletion, payments, and sends deserve explicit confirmation semantics — return a pending state the agent must confirm, or require parameters that make intent unambiguous. Assume the agent occasionally misreads intent, because it will. Version thoughtfully. Once agents rely on your functions, changed parameter schemas break workflows silently. Prefer additive evolution — new optional parameters, new functions — over signature changes. Log invocations. Instrument agent-initiated calls separately from UI-initiated ones. This tells you which capabilities agents actually use, feeds debugging when orchestration misbehaves, and quantifies the channel's business value.

Where This Fits Strategically

AppFunctions is discoverability infrastructure. As assistant-driven workflows grow, apps that expose clean tool surfaces get invoked inside agent workflows; apps that do not become invisible to that interaction layer entirely. The engineering cost of a first integration is modest — a few well-chosen functions, careful descriptions, correct security wiring — and it positions your app for an interaction model that is arriving quickly.

If your team wants help designing an agent-ready capability surface — choosing which functions to expose, writing metadata that agents reliably understand, and integrating with the broader on-device AI stack — NetConsulate builds Android AI integrations end to end, from AppFunctions to Gemini Nano 4 features and hybrid inference architectures.


Ready to make your Android app agent-ready? Submit a proposal request and our team will respond with an integration roadmap within 2 business days.
Related NetConsulate service
📱
AppFunctions (Android MCP) integration

We integrate your Android app with Google's intelligence system using AppFunctions — Android's Model Context Protocol. Your app's tools, services, and data become accessible to AI agents like Gemini, enabling seamless task automation directly within your app.

Get a proposal for this service