Skip to main content
Glama
wlmosv-png

apk-index

by wlmosv-png

apk-index —— Android 逆向 / Xposed 模块开发用的 APK 索引 MCP Server

M8ven Score 静态索引 APK / split APK / AAR / 裸 dex / vdex / compact-dex,给 Agent 提供 「查类、查成员、查签名、查字符串、查交叉引用、生成 hook 起手块」的只读能力。 纯 Python 3 标准库(zipfile / sqlite3 / struct / json / hashlib),stdio 上说 MCP JSON-RPC 2.0。

运行要求

  • Python >= 3.10,无第三方依赖

  • 可选:JDK 的 javac(AAR fixture)与 javap(.class 交叉校验);缺省时用内置 tools/classwrite.py 兜底

  • 只读挂载:索引写 APK_INDEX_CACHE(默认 ~/.cache/apk-index),永不写源文件

Related MCP server: Android Source Explorer MCP Server

多客户端隔离(0.5.1+)

同时开两个对话 / 两个工作连同一个实例时,会话按 owner(租户) 隔离:

  • HTTP:客户端 initialize 后回传 MCP-Session-Id 请求头 → 每个客户端一个 owner;不带头的老客户端归 default(兼容)。

  • stdio:每个进程随机 owner(可用环境变量 APK_INDEX_OWNER 固定)。

  • CLI:APK_INDEX_OWNER 或 cli。

隔离语义:sessionList 只显示本客户端的会话;unload 只解除自己的引用, 索引文件全局共享(同一包只建一次索引,多客户端 load 命中缓存), 最后一个引用者释放时才删文件。跨进程写索引有文件锁(<db>.lock), 两个客户端同时索引同一新包不会互相踩。

注册到 MCP 客户端

{
  "mcpServers": {
    "apk-index": {
      "command": "python3",
      "args": ["-m", "apkindex.mcp", "--stdio"],
      "env": {
        "PYTHONPATH": "/data/local/tmp/apk-index/src",
        "APK_INDEX_CACHE": "~/.cache/apk-index",
        "APK_INDEX_ALLOWED_ROOTS": "/sdcard:/data/local/tmp"
      }
    }
  }
}

APK_INDEX_ALLOWED_ROOTS(冒号分隔)是路径白名单,越界一律 INVALID_PATH —— 防止越权读宿主隐私文件。

23 个工具

分类

工具

一句话

装载

loadApk

APK(+split 目录) → 会话;返回指纹 / manifest 摘要 / 组件 / 加固探测

装载

loadAar

classes.jar + R.txt + consumer-rules.pro + jni/<abi>/*.so,可 mergeInto 并进 App 会话

装载

loadDex

裸 classes.dex / vdex / compact-dex,可追加进已有会话

会话

sessionList / stats / unload

忘了 sessionId 先 list;stats 看体量;unload 删登记(keepFiles 可选)

查询

searchClasses

exact / prefix / regex,`scope=app

查询

listMembers

类内方法与字段,include 走 namePattern

查询

getSignature

一个类或成员 → smali / reflector / java / helper-ktx 四形态 + hook 起手块 + 注解(含 visibility)

查询

searchByString

常量字符串反查方法(contains/exact/regex,按命中数聚合排序)

查询

findImplementations

接口实现 / 父类子类,含传递继承链

查询

xref

callers / callees,depth 控制跳数

代码

decompile

outline(骨架)/ smali / java,maxLines 截断

生成

matchSignature

只给结构条件(参数个数/类型/返回值/引用字符串)→ 候选成员 + 可粘 DSL

生成

diffSessions

两版本差分:exact-structure / structural-similarity / added / removed(入参 sessionA/sessionB)

综合

probe

一句人话问题 → 抽字面量/类名/关键词,串起 checkPacker+stats+各查询的证据包

探测

checkPacker

识别加固与 dex 加壳证据,只识别不静默脱壳

清单

listManifest

会话里的完整 manifest 摘要:包/版本/SDK/权限/四大组件/native 库,可按组件类型与 exported 过滤

清单

findComponent

按名字/类型/exported 精确定位一个组件(比 listManifest 窄)

资源

searchResources

resources.arsc 条目枚举(type/name/typeId);解析失败报 NOT_FOUND 不装可用

资源

resourceRefs

找代码里引用某资源的方法:字符串常量 + const-class 真实 xref

安全

resourceSecurity

一页安全面:exported 组件、危险权限、native 库、资源表可用性

运维

doctor

只读缓存体检:逐会话 quick_check / schemaVersion / 体积 / 未登记 db 文件

返回契约(每个工具都保证)

  • 成功:{ok:true, sessionId, total, items[], truncated, hint, tool, elapsedMs, ...专有字段}

  • 失败:{ok:false, code, message, suggestion, hint}

  • items 上限 100 条、单 item ≤ 32KB;超出置 truncated:true,hint 说明怎么收窄

  • hint 在成功与失败两种 envelope 里都恒存在,客户端只看一个字段就能决定下一步

  • 错误码:INVALID_PATH NOT_FOUND CLASS_NOT_FOUND INVALID_REFERENCE SESSION_NOT_FOUND APK_TOO_LARGE PACKED_DEX ENCRYPTED BACKEND_MISSING INTERNAL

自测

bash build.sh                              # 打 tar 分发包
python3 tools/make_fixture.py --out fixtures
APK_INDEX_CACHE=$PWD/.cache python3 tests/test_tools.py
# 真机样本(混淆目标 + 跨版本差分):
APK_INDEX_TEST_REAL_APK=/sdcard/Download/a.apk,/sdcard/Download/b.apk python3 tests/test_tools.py

已知边界

  • 只读静态索引,不脱壳、不改二进制;加壳 APK 只报证据与 PACKED_DEX

  • 无 JDK 时 decompile(java) 是内置可读伪 java(保留控制流与常量,不做数据流还原)

  • dex 解码是 mterp 子集,遇到未覆盖 opcode 标 partial:true 而不是猜

  • 未修问题清单见 TESTPLAN.md(真机 dex 的 mUTF-8 lone surrogate 已修,剩余为错误码与差分阈值)

参数与行为速查(对齐 tools/list 实测输出)

23 个工具:loadApk loadAar loadDex sessionList unload stats checkPacker searchClasses listMembers getSignature searchByString findImplementations xref decompile matchSignature diffSessions probe listManifest findComponent searchResources resourceRefs resourceSecurity doctor。

会话与索引

  • loadApk(path, splits?, fromDevice?, packageName?, backend?):按 dex 内容去重。 同一个 APK 无论重命名还是重打包,只要 dex 字节集合不变,拿回的是同一个 sessionId 和同一份索引。想强制重来用 unload(keepFiles=false) 再 load。

  • 参数名写错会直接拒收(BAD_ARGUMENT + 有效参数列表),不再静默丢弃。以前把 searchClasses 的 kind 写成 match,regex 不生效、返回 total=0,看着就像 "这个包里根本没有"——假的空结果比崩溃更害人。注意两个搜索工具的参数名不同: searchClasses(query, kind=exact|prefix|regex),searchByString(text, match=contains|exact|regex)。

  • loadAar(path, mergeInto?):读 classes.jar / R.txt / consumer-rules.pro / jni/**/*.so。mergeInto 是"并进宿主会话",宿主的 stats 会多出 library 类; 需要干净的对照会话时别用 mergeInto。

  • unload(sessionId, keepFiles?) / sessionList() / stats(sessionId) / checkPacker(sessionId):加固包在 checkPacker 里给壳特征与真实 dex 位置; 静态可索引类数为 0 时会明确写"索引不到不等于没有"。

查询

  • searchClasses(sessionId, query, kind=exact|prefix|regex, scope=app|library|all, packageFilter, annotatedWith, limit) —— annotatedWith 只回类上带该注解的(@Keep、dalvik.annotation.Keep、Lkotlin/Metadata; 三种写法都认),混淆包里挑名字稳定的锚点就用它。

  • listMembers(sessionId, class, include, namePattern, scope, withStrings, limit)

  • getSignature(sessionId, class, member, scope):一个类(或一个成员)的完整外形。注解走 annotations:descriptor / javaName / visibility(build|runtime|system) / values。

  • searchByString(sessionId, text, match=contains|exact|regex, minLen, scope, methodLimit, limit)

  • findImplementations(sessionId, interface|superClass, method, transitive, includeAbstract, scope, limit) —— 裸类名可用(等于该类的实现/继承查找)。

  • xref(sessionId, method, direction=callers|callees|both, depth, scope, limit) —— method 支持三种写法:com.a.b.C(整类全部成员的引用)、com.a.b.C->m(sig)、 com.a.b.C#m(sig);认不出来时报 NOT_FOUND 并把原样入参带回来,不静默返回空。

  • matchSignature(sessionId, params, returnType, namePattern, packagePrefix, modifiers, requireConstructor, invokedMethods, accessedFields, referredStrings, scope, limit) —— params 支持 any 与具体类型混写;参数个数上限 8(不是 3); packagePrefix 同时匹配 com/example/demo 与 com.example.demo 两种存法。

  • decompile(sessionId, target, format=java|smali, maxLines):三档兜底 SMALI → JAVA+SMALI → SMALI,结果里标 backend/format/truncated; JADX 不存在、崩了或导不出源码时不会装作成功。

差分与聚合

  • diffSessions(sessionA, sessionB, scope, minScore, limit):条目按 kind 分三种, 键不一样 —— renamed 有 from/to/score/how/fromDescriptor/toDescriptor/ packageFrom/packageTo/methodsOnlyInA/methodsOnlyInB;added/removed 只有 class/descriptor/superClass/source/methodCount/sampleMethods。 同一个会话自己比是合法的,结果为空差分。 改名配对是启发式(同外形 + 同常量),how 说明依据;库类混进旧会话会搅动候选集, 所以对照实验要在没被 mergeInto 污染过的会话上做。

  • probe(sessionId, question, target?, maxDepth?, limit):一句问题跑完整套侦察 (引号字面量→字符串检索、点号/驼峰→类检索、中文串→UI 文案检索、加固→提示脱壳)。 给了 target 就连带 xref + getSignature + decompile 深挖;问"有哪些实现类/ 子类"时给 implementations 分节。深挖失败另放 xrefErrors,不会把整次 probe 打死。

清单、资源与安全面(0.5.0 新增)

  • listManifest(sessionId, componentType?, query?, exported?, limit) —— 数据在 loadApk 时已写入会话 summary,查询不重新解包。componentType=component 看全部四类; exported=true 只匹配清单里显式 true 的组件,未声明(None)不算——Android 默认规则 和显式 false 不是一回事,安全审计上混淆这两者会给出假的干净列表。

  • findComponent(sessionId, name?, componentType?, exported?, limit) —— name 支持短名 后缀匹配(.MainActivity 和 MainActivity 都能命中)。

  • searchResources(sessionId, query?, match, resourceType?, limit) —— best-effort 解析 resources.arsc(纯 stdlib,条目样本上限 512)。解析不了时报 NOT_FOUND + notes, 不返回 ok+空列表冒充"这个包没有资源"。

  • resourceRefs(sessionId, resource?, resourceType?, limit) —— 两种证据:DEX 字符串常量 (app_name / R.string.app_name)与 const-class(R$string / BuildConfig)。 0 命中不等于没用到:getIdentifier、XML 直接引用、插件化加载都不留这两种痕迹, hint 里会说明。

  • resourceSecurity(sessionId) —— 单条目聚合,适合跨版本 diff:包/SDK/权限/危险权限/ exported 组件/native 库/资源类型。

  • doctor() —— 只读:catalog 里每个会话的 db 存在性、PRAGMA quick_check、schemaVersion、 缓存总大小、未登记的 .db 文件。它不修任何东西;处置是 unload + loadApk 重建。

注意:loadAar(mergeInto=...) 在 0.5.0 前会把库自己的 manifest 顶掉宿主清单;已修, 但旧的混合会话要 force=true 重建才能拿回正确的组件与安全面。

命令行

PYTHONPATH=src python3 -m apkindex.cli doctor fixtures/demo-v1.apk
PYTHONPATH=src python3 -m apkindex.cli sessions
PYTHONPATH=src python3 -m apkindex.cli tools
PYTHONPATH=src python3 -m apkindex.cli call loadApk '{"path":"fixtures/demo-v1.apk"}'
PYTHONPATH=src python3 -m apkindex.cli call xref '{"sessionId":"ses_xxx","method":"com.example.demo.User","direction":"callers"}'
PYTHONPATH=src python3 -m apkindex.cli pull com.tencent.mm   # adb 在连时才可用
# 子命令只有 tools/call/pull/doctor/sessions;具体查询一律走 call <tool> '<json>'

从设备取包(adb)

没有独立脚本,走 loadApk 的入参:

{ "name": "loadApk",
  "arguments": { "fromDevice": true, "packageName": "com.example.app",
                 "path": "" } }

fromDevice=true 时依次 adb shell pm path <pkg>(拿 base.apk 与 split APK 全部路径) → adb shell dumpsys package <pkg>(取 versionName/versionCode/minSdk/targetSdk, 和清单里的值互相校正)→ adb pull 到缓存目录再索引。ADB 环境变量指定 adb 路径 (默认 adb_bin() 的探测结果)。找不到 adb、设备没授权或超时都报 ADB_UNAVAILABLE 并给出下一步;不假装能拉到自己拉不动的东西 —— 系统分区或受保护路径请在已 root 的机器上直接把 /data/app/~~xxxx/.../base.apk 当 path 传进来。

自测

python3 tests/test_tools.py                    # 38 组
APK_INDEX_TEST_REAL_APK=/path/a.apk,/path/b1.apk,/path/b2.apk python3 tests/test_tools.py

真机校准样本:TGAutoSign 1.2.1(1131 类,索引 ~1.6 s)、LocusMimic 2.0.0 ↔ 2.0.2 (差分识别 12 组改名候选)。只读校验用 APK_INDEX_CACHE + APK_INDEX_ALLOWED_ROOTS 把索引和读取范围钉在临时目录里。

apkindex.cli doctor 的 ok:false 只代表依赖不齐:缺 aapt2 / tools/dexlib.jar / tools/baksmali.jar / tools/apktool.jar / jadx/bin/jadx 时资源与反编译走降级路径,索引与结构化查询不受影响(缺什么会逐项列名,不猜)。

Streamable HTTP 传输(给只能填 URL 的客户端)

python3 -m apkindex.cli serve-http --host 127.0.0.1 --port 8732
# 手机上更靠谱的是这个(Android root,服务不会跟着工具环境一起被回收):
sh tools/httpd-android.sh 8732

端点 http://127.0.0.1:8732/mcp:POST 一条 JSON-RPC → 200;客户端 Accept 里写了 text/event-stream 就回 SSE 单帧(event: message + data: <同一份 JSON>), 只要 application/json 就回裸 JSON —— Kotlin/Java 系的 MCP SDK 常常只认 SSE。 通知(无 id)→ 202 空 body;批量 → 200 + 数组;GET → 健康检查(Accept 要 event-stream 则开常驻流,只发注释帧保活);DELETE → 200(回 501 会被客户端判成服务挂了)。 initialize 的应答带 Mcp-Session-Id 与 MCP-Protocol-Version 头(只发不校验: 拒绝未知会话 id 会让实现松一点的客户端直接卡死,本服务无状态可泄漏)。 与 stdio 版共用同一个 Server.handle(),23 个工具、参数校验、响应体积预算完全一致。 默认只绑回环;本机 loopback 与宿主 Android 是同一个 network namespace, 所以手机上的 App(例:LSPilot 的 MCP 拓展 → 本地 → 新建 → Streamable HTTP) 直接填 http://127.0.0.1:8732/mcp 就能连。

鉴权、状态码与完整部署说明

见 docs/HTTP-DEPLOY.md:启动器与开机自启、chroot 路径绑定、路由与帧的选择、 JSON-RPC → HTTP 状态码映射表、四个大小上限、token 鉴权(APK_INDEX_MCP_TOKEN, 非回环地址无 token 直接拒启)、decompile 的 auto 降级契约、curl 全流程、排障表。

三条最容易踩的先放在这里:

  • 业务错误不占状态码:tools/call 成败都回 200,看信封里的 ok / code。

  • tools/call 的 JSON-RPC method 恒为 tools/call,工具名在 params.name。 把工具名当 method 发会得到 404 / -32601。

  • GET /mcp 免鉴权,是健康检查;它不含任何包内容。

HTTP 帧的选择

Accept 里同时有 application/json 与 text/event-stream(ktor/OkHttp 客户端的默认写法)时, 服务端回裸 JSON;只有客户端只接受 text/event-stream 才回 SSE 单帧(event: message + data: <同一份 JSON>)。 原因是有些客户端一进 SSE 模式就按"长流"读,单条响应要等流结束才交给上层,白吃一个 request timeout。 --verbose 会把每个请求的方法/工具名、字节数和超过 500ms 的耗时打到 stderr, 慢在哪个工具、有没有卡在锁上,看一眼日志就知道。

输出:人读文本 + 机器 JSON 双通道

tools/call 的结果给两份:

  • content[0].text —— 给人读的渲染(src/apkindex/render.py):一行结论 + 对齐条目 + 一句下一步;空结果会解释"为什么可能是空"。descriptor / smali / reflector 这些 干活要用的字段一个都不省,因为有的客户端只把 text 喂给模型。

  • structuredContent —— 完整 envelope JSON,字段与早期版本一致,给程序解析。

以前两份内容都是同一坨 JSON,客户端常常重复显示两遍,人读很费劲;现在 text 走渲染, JSON 只留一份。传输层不变:客户端 Accept 里同时允许 JSON 就回裸 JSON,只要 SSE 才回帧。

要自定义某个工具的样子:render.RENDERERS["toolName"] = fn(fn 收 envelope 返回字符串); 没注册的工具自动走 _r_generic,至少是 key 行 + 编号条目,不会退回裸 JSON。

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    A
    maintenance
    Enables precise, offline-ready exploration of AOSP internals and Jetpack/AndroidX sources, including class and method lookup, hierarchy inspection, regex search, and optional cross-file definition/reference navigation.
    -
  • F
    license
    Not graded
    quality
    A
    maintenance
    Enables AI tools to perform precise, on-demand lookups of AOSP and AndroidX source code, including class and method extraction, inheritance hierarchies, full-text search, and optional cross-file navigation via LSP. It maintains a local synced cache of framework and Jetpack sources so framework internals can be explored offline.
    -
  • F
    license
    Not graded
    quality
    A
    maintenance
    Enables AI assistants to query the actual AOSP and AndroidX source code locally, searching classes, extracting method bodies and Javadoc, listing class members, resolving inheritance hierarchies, and (via optional LSP) finding cross-file definitions and references. This gives precise, offline access to framework internals like the Activity lifecycle or Compose internals instead of relying on outdated training data.
    -