Skip to content

Atul206/androidclaudio

Repository files navigation

AndroClaudio

AndroidClaudio

Give Claude/Codex/Cursor Code instant live knowledge of your running Android app — no source files read, no credentials exposed, zero production overhead.

Claude Code connects to an MCP server embedded in your debug APK. It discovers your app's classes and public functions, calls them live on the emulator, and verifies full feature flows using intelligent mock responses. When you're ready, flip individual groups to live mode for real API testing.


How It Works

Your project                    AndroClaudio
────────────────                ──────────────────────────────────────────
groups.json          ──KSP──▶  Generated_*Registry.kt   (compile time)
Application.onCreate ──────▶   MCP server starts on port 5173  (runtime)
adb forward tcp:5173 ──────▶   Claude Code calls /tools/list + /tools/call

Three components, zero production impact:

Component What it does When it runs
androplaudio-setup CLI Scans project, writes groups.json Once per project setup
androplaudio-ksp Reads groups.json, generates type-safe call registries Compile time (KSP)
androplaudio-core AAR Embeds MCP server in debug APK Debug runtime only

debugImplementation means the AAR is completely absent from release builds — no code, no size, no overhead.


Requirements

  • Android minSdk 21+
  • Kotlin 1.9+
  • KSP plugin 1.9.23-1.0.20 or newer
  • Node.js 18+ (setup CLI, one time only)

Quick Start

# 1. Scan your project
npx androplaudio-setup --project-dir . --output androplaudio-groups.json

# 2. Add files, configure Gradle, add one line to Application (see below)

# 3. Build
./gradlew assembleDebug

# 4. Forward port
adb forward tcp:5173 tcp:5173

# 5. Verify
curl http://localhost:5173/tools/list | jq .

Integration — Android App

Step 1 — Copy library files

Download from releases/1.0.0/ and place in your app module:

your-app/
└── app/
    └── libs/
        ├── androplaudio-core-1.0.0.aar   ← runtime MCP server (debug only)
        └── androplaudio-ksp-1.0.0.jar    ← KSP code generator (build time only)

Step 2 — Configure Gradle

app/build.gradle.kts

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("com.google.devtools.ksp") version "1.9.23-1.0.20"   // add if not present
}

ksp {
    // path to the groups.json you generated in the previous step
    arg("androplaudio.groupsJson", "${rootDir}/androplaudio-groups.json")
}

dependencies {
    // KSP processor — runs at build time only, generates call registries
    ksp(files("libs/androplaudio-ksp-1.0.0.jar"))

    // Runtime MCP server — debug builds only, completely absent from release
    debugImplementation(files("libs/androplaudio-core-1.0.0.aar"))
}

If you already have the KSP plugin, just add the two dependency lines and the ksp { } block.


Step 3 — Run the setup CLI

npx androplaudio-setup --project-dir /path/to/your/app --output androplaudio-groups.json

This scans your project and writes androplaudio-groups.json. It detects your DI framework automatically (Koin, Hilt, Dagger, or manual) and finds every registered class.

Example output:

{
  "version": "1",
  "platform": "android",
  "framework": "koin",
  "groups": [
    { "id": "payment.repository", "layer": "shared", "class": "com.myapp.data.PaymentRepository" },
    { "id": "order.use.case",     "layer": "shared", "class": "com.myapp.domain.OrderUseCase" },
    { "id": "cart.manager",       "layer": "shared", "class": "com.myapp.domain.CartManager" }
  ]
}

Re-run whenever you add a new class. The file is plain JSON — you can also edit it manually.


Step 4 — Add one line to your Application class

class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()

        // your existing DI setup — completely unchanged
        startKoin {
            androidContext(this@MyApp)
            modules(appModule)
        }

        // add this one line
        if (BuildConfig.DEBUG) AndroClaudio.initialize(this)
    }
}

That's it. No other changes to your app.

What initialize() does:

  1. Loads GeneratedGroupRegistry (generated by KSP at build time) via reflection
  2. Starts a Ktor CIO HTTP server on port 5173 in a background thread
  3. Wires up Koin auto-resolution so any class in your DI graph is callable

Step 5 — Build and run

./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk

Confirm it started — look for this in logcat:

I/AndroClaudio: MCP server started on port 5173 — 4 groups registered

Step 6 — Forward the port

Run this once per emulator/device session:

adb forward tcp:5173 tcp:5173

Verify:

curl http://localhost:5173/tools/list | jq .

You should see all your registered groups.


DI Framework Support

Koin — automatic (no extra code)

AndroClaudio auto-resolves Koin singletons using GlobalContext.get() reflection. Your initialize(this) call is all you need.

// SampleApp.kt — nothing extra needed
if (BuildConfig.DEBUG) AndroClaudio.initialize(this)

Hilt / Dagger — register instances explicitly

Hilt and Dagger don't expose a global service locator, so register the instances you want Claude Code to call after your DI graph is built:

@HiltAndroidApp
class MyApp : Application() {

    @Inject lateinit var paymentRepository: PaymentRepository
    @Inject lateinit var orderUseCase: OrderUseCase

    override fun onCreate() {
        super.onCreate()
        // Hilt injects fields above before this point

        if (BuildConfig.DEBUG) {
            AndroClaudio.initialize(this)
            AndroClaudio.registerInstance(
                "com.myapp.data.PaymentRepository", paymentRepository
            )
            AndroClaudio.registerInstance(
                "com.myapp.domain.OrderUseCase", orderUseCase
            )
        }
    }
}

Manual DI — same pattern

if (BuildConfig.DEBUG) {
    AndroClaudio.initialize(this)
    AndroClaudio.registerInstance(
        "com.myapp.data.PaymentRepository",
        ServiceLocator.paymentRepository
    )
}

registerInstance() always wins over Koin auto-resolution — use it to override which instance is used for any class.


Reactive UI Updates

When Claude Code calls a function via MCP, it hits the same live instance your UI is using. If that class exposes a StateFlow, the UI updates automatically with no extra code.

Pattern — expose StateFlow from your domain class:

class CartManager {

    private val items = mutableMapOf<Int, Int>()

    // Expose state reactively
    private val _cartFlow = MutableStateFlow<Map<Int, Int>>(emptyMap())
    val cartFlow: StateFlow<Map<Int, Int>> = _cartFlow.asStateFlow()

    fun addItem(productId: Int, quantity: Int = 1) {
        items[productId] = (items[productId] ?: 0) + quantity
        _cartFlow.value = items.toMap()   // ← emit on every change
    }
}

ViewModel collects the flow:

class CartViewModel : ViewModel(), KoinComponent {
    private val cartManager: CartManager by inject()

    private val _cartCount = MutableStateFlow(0)
    val cartCount: StateFlow<Int> = _cartCount

    init {
        viewModelScope.launch {
            cartManager.cartFlow.collect { items ->
                _cartCount.value = items.values.sum()
            }
        }
    }
}

Now when Claude Code calls cart.manager.addItem via MCP:

MCP → CartManager.addItem() → _cartFlow emits → ViewModel collects → screen updates

No ViewModels need to be MCP-callable. The reactive chain handles screen updates automatically.


Mock Mode and Live Mode

Mock mode is the default. Every function call returns an intelligent, structurally correct response — no real API, no credentials needed.

Mock responses are generated from the function name and parameter names:

MCP call Mock response
processPayment(amount: 1000, currency: "INR") { success: true, txnId: "MOCK_TXN_001", amount: 1000, _mock: true }
getBalance(accountId: "ACC_001") { accountId: "ACC_001", balance: 50000.0, _mock: true }
isUserLoggedIn() { result: true, _mock: true }
searchProducts(query: "book") [{ name: "book_result", _mock: true }]

All mock responses include "_mock": true so they are always distinguishable.

Switching groups to live mode

Add androplaudio.config.json to src/debug/assets/:

app/src/debug/assets/androplaudio.config.json
{
  "defaultMode": "mock",
  "groups": {
    "payment.repository": "live",
    "cart.manager": "live",
    "order.use.case": "mock"
  }
}
  • Groups not listed use defaultMode
  • Changing this file takes effect on the next app launch (no rebuild needed)
  • Add to .gitignore if it contains staging credentials

In live mode, AndroClaudio resolves the real DI instance and calls the actual function. The result is the same object your app would return.


MCP API Reference

Once the app is running and port is forwarded, these endpoints are available:

GET /

Health check.

{ "server": "androplaudio", "version": "1.0", "port": 5173 }

GET /tools/list

Lists all registered groups.

[
  { "id": "cart.manager", "layer": "shared", "className": "com.myapp.CartManager", "toolCount": 8 },
  { "id": "payment.repository", "layer": "shared", "className": "com.myapp.PaymentRepository", "toolCount": 6 }
]

GET /tools/list?group=<id>

Lists all public functions in a group with parameter types.

{
  "id": "cart.manager",
  "tools": [
    { "name": "addItem", "params": [{ "name": "productId", "type": "Int" }, { "name": "quantity", "type": "Int" }], "returnType": "Unit" },
    { "name": "getItemCount", "params": [], "returnType": "Int" }
  ]
}

POST /tools/call

Calls a function and returns the result.

Request:

{
  "group": "cart.manager",
  "fn": "addItem",
  "args": { "productId": 1, "quantity": 2 }
}

Response (live):

{ "result": null }

null result is correct for Unit-returning functions — it means the call succeeded.

Response (mock):

{ "result": { "_mock": true }, "mock": true }

Response (error):

{ "error": "Group not found: unknown.group" }

KMM / CMP Project

// shared/build.gradle.kts
kotlin {
    sourceSets {
        androidMain.dependencies {
            debugImplementation(files("libs/androplaudio-core-1.0.0.aar"))
        }
    }
}

dependencies {
    add("kspAndroid", files("libs/androplaudio-ksp-1.0.0.jar"))
}
// androidMain — Application entry point
if (BuildConfig.DEBUG) AndroClaudio.initialize(application)

The setup CLI detects KMM/CMP projects and marks shared classes as "layer": "shared" and Android-specific classes as "layer": "android".


iOS Integration

Step 1 — Add the XCFramework

Drag AndroClaudio.xcframework from releases/1.0.0/ into Xcode.
In Build Phases → Link Binary With Libraries, add it under the Debug configuration only.

Step 2 — AppDelegate.swift

import AndroClaudio

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        #if DEBUG
        AndroClaudio.shared.initialize(port: 5173) { registry in
            GeneratedGroupRegistry.shared.registerAll(registry: registry)
        }
        #endif
        return true
    }
}

Step 3 — Add config file (optional)

Add androplaudio.config.json to your Xcode bundle resources (Debug configuration only).

Step 4 — Forward port and verify

iproxy 5173 5173 &
curl http://localhost:5173/tools/list | jq .

iproxy is part of libimobiledevice: brew install libimobiledevice


Verifying the Setup End to End

# 1. List all groups
curl -s http://localhost:5173/tools/list | jq .

# 2. Inspect a specific group
curl -s "http://localhost:5173/tools/list?group=cart.manager" | jq .

# 3. Call a function (mock mode)
curl -s -X POST http://localhost:5173/tools/call \
  -H "Content-Type: application/json" \
  -d '{"group":"cart.manager","fn":"addItem","args":{"productId":1,"quantity":2}}' | jq .

# 4. Verify live state
curl -s -X POST http://localhost:5173/tools/call \
  -H "Content-Type: application/json" \
  -d '{"group":"cart.manager","fn":"getItemCount","args":{}}' | jq .result

groups.json Format

{
  "version": "1",
  "platform": "android",
  "framework": "koin",
  "groups": [
    {
      "id": "payment.repository",
      "layer": "shared",
      "class": "com.myapp.data.PaymentRepository"
    }
  ]
}
Field Values Description
id string (dot/hyphen separated) Unique identifier used in MCP calls
layer shared, android, ui Architectural layer — informational
class fully qualified class name The class KSP will generate a registry for

The CLI generates this automatically. Edit it manually to add, remove, or rename groups.


What Gets Excluded Automatically

KSP automatically filters out these from the generated tool list:

  • Android lifecycle methods: onCreate, onDestroy, onStart, onStop, onPause, onResume, onBind, onUnbind, onStartCommand, onRebind, onReceive
  • Worker methods: doWork, onStopped, getForegroundInfo
  • Kotlin object methods: equals, hashCode, toString, copy
  • ContentProvider methods: query, insert, delete, update, getType, openFile
  • Kotlin synthetic: methods prefixed component, access$
  • Constructors: <init>, <clinit>

Only public business logic functions appear as callable tools.


Token Efficiency

AndroClaudio uses a two-level discovery strategy:

Request Tokens When
GET /tools/list ~300 First call — always
GET /tools/list?group=<id> ~800–1500 per group Only groups Claude needs
POST /tools/call ~50 per call Each function execution

A typical session (5 groups, 10 calls): ~6,000 tokens
Without AndroClaudio (reading source files): 20,000–50,000 tokens just to understand structure.


Troubleshooting

GeneratedGroupRegistry not found in logcat
KSP hasn't run yet. Run ./gradlew assembleDebug. The generated files appear in:
app/build/generated/ksp/debug/kotlin/com/androplaudio/generated/

Port 5173 not reachable after emulator restart
Run adb forward tcp:5173 tcp:5173 — this must be re-run after each restart.

addItem returns null — is something wrong?
No. Functions that return Unit (void) correctly produce null in JSON. Check the actual state by calling a getter:
curl ... -d '{"group":"cart.manager","fn":"getItemCount","args":{}}'

Group shows mock responses even in live mode
Check androplaudio.config.json is in src/debug/assets/ (not src/main/assets/) and the group id matches exactly (case-sensitive).

Koin instance not resolving — getting a fresh object each call
AndroClaudio auto-resolves Koin single definitions. If your class is a factory (new instance each time) or not in Koin at all, use AndroClaudio.registerInstance() after your DI setup to pin the exact instance.

Screen doesn't update when MCP calls a function
Expose a StateFlow from your domain class and collect it in the ViewModel (see Reactive UI Updates section). Changes via MCP then propagate to the screen automatically.

null cannot be cast to non-null type kotlin.Int error
You're calling a function with a parameter that has a default value but not passing it explicitly. Always pass all parameters when calling via MCP:
"args": {"productId": 1, "quantity": 1} not "args": {"productId": 1}


Public API

Everything in the library is internal except these:

// Android (androidMain)
object AndroClaudio {
    fun initialize(app: Application, port: Int = 5173)
    fun registerInstance(fqcn: String, instance: Any)
    fun stop()
}

// iOS / CMP (iosMain)
object AndroClaudio {
    fun initialize(port: Int = 5173, registerGroups: (GroupRegistry) -> Unit = {})
    fun registerInstance(fqcn: String, instance: Any)
    fun stop()
}

// Used in KSP-generated code
data class ToolMetadata(val name: String, val params: List<ParamMetadata>, val returnType: String)
data class ParamMetadata(val name: String, val type: String)
class GroupRegistry

Project Structure

androplaudio/
├── androplaudio-agent-skill/     Node.js CLI — npx androplaudio-setup
│   └── src/
│       ├── cli.js                entry point
│       ├── detector.js           detects platform + DI framework
│       ├── scanner.js            extracts class names from source + manifests
│       └── writer.js             writes groups.json
├── androplaudio-ksp/             KSP annotation processor
│   └── src/main/kotlin/
│       ├── GroupProcessor.kt     reads groups.json → generates registries
│       ├── LifecycleFilter.kt    excludes lifecycle/boilerplate methods
│       └── GroupModels.kt        data classes for groups.json parsing
└── androplaudio-core/            KMP runtime library
    └── src/
        ├── commonMain/           shared logic — no platform imports
        │   ├── ToolMetadata.kt
        │   ├── GroupRegistry.kt
        │   ├── MockResponseGenerator.kt
        │   ├── MCPServer.kt       (expect)
        │   └── ModeResolver.kt   (expect)
        ├── androidMain/          Android implementation
        │   ├── MCPServer.android.kt   Ktor CIO server
        │   ├── ModeResolver.android.kt  reads assets
        │   ├── DIResolver.kt      Koin auto + manual fallback
        │   └── AndroClaudio.kt    public entry point
        └── iosMain/              iOS implementation
            ├── MCPServer.ios.kt   Ktor CIO (Native)
            ├── ModeResolver.ios.kt  reads NSBundle
            ├── IOSResolver.kt     manual-only resolver
            └── AndroClaudio.kt    public entry point

About

Give Agent Code live knowledge of your running Android app — MCP server embedded in debug APK

Resources

License

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors