1use crate::state::ServerState;
80use ghidra_ipc::error::{ErrorCode, ErrorEnvelope};
81use ghidra_ipc::protocol::FunctionContext;
82use rmcp::handler::server::router::tool::ToolRouter;
83use rmcp::handler::server::wrapper::Parameters;
84use rmcp::model::{CallToolResult, ContentBlock, ServerCapabilities, ServerConfig};
85use rmcp::ServerHandler;
86use std::sync::Arc;
87
88pub struct GhidraMcpServer {
91 pub state: Arc<ServerState>,
92 #[allow(dead_code)]
100 tool_router: ToolRouter<Self>,
101}
102
103impl GhidraMcpServer {
104 pub fn new(state: Arc<ServerState>) -> Self {
105 Self {
106 state,
107 tool_router: Self::tool_router(),
108 }
109 }
110
111 async fn err_canon(&self, mut e: ghidra_ipc::error::ErrorEnvelope) -> CallToolResult {
115 if e.error.code == ghidra_ipc::error::ErrorCode::AmbiguousSymbol {
116 if let Some(d) = e.error.details.as_mut() {
117 if let Some(canon) = crate::execute::current_canon(&self.state).await {
118 crate::tools::canonicalize_candidates(&canon, d);
119 }
120 }
121 }
122 err_result(&e)
123 }
124}
125
126pub async fn serve(raw: crate::config::RawConfig) -> std::io::Result<()> {
129 let script_dir = crate::paths::versioned_script_dir();
130 let state = Arc::new(ServerState::from_raw(raw, script_dir));
134 if let Err(e) = state.cfg() {
135 tracing::warn!(
136 reason = %e.error.message,
137 "no usable Ghidra configuration; tools will list but every call will fail"
138 );
139 }
140 let server = GhidraMcpServer::new(Arc::clone(&state));
141 state.start_warmup().await;
144 let running = rmcp::ServiceExt::serve(server, rmcp::transport::stdio())
146 .await
147 .map_err(std::io::Error::other)?;
148 tokio::select! {
150 _ = running.waiting() => { tracing::info!("stdio closed; shutting down"); }
151 _ = shutdown_signal() => { tracing::info!("signal received; shutting down"); }
152 }
153 graceful_shutdown(&state).await;
154 Ok(())
155}
156
157async fn shutdown_signal() {
158 #[cfg(unix)]
159 {
160 use tokio::signal::unix::{signal, SignalKind};
161 let mut term = match signal(SignalKind::terminate()) {
162 Ok(s) => s,
163 Err(_) => {
164 let _ = tokio::signal::ctrl_c().await;
165 return;
166 }
167 };
168 tokio::select! {
169 _ = tokio::signal::ctrl_c() => {}
170 _ = term.recv() => {}
171 }
172 }
173 #[cfg(not(unix))]
174 {
175 let _ = tokio::signal::ctrl_c().await;
176 }
177}
178
179pub async fn graceful_shutdown(state: &Arc<ServerState>) {
183 use crate::state::mk;
184 let mut slot = state.worker.lock().await;
185 if let crate::state::WorkerSlot::Ready(h) = &mut *slot {
186 let _ = h
187 .booted
188 .conn
189 .send(&mk("shutdown", serde_json::json!({})))
190 .await;
191 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
192 loop {
193 match h.booted.process.child.try_wait() {
194 Ok(Some(_status)) => break,
195 Ok(None) if std::time::Instant::now() < deadline => {
196 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
197 }
198 _ => break,
199 }
200 }
201 }
202}
203
204#[derive(serde::Deserialize, schemars::JsonSchema)]
207pub struct ListProgramsArgs {
208 #[serde(default)]
209 pub offset: u64,
210 #[serde(default = "default_limit")]
211 pub limit: u64,
212}
213fn default_limit() -> u64 {
214 200
215}
216
217#[derive(serde::Deserialize, schemars::JsonSchema)]
218pub struct AttachArgs {
219 pub program_path: String,
220}
221
222#[derive(serde::Deserialize, schemars::JsonSchema)]
223pub struct InspectArgs {
224 pub function: String,
225 #[serde(default = "default_max_c_lines")]
226 pub max_c_lines: u64,
227}
228fn default_max_c_lines() -> u64 {
229 2000
230}
231
232#[derive(serde::Deserialize, schemars::JsonSchema)]
233pub struct FindFunctionsArgs {
234 #[serde(default)]
235 pub filter: Option<String>,
236 #[serde(default)]
237 pub regex: bool,
238 #[serde(default)]
239 pub offset: u64,
240 #[serde(default = "default_find_limit")]
241 pub limit: u64,
242}
243fn default_find_limit() -> u64 {
244 100
245}
246
247#[derive(serde::Deserialize, schemars::JsonSchema)]
248pub struct ResolveSymbolArgs {
249 pub name: String,
250}
251
252#[derive(serde::Deserialize, schemars::JsonSchema)]
253pub struct ListSymbolsArgs {
254 #[serde(default)]
255 pub kind: Option<String>,
256 #[serde(default)]
257 pub filter: Option<String>,
258 #[serde(default)]
259 pub regex: bool,
260 #[serde(default)]
261 pub offset: u64,
262 #[serde(default = "default_find_limit")]
263 pub limit: u64,
264}
265
266#[derive(serde::Deserialize, schemars::JsonSchema)]
267pub struct ListStringsArgs {
268 #[serde(default)]
269 pub filter: Option<String>,
270 #[serde(default)]
271 pub regex: bool,
272 #[serde(default)]
273 pub offset: u64,
274 #[serde(default = "default_list_strings_limit")]
275 pub limit: u64,
276}
277fn default_list_strings_limit() -> u64 {
278 200
279}
280
281#[derive(serde::Deserialize, schemars::JsonSchema)]
282pub struct GetXrefsArgs {
283 pub target: String,
284 #[serde(default = "default_direction")]
285 pub direction: String,
286 #[serde(default)]
287 pub offset: u64,
288 #[serde(default = "default_find_limit")]
289 pub limit: u64,
290}
291fn default_direction() -> String {
292 "both".to_string()
293}
294
295#[derive(serde::Deserialize, schemars::JsonSchema)]
296pub struct GetDisassemblyArgs {
297 pub target: String,
298 #[serde(default)]
299 pub offset: u64,
300 #[serde(default = "default_list_strings_limit")]
301 pub limit: u64,
302}
303
304#[derive(serde::Deserialize, schemars::JsonSchema)]
305pub struct ReadBytesArgs {
306 pub address: String,
307 #[serde(default)]
308 pub offset: i64,
309 #[serde(default = "default_read_bytes_length")]
310 pub length: u64,
311}
312fn default_read_bytes_length() -> u64 {
313 256
314}
315
316#[derive(serde::Deserialize, schemars::JsonSchema)]
317pub struct GetDatatypeArgs {
318 pub name: String,
319}
320
321#[derive(serde::Deserialize, schemars::JsonSchema)]
322pub struct ListSegmentsArgs {}
323
324#[derive(serde::Deserialize, schemars::JsonSchema)]
325pub struct ListDataItemsArgs {
326 #[serde(default)]
327 pub filter: Option<String>,
328 #[serde(default)]
329 pub regex: bool,
330 #[serde(default)]
331 pub offset: u64,
332 #[serde(default = "default_list_strings_limit")]
333 pub limit: u64,
334}
335
336#[derive(serde::Deserialize, schemars::JsonSchema)]
337pub struct DescribeAddressArgs {
338 pub address: String,
339}
340
341#[derive(serde::Deserialize, schemars::JsonSchema)]
342pub struct RenameArgs {
343 pub target: String,
344 pub expected_name: String,
345 pub new_name: String,
346}
347
348#[derive(serde::Deserialize, serde::Serialize, schemars::JsonSchema, Clone, Copy)]
351#[serde(rename_all = "lowercase")]
352pub enum CommentChannel {
353 Eol,
354 Pre,
355 Post,
356 Plate,
357 Repeatable,
358}
359impl CommentChannel {
360 fn as_str(self) -> &'static str {
361 match self {
362 CommentChannel::Eol => "eol",
363 CommentChannel::Pre => "pre",
364 CommentChannel::Post => "post",
365 CommentChannel::Plate => "plate",
366 CommentChannel::Repeatable => "repeatable",
367 }
368 }
369}
370
371#[derive(serde::Deserialize, schemars::JsonSchema)]
372pub struct CommentArgs {
373 pub target: String,
374 pub comment_type: CommentChannel,
375 pub new_comment: String,
376 #[serde(default, skip_serializing_if = "Option::is_none")]
377 pub expected_comment: Option<String>,
378}
379
380#[derive(serde::Deserialize, schemars::JsonSchema)]
381pub struct SetDatatypeArgs {
382 pub target: String,
383 pub datatype_name: String,
384 #[serde(default, skip_serializing_if = "Option::is_none")]
385 pub expected_datatype: Option<String>,
386}
387
388#[derive(serde::Deserialize, schemars::JsonSchema)]
391pub struct ParamArg {
392 #[serde(default, skip_serializing_if = "Option::is_none")]
393 pub name: Option<String>,
394 #[serde(rename = "type")]
395 pub type_: String,
396}
397
398#[derive(serde::Deserialize, schemars::JsonSchema)]
399pub struct SetPrototypeArgs {
400 pub target: String,
401 pub return_type: String,
402 pub calling_convention: String,
403 pub params: Vec<ParamArg>,
405 #[serde(default)]
406 pub variadic: bool,
407 #[serde(default, skip_serializing_if = "Option::is_none")]
408 pub expected_signature: Option<String>,
409}
410
411#[derive(serde::Deserialize, schemars::JsonSchema)]
412pub struct SetLocalArgs {
413 pub target: String,
414 pub variable: String,
416 #[serde(default, skip_serializing_if = "Option::is_none")]
417 pub new_type: Option<String>,
418 #[serde(default, skip_serializing_if = "Option::is_none")]
419 pub new_name: Option<String>,
420 #[serde(default, skip_serializing_if = "Option::is_none")]
421 pub expected_type: Option<String>,
422 #[serde(default, skip_serializing_if = "Option::is_none")]
423 pub expected_name: Option<String>,
424}
425
426fn err_result(env: &ErrorEnvelope) -> CallToolResult {
428 let text = serde_json::to_string(env).unwrap_or_else(|_| "{\"error\":\"serialize\"}".into());
429 CallToolResult::error(vec![ContentBlock::text(text)])
430}
431
432fn ok_result(v: &serde_json::Value) -> CallToolResult {
434 let text = serde_json::to_string(v).unwrap_or_else(|_| "null".into());
435 CallToolResult::success(vec![ContentBlock::text(text)])
436}
437
438#[rmcp::tool_router]
439impl GhidraMcpServer {
440 #[rmcp::tool(
441 description = "List the programs (binaries) in the open Ghidra project as VFS paths (e.g. \"/add.exe\"). Use this first to discover a program_path to pass to attach_program. Results are paginated: pass offset and limit (default 200); the reply's `total` and `truncated` tell you if more remain. Read-only; no program needs to be attached."
442 )]
443 async fn list_project_programs(
444 &self,
445 Parameters(args): Parameters<ListProgramsArgs>,
446 ) -> Result<CallToolResult, rmcp::ErrorData> {
447 let params = serde_json::json!({ "offset": args.offset, "limit": args.limit });
448 match crate::execute::call_worker(
449 &self.state,
450 "list_project_programs",
451 "",
452 "list_project_programs",
453 params,
454 )
455 .await
456 {
457 Ok((v, _canon)) => Ok(ok_result(&v)),
458 Err(e) => Ok(err_result(&e)),
459 }
460 }
461
462 #[rmcp::tool(
463 description = "Attach the worker to a program so inspect_function targets it. `program_path` is the absolute VFS path from list_project_programs (must begin with '/', e.g. \"/add.exe\"). One program is attached at a time; attaching switches. On boot a bootstrap program is already attached, so you can inspect_function immediately without calling this first."
464 )]
465 async fn attach_program(
466 &self,
467 Parameters(args): Parameters<AttachArgs>,
468 ) -> Result<CallToolResult, rmcp::ErrorData> {
469 match crate::execute::attach_program(&self.state, &args.program_path).await {
470 Ok(v) => Ok(ok_result(&v)),
471 Err(e) => Ok(err_result(&e)),
472 }
473 }
474
475 #[rmcp::tool(
476 description = "Decompile and describe one function in the attached program. `function` is EITHER a symbol name (e.g. \"main\") OR a canonical address. Addresses use the form space:offset in hex — the default code space renders as a bare 0x value like \"0x00401000\", and other spaces keep their prefix like \"EXTERNAL:0x00000001\". If a name matches multiple functions you get an AMBIGUOUS_SYMBOL error listing each candidate's address — re-call with the disambiguating address. The reply includes the decompiled C (capped from the top by max_c_lines, default 2000; c_truncated=true means the body was longer — raise max_c_lines to see more), the signature, calling convention, thunk info, and callers/callees. A caller or callee with external=true is an imported library function with NO body — identify it by name; do not pass it back to inspect_function. To reach a function you can't name, navigate callers/callees from one you can (e.g. an entry point or export)."
477 )]
478 async fn inspect_function(
479 &self,
480 Parameters(args): Parameters<InspectArgs>,
481 ) -> Result<CallToolResult, rmcp::ErrorData> {
482 let params =
483 serde_json::json!({ "function": args.function, "max_c_lines": args.max_c_lines });
484 match crate::execute::call_worker(
485 &self.state,
486 "inspect_function",
487 &args.function,
488 "inspect_function",
489 params,
490 )
491 .await
492 {
493 Ok((v, canon)) => match serde_json::from_value::<FunctionContext>(v) {
494 Ok(fc) => {
495 let fc = crate::tools::canonicalize_function_context(&canon, fc);
496 let v = serde_json::to_value(&fc).unwrap_or(serde_json::Value::Null);
497 Ok(ok_result(&v))
498 }
499 Err(e) => Ok(err_result(&ErrorEnvelope::new(
500 ErrorCode::ParseError,
501 format!("inspect_function result shape: {e}"),
502 ))),
503 },
504 Err(e) => Ok(self.err_canon(e).await),
505 }
506 }
507
508 #[rmcp::tool(
509 description = "List defined functions in the attached program, optionally narrowed by a `filter` matched against the function name IN the worker (so only matches cross the wire). The filter is a substring match by default; pass regex:true for a regular-expression match (linear-time, length-capped). Returns cheap metadata per hit (name, canonical entry_point, signature, calling_convention, is_thunk, size, external) — NOT the decompiled C or call edges; use inspect_function for those. Paginated: pass offset and limit (default 100); page until has_more is false."
510 )]
511 async fn find_functions(
512 &self,
513 Parameters(args): Parameters<FindFunctionsArgs>,
514 ) -> Result<CallToolResult, rmcp::ErrorData> {
515 let params = serde_json::json!({
516 "filter": args.filter,
517 "regex": args.regex,
518 "offset": args.offset,
519 "limit": args.limit,
520 });
521 match crate::execute::call_worker(
522 &self.state,
523 "find_functions",
524 "",
525 "find_functions",
526 params,
527 )
528 .await
529 {
530 Ok((mut v, canon)) => {
531 crate::tools::canonicalize_functions(&canon, &mut v);
532 Ok(ok_result(&v))
533 }
534 Err(e) => Ok(err_result(&e)),
535 }
536 }
537
538 #[rmcp::tool(
539 description = "Resolve a symbol NAME to its address(es); enumerates every match; 0 → SYMBOL_NOT_FOUND; call this after an AMBIGUOUS_SYMBOL from inspect_function."
540 )]
541 async fn resolve_symbol(
542 &self,
543 Parameters(args): Parameters<ResolveSymbolArgs>,
544 ) -> Result<CallToolResult, rmcp::ErrorData> {
545 let params = serde_json::json!({ "name": args.name });
546 match crate::execute::call_worker(
547 &self.state,
548 "resolve_symbol",
549 &args.name,
550 "resolve_symbol",
551 params,
552 )
553 .await
554 {
555 Ok((mut v, canon)) => {
556 crate::tools::canonicalize_addr_array(&canon, &mut v, "candidates", "address");
557 Ok(ok_result(&v))
558 }
559 Err(e) => Ok(err_result(&e)),
560 }
561 }
562
563 #[rmcp::tool(
564 description = "List raw symbol-table entries in the attached program, optionally narrowed by `kind` (import | export | defined | all, default all) and by a `filter` matched against the symbol name IN the worker (so only matches cross the wire). The filter is a substring match by default; pass regex:true for a regular-expression match (linear-time, length-capped). For a function's signature, calling convention, or size, use find_functions instead — this returns raw symbol-table entries, not function metadata. Paginated: pass offset and limit (default 100); page until has_more is false."
565 )]
566 async fn list_symbols(
567 &self,
568 Parameters(args): Parameters<ListSymbolsArgs>,
569 ) -> Result<CallToolResult, rmcp::ErrorData> {
570 let params = serde_json::json!({
571 "kind": args.kind,
572 "filter": args.filter,
573 "regex": args.regex,
574 "offset": args.offset,
575 "limit": args.limit,
576 });
577 match crate::execute::call_worker(&self.state, "list_symbols", "", "list_symbols", params)
578 .await
579 {
580 Ok((mut v, canon)) => {
581 crate::tools::canonicalize_addr_array(&canon, &mut v, "symbols", "address");
582 Ok(ok_result(&v))
583 }
584 Err(e) => Ok(err_result(&e)),
585 }
586 }
587
588 #[rmcp::tool(
589 description = "List defined string data in the attached program, optionally narrowed by a `filter` matched against the string value IN the worker (so only matches cross the wire). The filter is a substring match by default; pass regex:true for a regular-expression match (linear-time, length-capped) — e.g. to find URL-shaped strings without pulling every string across the wire, use filter:\"https?://.*\", regex:true. Paginated: pass offset and limit (default 200); page until has_more is false."
590 )]
591 async fn list_strings(
592 &self,
593 Parameters(args): Parameters<ListStringsArgs>,
594 ) -> Result<CallToolResult, rmcp::ErrorData> {
595 let params = serde_json::json!({
596 "filter": args.filter,
597 "regex": args.regex,
598 "offset": args.offset,
599 "limit": args.limit,
600 });
601 match crate::execute::call_worker(&self.state, "list_strings", "", "list_strings", params)
602 .await
603 {
604 Ok((mut v, canon)) => {
605 crate::tools::canonicalize_addr_array(&canon, &mut v, "strings", "address");
606 Ok(ok_result(&v))
607 }
608 Err(e) => Ok(err_result(&e)),
609 }
610 }
611
612 #[rmcp::tool(
613 description = "List cross-references to or from `target`, a name-or-address selector (any symbol — function, data label, or string, not just functions). `direction` is one of to | from | both (default both): `to` returns references INTO target, `from` returns references OUT of target. If `target` names multiple symbols you get an AMBIGUOUS_SYMBOL error enumerating each candidate's address — re-call with the disambiguating address. Each result row has from_address, to_address, ref_type (e.g. CALL, DATA, READ), and from_function when the reference's source falls inside a defined function. Paginated: pass offset and limit (default 100); page until has_more is false."
614 )]
615 async fn get_xrefs(
616 &self,
617 Parameters(args): Parameters<GetXrefsArgs>,
618 ) -> Result<CallToolResult, rmcp::ErrorData> {
619 let params = serde_json::json!({
620 "target": args.target,
621 "direction": args.direction,
622 "offset": args.offset,
623 "limit": args.limit,
624 });
625 match crate::execute::call_worker(
626 &self.state,
627 "get_xrefs",
628 &args.target,
629 "get_xrefs",
630 params,
631 )
632 .await
633 {
634 Ok((mut v, canon)) => {
635 crate::tools::canonicalize_addr_array(&canon, &mut v, "xrefs", "from_address");
636 crate::tools::canonicalize_addr_array(&canon, &mut v, "xrefs", "to_address");
637 Ok(ok_result(&v))
638 }
639 Err(e) => Ok(self.err_canon(e).await),
640 }
641 }
642
643 #[rmcp::tool(
644 description = "List the disassembled instructions of one function in the attached program. `target` is EITHER a symbol name OR a canonical address, resolved the same way as inspect_function's `function` parameter. If `target` names multiple functions you get an AMBIGUOUS_SYMBOL error enumerating each candidate's address — re-call with the disambiguating address. Each instruction has address, bytes (lowercase hex), mnemonic, operands, and comment when an EOL comment is present. The reply's `total` is the EXACT instruction count of the function's body. Paginated: pass offset and limit (default 200); page until has_more is false."
645 )]
646 async fn get_disassembly(
647 &self,
648 Parameters(args): Parameters<GetDisassemblyArgs>,
649 ) -> Result<CallToolResult, rmcp::ErrorData> {
650 let params = serde_json::json!({
651 "target": args.target,
652 "offset": args.offset,
653 "limit": args.limit,
654 });
655 match crate::execute::call_worker(
656 &self.state,
657 "get_disassembly",
658 &args.target,
659 "get_disassembly",
660 params,
661 )
662 .await
663 {
664 Ok((mut v, canon)) => {
665 crate::tools::canonicalize_addr_array(&canon, &mut v, "instructions", "address");
666 Ok(ok_result(&v))
667 }
668 Err(e) => Ok(self.err_canon(e).await),
669 }
670 }
671
672 #[rmcp::tool(
673 description = "Read raw bytes at `address` in the attached program, optionally shifted by a signed DECIMAL byte `offset` (default 0) — the reply's `address` echoes address+offset, canonicalized. `length` is the number of bytes to read (default 256), clamped to a maximum of 4096; the reply's `truncated` flag is true when the clamp applied, and `length` then reflects the actual (clamped) byte count returned. `bytes` is lowercase hex. ADDRESS_NOT_FOUND if address+offset is out of bounds or the memory there is not readable."
674 )]
675 async fn read_bytes(
676 &self,
677 Parameters(args): Parameters<ReadBytesArgs>,
678 ) -> Result<CallToolResult, rmcp::ErrorData> {
679 let params = serde_json::json!({
680 "address": args.address,
681 "offset": args.offset,
682 "length": args.length,
683 });
684 match crate::execute::call_worker(
685 &self.state,
686 "read_bytes",
687 &args.address,
688 "read_bytes",
689 params,
690 )
691 .await
692 {
693 Ok((mut v, canon)) => {
694 crate::tools::canonicalize_addr_field(&canon, &mut v, "address");
695 Ok(ok_result(&v))
696 }
697 Err(e) => Ok(err_result(&e)),
698 }
699 }
700
701 #[rmcp::tool(
702 description = "Look up a data type DEFINITION by name (e.g. \"Rect\", \"Color\") in the attached program's data type manager — for defined data INSTANCES/globals use list_data_items instead. Returns `kind` (\"struct\"|\"union\"|\"enum\"|other) and `size`; struct/union kinds also return `members` (name, type, offset, size) and enum kinds return `values` (name, value — value is a STRING, not a number, since Ghidra enum constants can carry full 64-bit values that would lose precision as a bare JSON number). If `name` matches more than one type you get an AMBIGUOUS_DATATYPE error listing each candidate's fully-qualified path in details.candidates — re-call get_datatype with that qualified name."
703 )]
704 async fn get_datatype(
705 &self,
706 Parameters(args): Parameters<GetDatatypeArgs>,
707 ) -> Result<CallToolResult, rmcp::ErrorData> {
708 let params = serde_json::json!({ "name": args.name });
709 match crate::execute::call_worker(
710 &self.state,
711 "get_datatype",
712 &args.name,
713 "get_datatype",
714 params,
715 )
716 .await
717 {
718 Ok((v, _canon)) => Ok(ok_result(&v)),
719 Err(e) => Ok(err_result(&e)),
720 }
721 }
722
723 #[rmcp::tool(
724 description = "List the memory map (segments/blocks) of the attached program: name, start/end (canonical addresses), size, and the readable/writable/executable/initialized flags. NOT paginated — segment counts are small. Use this to distinguish code (e.g. .text, executable=true) from data/bss segments before reading bytes or listing data items."
725 )]
726 async fn list_segments(
727 &self,
728 Parameters(_args): Parameters<ListSegmentsArgs>,
729 ) -> Result<CallToolResult, rmcp::ErrorData> {
730 let params = serde_json::json!({});
731 match crate::execute::call_worker(&self.state, "list_segments", "", "list_segments", params)
732 .await
733 {
734 Ok((mut v, canon)) => {
735 crate::tools::canonicalize_addr_array(&canon, &mut v, "segments", "start");
736 crate::tools::canonicalize_addr_array(&canon, &mut v, "segments", "end");
737 Ok(ok_result(&v))
738 }
739 Err(e) => Ok(err_result(&e)),
740 }
741 }
742
743 #[rmcp::tool(
744 description = "List defined data INSTANCES/globals in the attached program (address, name when a listing label is present, data_type, value when cheaply renderable, size) — for type DEFINITIONS (struct/union/enum layouts) use get_datatype instead. Optionally narrowed by a `filter` matched against the item's label IN the worker (so only matches cross the wire). The filter is a substring match by default; pass regex:true for a regular-expression match (linear-time, length-capped). Paginated: pass offset and limit (default 200); page until has_more is false."
745 )]
746 async fn list_data_items(
747 &self,
748 Parameters(args): Parameters<ListDataItemsArgs>,
749 ) -> Result<CallToolResult, rmcp::ErrorData> {
750 let params = serde_json::json!({
751 "filter": args.filter,
752 "regex": args.regex,
753 "offset": args.offset,
754 "limit": args.limit,
755 });
756 match crate::execute::call_worker(
757 &self.state,
758 "list_data_items",
759 "",
760 "list_data_items",
761 params,
762 )
763 .await
764 {
765 Ok((mut v, canon)) => {
766 crate::tools::canonicalize_addr_array(&canon, &mut v, "data", "address");
767 Ok(ok_result(&v))
768 }
769 Err(e) => Ok(err_result(&e)),
770 }
771 }
772
773 #[rmcp::tool(
774 description = "address → what is here (dual of resolve_symbol); total — mapped:false for an unmapped address, never an error. `address` is a canonical space:offset string. On a mapped address the reply also carries `segment` (the containing memory block's name), `is_code` (true when an instruction starts there), `containing_function` (name + entry_point, when the address falls inside a defined function), `symbol` (name + kind, the primary symbol at the address, when one exists), and `data_type` (when defined data starts there). Use this to identify an address you got from get_xrefs, read_bytes, or elsewhere without knowing in advance whether it names a function, data, or neither."
775 )]
776 async fn describe_address(
777 &self,
778 Parameters(args): Parameters<DescribeAddressArgs>,
779 ) -> Result<CallToolResult, rmcp::ErrorData> {
780 let params = serde_json::json!({ "address": args.address });
781 match crate::execute::call_worker(
782 &self.state,
783 "describe_address",
784 &args.address,
785 "describe_address",
786 params,
787 )
788 .await
789 {
790 Ok((mut v, canon)) => {
791 crate::tools::canonicalize_addr_field(&canon, &mut v, "address");
792 if let Some(cf) = v.get_mut("containing_function") {
793 crate::tools::canonicalize_addr_field(&canon, cf, "entry_point");
794 }
795 Ok(ok_result(&v))
796 }
797 Err(e) => Ok(err_result(&e)),
798 }
799 }
800
801 #[rmcp::tool(
802 description = "Rename the primary symbol (function or label) at an address. Compare-and-swap: pass expected_name = the symbol's CURRENT name; the rename applies only if it still matches (else RENAME_CONFLICT with the actual name). If it already equals new_name you get status:already_applied. The edit is saved to disk (durable). new_name must be a bare local name (no namespaces). target is an address or the current name."
803 )]
804 async fn rename(
805 &self,
806 Parameters(args): Parameters<RenameArgs>,
807 ) -> Result<CallToolResult, rmcp::ErrorData> {
808 let params = serde_json::json!({
809 "target": args.target, "expected_name": args.expected_name, "new_name": args.new_name
810 });
811 match crate::execute::call_worker(&self.state, "rename", &args.target, "rename", params)
812 .await
813 {
814 Ok((mut v, canon)) => {
815 crate::tools::canonicalize_addr_field(&canon, &mut v, "address");
816 Ok(ok_result(&v))
817 }
818 Err(e) => Ok(self.err_canon(e).await),
819 }
820 }
821
822 #[rmcp::tool(
823 description = "Set or clear a comment on the code unit at an address. comment_type selects the channel (eol|pre|post|plate|repeatable); plate is the function/block header. new_comment is the text; an empty string clears it. Idempotent: if the comment already equals new_comment you get status:already_applied. Pass expected_comment ONLY to guard against drift (else STATE_CONFLICT with the actual value); you do not need to echo the current comment on the normal path. The edit is saved to disk (durable). target is an address or a symbol name."
824 )]
825 async fn comment(
826 &self,
827 Parameters(args): Parameters<CommentArgs>,
828 ) -> Result<CallToolResult, rmcp::ErrorData> {
829 let mut params = serde_json::json!({
830 "target": args.target,
831 "comment_type": args.comment_type.as_str(),
832 "new_comment": args.new_comment,
833 });
834 if let Some(exp) = &args.expected_comment {
835 params["expected_comment"] = serde_json::Value::String(exp.clone());
836 }
837 match crate::execute::call_worker(&self.state, "comment", &args.target, "comment", params)
838 .await
839 {
840 Ok((mut v, canon)) => {
841 crate::tools::canonicalize_addr_field(&canon, &mut v, "address");
842 Ok(ok_result(&v))
843 }
844 Err(e) => Ok(self.err_canon(e).await),
845 }
846 }
847
848 #[rmcp::tool(
849 description = "Apply a named data type to the DATA at an address (retype a global/data location). datatype_name is resolved by name (DATATYPE_NOT_FOUND / AMBIGUOUS_DATATYPE with candidates). The literal \"undefined\" CLEARS the defined data at the address (frees the space). Idempotent: if the location already has an equivalent type you get status:already_applied. Pass expected_datatype ONLY to guard against drift (its value is the current type's display name, or \"undefined\" to assert the location is untyped). Retyping a function PARAMETER is part of set_prototype (whole-signature); retyping a function LOCAL variable is set_local. The edit is saved to disk (durable). target is an address or a symbol name."
850 )]
851 async fn set_datatype(
852 &self,
853 Parameters(args): Parameters<SetDatatypeArgs>,
854 ) -> Result<CallToolResult, rmcp::ErrorData> {
855 let mut params = serde_json::json!({
856 "target": args.target,
857 "datatype_name": args.datatype_name,
858 });
859 if let Some(exp) = &args.expected_datatype {
860 params["expected_datatype"] = serde_json::Value::String(exp.clone());
861 }
862 match crate::execute::call_worker(
863 &self.state,
864 "set_datatype",
865 &args.target,
866 "set_datatype",
867 params,
868 )
869 .await
870 {
871 Ok((mut v, canon)) => {
872 crate::tools::canonicalize_addr_field(&canon, &mut v, "address");
873 Ok(ok_result(&v))
874 }
875 Err(e) => Ok(self.err_canon(e).await),
876 }
877 }
878
879 #[rmcp::tool(
880 description = "Replace a function's whole signature at once: return type, parameters, calling convention, and varargs. Supply the signature as STRUCTURED fields, not a C string. `target` is the function's address or current name. `return_type` and each `params[].type` are resolved by name (DATATYPE_NOT_FOUND / AMBIGUOUS_DATATYPE with candidates); a function-pointer or anonymous type you spell out (e.g. \"void (*)(int)\") is synthesized on the fly. `params` is an ordered list of {name?, type}; an empty list [] means no arguments (do NOT pass [{type:\"void\"}]). `calling_convention` must be one the program recognizes (e.g. __cdecl, __stdcall, __fastcall, or Ghidra's default/unknown) — an unknown one returns INVALID_PARAMS listing the valid set. `variadic:true` adds trailing ... . Read the current signature as these same fields from inspect_function's `prototype`, change what you need, and send the whole thing back. Idempotent: if the signature is already equivalent you get status:already_applied. Pass expected_signature (the current `signature` string from inspect_function) ONLY to guard against drift (else STATE_CONFLICT). Correcting a wrong signature is the highest-leverage edit — it re-derives the decompiled C (fixes in_<reg>/extraout_<reg> noise). A thunk, an external import, or a function with custom variable storage is rejected with INVALID_PARAMS — do not retry it. The edit is saved to disk (durable)."
881 )]
882 async fn set_prototype(
883 &self,
884 Parameters(args): Parameters<SetPrototypeArgs>,
885 ) -> Result<CallToolResult, rmcp::ErrorData> {
886 let mut params = serde_json::json!({
887 "target": args.target,
888 "return_type": args.return_type,
889 "calling_convention": args.calling_convention,
890 "params": args.params.iter().map(|p| {
891 let mut o = serde_json::json!({ "type": p.type_ });
892 if let Some(n) = &p.name {
893 o["name"] = serde_json::Value::String(n.clone());
894 }
895 o
896 }).collect::<Vec<_>>(),
897 "variadic": args.variadic,
898 });
899 if let Some(exp) = &args.expected_signature {
900 params["expected_signature"] = serde_json::Value::String(exp.clone());
901 }
902 match crate::execute::call_worker(
903 &self.state,
904 "set_prototype",
905 &args.target,
906 "set_prototype",
907 params,
908 )
909 .await
910 {
911 Ok((mut v, canon)) => {
912 crate::tools::canonicalize_addr_field(&canon, &mut v, "address");
913 Ok(ok_result(&v))
914 }
915 Err(e) => Ok(self.err_canon(e).await),
916 }
917 }
918
919 #[rmcp::tool(
920 description = "Retype and/or rename ONE local variable of a function (a decompiler body variable like uVar1 or local_18 — NOT a parameter, which is set_prototype's job, and NOT a global, which is set_datatype's). `target` is the function's address or name. `variable` is the local's STORAGE handle (e.g. \"stack:-0x18\", \"EAX:4\") as shown in inspect_function's `locals[]` — prefer it, because the decompiler silently renames unlocked locals across edits; a bare current name also works but is a weaker handle. Supply `new_type` (resolved by name, DATATYPE_NOT_FOUND / AMBIGUOUS_DATATYPE; a spelled-out fn-pointer/anon type is synthesized) and/or `new_name` (bare, no namespaces); at least one is required. Idempotent: if every field you provide already matches you get status:already_applied. Pass expected_type / expected_name ONLY to guard against drift (else STATE_CONFLICT). A too-large or overlapping type, or a name that collides with another local, returns INVALID_PARAMS with the reason — pick a smaller type or a free name. A miss on the storage/name handle returns VARIABLE_NOT_FOUND (re-read locals[]). The edit is saved to disk (durable), and re-derives the decompiled C. Read locals from inspect_function, change one, send it back."
921 )]
922 async fn set_local(
923 &self,
924 Parameters(args): Parameters<SetLocalArgs>,
925 ) -> Result<CallToolResult, rmcp::ErrorData> {
926 let mut params = serde_json::json!({
927 "target": args.target,
928 "variable": args.variable,
929 });
930 if let Some(t) = &args.new_type {
931 params["new_type"] = serde_json::Value::String(t.clone());
932 }
933 if let Some(n) = &args.new_name {
934 params["new_name"] = serde_json::Value::String(n.clone());
935 }
936 if let Some(t) = &args.expected_type {
937 params["expected_type"] = serde_json::Value::String(t.clone());
938 }
939 if let Some(n) = &args.expected_name {
940 params["expected_name"] = serde_json::Value::String(n.clone());
941 }
942 match crate::execute::call_worker(
943 &self.state,
944 "set_local",
945 &args.target,
946 "set_local",
947 params,
948 )
949 .await
950 {
951 Ok((v, _canon)) => Ok(ok_result(&v)),
952 Err(e) => Ok(self.err_canon(e).await),
953 }
954 }
955}
956
957#[rmcp::tool_handler(router = self.tool_router)]
966impl ServerHandler for GhidraMcpServer {
967 fn get_info(&self) -> ServerConfig {
968 ServerConfig::new(ServerCapabilities::builder().enable_tools().build()).with_instructions(
969 "ghidra-mcp: inspect a Ghidra project's programs and decompiled functions. Start with \
970 list_project_programs to find a program_path, optionally attach_program to switch \
971 targets, then inspect_function by name or canonical address.",
972 )
973 }
974}
975
976#[cfg(test)]
977mod tests {
978 use super::*;
979
980 #[test]
984 fn tool_descriptions_match_tools_rs_consts() {
985 let list = GhidraMcpServer::list_project_programs_tool_attr();
986 let attach = GhidraMcpServer::attach_program_tool_attr();
987 let inspect = GhidraMcpServer::inspect_function_tool_attr();
988 let find = GhidraMcpServer::find_functions_tool_attr();
989 let resolve = GhidraMcpServer::resolve_symbol_tool_attr();
990 let list_symbols = GhidraMcpServer::list_symbols_tool_attr();
991 let list_strings = GhidraMcpServer::list_strings_tool_attr();
992 let get_xrefs = GhidraMcpServer::get_xrefs_tool_attr();
993 let get_disassembly = GhidraMcpServer::get_disassembly_tool_attr();
994 let read_bytes = GhidraMcpServer::read_bytes_tool_attr();
995 let get_datatype = GhidraMcpServer::get_datatype_tool_attr();
996 let list_segments = GhidraMcpServer::list_segments_tool_attr();
997 let list_data_items = GhidraMcpServer::list_data_items_tool_attr();
998 let describe_address = GhidraMcpServer::describe_address_tool_attr();
999 let rename = GhidraMcpServer::rename_tool_attr();
1000 let comment = GhidraMcpServer::comment_tool_attr();
1001 let set_datatype = GhidraMcpServer::set_datatype_tool_attr();
1002 let set_prototype = GhidraMcpServer::set_prototype_tool_attr();
1003 let set_local = GhidraMcpServer::set_local_tool_attr();
1004 assert_eq!(
1005 list.description.as_deref(),
1006 Some(crate::tools::DESC_LIST_PROJECT_PROGRAMS)
1007 );
1008 assert_eq!(
1009 attach.description.as_deref(),
1010 Some(crate::tools::DESC_ATTACH_PROGRAM)
1011 );
1012 assert_eq!(
1013 inspect.description.as_deref(),
1014 Some(crate::tools::DESC_INSPECT_FUNCTION)
1015 );
1016 assert_eq!(
1017 find.description.as_deref(),
1018 Some(crate::tools::DESC_FIND_FUNCTIONS)
1019 );
1020 assert_eq!(
1021 resolve.description.as_deref(),
1022 Some(crate::tools::DESC_RESOLVE_SYMBOL)
1023 );
1024 assert_eq!(
1025 list_symbols.description.as_deref(),
1026 Some(crate::tools::DESC_LIST_SYMBOLS)
1027 );
1028 assert_eq!(
1029 list_strings.description.as_deref(),
1030 Some(crate::tools::DESC_LIST_STRINGS)
1031 );
1032 assert_eq!(
1033 get_xrefs.description.as_deref(),
1034 Some(crate::tools::DESC_GET_XREFS)
1035 );
1036 assert_eq!(
1037 get_disassembly.description.as_deref(),
1038 Some(crate::tools::DESC_GET_DISASSEMBLY)
1039 );
1040 assert_eq!(
1041 read_bytes.description.as_deref(),
1042 Some(crate::tools::DESC_READ_BYTES)
1043 );
1044 assert_eq!(
1045 get_datatype.description.as_deref(),
1046 Some(crate::tools::DESC_GET_DATATYPE)
1047 );
1048 assert_eq!(
1049 list_segments.description.as_deref(),
1050 Some(crate::tools::DESC_LIST_SEGMENTS)
1051 );
1052 assert_eq!(
1053 list_data_items.description.as_deref(),
1054 Some(crate::tools::DESC_LIST_DATA_ITEMS)
1055 );
1056 assert_eq!(
1057 describe_address.description.as_deref(),
1058 Some(crate::tools::DESC_DESCRIBE_ADDRESS)
1059 );
1060 assert_eq!(
1061 rename.description.as_deref(),
1062 Some(crate::tools::DESC_RENAME)
1063 );
1064 assert_eq!(
1065 comment.description.as_deref(),
1066 Some(crate::tools::DESC_COMMENT)
1067 );
1068 assert_eq!(
1069 set_datatype.description.as_deref(),
1070 Some(crate::tools::DESC_SET_DATATYPE)
1071 );
1072 assert_eq!(
1073 set_prototype.description.as_deref(),
1074 Some(crate::tools::DESC_SET_PROTOTYPE)
1075 );
1076 assert_eq!(
1077 set_local.description.as_deref(),
1078 Some(crate::tools::DESC_SET_LOCAL)
1079 );
1080 }
1081
1082 #[test]
1083 fn comment_args_rejects_bad_channel_and_missing_field() {
1084 let bad = serde_json::json!({"target":"main","comment_type":"footnote","new_comment":"x"});
1085 assert!(serde_json::from_value::<CommentArgs>(bad).is_err());
1086 let missing = serde_json::json!({"target":"main","new_comment":"x"});
1087 assert!(serde_json::from_value::<CommentArgs>(missing).is_err());
1088 let ok = serde_json::json!({"target":"main","comment_type":"plate","new_comment":"x"});
1089 assert!(serde_json::from_value::<CommentArgs>(ok).is_ok());
1090 }
1091
1092 #[test]
1093 fn set_datatype_args_schema() {
1094 let missing = serde_json::json!({"target":"main"}); assert!(serde_json::from_value::<SetDatatypeArgs>(missing).is_err());
1096 let ok = serde_json::json!({"target":"0x402000","datatype_name":"int"});
1097 assert!(serde_json::from_value::<SetDatatypeArgs>(ok).is_ok());
1098 let guarded =
1099 serde_json::json!({"target":"g","datatype_name":"int","expected_datatype":"undefined"});
1100 assert!(serde_json::from_value::<SetDatatypeArgs>(guarded).is_ok());
1101
1102 let missing = serde_json::json!({"target":"compute","return_type":"int","calling_convention":"__cdecl"});
1104 assert!(serde_json::from_value::<SetPrototypeArgs>(missing).is_err());
1105 let ok = serde_json::json!({"target":"compute","return_type":"int","calling_convention":"__cdecl","params":[]});
1106 assert!(serde_json::from_value::<SetPrototypeArgs>(ok).is_ok());
1107 let bad_param = serde_json::json!({"target":"c","return_type":"int","calling_convention":"__cdecl","params":[{"name":"a"}]});
1108 assert!(serde_json::from_value::<SetPrototypeArgs>(bad_param).is_err()); let full = serde_json::json!({"target":"c","return_type":"int","calling_convention":"__cdecl","params":[{"name":"a","type":"int"}],"variadic":true,"expected_signature":"x"});
1110 assert!(serde_json::from_value::<SetPrototypeArgs>(full).is_ok());
1111 }
1112
1113 #[test]
1114 fn set_local_args_schema_has_target_and_variable() {
1115 let schema = schemars::schema_for!(SetLocalArgs);
1116 let json = serde_json::to_value(&schema).unwrap();
1117 let props = &json["properties"];
1118 assert!(props.get("target").is_some());
1119 assert!(props.get("variable").is_some());
1120 assert!(props.get("new_type").is_some());
1121 assert!(props.get("new_name").is_some());
1122 }
1123}