mirror of
https://github.com/morluto/rea.git
synced 2026-10-18 20:10:13 -05:00
Resolve smali using frozen raw method names and full DEX descriptors. Select one complete block before truncation for missing Java, static initializers and decompiler marker fallbacks. Reject ambiguous or incomplete blocks and preserve upstream disassembly failure notes. Bind method replies to the selected descriptor with bridge protocol 3. Remove consumer placeholder/error-prefix interpretations and the superseded blanket overload fallback rejection. Producer failures now use the shared failed-tool path before truncation. Migrate protocol fixtures and consumers. Adapt the direct selector guard from PR #3450; recover original bytecode instead of inferring a holder from generated comments. Validation: 122 focused tests in 9 files; real pinned-JAR baseline regression fails and candidate passes, including overloads, aliases after generation, branches/handlers, 16-byte output and negative selection controls. Nine real CLI and nine MCP fixture calls pass with parity/identity/digest/no-mutation checks. Static/docs checks and two source reviews pass. Six completed real MCP cost workflows recorded; same calls with improved source, no speedup claim. Tiny authored APKs, macOS/JDK 21 only; reported editor APK and native Linux/Windows unverified. Heavy profiling and stress remain stopped. Fixes #3443 Co-authored-by: Taksh <takshkothari09@gmail.com>
202 lines
6.4 KiB
JavaScript
202 lines
6.4 KiB
JavaScript
import { McpServer } from "@modelcontextprotocol/server";
|
|
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
import { z } from "zod";
|
|
import { JADX_BRIDGE_IDENTITY } from "../../../src/android/JadxRelease.ts";
|
|
|
|
// An actual child MCP server exercising the production stdio/process seam.
|
|
// It is a protocol fixture, not evidence of JADX decompiler support.
|
|
const mode = process.env.REA_FAKE_JADX_MODE ?? "normal";
|
|
const server = new McpServer({
|
|
name: mode === "empty-server-identity" ? "" : "rea-jadx-bridge",
|
|
version:
|
|
mode === "empty-server-identity"
|
|
? ""
|
|
: mode === "version"
|
|
? "99.0.0"
|
|
: JADX_BRIDGE_IDENTITY.version,
|
|
});
|
|
const requestedCount = Number.parseInt(
|
|
process.env.REA_FAKE_JADX_CLASS_COUNT ?? "",
|
|
10,
|
|
);
|
|
const generatedCount =
|
|
Number.isInteger(requestedCount) && requestedCount >= 0
|
|
? requestedCount
|
|
: mode === "large-inventory"
|
|
? 9000
|
|
: 400;
|
|
const classes = [
|
|
"fixture.Target",
|
|
...Array.from(
|
|
{ length: generatedCount },
|
|
(_, index) => `fixture.Class${index}`,
|
|
),
|
|
...(mode === "duplicate-class-name" ? ["fixture.Target"] : []),
|
|
];
|
|
const methods = [
|
|
{
|
|
name: "onCreate",
|
|
signature: "onCreate(Bundle): void",
|
|
dex_descriptor: "(Landroid/os/Bundle;)V",
|
|
is_constructor: false,
|
|
def_pos: 0,
|
|
},
|
|
{
|
|
name: "choose",
|
|
signature: "choose(String): void",
|
|
dex_descriptor: "(Ljava/lang/String;)V",
|
|
is_constructor: false,
|
|
def_pos: 0,
|
|
},
|
|
{
|
|
name: "choose",
|
|
signature: "choose(int): void",
|
|
dex_descriptor: "(I)V",
|
|
is_constructor: false,
|
|
def_pos: 0,
|
|
},
|
|
];
|
|
const text = (value) => ({ content: [{ type: "text", text: value }] });
|
|
const json = (value) => text(JSON.stringify(value));
|
|
const register = (name, handler) =>
|
|
server.registerTool(name, { inputSchema: z.looseObject({}) }, handler);
|
|
register("rea_jvm_status", () =>
|
|
json({
|
|
engine_reported_version: mode === "engine-version" ? "0.7.0" : "0.7.1",
|
|
max_heap_bytes: 8 * 1024 * 1024 * 1024,
|
|
available_processors: 4,
|
|
java_version: "21-fixture",
|
|
metadata_scope: "parsed_members",
|
|
}),
|
|
);
|
|
register("load_apk", async ({ path }) => {
|
|
if (mode === "stall") await new Promise(() => {});
|
|
return json({
|
|
state: "LOADED",
|
|
apk_path: mode === "wrong-apk" ? "/different.apk" : path,
|
|
class_count: classes.length,
|
|
resource_count: 1,
|
|
threads: 1,
|
|
resources: "full",
|
|
});
|
|
});
|
|
register("get_app_info", () => {
|
|
if (mode === "rpc-disconnect") process.exit(17);
|
|
return json({
|
|
package: "fixture",
|
|
version_name: "1",
|
|
version_code: "1",
|
|
min_sdk: "26",
|
|
target_sdk: "33",
|
|
permissions: ["android.permission.INTERNET"],
|
|
});
|
|
});
|
|
register("get_android_manifest", () => {
|
|
if (mode === "tool-error" || mode === "cleanup-failure")
|
|
return {
|
|
...text("manifest decoder rejected malformed binary XML"),
|
|
isError: true,
|
|
};
|
|
if (mode === "frame-overflow") return text("x".repeat(9 * 1024 * 1024));
|
|
if (mode === "large-manifest") return text("x".repeat(1024 * 1024));
|
|
return text('<manifest package="fixture"/>');
|
|
});
|
|
register("list_classes", ({ offset, limit, query = "" }) => {
|
|
const matching = [];
|
|
let nextOffset = offset;
|
|
while (nextOffset < classes.length && matching.length < limit) {
|
|
const name = classes[nextOffset++];
|
|
if (name.includes(query)) matching.push(name);
|
|
}
|
|
return json({
|
|
scope: "top_level",
|
|
total_classes: classes.length,
|
|
next_offset: nextOffset,
|
|
offset,
|
|
limit,
|
|
items: mode === "empty-page" ? [] : matching,
|
|
});
|
|
});
|
|
register("get_class_summary", () =>
|
|
json({
|
|
full_name: mode === "wrong-class" ? "fixture.Other" : "fixture.Target",
|
|
name: "Target",
|
|
method_count: mode === "summary-error" ? -1 : methods.length,
|
|
field_count: 0,
|
|
inner_class_count: 0,
|
|
methods: mode === "summary-error" ? [] : methods,
|
|
fields: [],
|
|
inner_classes: [],
|
|
...(mode === "summary-error" ? { error: "summary timed out" } : {}),
|
|
}),
|
|
);
|
|
register("get_method_body", ({ method_name, overload_index }) => {
|
|
if (mode === "body-error")
|
|
return {
|
|
...text("// ERROR: decompile timed out or failed"),
|
|
isError: true,
|
|
};
|
|
const selected = methods.filter((method) => method.name === method_name)[
|
|
overload_index
|
|
];
|
|
return json({
|
|
class_name: "fixture.Target",
|
|
method_name,
|
|
full_name: `fixture.Target.${method_name}`,
|
|
overload_index:
|
|
mode === "wrong-method" ? overload_index + 1 : overload_index,
|
|
overload_count: method_name === "choose" ? 2 : 1,
|
|
dex_descriptor:
|
|
mode === "wrong-descriptor" ? "()V" : selected.dex_descriptor,
|
|
mode:
|
|
mode === "smali" || mode === "selected-overload-smali" ? "smali" : "java",
|
|
fell_back: mode === "smali" || mode === "selected-overload-smali",
|
|
markers:
|
|
mode === "smali" || mode === "selected-overload-smali"
|
|
? ["Method not decompiled"]
|
|
: mode === "native-method"
|
|
? ["native_method"]
|
|
: [],
|
|
body:
|
|
mode === "smali" || mode === "selected-overload-smali"
|
|
? `.method public ${method_name}${selected.dex_descriptor}\n .registers 2\n return-void\n.end method`
|
|
: mode === "native-method"
|
|
? ""
|
|
: mode === "truncated"
|
|
? "void onCreate(){}\n\n... [truncated: exceeds 18 bytes, total 100 bytes]"
|
|
: `void ${method_name}() { /* overload ${overload_index} */ }`,
|
|
});
|
|
});
|
|
for (const name of ["get_xrefs_to_class", "get_xrefs_to_method"])
|
|
register(name, ({ class_name, method_name, overload_index }) =>
|
|
json({
|
|
target:
|
|
method_name === undefined ? class_name : `${class_name}.${method_name}`,
|
|
...(method_name === undefined
|
|
? {}
|
|
: {
|
|
overload_index,
|
|
reported_signature: methods.filter(
|
|
(method) => method.name === method_name,
|
|
)[overload_index]?.signature,
|
|
}),
|
|
total: mode === "partial-xrefs" ? 2 : 1,
|
|
count: 1,
|
|
resolve_line: false,
|
|
items: [
|
|
{
|
|
kind: "method",
|
|
name: "caller",
|
|
full_name: "fixture.Other.caller",
|
|
containing_class: "fixture.Other",
|
|
top_class: "fixture.Other",
|
|
def_pos: 42,
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
await server.connect(new StdioServerTransport());
|
|
// A refused cleanup must leave a live process for verified fixture teardown.
|
|
// Otherwise stdin EOF races the ownership scan with the server's natural exit.
|
|
if (mode === "cleanup-failure") setInterval(() => {}, 1000);
|