Skip to main content

ghidra_mcp/
tools.rs

1//! MCP tool handlers (spec §3). Each maps a worker `result` Value into the client-facing shape, then
2//! canonicalizes every address string through the attached program's `AddressCanonicalizer` (spec §6).
3//! Domain errors become `CallToolResult{isError:true}` with the `ErrorEnvelope` as text (spec §4.2).
4
5use ghidra_ipc::address::AddressCanonicalizer;
6use ghidra_ipc::protocol::FunctionContext;
7use serde_json::Value;
8
9/// Re-render a worker `space:offset` address into the canonical form. If the worker string doesn't
10/// parse, pass it through unchanged rather than error: a display glitch must never fail a good result.
11fn canon_addr(canon: &AddressCanonicalizer, raw: &str) -> String {
12    canon.canonicalize(raw).unwrap_or_else(|_| raw.to_string())
13}
14
15/// Canonicalize every address in a `FunctionContext` (entry_point, thunked_to, all edge addresses,
16/// every `data_refs[].address` — Task 16, and every `comments[].address` — M2b read-back tidy).
17pub fn canonicalize_function_context(
18    canon: &AddressCanonicalizer,
19    mut fc: FunctionContext,
20) -> FunctionContext {
21    fc.entry_point = canon_addr(canon, &fc.entry_point);
22    fc.thunked_to = fc.thunked_to.map(|a| canon_addr(canon, &a));
23    for e in fc.callers.iter_mut() {
24        e.address = canon_addr(canon, &e.address);
25    }
26    for e in fc.callees.iter_mut() {
27        e.address = canon_addr(canon, &e.address);
28    }
29    for e in fc.data_refs.iter_mut() {
30        e.address = canon_addr(canon, &e.address);
31    }
32    for e in fc.comments.iter_mut() {
33        e.address = canon_addr(canon, &e.address);
34    }
35    fc
36}
37
38/// Canonicalize the `address` in each AMBIGUOUS_SYMBOL candidate (spec §6).
39pub fn canonicalize_candidates(canon: &AddressCanonicalizer, details: &mut Value) {
40    if let Some(cands) = details.get_mut("candidates").and_then(|c| c.as_array_mut()) {
41        for c in cands.iter_mut() {
42            if let Some(a) = c.get("address").and_then(|a| a.as_str()) {
43                let canon_a = canon_addr(canon, a);
44                c["address"] = Value::String(canon_a);
45            }
46        }
47    }
48}
49
50pub const DESC_LIST_PROJECT_PROGRAMS: &str = "\
51List the programs (binaries) in the open Ghidra project as VFS paths (e.g. \"/add.exe\"). Use this \
52first to discover a program_path to pass to attach_program. Results are paginated: pass offset and \
53limit (default 200); the reply's `total` and `truncated` tell you if more remain. Read-only; no \
54program needs to be attached.";
55
56pub const DESC_ATTACH_PROGRAM: &str = "\
57Attach the worker to a program so inspect_function targets it. `program_path` is the absolute VFS path \
58from list_project_programs (must begin with '/', e.g. \"/add.exe\"). One program is attached at a time; \
59attaching switches. On boot a bootstrap program is already attached, so you can inspect_function \
60immediately without calling this first.";
61
62pub const DESC_INSPECT_FUNCTION: &str = "\
63Decompile and describe one function in the attached program. `function` is EITHER a symbol name \
64(e.g. \"main\") OR a canonical address. Addresses use the form space:offset in hex — the default code \
65space renders as a bare 0x value like \"0x00401000\", and other spaces keep their prefix like \
66\"EXTERNAL:0x00000001\". If a name matches multiple functions you get an AMBIGUOUS_SYMBOL error listing \
67each candidate's address — re-call with the disambiguating address. The reply includes the decompiled \
68C (capped from the top by max_c_lines, default 2000; c_truncated=true means the body was longer — raise \
69max_c_lines to see more), the signature, calling convention, thunk info, and callers/callees. A caller \
70or callee with external=true is an imported library function with NO body — identify it by name; do not \
71pass it back to inspect_function. To reach a function you can't name, navigate callers/callees from one \
72you can (e.g. an entry point or export).";
73
74pub const DESC_FIND_FUNCTIONS: &str = "\
75List defined functions in the attached program, optionally narrowed by a `filter` matched against the \
76function name IN the worker (so only matches cross the wire). The filter is a substring match by \
77default; pass regex:true for a regular-expression match (linear-time, length-capped). Returns cheap \
78metadata per hit (name, canonical entry_point, signature, calling_convention, is_thunk, size, external) \
79— NOT the decompiled C or call edges; use inspect_function for those. Paginated: pass offset and limit \
80(default 100); page until has_more is false.";
81
82/// Canonicalize `entry_point` (+ `thunked_to`) in each function of a find_functions result, in place.
83pub fn canonicalize_functions(canon: &AddressCanonicalizer, v: &mut Value) {
84    if let Some(arr) = v.get_mut("functions").and_then(|x| x.as_array_mut()) {
85        for f in arr {
86            if let Some(a) = f.get("entry_point").and_then(|a| a.as_str()) {
87                let c = canon_addr(canon, a);
88                f["entry_point"] = Value::String(c);
89            }
90            if let Some(a) = f.get("thunked_to").and_then(|a| a.as_str()) {
91                let c = canon_addr(canon, a);
92                f["thunked_to"] = Value::String(c);
93            }
94        }
95    }
96}
97
98pub const DESC_RESOLVE_SYMBOL: &str = "\
99Resolve a symbol NAME to its address(es); enumerates every match; 0 → SYMBOL_NOT_FOUND; call this \
100after an AMBIGUOUS_SYMBOL from inspect_function.";
101
102pub const DESC_LIST_SYMBOLS: &str = "\
103List raw symbol-table entries in the attached program, optionally narrowed by `kind` (import | export | \
104defined | all, default all) and by a `filter` matched against the symbol name IN the worker (so only \
105matches cross the wire). The filter is a substring match by default; pass regex:true for a \
106regular-expression match (linear-time, length-capped). For a function's signature, calling convention, \
107or size, use find_functions instead — this returns raw symbol-table entries, not function metadata. \
108Paginated: pass offset and limit (default 100); page until has_more is false.";
109
110pub const DESC_LIST_STRINGS: &str = "\
111List defined string data in the attached program, optionally narrowed by a `filter` matched against \
112the string value IN the worker (so only matches cross the wire). The filter is a substring match by \
113default; pass regex:true for a regular-expression match (linear-time, length-capped) — e.g. to find \
114URL-shaped strings without pulling every string across the wire, use filter:\"https?://.*\", regex:true. \
115Paginated: pass offset and limit (default 200); page until has_more is false.";
116
117/// Canonicalize a `field` address string on every object in `v[array_key]`, in place. Total:
118/// an unparseable string passes through unchanged (canon_addr is total).
119pub fn canonicalize_addr_array(
120    canon: &AddressCanonicalizer,
121    v: &mut Value,
122    array_key: &str,
123    field: &str,
124) {
125    if let Some(arr) = v.get_mut(array_key).and_then(|x| x.as_array_mut()) {
126        for e in arr {
127            if let Some(a) = e.get(field).and_then(|a| a.as_str()) {
128                let c = canon_addr(canon, a);
129                e[field] = Value::String(c);
130            }
131        }
132    }
133}
134
135pub const DESC_GET_XREFS: &str = "\
136List cross-references to or from `target`, a name-or-address selector (any symbol — function, data \
137label, or string, not just functions). `direction` is one of to | from | both (default both): `to` \
138returns references INTO target, `from` returns references OUT of target. If `target` names multiple \
139symbols you get an AMBIGUOUS_SYMBOL error enumerating each candidate's address — re-call with the \
140disambiguating address. Each result row has from_address, to_address, ref_type (e.g. CALL, DATA, READ), \
141and from_function when the reference's source falls inside a defined function. Paginated: pass offset \
142and limit (default 100); page until has_more is false.";
143
144pub const DESC_GET_DISASSEMBLY: &str = "\
145List the disassembled instructions of one function in the attached program. `target` is EITHER a \
146symbol name OR a canonical address, resolved the same way as inspect_function's `function` parameter. \
147If `target` names multiple functions you get an AMBIGUOUS_SYMBOL error enumerating each candidate's \
148address — re-call with the disambiguating address. Each instruction has address, bytes (lowercase hex), \
149mnemonic, operands, and comment when an EOL comment is present. The reply's `total` is the EXACT \
150instruction count of the function's body. Paginated: pass offset and limit (default 200); page until \
151has_more is false.";
152
153/// Canonicalize a top-level `field` address string on `v`, in place (total). Mirrors
154/// `canonicalize_addr_array` for a scalar field rather than an array of objects.
155pub fn canonicalize_addr_field(canon: &AddressCanonicalizer, v: &mut Value, field: &str) {
156    if let Some(a) = v.get(field).and_then(|a| a.as_str()) {
157        let c = canon_addr(canon, a);
158        v[field] = Value::String(c);
159    }
160}
161
162pub const DESC_READ_BYTES: &str = "\
163Read raw bytes at `address` in the attached program, optionally shifted by a signed DECIMAL byte \
164`offset` (default 0) — the reply's `address` echoes address+offset, canonicalized. `length` is the \
165number of bytes to read (default 256), clamped to a maximum of 4096; the reply's `truncated` flag is \
166true when the clamp applied, and `length` then reflects the actual (clamped) byte count returned. \
167`bytes` is lowercase hex. ADDRESS_NOT_FOUND if address+offset is out of bounds or the memory there is \
168not readable.";
169
170pub const DESC_GET_DATATYPE: &str = "\
171Look up a data type DEFINITION by name (e.g. \"Rect\", \"Color\") in the attached program's data type \
172manager — for defined data INSTANCES/globals use list_data_items instead. Returns `kind` \
173(\"struct\"|\"union\"|\"enum\"|other) and `size`; struct/union kinds also return `members` (name, type, \
174offset, size) and enum kinds return `values` (name, value — value is a STRING, not a number, since \
175Ghidra enum constants can carry full 64-bit values that would lose precision as a bare JSON number). If \
176`name` matches more than one type you get an AMBIGUOUS_DATATYPE error listing each candidate's \
177fully-qualified path in details.candidates — re-call get_datatype with that qualified name.";
178
179pub const DESC_LIST_SEGMENTS: &str = "\
180List the memory map (segments/blocks) of the attached program: name, start/end (canonical addresses), \
181size, and the readable/writable/executable/initialized flags. NOT paginated — segment counts are small. \
182Use this to distinguish code (e.g. .text, executable=true) from data/bss segments before reading bytes \
183or listing data items.";
184
185pub const DESC_LIST_DATA_ITEMS: &str = "\
186List defined data INSTANCES/globals in the attached program (address, name when a listing label is \
187present, data_type, value when cheaply renderable, size) — for type DEFINITIONS (struct/union/enum \
188layouts) use get_datatype instead. Optionally narrowed by a `filter` matched against the item's label IN \
189the worker (so only matches cross the wire). The filter is a substring match by default; pass \
190regex:true for a regular-expression match (linear-time, length-capped). Paginated: pass offset and \
191limit (default 200); page until has_more is false.";
192
193pub const DESC_DESCRIBE_ADDRESS: &str = "\
194address → what is here (dual of resolve_symbol); total — mapped:false for an unmapped address, never \
195an error. `address` is a canonical space:offset string. On a mapped address the reply also carries \
196`segment` (the containing memory block's name), `is_code` (true when an instruction starts there), \
197`containing_function` (name + entry_point, when the address falls inside a defined function), `symbol` \
198(name + kind, the primary symbol at the address, when one exists), and `data_type` (when defined data \
199starts there). Use this to identify an address you got from get_xrefs, read_bytes, or elsewhere without \
200knowing in advance whether it names a function, data, or neither.";
201
202pub const DESC_RENAME: &str = "\
203Rename the primary symbol (function or label) at an address. Compare-and-swap: pass expected_name = the \
204symbol's CURRENT name; the rename applies only if it still matches (else RENAME_CONFLICT with the actual \
205name). If it already equals new_name you get status:already_applied. The edit is saved to disk (durable). \
206new_name must be a bare local name (no namespaces). target is an address or the current name.";
207
208pub const DESC_COMMENT: &str = "\
209Set or clear a comment on the code unit at an address. comment_type selects the channel \
210(eol|pre|post|plate|repeatable); plate is the function/block header. new_comment is the text; an empty \
211string clears it. Idempotent: if the comment already equals new_comment you get status:already_applied. \
212Pass expected_comment ONLY to guard against drift (else STATE_CONFLICT with the actual value); you do not \
213need to echo the current comment on the normal path. The edit is saved to disk (durable). target is an \
214address or a symbol name.";
215
216pub const DESC_SET_DATATYPE: &str = "\
217Apply a named data type to the DATA at an address (retype a global/data location). datatype_name is \
218resolved by name (DATATYPE_NOT_FOUND / AMBIGUOUS_DATATYPE with candidates). The literal \"undefined\" \
219CLEARS the defined data at the address (frees the space). Idempotent: if the location already has an \
220equivalent type you get status:already_applied. Pass expected_datatype ONLY to guard against drift (its \
221value is the current type's display name, or \"undefined\" to assert the location is untyped). Retyping a \
222function PARAMETER is part of set_prototype (whole-signature); retyping a function LOCAL variable is \
223set_local. The edit is saved to \
224disk (durable). target is an address or a symbol name.";
225
226pub const DESC_SET_PROTOTYPE: &str = "\
227Replace a function's whole signature at once: return type, parameters, calling convention, and varargs. \
228Supply the signature as STRUCTURED fields, not a C string. `target` is the function's address or current \
229name. `return_type` and each `params[].type` are resolved by name (DATATYPE_NOT_FOUND / AMBIGUOUS_DATATYPE \
230with candidates); a function-pointer or anonymous type you spell out (e.g. \"void (*)(int)\") is \
231synthesized on the fly. `params` is an ordered list of {name?, type}; an empty list [] means no arguments \
232(do NOT pass [{type:\"void\"}]). `calling_convention` must be one the program recognizes (e.g. __cdecl, \
233__stdcall, __fastcall, or Ghidra's default/unknown) — an unknown one returns INVALID_PARAMS listing the \
234valid set. `variadic:true` adds trailing ... . Read the current signature as these same fields from \
235inspect_function's `prototype`, change what you need, and send the whole thing back. Idempotent: if the \
236signature is already equivalent you get status:already_applied. Pass expected_signature (the current \
237`signature` string from inspect_function) ONLY to guard against drift (else STATE_CONFLICT). Correcting a \
238wrong signature is the highest-leverage edit — it re-derives the decompiled C (fixes in_<reg>/extraout_<reg> \
239noise). A thunk, an external import, or a function with custom variable storage is rejected with \
240INVALID_PARAMS — do not retry it. The edit is saved to disk (durable).";
241
242pub const DESC_SET_LOCAL: &str = "\
243Retype and/or rename ONE local variable of a function (a decompiler body variable like uVar1 or \
244local_18 — NOT a parameter, which is set_prototype's job, and NOT a global, which is set_datatype's). \
245`target` is the function's address or name. `variable` is the local's STORAGE handle (e.g. \"stack:-0x18\", \
246\"EAX:4\") as shown in inspect_function's `locals[]` — prefer it, because the decompiler silently renames \
247unlocked locals across edits; a bare current name also works but is a weaker handle. Supply `new_type` \
248(resolved by name, DATATYPE_NOT_FOUND / AMBIGUOUS_DATATYPE; a spelled-out fn-pointer/anon type is \
249synthesized) and/or `new_name` (bare, no namespaces); at least one is required. Idempotent: if every \
250field you provide already matches you get status:already_applied. Pass expected_type / expected_name ONLY \
251to guard against drift (else STATE_CONFLICT). A too-large or overlapping type, or a name that collides \
252with another local, returns INVALID_PARAMS with the reason — pick a smaller type or a free name. A miss \
253on the storage/name handle returns VARIABLE_NOT_FOUND (re-read locals[]). The edit is saved to disk \
254(durable), and re-derives the decompiled C. Read locals from inspect_function, change one, send it back.";
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259    use ghidra_ipc::protocol::{CallEdge, CommentEntry, DataRef};
260
261    fn canon() -> AddressCanonicalizer {
262        AddressCanonicalizer::new("ram", 4)
263    }
264
265    #[test]
266    fn canonicalizes_all_addresses_in_context() {
267        let fc = FunctionContext {
268            name: "f".into(),
269            entry_point: "ram:401000".into(),
270            signature: "s".into(),
271            calling_convention: "__cdecl".into(),
272            is_thunk: true,
273            thunked_to: Some("ram:401100".into()),
274            c: "…".into(),
275            c_truncated: false,
276            callers: vec![CallEdge {
277                name: "m".into(),
278                address: "ram:401040".into(),
279                external: false,
280            }],
281            callees: vec![CallEdge {
282                name: "X".into(),
283                address: "EXTERNAL:1".into(),
284                external: true,
285            }],
286            data_refs: vec![DataRef {
287                address: "ram:403000".into(),
288                name: Some("g_banner".into()),
289                ref_type: "DATA".into(),
290            }],
291            data_refs_truncated: false,
292            comments: vec![CommentEntry {
293                address: "ram:404000".into(),
294                kind: "plate".into(),
295                comment: "hdr".into(),
296            }],
297            comments_truncated: false,
298            prototype: None,
299            locals: None,
300            locals_truncated: None,
301        };
302        let out = canonicalize_function_context(&canon(), fc);
303        assert_eq!(out.entry_point, "0x00401000");
304        assert_eq!(out.thunked_to.unwrap(), "0x00401100");
305        assert_eq!(out.callers[0].address, "0x00401040");
306        assert_eq!(out.callees[0].address, "EXTERNAL:0x00000001");
307        assert_eq!(out.data_refs[0].address, "0x00403000");
308        assert_eq!(out.comments[0].address, "0x00404000");
309    }
310}