apk-index
Provides tools for indexing and querying Android APK/AAR/DEX packages, enabling analysis of classes, methods, strings, annotations, call references, decompilation, packer detection, and version comparison for reverse engineering and hook development.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@apk-indexindex /path/to/target.apk and check if it's packed"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
apk-index —— Android 逆向 / Xposed 模块开发用的 APK 索引 MCP Server
静态索引 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 个工具
分类 | 工具 | 一句话 |
装载 |
| APK(+split 目录) → 会话;返回指纹 / manifest 摘要 / 组件 / 加固探测 |
装载 |
| classes.jar + R.txt + consumer-rules.pro + jni/<abi>/*.so,可 |
装载 |
| 裸 classes.dex / vdex / compact-dex,可追加进已有会话 |
会话 |
| 忘了 sessionId 先 list;stats 看体量;unload 删登记( |
查询 |
| exact / prefix / regex,`scope=app |
查询 |
| 类内方法与字段, |
查询 |
| 一个类或成员 → smali / reflector / java / helper-ktx 四形态 + hook 起手块 + 注解(含 visibility) |
查询 |
| 常量字符串反查方法(contains/exact/regex,按命中数聚合排序) |
查询 |
| 接口实现 / 父类子类,含传递继承链 |
查询 |
| callers / callees, |
代码 |
|
|
生成 |
| 只给结构条件(参数个数/类型/返回值/引用字符串)→ 候选成员 + 可粘 DSL |
生成 |
| 两版本差分:exact-structure / structural-similarity / added / removed(入参 |
综合 |
| 一句人话问题 → 抽字面量/类名/关键词,串起 checkPacker+stats+各查询的证据包 |
探测 |
| 识别加固与 dex 加壳证据,只识别不静默脱壳 |
清单 |
| 会话里的完整 manifest 摘要:包/版本/SDK/权限/四大组件/native 库,可按组件类型与 exported 过滤 |
清单 |
| 按名字/类型/exported 精确定位一个组件(比 listManifest 窄) |
资源 |
| resources.arsc 条目枚举(type/name/typeId);解析失败报 NOT_FOUND 不装可用 |
资源 |
| 找代码里引用某资源的方法:字符串常量 + const-class 真实 xref |
安全 |
| 一页安全面:exported 组件、危险权限、native 库、资源表可用性 |
运维 |
| 只读缓存体检:逐会话 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_PATHNOT_FOUNDCLASS_NOT_FOUNDINVALID_REFERENCESESSION_NOT_FOUNDAPK_TOO_LARGEPACKED_DEXENCRYPTEDBACKEND_MISSINGINTERNAL
自测
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-RPCmethod恒为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。
This server cannot be deployed
Maintenance
Related MCP Connectors
MCP server for static security analysis of Android source code
Code intelligence for coding agents: semantic, AST, graph, and full-text search. 279+ languages.
Enterprise code intelligence for M&A, security audits, and tech debt. Hosted server with 200k free.
AI-powered codebase analysis — call graphs, security, dead code, complexity. 150+ tools.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceAnalyzes Java codebases using an internal Knowledge Graph stored in SQLite, enabling symbol search, endpoint tracing, and impact analysis without requiring AI APIs.-
- FlicenseNot gradedqualityAmaintenanceEnables 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.-
- FlicenseNot gradedqualityAmaintenanceEnables 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.-
- FlicenseNot gradedqualityAmaintenanceEnables 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.-