This is page 1 of 2. Use http://codebase.md/dnakov/radare2-mcp?page={x} to view the full context.
# Directory Structure
```
├── .clang-format
├── .github
│   └── workflows
│       └── ci.yml
├── .gitignore
├── AGENTS.md
├── autogen.sh
├── config.h.w64
├── configure
├── configure.acr
├── dist
│   ├── debian
│   │   ├── build.sh
│   │   ├── CONFIG
│   │   ├── deb.mk
│   │   ├── DESCR
│   │   └── Makefile
│   ├── docker
│   │   ├── dockcross
│   │   └── Dockerfile
│   └── scripts
│       ├── indent.pl
│       └── indent.py
├── INSTALL.md
├── LICENSE
├── Makefile
├── meson_options.txt
├── meson.build
├── r2mcp.png
├── r2mcp.svg
├── README.md
├── src
│   ├── config.h.acr
│   ├── curl.inc.c
│   ├── dsltest.c
│   ├── main.c
│   ├── Makefile
│   ├── plugin.c
│   ├── prompts.c
│   ├── prompts.h
│   ├── r2api.inc.c
│   ├── r2mcp.c
│   ├── r2mcp.h
│   ├── readbuffer.c
│   ├── readbuffer.h
│   ├── tools.c
│   ├── tools.h
│   └── utils.inc.c
├── svc
│   ├── Makefile
│   ├── r2mcp-svc.c
│   └── README.md
├── test.sh
└── TODO.md
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
.DS_Store
r2mcp
src/r2mcp
src/config.h
src/Makefile
svc/r2mcp-svc
src/r2mcp
third_party
.cache
***/*.dSYM
***/*.dylib
***/*.dll
***/*.swp
***/*.so
***/*.o
```
--------------------------------------------------------------------------------
/.clang-format:
--------------------------------------------------------------------------------
```
BasedOnStyle: LLVM
Language: Cpp
PointerAlignment: Right
AlwaysBreakAfterDefinitionReturnType: None
BinPackParameters: true
MaxEmptyLinesToKeep: 1
SpaceInEmptyParentheses: false
SpacesInContainerLiterals: true
SpaceBeforeParens: Custom
SpaceBeforeParensOptions:
  AfterIfMacros: true
  AfterFunctionDefinitionName: false
  AfterFunctionDeclarationName: false
  AfterForeachMacros: true
  AfterControlStatements: true
  BeforeNonEmptyParentheses: false
SpacesInParentheses: false
InsertBraces: true
ContinuationIndentWidth: 8
IndentCaseLabels: false
IndentFunctionDeclarationAfterType: false
IndentWidth: 8
UseTab: ForContinuationAndIndentation
ColumnLimit: 0
BreakBeforeBraces: Attach
BreakBeforeTernaryOperators: true
SpaceBeforeSquareBrackets: false
SpaceInEmptyBlock: false
AllowShortIfStatementsOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: true
AllowShortFunctionsOnASingleLine: Inline
AllowShortLoopsOnASingleLine: false
AlignAfterOpenBracket: DontAlign
AlignEscapedNewlines: DontAlign
AlignConsecutiveMacros: true
AlignTrailingComments: false
AlignOperands: false
Cpp11BracedListStyle: false
ForEachMacros: ['r_list_foreach', 'ls_foreach', 'fcn_tree_foreach_intersect', 'r_skiplist_foreach', 'graph_foreach_anode', 'r_list_foreach_safe', 'r_pvector_foreach', 'r_rbtree_foreach', 'r_interval_    tree_foreach']
SortIncludes: false
```
--------------------------------------------------------------------------------
/svc/README.md:
--------------------------------------------------------------------------------
```markdown
# SuperVisor Console for r2mcp
## Overview
The R2 MCP SBC (Supervisor Control) acts as a supervisor for every tool execution within the R2 MCP (Model Context Protocol) environment. It provides an additional layer of protection and control over what agents execute in headless MCP sessions, where users typically lack real-time oversight.
## Purpose
While some MCP agents offer built-in controls like accept, execute, cancel, reject, or JOLO (Just One Look Over) modes, these capabilities depend on the specific agent implementation rather than the MCP itself. The R2 MCP SBC extends these protection capabilities universally, allowing any agent to benefit from enhanced supervision.
## How It Works
When running the R2 MCP SBC service, it collects all calls made to R2 MCP tools. Upon receiving a connection from an MCP instance, the SBC provides users with options to:
- **Accept**: Allow the tool call to proceed as requested
- **Reject**: Block the tool call entirely
- **Modify**: Alter the query or parameters before execution
- **Respond with Error**: Return a custom error message
- **Provide Different Instructions**: Substitute alternative instructions
## Integration with R2 MCP
The SBC integrates seamlessly with R2 MCP through a simple flag-based mechanism:
- Run R2 MCP with a specific flag specifying the SBC host and port
- R2 MCP will then execute tool calls against the target SBC URL
- The SBC service receives these connections and prompts the user via command-line interface for the desired action
## Control Protocol
The supervision control is implemented using JSON over HTTP, ensuring compatibility and ease of integration.
## User Interface
The SBC provides a command-line tool interface for user interaction and decision-making.
## Default Behavior
By default, R2 MCP operates normally without supervision. When the SBC flag is provided:
- R2 MCP attempts to connect to the specified SBC endpoint
- If connection fails, R2 MCP falls back to normal operation (no supervision)
- If connection succeeds, R2 MCP waits for SBC responses before proceeding with tool executions
This design ensures that supervision is optional and doesn't break existing workflows when the SBC is unavailable.
## Building and Running
To build the R2 MCP-SBC tool:
```bash
make
```
This will create the `r2mcp-svc` executable.
To run the SBC server on a specific port:
```bash
./r2mcp-svc <port>
```
For example:
```bash
./r2mcp-svc 8080
```
The SBC will listen for HTTP POST requests containing JSON tool call data. When a request is received, it will prompt the user interactively for the desired action.
## Integration with R2 MCP
In R2 MCP, use the supervision flag to specify the SBC endpoint:
```
r2mcp --supervise http://localhost:8080
```
If the SBC is unreachable, R2 MCP will operate normally without supervision.
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Radare2 MCP Server
[](https://github.com/radareorg/radare2-mcp/actions/workflows/ci.yml)
[](https://github.com/radareorg/radare2)
<img width="400" alt="r2mcp logo" src="./r2mcp.png" />
An MCP server to use **radare2** with AI agents such as OpenCode, Mai, VSCode, Claude, CLION, ...
## Features
This implementation provides:
- 💻 Fully written in C using the native r2 APIs
- 🧩 Works from the CLI, as an r2 plugin and as an MCP server
- 🔍 Seamless binary analysis with radare2
- 🔗 Connect to any local or remote r2/iaito session via r2pipe
- 🔒 Supports readonly mode, sandbox lock and restrict tools
- 🔩 Fine grained tools configuration
- 🔁 Direct stdin/stdout communication model
- 🛠️ Optional raw access to run r2 commands or r2js scripts
## Installation
<img width="400" alt="Screenshot_2025-03-22_at_5 34 47_PM" src="https://github.com/user-attachments/assets/5322c3fc-fc07-4770-96a3-5a6d82d439c2" />
<img width="400" alt="Screenshot_2025-03-22_at_5 36 17_PM" src="https://github.com/user-attachments/assets/132a1de0-6978-4202-8dce-aa3d60551b9a" />
### Using r2pm
The simplest way to install the package is by using `r2pm`:
```bash
$ r2pm -Uci r2mcp
```
The `r2mcp` executable will be copied into r2pm's bindir in your home directory. However, this binary is not supposed to be executed directly from the shell; it will only work when launched by the MCP service handler of your language model of choice.
```bash
$ r2pm -r r2mcp
```
That's the common mcpServer JSON configuration file:
```json
{
  "mcpServers": {
    "radare2": {
      "command": "r2pm",
      "args": ["-r", "r2mcp"]
    }
  }
}
```
### Using Docker
Alternatively, you can build the Docker image:
```bash
docker build -t r2mcp .
```
Update your MCP client configuration file (see below) to use the Docker image to use:
- `"command": "docker"`
- `"args": ["run", "--rm", "-i", "-v", "/tmp/data:/data", "r2mcp"]`.
## Configuration
### Claude Desktop Integration
In the Claude Desktop app, press `CMD + ,` to open the Developer settings. Edit the configuration file and restart the client after editing the JSON file as explained below:
1. Locate your Claude Desktop configuration file:
   - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
   - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
2. Add the following to your configuration file:
```json
{
  "mcpServers": {
    "radare2": {
      "command": "r2pm",
      "args": ["-r", "r2mcp"]
    }
  }
}
```
## VS Code Integration
To use r2mcp with GitHub Copilot Chat in Visual Studio Code by [adding it to your user configuration](https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server-to-your-user-configuration) (see other options [here](https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server)):
1. Open the Command Palette with `CMD + Shift + P` (macOS) or `Ctrl + Shift + P` (Windows/Linux).
2. Search for and select `Copilot: Open User Configuration` (typically found in `~/Library/Application Support/Code/User/mcp.json` in macOS).
3. Add the following to your configuration file:
```json
{
  "servers": {
    "radare2": {
      "type": "stdio",
      "command": "r2pm",
      "args": ["-r", "r2mcp"]
    }
  },
  "inputs": []
}
```
## Zed Integration
You can use r2mcp with Zed as well by [adding it to your configuration](https://zed.dev/docs/ai/mcp):
1. Open the command palette: `CMD + Shift + P` (macOS) or `Ctrl + Shift + P` (Windows/Linux). 
2. Search of `agent: open configuration` or search of `settings`.
3. Add your server as such:
```json
  "context_servers": {
    "r2-mcp-server": {
      "source": "custom",
      "command": "r2pm",
      "args": ["-r", "r2mcp"],
      "env": {}
    }
  }
```
Note: you will need another LLM agent, such as Claude, Gemini or else to be able to use it.
## For Developers
### Build from Source
#### Linux/macOS
To test the server locally, you can build and install it with make:
```bash
make install
```
This will compile the server and place the `r2mcp` binary in `/usr/local/bin` on macOS.
#### Windows
For Windows, just use meson and ninja like it's done in the CI:
```cmd
meson b
ninja -C b
```
```
--------------------------------------------------------------------------------
/AGENTS.md:
--------------------------------------------------------------------------------
```markdown
# Agentic Coding Guidelines for the r2mcp (radare2 MCP) server
This document contains repository- and project-specific guidance for editing and building the r2mcp server. It augments the general AGENTS rules and encodes conventions observed in `src/`.
**Scope**
- The primary source lives in `src/`. Small helper headers and include-fragments (files named `*.inc.c`) are included into TUs and must be treated accordingly.
**Repository layout (important files)**
- `src/main.c` - program entry, CLI parsing, signal setup and high-level program lifecycle.
- `src/r2mcp.c` - main server machinery: JSON-RPC handling, event loop, dispatch to `tools` and `prompts` registries.
- `src/tools.c`, `src/prompts.c` - registries and implementations for tools and prompts.
- `src/readbuffer.c` - framed message reader used by the MCP direct mode loop.
- `src/r2api.inc.c`, `src/utils.inc.c` - implementation fragments included into `r2mcp.c`. These are not separate compilation units.
- `src/r2mcp.h`, `src/tools.h`, `src/readbuffer.h`, `src/prompts.h` - public headers for the modules above.
Coding style and rules (project-specific)
- Indentation: use **TABS** for indentation (project convention).
- Function calls: include a space before the parenthesis, e.g. `foo ()`.
- Always use braces `{}` for conditionals and loops, even if a single statement.
- `case` labels in `switch` statements must be aligned at the same column as other cases.
- Define loop variables before the `for` statement (older C style used in this codebase).
- Prefer `!strcmp ()` instead of `strcmp () == 0`.
- Use `R_RETURN_*` macros in public APIs (functions exported as `R_API`) to declare preconditions and avoid returning invalid values.
Memory and ownership
- `R_NEW`/`R_NEW0` macros in this project are assumed never to return NULL; code can rely on that.
- Do not check for NULL before calling `free` or other `*_free` helpers (the codebase follows this convention).
- `r_json_parse` does not take ownership of the input string: after calling `r_json_parse (buf)` and later `r_json_free (parser)`, the caller is still responsible for freeing the original buffer if it was dynamically allocated. When parsing string data that will be reused or freed, prefer calling `strdup` or ensure the buffer lifetime outlives the parser.
- When using `r_strbuf_free`, `r_core_free`, `r_list_free` or similar, pass only previously-initialized objects; do not NULL-check before freeing.
Build and test
- To quickly compile only the code in `src/`, run: `make -C src -j` (run this from the repo root or from `src/`). This avoids rebuilding unrelated targets.
- The primary output binary is `src/r2mcp`. Run `src/r2mcp -t` to list available tools and `src/r2mcp -h` for CLI help.
- Use `make -C src -j > /dev/null` when you want quieter output during iterative development.
Guidelines for editing the code
- Keep changes minimal and narrowly scoped; prefer fixing the root cause.
- When adding new tools or commands, implement a `?` subcommand to print help for that tool.
- Prefer using `r_str_newf` for formatted strings instead of manual `malloc` + `snprintf`.
- Avoid `r_str_append` for large concatenations; favour `RStrBuf *sb = r_strbuf_new (NULL);` and `r_strbuf_appendf` / `r_strbuf_append` loops, then `r_strbuf_drain` / `r_strbuf_free`.
- Use `r_str_pad2` to construct repeated-character strings when needed.
- When introducing new public APIs, follow the `R_API` and `R_RETURN_*` conventions already present in the repo.
Working with `*.inc.c` files
- Files such as `r2api.inc.c` and `utils.inc.c` are included into `r2mcp.c` (see `#include "utils.inc.c"`). They are not standalone translation units. Keep these files self-contained (no duplicate symbol definitions across other TUs) and avoid adding non-static global symbols there. If you need new public functions, prefer adding a `.c` + `.h` pair.
Logging and diagnostics
- This codebase uses `r2mcp_log`, `r2mcp_log_pub`, `r2mcp_log_reset` and `r2mcp_log_drain` for structured log capture surrounding r2 core operations. Use these helpers where appropriate so logs can be captured and emitted in responses.
JSON and protocol handling
- The server implements a JSON-RPC 2.0-like protocol. Use the existing helpers to build responses (`pj_new`/`pj_*` helpers in this repo) and follow existing patterns in `r2mcp.c` for success and error responses.
- For request parsing: `r_json_parse` returns a parser which must be freed with `r_json_free`. The code should then free the original message buffer if it was dynamically allocated.
- Distinguish between notifications (no `id` field) and requests (have `id`). Notifications must not produce a response.
Signals and event loop
- `setup_signals` is defined in `src/main.c`. Use `write` in signal handlers (no non-reentrant calls). Changing signal handling should be done with care.
- The main MCP direct mode loop is in `r2mcp_eventloop` in `r2mcp.c` and uses `readbuffer.c` to accumulate framed messages. When modifying framing or message parsing, update `readbuffer.c` accordingly and test the loop with piped input.
Incidental tips
- When making changes that affect only `src/` files, run `make -C src -j` from the repo root to recompile only `src/`.
- Avoid adding new dependencies. This project expects to build against existing radare2 headers (`r_core.h`, `r_util/*`).
- When adding tests or additional tooling, prefer placing small test drivers under `b/` (repo already uses `b/` for auxiliary build/test files).
Checklist before submitting a patch
- Run `make -C src -j` and exercise the binary: `src/r2mcp -t`, `src/r2mcp -h`, and a simple direct-mode message roundtrip using `printf` or `jq`.
- Ensure all new public APIs use `R_RETURN_*` where appropriate.
- Follow TAB indentation and other style rules above.
If something in the codebase looks inconsistent with these rules, point it out in the PR rather than applying large style-only changes across many files.
```
--------------------------------------------------------------------------------
/meson_options.txt:
--------------------------------------------------------------------------------
```
option('r2_prefix',
  type : 'string',
  value : '',
  description : 'Path prefix where radare2 is installed (include/ and lib/)',
)
```
--------------------------------------------------------------------------------
/autogen.sh:
--------------------------------------------------------------------------------
```bash
#!/bin/sh
[ -z "${EDITOR}" ] && EDITOR=vim
acr -p
V=`./configure -qV | cut -d - -f -1`
meson rewrite kwargs set project / version "$V"
${EDITOR} src/r2mcp.h
```
--------------------------------------------------------------------------------
/src/readbuffer.h:
--------------------------------------------------------------------------------
```
#ifndef R2MCP_READBUFFER_H
#define R2MCP_READBUFFER_H 1
#include <string.h>
#include <stdlib.h>
#define BUFFER_SIZE     65536
typedef struct {
	char *data;
	size_t size;
	size_t capacity;
} ReadBuffer;
ReadBuffer *read_buffer_new(void);
void read_buffer_free(ReadBuffer *buf);
void read_buffer_append(ReadBuffer *buf, const char *data, size_t len);
char *read_buffer_get_message(ReadBuffer *buf);
#endif
```
--------------------------------------------------------------------------------
/INSTALL.md:
--------------------------------------------------------------------------------
```markdown
Add the following JSON in your claude's config:
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
- Linux: `~/.config/Claude/claude_desktop_config.json`
```json
{
  "mcpServers": {
    "radare2": {
      "command": "r2pm",
      "args": ["-r", "r2mcp"]
    }
  }
}
```
To use r2mcp with OpenWebUI and local models run the mcp-server proxy like this:
```bash
pip install mcpo
mcpo -- r2pm -r r2mcp
```
```
--------------------------------------------------------------------------------
/test.sh:
--------------------------------------------------------------------------------
```bash
#!/usr/bin/env bash
set -euo pipefail
echo "== Build =="
make -C src -j > /dev/null
BIN="src/r2mcp"
echo "== List tools =="
${BIN} -t | sed -n '1,10p'
echo "== DSL: open_file + listFunctions (no -p) =="
${BIN} -T 'open_file file_path="/bin/ls"; list_functions only_named=true; close_file' | sed -n '1,8p'
echo "== DSL: unquoted value and multiple tools =="
${BIN} -T 'open_file file_path=/bin/ls; show_headers; get_current_address; close_file' | sed -n '1,12p'
echo "== DSL: ensure error when missing open_file =="
set +e
${BIN} -T 'list_functions only_named=true' | grep -q "open_file" && echo "(expected) tool enforces open_file first" || echo "(warning) missing expected open_file hint"
set -e
echo "== OK =="
```
--------------------------------------------------------------------------------
/src/prompts.h:
--------------------------------------------------------------------------------
```
#pragma once
#include <stdbool.h>
typedef struct {
	const char *name;
	const char *description;
	bool required;
} PromptArg;
typedef struct PromptSpec {
	const char *name;
	const char *description;
	const PromptArg *args;
	int nargs;
	// Render returns a JSON object string with { messages: [...] }
	char *(*render)(const struct PromptSpec *spec, RJson *arguments);
} PromptSpec;
#include "r2mcp.h"
// Initialize and shutdown the prompts registry stored in ServerState
void prompts_registry_init(ServerState *ss);
void prompts_registry_fini(ServerState *ss);
// Build list JSON for prompts with optional pagination
char *prompts_build_list_json(const ServerState *ss, const char *cursor, int page_size);
// Resolve a prompt by name and arguments, returning a JSON object string
char *prompts_get_json(const ServerState *ss, const char *name, RJson *arguments);
```
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
```markdown
# TODO
Ideas and future plans for r2mcp
* Extensions/Plugins
  * Let the user load an .r2.js script or yaml file to define new tools or prompts
  * Support loading custom plugins written in C or other languages
* Resources and Templates
  * Strings, symbols, relocs, imports, libraries, ..
  * Reversing context with user comments and project memory
* Prompts in filesystem instead of hardcoded inside the executable
  * Easier to maintain, user-customizable prompts for different analysis scenarios
  * Support templates (yaml definition for parameters?) to make prompts more concise
  * Automated report generation
* Advanced Analysis Tools
  * Find path between two points in the program
  * Progressive analysis and avoid analyzing twice (Optimized analysis for large binaries)
  * Support loading and unloading multiple files
* Projects Support
  * Function signature matching and library identification
  * Caching of analysis results
  * Export analysis results to various formats (JSON, XML, GraphML)
  * Import external analysis data
* Debugging Integration (Requires providing permissions to do it)
  * Support for emulation and native debugging
  * Spawn or Attach to running processes
  * Step-through debugging with breakpoints
  * Memory inspection and modification
  * Register state analysis
* User Interface and Testing Q&A
  * Documentation and tutorials
  * User-contributed scripts and templates
  * Comprehensive test suite for all tools
```
--------------------------------------------------------------------------------
/src/tools.h:
--------------------------------------------------------------------------------
```
#pragma once
#include <stdbool.h>
#include "r2mcp.h"
typedef enum {
	TOOL_MODE_MINI = 1 << 0,
	TOOL_MODE_HTTP = 1 << 1,
	TOOL_MODE_NORMAL = 1 << 2,
	TOOL_MODE_RO = 1 << 3,
} ToolMode;
typedef struct {
	const char *name;
	const char *description;
	const char *schema_json;
	int modes; // bitmask of ToolMode
} ToolSpec;
// Tool handler signature: returns heap-allocated JSON string describing
// the tool result (typically jsonrpc_tooltext_response() content or other
// structured JSON). Caller must free the returned string.
typedef char *(*ToolHandler)(ServerState *ss, RJson *args);
// Tool flags (for future use). For now, we use one to require an open file.
#define TOOL_FLAG_REQUIRES_OPENFILE (1 << 0)
// Initialize and shutdown the tools registry stored in ServerState
void tools_registry_init(ServerState *ss);
void tools_registry_fini(ServerState *ss);
// Build catalog JSON for the current server mode with optional pagination
char *tools_build_catalog_json(const ServerState *ss, const char *cursor, int page_size);
// Check if a tool is allowed for the current mode (honors permissive flag)
bool tools_is_tool_allowed(const ServerState *ss, const char *name);
// Call a tool by name; returns heap-allocated JSON (tool "result") or
// a JSON error result if the tool is unavailable or arguments are invalid.
// The returned string must be freed by the caller.
char *tools_call(ServerState *ss, const char *tool_name, RJson *args);
// Print a human friendly table of available tools for the current mode
// Columns: name | mini-mode (yes/no) | description
void tools_print_table(const ServerState *ss);
```
--------------------------------------------------------------------------------
/src/plugin.c:
--------------------------------------------------------------------------------
```cpp
/* radare - Copyright 2025 - pancake */
#define R_LOG_ORIGIN "core.r2mcp"
#include <r_core.h>
#include "r2mcp.h"
#include "tools.h"
int r2mcp_run_dsl_tests(ServerState *ss, const char *dsl, RCore *core);
typedef struct r2mcp_data_t {
	ServerState *ss;
} R2mcpData;
static bool r2mcp_call(RCorePluginSession *cps, const char *input) {
	RCore *core = cps->core;
	R2mcpData *data = cps->data;
	if (!r_str_startswith (input, "r2mcp")) {
		return false;
	}
	// Skip "r2mcp" command name
	const char *args = r_str_trim_head_ro (input + strlen ("r2mcp"));
	// Initialize server state if not already done
	if (!data->ss) {
		data->ss = R_NEW0 (ServerState);
		// Initialize the tools registry
		tools_registry_init (data->ss);
		// Set up the core reference
		data->ss->rstate.core = core;
		data->ss->rstate.file_opened = true; // We're already in r2 with a file
	}
	if (R_STR_ISEMPTY (args)) {
		tools_print_table (data->ss);
	} else {
		if (r2mcp_run_dsl_tests (data->ss, args, core) != 0) {
			R_LOG_ERROR ("Error executing r2mcp command");
		}
	}
	return true;
}
static bool r2mcp_init(RCorePluginSession *cps) {
	R2mcpData *data = R_NEW0 (R2mcpData);
	cps->data = data;
	return true;
}
static bool r2mcp_fini(RCorePluginSession *cps) {
	R2mcpData *data = cps->data;
	if (data) {
		if (data->ss) {
			tools_registry_fini (data->ss);
			free (data->ss);
		}
		free (data);
	}
	return true;
}
// PLUGIN Definition Info
RCorePlugin r_core_plugin_r2mcp = {
	.meta = {
		.name = "core-r2mcp",
		.desc = "r2mcp command integration for radare2",
		.author = "pancake",
		.license = "MIT",
	},
	.call = r2mcp_call,
	.init = r2mcp_init,
	.fini = r2mcp_fini,
};
#ifndef R2_PLUGIN_INCORE
R_API RLibStruct radare_plugin = {
	.type = R_LIB_TYPE_CORE,
	.data = &r_core_plugin_r2mcp,
	.version = R2_VERSION,
	.abiversion = R2_ABIVERSION
};
#endif
```
--------------------------------------------------------------------------------
/src/readbuffer.c:
--------------------------------------------------------------------------------
```cpp
#include <r_types.h>
#include "readbuffer.h"
ReadBuffer *read_buffer_new(void) {
	ReadBuffer *buf = R_NEW (ReadBuffer);
	buf->data = malloc (BUFFER_SIZE);
	buf->size = 0;
	buf->capacity = BUFFER_SIZE;
	return buf;
}
void read_buffer_free(ReadBuffer *buf) {
	if (buf) {
		free (buf->data);
		free (buf);
	}
}
void read_buffer_append(ReadBuffer *buf, const char *data, size_t len) {
	if (buf->size + len > buf->capacity) {
		size_t new_capacity = buf->capacity * 2;
		char *new_data = realloc (buf->data, new_capacity);
		if (!new_data) {
			// R_LOG_ERROR ("Failed to resize buffer");
			return;
		}
		buf->data = new_data;
		buf->capacity = new_capacity;
	}
	memcpy (buf->data + buf->size, data, len);
	buf->size += len;
}
// Modified read_buffer functions to handle partial reads better
char *read_buffer_get_message(ReadBuffer *buf) {
	// Search for a complete JSON-RPC message
	// We need to find a properly balanced set of braces {}
	if (buf->size == 0) {
		return NULL;
	}
	// Ensure the buffer is null-terminated for string operations
	if (buf->size < buf->capacity) {
		buf->data[buf->size] = '\0';
	} else {
		// Expand capacity if needed
		buf->capacity += 1;
		buf->data = realloc (buf->data, buf->capacity);
		buf->data[buf->size] = '\0';
	}
	// Look for a complete JSON message by counting braces
	int brace_count = 0;
	int start_pos = -1;
	size_t i;
	for (i = 0; i < buf->size; i++) {
		const char c = buf->data[i];
		// Find the first opening brace if we haven't already
		if (start_pos == -1 && c == '{') {
			start_pos = i;
			brace_count = 1;
			continue;
		}
		// Count braces within a JSON object
		if (start_pos != -1) {
			if (c == '{') {
				brace_count++;
			} else if (c == '}') {
				brace_count--;
				// If we've found a complete JSON object
				if (brace_count == 0) {
					// We have a complete message from start_pos to i (inclusive)
					size_t msg_len = i - start_pos + 1;
					char *msg = malloc (msg_len + 1);
					memcpy (msg, buf->data + start_pos, msg_len);
					msg[msg_len] = '\0';
					// Move any remaining data to the beginning of the buffer
					size_t remaining = buf->size - (i + 1);
					if (remaining > 0) {
						memmove (buf->data, buf->data + i + 1, remaining);
					}
					buf->size = remaining;
					// r2mcp_log ("Extracted complete JSON message");
					return msg;
				}
			}
		}
	}
	// If we get here, we don't have a complete message yet
	return NULL;
}
```
--------------------------------------------------------------------------------
/src/r2mcp.h:
--------------------------------------------------------------------------------
```
#pragma once
#include <stdbool.h>
#include <r_core.h>
#include <r_util/r_json.h>
#include <r_util/r_strbuf.h>
#include "readbuffer.h"
/* Version fallback if not provided by build */
#ifndef R2MCP_VERSION
#define R2MCP_VERSION "1.4.2"
#endif
/* Pagination limits for tool responses */
#define R2MCP_DEFAULT_PAGE_SIZE 1000
#define R2MCP_MAX_PAGE_SIZE 10000
typedef struct {
	const char *name;
	const char *version;
} ServerInfo;
typedef struct {
	bool tools;
	bool prompts;
	bool resources;
} ServerCapabilities;
typedef struct {
	RCore *core;
	bool file_opened;
	char *current_file;
} RadareState;
typedef struct {
	ServerInfo info;
	ServerCapabilities capabilities;
	const char *instructions;
	bool initialized;
	bool minimode;
	bool permissive_tools; // allow calling tools not exposed for current mode
	bool enable_run_command_tool;
	/* When true operate in read-only mode: only expose non-mutating tools */
	bool readonly_mode;
	/* When true, operate in HTTP r2pipe client mode and do NOT use r2 C APIs */
	bool http_mode;
	/* Base URL of the remote r2 webserver (if http_mode is true) */
	char *baseurl;
	/* Base URL of the supervisor control service (if set) */
	char *svc_baseurl;
	/* Optional sandbox path. When set, only allow opening files under this dir */
	char *sandbox;
	/* Optional path to append debug logs when set via -l */
	char *logfile;
	/* When true, ignore the analysis level specified in analyze calls */
	bool ignore_analysis_level;
	const RJson *client_capabilities;
	const RJson *client_info;
	RadareState rstate;
	RStrBuf *sb;
	RList *tools; // registry of ToolSpec* (RList*), opaque here
	/* Optional whitelist of tool names enabled via command line -e options.
	 * When non-NULL, only tools whose name appears in this list will be
	 * registered in the runtime tools registry. Items are heap-allocated
	 * strings and the list should be created with `r_list_newf(free)`.
	 */
	RList *enabled_tools;
	void *prompts; // registry of PromptSpec* (RList*), opaque here
} ServerState;
/* Entry point wrapper implemented in r2mcp.c */
int r2mcp_main(int argc, const char **argv);
/* Exposed helpers implemented in r2mcp.c */
void setup_signals(void);
void r2mcp_eventloop(ServerState *ss);
void r2mcp_help(void);
void r2mcp_version(void);
void r2mcp_running_set(int value);
/* Public wrappers for internal r2 helpers (implemented in r2mcp.c) */
bool r2mcp_state_init(ServerState *ss);
void r2mcp_state_fini(ServerState *ss);
char *r2mcp_cmd(ServerState *ss, const char *cmd);
char *r2mcp_cmdf(ServerState *ss, const char *fmt, ...);
void r2mcp_log_pub(ServerState *ss, const char *msg);
// Additional public wrappers exposed so other modules (eg. tools.c) can use
// functionality implemented in r2api.inc.c. These simply forward to the
// internal static helpers so we keep the original separation.
bool r2mcp_open_file(ServerState *ss, const char *filepath);
char *r2mcp_analyze(ServerState *ss, int level);
// Run a small domain-specific language (DSL) used for testing tools from the
// command-line. The DSL describes a sequence of tool calls with arguments and
// prints their results. If core is provided, output goes to r2 console, else stdout.
// Returns 0 on success, non-zero on failure.
int r2mcp_run_dsl_tests(ServerState *ss, const char *dsl, RCore *core);
// HTTP POST helper for svc communication
char *curl_post_capture(const char *url, const char *msg, int *exit_code_out);
```
--------------------------------------------------------------------------------
/src/utils.inc.c:
--------------------------------------------------------------------------------
```cpp
// Helper function to create a simple text tool result
static inline char *jsonrpc_tooltext_response(const char *text) {
	PJ *pj = pj_new ();
	pj_o (pj);
	pj_k (pj, "content");
	pj_a (pj);
	pj_o (pj);
	pj_ks (pj, "type", "text");
	pj_ks (pj, "text", text);
	pj_end (pj);
	pj_end (pj);
	pj_end (pj);
	return pj_drain (pj);
}
// Helper function to create a paginated text tool result
static inline char *jsonrpc_tooltext_response_paginated(const char *text, bool has_more, const char *next_cursor) {
	PJ *pj = pj_new ();
	pj_o (pj);
	pj_k (pj, "content");
	pj_a (pj);
	pj_o (pj);
	pj_ks (pj, "type", "text");
	pj_ks (pj, "text", text);
	pj_end (pj);
	pj_end (pj);
	if (has_more || next_cursor) {
		pj_k (pj, "pagination");
		pj_o (pj);
		if (has_more) {
			pj_kb (pj, "hasMore", true);
		}
		if (next_cursor) {
			pj_ks (pj, "nextCursor", next_cursor);
		}
		pj_end (pj);
	}
	pj_end (pj);
	return pj_drain (pj);
}
// Render tool output as a JSON array of lines for frontend filtering compatibility
static inline char *jsonrpc_tooltext_response_lines(const char *text) {
	PJ *pj = pj_new ();
	pj_o (pj);
	pj_k (pj, "content");
	pj_a (pj);
	if (text) {
		RList *lines = r_str_split_list ((char *)text, "\n", 0);
		if (lines) {
			RListIter *it;
			char *line;
			r_list_foreach (lines, it, line) {
				pj_s (pj, line);
			}
			r_list_free (lines);
		}
	}
	pj_end (pj);
	pj_end (pj);
	return pj_drain (pj);
}
#if R2_VERSION_NUMBER < 50909
static st64 r_json_get_num(const RJson *json, const char *key) {
	if (!json || !key) {
		return 0;
	}
	const RJson *field = r_json_get (json, key);
	if (!field) {
		return 0;
	}
	switch (field->type) {
	case R_JSON_STRING:
		return r_num_get (NULL, field->str_value);
	case R_JSON_INTEGER:
		return field->num.s_value;
	case R_JSON_BOOLEAN:
		return field->num.u_value;
	case R_JSON_DOUBLE:
		return (int)field->num.dbl_value;
	default:
		return 0;
	}
}
static const char *r_json_get_str(const RJson *json, const char *key) {
	if (!json || !key) {
		return NULL;
	}
	const RJson *field = r_json_get (json, key);
	if (!field || field->type != R_JSON_STRING) {
		return NULL;
	}
	return field->str_value;
}
#endif
// Helper to paginate text by lines
static inline char *paginate_text_by_lines(char *text, const char *cursor, int page_size, bool *has_more, char **next_cursor) {
	if (!text) {
		return NULL;
	}
	RList *lines = r_str_split_list (text, "\n", 0);
	if (!lines) {
		return NULL;
	}
	int total_lines = r_list_length (lines);
	int start_line = 0;
	if (cursor) {
		start_line = atoi (cursor);
		if (start_line < 0) {
			start_line = 0;
		}
	}
	int end_line = start_line + page_size;
	if (end_line > total_lines) {
		end_line = total_lines;
	}
	RStrBuf *sb = r_strbuf_new ("");
	int idx = 0;
	RListIter *it;
	char *line;
	r_list_foreach (lines, it, line) {
		if (idx >= start_line && idx < end_line) {
			if (r_strbuf_length (sb) > 0) {
				r_strbuf_append (sb, "\n");
			}
			r_strbuf_append (sb, line);
		}
		idx++;
	}
	r_list_free (lines);
	char *result = r_strbuf_drain (sb);
	if (has_more) {
		*has_more = end_line < total_lines;
	}
	if (next_cursor) {
		if (end_line < total_lines) {
			*next_cursor = r_str_newf ("%d", end_line);
		} else {
			*next_cursor = NULL;
		}
	}
	return result;
}
// JSON-RPC error response builder. Returns heap-allocated JSON string (caller frees).
static char *jsonrpc_error_response(int code, const char *message, const char *id, const char *uri) {
	PJ *pj = pj_new ();
	pj_o (pj);
	pj_ks (pj, "jsonrpc", "2.0");
	if (id) {
		pj_ks (pj, "id", id);
	}
	pj_k (pj, "error");
	pj_o (pj);
	pj_ki (pj, "code", code);
	pj_ks (pj, "message", message);
	if (uri) {
		pj_k (pj, "data");
		pj_o (pj);
		pj_ks (pj, "uri", uri);
		pj_end (pj);
	}
	pj_end (pj);
	pj_end (pj);
	return pj_drain (pj);
}
// Intentionally no generic require_str_param helper; callers validate params inline
```
--------------------------------------------------------------------------------
/src/dsltest.c:
--------------------------------------------------------------------------------
```cpp
#include "r2mcp.h"
// Simple DSL runner for invoking tools from the CLI for testing.
// DSL grammar (very small):
//   program := stmt (';' stmt)*
//   stmt := TOOLNAME (WS key=val)*
// Values may be quoted with double-quotes. Keys/values do not support
// nested structures. Example:
//   open_file file_path="/bin/ls"; list_functions only_named=true; close_file
char *tools_call(ServerState *ss, const char *tool_name, RJson *tool_args);
// Parse a single statement of the form: toolName [key=val ...]
// Returns 0 on success.
static int run_statement(ServerState *ss, char *stmt, RCore *core) {
	r_str_trim (stmt);
	if (R_STR_ISEMPTY (stmt)) {
		return 0;
	}
	// extract tool name (first token up to whitespace)
	char *p = stmt;
	while (*p && !isspace ((unsigned char)*p)) {
		p++;
	}
	char saved = *p;
	if (saved) {
		*p++ = '\0';
	}
	const char *tool = stmt;
	RStrBuf *sb = r_strbuf_new ("{");
	bool first = true;
	// parse key=val tokens
	while (*p) {
		p = (char *)r_str_trim_head_ro (p);
		if (!*p) {
			break;
		}
		// key
		char *key = p;
		// advance to '=' or whitespace
		while (*p && *p != '=' && !isspace ((ut8)*p)) {
			p++;
		}
		if (!*p || *p != '=') {
			// no key=value, treat rest as error or ignore
			break;
		}
		*p++ = '\0';
		// value
		char *val = p;
		if (*p == '"') {
			p++;
			val = p;
			while (*p && *p != '"') {
				if (*p == '\\' && p[1]) {
					p += 2;
				} else {
					p++;
				}
			}
			if (*p == '"') {
				*p++ = '\0';
			}
		} else {
			p = (char *)r_str_trim_head_ro (p);
			if (*p) {
				*p++ = '\0';
			}
		}
		// append to JSON
		if (!first) {
			r_strbuf_append (sb, ",");
		}
		first = false;
		// determine if val is a bare true/false or number
		bool bare = false;
		if (!strcmp (val, "true") || !strcmp (val, "false")) {
			bare = true;
		} else {
			// check if integer
			char *q = (char *)val;
			bool digits = true;
			if (*q == '-' || *q == '+') {
				q++;
			}
			while (*q) {
				if (!isdigit ((ut8)*q)) {
					digits = false;
					break;
				}
				q++;
			}
			if (digits && *val) {
				bare = true;
			}
		}
		if (bare) {
			r_strbuf_appendf (sb, "\"%s\":%s", key, val);
		} else {
			char *esc = strdup (val); // r_str_escape_json (val, -1);
			r_strbuf_appendf (sb, "\"%s\":\"%s\"", key, esc);
			free (esc);
		}
	}
	// no need to restore the separator char
	r_strbuf_append (sb, "}");
	char *jsonbuf = r_strbuf_drain (sb);
	// debug: built args JSON (left commented intentionally)
	// printf ("[DSL] args json: %s\n", jsonbuf);
	RJson *args = NULL;
	if (strlen (jsonbuf) > 2) {
		// parse it (parser does not take ownership; keep jsonbuf alive until free)
		args = r_json_parse (jsonbuf);
		if (!args) {
			printf ("[DSL] Failed to parse arguments for tool %s\n", tool);
			free (jsonbuf);
			return -1;
		}
	}
	// call the tool
	char *res = tools_call (ss, tool, args);
	if (args) {
		r_json_free (args);
	}
	if (res) {
		if (core) {
			// Extract text from JSON response
			const char *text_start = strstr (res, "\"text\":\"");
			if (text_start) {
				text_start += 8; // skip "text":"
				const char *text_end = strstr (text_start, "\"");
				if (text_end) {
					char *text = r_str_ndup (text_start, text_end - text_start);
					r_str_replace_in (text, -1, "\\n", "\n", true);
					r_cons_printf (core->cons, "%s\n", text);
					free (text);
				} else {
					r_cons_printf (core->cons, "(malformed text in response)\n");
				}
			} else {
				r_cons_printf (core->cons, "(no text in response)\n");
			}
		} else {
			printf ("[DSL] %s -> %s\n", tool, res);
		}
		free (res);
	} else {
		if (core) {
			r_cons_printf (core->cons, "(no result)\n");
		} else {
			printf ("[DSL] %s -> (no result)\n", tool);
		}
	}
	free (jsonbuf);
	return 0;
}
int r2mcp_run_dsl_tests(ServerState *ss, const char *dsl, RCore *core) {
	R_RETURN_VAL_IF_FAIL (dsl, 1);
	char *copy = strdup (dsl);
	char *cur = copy;
	char *semi;
	int rc = 0;
	while ((semi = strchr (cur, ';')) != NULL) {
		*semi = '\0';
		if (run_statement (ss, cur, core) != 0) {
			rc = 1;
		}
		cur = semi + 1;
	}
	if (*cur) {
		if (run_statement (ss, cur, core) != 0) {
			rc = 1;
		}
	}
	free (copy);
	return rc;
}
```
--------------------------------------------------------------------------------
/src/curl.inc.c:
--------------------------------------------------------------------------------
```cpp
/* r2mcp - MIT - Copyright 2025 - pancake, dnakov */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#if defined(R2__WINDOWS__)
#include <windows.h>
#include <io.h>
#include <fcntl.h>
#elif defined(R2__UNIX__)
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#else
#error please define R2__WINDOWS__ or R2__UNIX__ for platform detection
#endif
/**
 * Execute: curl -sS -d "<msg>" <url>
 * Capture stdout (HTTP response body) and return it as a malloc'd NUL-terminated string.
 *
 * @param url  Target URL (non-NULL)
 * @param msg  Message for -d (non-NULL), e.g. "key=value" or JSON string
 * @param exit_code_out Optional: receives curl's exit code (0 on success). Pass NULL if not needed.
 *
 * @return malloc'd buffer with the response (caller must free), or NULL on error.
 *         On error, *exit_code_out (if provided) is set to a negative number when possible.
 */
char *curl_post_capture(const char *url, const char *msg, int *exit_code_out) {
	if (exit_code_out) {
		*exit_code_out = -1;
	}
	if (!url || !msg) {
		errno = EINVAL;
		return NULL;
	}
	char *buf = NULL;
	size_t len = 0;
	int exit_code = -1;
#if R2__WINDOWS__
	SECURITY_ATTRIBUTES sa;
	sa.nLength = sizeof (SECURITY_ATTRIBUTES);
	sa.lpSecurityDescriptor = NULL;
	sa.bInheritHandle = TRUE;
	HANDLE read_h = NULL, write_h = NULL;
	if (!CreatePipe (&read_h, &write_h, &sa, 0)) {
		return NULL;
	}
	// Ensure the read handle is not inherited
	SetHandleInformation (read_h, HANDLE_FLAG_INHERIT, 0);
	char *escaped_msg = r_str_escape_sh (msg);
	char *escaped_url = r_str_escape_sh (url);
	char *cmd = r_str_newf ("curl -sS -d \"%s\" \"%s\"", escaped_msg, escaped_url);
	free (escaped_msg);
	free (escaped_url);
	STARTUPINFOA si;
	PROCESS_INFORMATION pi;
	ZeroMemory (&si, sizeof (si));
	si.cb = sizeof (si);
	si.hStdOutput = write_h;
	si.hStdError = GetStdHandle (STD_ERROR_HANDLE);
	si.dwFlags |= STARTF_USESTDHANDLES;
	ZeroMemory (&pi, sizeof (pi));
	// Create process
	BOOL ok = CreateProcessA (NULL, cmd, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi);
	free (cmd);
	// Close the write handle in parent after creating the child
	CloseHandle (write_h);
	if (!ok) {
		CloseHandle (read_h);
		return NULL;
	}
	// Read child's stdout
	size_t cap = 8192;
	buf = malloc (cap);
	if (!buf) {
		CloseHandle (read_h);
		CloseHandle (pi.hProcess);
		CloseHandle (pi.hThread);
		WaitForSingleObject (pi.hProcess, INFINITE);
		return NULL;
	}
	for (;;) {
		if (len + 4096 + 1 > cap) {
			size_t ncap = cap * 2;
			char *tmp = realloc (buf, ncap);
			if (!tmp) {
				R_FREE (buf);
				break;
			}
			buf = tmp;
			cap = ncap;
		}
		DWORD nread = 0;
		BOOL r = ReadFile (read_h, buf + len, 4096, &nread, NULL);
		if (r && nread > 0) {
			len += (size_t)nread;
			continue;
		}
		if (!r) {
			DWORD err = GetLastError ();
			if (err == ERROR_BROKEN_PIPE) {
				break; // EOF
			}
			R_FREE (buf);
			break;
		}
		// r == TRUE but nread == 0 -> EOF
		break;
	}
	CloseHandle (read_h);
	// Wait for process and get exit code
	WaitForSingleObject (pi.hProcess, INFINITE);
	DWORD exitcode = 0;
	if (!GetExitCodeProcess (pi.hProcess, &exitcode)) {
		exitcode = (DWORD)-1;
	}
	exit_code = (int)exitcode;
	CloseHandle (pi.hProcess);
	CloseHandle (pi.hThread);
#elif R2__UNIX__
	int pipefd[2];
	if (pipe (pipefd) == -1) {
		return NULL;
	}
	pid_t pid = fork ();
	if (pid == -1) {
		int e = errno;
		close (pipefd[0]);
		close (pipefd[1]);
		errno = e;
		return NULL;
	}
	if (pid == 0) {
		// Child: stdout -> pipe write end
		// stderr unchanged (so errors still show on parent stderr because of -sS)
		if (dup2 (pipefd[1], STDOUT_FILENO) == -1) {
			_exit (127);
		}
		close (pipefd[0]);
		close (pipefd[1]);
		// Build argv; no shell is involved (safe for spaces/quotes in msg/url).
		// Note: if msg starts with '@', curl treats it as a file. If that's unwanted,
		// use "--data-raw" instead of "-d".
		char *const argv[] = {
			"curl",
			"-sS",
			"-d", (char *)msg,
			(char *)url,
			NULL
		};
		execvp ("curl", argv);
		// If exec fails:
		_exit (127);
	}
	// Parent
	close (pipefd[1]); // we read from pipefd[0]
	// Read child's stdout fully into a dynamic buffer
	size_t cap = 8192;
	buf = malloc (cap);
	if (!buf) {
		close (pipefd[0]);
		// Reap child to avoid a zombie
		int st;
		waitpid (pid, &st, 0);
		return NULL;
	}
	for (;;) {
		if (len + 4096 + 1 > cap) {
			size_t ncap = cap * 2;
			char *tmp = realloc (buf, ncap);
			if (!tmp) {
				R_FREE (buf);
				break;
			}
			buf = tmp;
			cap = ncap;
		}
		ssize_t n = read (pipefd[0], buf + len, 4096);
		if (n > 0) {
			len += (size_t)n;
		} else if (n == 0) {
			break; // EOF
		} else if (errno != EINTR) {
			free (buf);
			buf = NULL; // read error
			break;
		}
	}
	close (pipefd[0]);
	// Reap curl
	int status = 0;
	if (waitpid (pid, &status, 0) != -1) {
		if (WIFEXITED (status)) {
			exit_code = WEXITSTATUS (status);
		} else if (WIFSIGNALED (status)) {
			exit_code = 128 + WTERMSIG (status);
		}
	}
#else
#error unsupported platform for curl_post_capture
#endif
	if (exit_code_out) {
		*exit_code_out = exit_code;
	}
	if (!buf) {
		return NULL;
	}
	// NUL-terminate (even if empty)
	buf[len] = '\0';
	return buf;
}
```
--------------------------------------------------------------------------------
/svc/r2mcp-svc.c:
--------------------------------------------------------------------------------
```cpp
#include <r_socket.h>
#include <r_util/r_json.h>
#include <r_util/r_str.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
// ANSI color codes
#define COLOR_RED "\033[31m"
#define COLOR_GREEN "\033[32m"
#define COLOR_YELLOW "\033[33m"
#define COLOR_BLUE "\033[34m"
#define COLOR_MAGENTA "\033[35m"
#define COLOR_CYAN "\033[36m"
#define COLOR_RESET "\033[0m"
#define COLOR_BOLD "\033[1m"
// Emojis
#define EMOJI_ROCKET "🚀"
#define EMOJI_WRENCH "🔧"
#define EMOJI_CHECK "✅"
#define EMOJI_CROSS "❌"
#define EMOJI_LIGHTNING "⚡"
#define EMOJI_PENCIL "✏️"
#define EMOJI_COMPUTER "🖥️"
#define EMOJI_QUESTION "❓"
#define EMOJI_WARNING "⚠️"
static bool yolo_mode = false;
int main(int argc, char **argv) {
	int port_index = 1;
	if (argc >= 2 && !strcmp (argv[1], "-y")) {
		yolo_mode = true;
		port_index = 2;
	}
	if (argc != port_index + 1) {
		fprintf (stderr, "Usage: %s [-y] <port>\n", argv[0]);
		return 1;
	}
	int port = atoi (argv[port_index]);
	if (port <= 0) {
		fprintf (stderr, "Invalid port\n");
		return 1;
	}
	RSocket *server = r_socket_new (false);
	if (!server) {
		R_LOG_ERROR ("Cannot create socket");
		return 1;
	}
	if (!r_socket_listen (server, argv[port_index], NULL)) {
		R_LOG_ERROR ("Cannot listen on port %s", argv[port_index]);
		r_socket_free (server);
		return 1;
	}
	R_LOG_INFO (COLOR_GREEN EMOJI_ROCKET " R2 MCP-SBC listening on port %s" COLOR_RESET, argv[port_index]);
	RSocketHTTPOptions so = { 0 };
	so.timeout = 3;
	while (true) {
		eprintf (COLOR_CYAN "🛡️ [r2mcp-supervisor]> " COLOR_RESET);
		fflush (stderr);
		RSocketHTTPRequest *rs = r_socket_http_accept (server, &so);
		if (!rs) {
			continue;
		}
		// Only accept POST requests
		if (strcmp (rs->method, "POST")) {
			r_socket_http_response (rs, 405, "Method not allowed", 0, NULL);
			r_socket_http_free (rs);
			continue;
		}
		if (!rs->data) {
			r_socket_http_response (rs, 400, "No data", 0, NULL);
			r_socket_http_free (rs);
			continue;
		}
		// Parse JSON
		char *body_copy = strdup ((char *)rs->data); // r_json_parse modifies the string
		RJson *j = r_json_parse (body_copy);
		if (!j) {
			free (body_copy);
			r_socket_http_response (rs, 400, "Invalid JSON", 0, NULL);
			r_socket_http_free (rs);
			continue;
		}
		const char *tool = r_json_get_str (j, "tool");
		char *response_body = NULL;
		if (yolo_mode) {
			// Auto accept
			printf (COLOR_YELLOW EMOJI_LIGHTNING " YOLO: Received message:" COLOR_RESET " %s\n", (char *)rs->data);
			printf (COLOR_YELLOW EMOJI_LIGHTNING " YOLO: Tool executed:" COLOR_RESET " %s\n", tool? tool: "unknown");
			response_body = strdup ((char *)rs->data);
		} else {
			printf ("\n" COLOR_BOLD COLOR_YELLOW "╔══════════════════════════════════════╗\n" COLOR_RESET);
			printf (COLOR_BOLD COLOR_YELLOW "║" COLOR_RESET " " COLOR_CYAN EMOJI_WRENCH " Tool Call Request" COLOR_RESET " " COLOR_BOLD COLOR_YELLOW "║\n" COLOR_RESET);
			printf (COLOR_BOLD COLOR_YELLOW "╚══════════════════════════════════════╝\n" COLOR_RESET);
			printf (COLOR_GREEN "Tool:" COLOR_RESET " %s\n", tool? tool: "unknown");
			printf (COLOR_BLUE "Request:" COLOR_RESET " %s\n", (char *)rs->data);
			printf ("\n" COLOR_BOLD "Available Actions:\n" COLOR_RESET);
			printf (COLOR_GREEN "1. " EMOJI_CHECK " Accept" COLOR_RESET "\n");
			printf (COLOR_RED "2. " EMOJI_CROSS " Reject" COLOR_RESET "\n");
			printf (COLOR_YELLOW "3. " EMOJI_LIGHTNING " Accept all (YOLO mode)" COLOR_RESET "\n");
			printf (COLOR_MAGENTA "4. " EMOJI_PENCIL " Modify tool" COLOR_RESET "\n");
			printf (COLOR_CYAN "5. " EMOJI_COMPUTER " Run r2 command" COLOR_RESET "\n");
			printf (COLOR_BOLD EMOJI_QUESTION " Your choice:" COLOR_RESET " ");
			fflush (stdout);
			char input[256];
			if (!fgets (input, sizeof (input), stdin)) {
				response_body = strdup ("{\"error\":\"input failed\"}");
			} else {
				int choice = atoi (input);
				switch (choice) {
				case 1: // Accept
					response_body = strdup ((char *)rs->data);
					break;
				case 2: // Reject
					response_body = strdup ("{\"error\":\"rejected by user\"}");
					break;
				case 3: // YOLO
					yolo_mode = true;
					response_body = strdup ((char *)rs->data);
					break;
				case 4: // Modify
					printf (COLOR_MAGENTA EMOJI_PENCIL " Enter new tool name:" COLOR_RESET " ");
					fflush (stdout);
					char new_tool[256];
					if (fgets (new_tool, sizeof (new_tool), stdin)) {
						new_tool[strcspn (new_tool, "\n")] = 0;
						char *tool_key = "\"tool\":\"";
						char *pos = strstr ((char *)rs->data, tool_key);
						if (pos) {
							char *start = pos + strlen (tool_key);
							char *end = strchr (start, '"');
							if (end) {
								size_t prefix_len = start - (char *)rs->data;
								size_t suffix_len = strlen (end);
								response_body = malloc (prefix_len + strlen (new_tool) + suffix_len + 1);
								if (response_body) {
									memcpy (response_body, rs->data, prefix_len);
									strcpy (response_body + prefix_len, new_tool);
									strcpy (response_body + prefix_len + strlen (new_tool), end);
								} else {
									response_body = strdup ("{\"error\":\"alloc failed\"}");
								}
							} else {
								response_body = strdup ("{\"error\":\"invalid json\"}");
							}
						} else {
							response_body = strdup ("{\"error\":\"no tool field\"}");
						}
					} else {
						response_body = strdup ("{\"error\":\"input failed\"}");
					}
					break;
				case 5: // Run r2 command
					printf (COLOR_CYAN EMOJI_COMPUTER " Enter r2 command:" COLOR_RESET " ");
					fflush (stdout);
					char r2cmd[1024];
					if (fgets (r2cmd, sizeof (r2cmd), stdin)) {
						r2cmd[strcspn (r2cmd, "\n")] = 0;
						// Simple JSON escape: replace " with \"
						char *escaped = r_str_replace (strdup (r2cmd), "\"", "\\\"", 1);
						response_body = r_str_newf ("{\"r2cmd\":\"%s\"}", escaped);
						free (escaped);
					} else {
						response_body = strdup ("{\"error\":\"input failed\"}");
					}
					break;
				default:
					response_body = strdup ("{\"error\":\"invalid choice\"}");
				}
			}
		}
		if (response_body) {
			r_socket_http_response (rs, 200, response_body, 0, "Content-Type: application/json\r\n");
			free (response_body);
		}
		r_json_free (j);
		free (body_copy);
		r_socket_http_free (rs);
	}
	r_socket_free (server);
	return 0;
}
```
--------------------------------------------------------------------------------
/src/main.c:
--------------------------------------------------------------------------------
```cpp
#include "config.h"
#include "r2mcp.h"
#include "tools.h"
#include "prompts.h"
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#if R2__UNIX__
/* Signal handling moved from r2mcp.c */
static void signal_handler(int signum) {
	const char msg[] = "\nInterrupt received, shutting down...\n";
	write (STDERR_FILENO, msg, sizeof (msg) - 1);
	r2mcp_running_set (0);
	signal (signum, SIG_DFL);
}
void setup_signals(void) {
	struct sigaction sa = { 0 };
	sa.sa_flags = 0;
	sa.sa_handler = signal_handler;
	sigemptyset (&sa.sa_mask);
	sigaction (SIGINT, &sa, NULL);
	sigaction (SIGTERM, &sa, NULL);
	sigaction (SIGHUP, &sa, NULL);
	signal (SIGPIPE, SIG_IGN);
}
#endif
/* Help and version moved from r2mcp.c */
void r2mcp_help(void) {
	const char help_text[] =
		"Usage: r2mcp [-flags]\n"
		" -c [cmd]   run those commands before entering the mcp loop\n"
		" -d [pdc]   select a different decompiler (pdc by default)\n"
		" -u [url]   use remote r2 webserver base URL (HTTP r2pipe client mode)\n"
		" -l [file]  append debug logs to this file\n"
		" -s [dir]   enable sandbox mode; only allow files under [dir]\n"
		" -e [tool]  enable only the specified tool (repeatable)\n"
		" -h         show this help\n"
		" -r         enable the dangerous runCommand tool\n"
		" -R         enable read-only mode (expose only non-mutating tools)\n"
		" -m         expose minimum amount of tools\n"
		" -t         list available tools and exit\n"
		" -T [tests] run DSL tests and exit\n"
		" -p         permissive tools: allow calling non-listed tools\n"
		" -n         do not load any plugin or radare2rc\n"
		" -i         ignore analysis level specified in analyze calls\n"
		" -S [url]   enable supervisor control; connect to svc at [url]\n"
		" -v         show version\n";
	printf ("%s", help_text);
}
void r2mcp_version(void) {
	printf ("%s\n", R2MCP_VERSION);
}
/* Program entry point wrapper */
int main(int argc, const char **argv) {
	return r2mcp_main (argc, argv);
}
/* Moved from r2mcp.c to isolate main concerns here */
int r2mcp_main(int argc, const char **argv) {
	bool minimode = false;
	bool enable_run_command_tool = false;
	bool readonly_mode = false;
	bool list_tools = false;
	RList *cmds = r_list_new ();
	/* Whitelist of enabled tool names (populated via repeated -e flags) */
	RList *enabled_tools = NULL;
	bool loadplugins = true;
	const char *deco = NULL;
	bool http_mode = false;
	bool permissive = false;
	char *baseurl = NULL;
	char *svc_baseurl = NULL;
	char *sandbox = NULL;
	char *logfile = NULL;
	bool ignore_analysis_level = false;
	const char *dsl_tests = NULL;
	RGetopt opt;
	r_getopt_init (&opt, argc, argv, "hmvpd:nc:u:l:s:rite:RT:S:");
	int c;
	while ((c = r_getopt_next (&opt)) != -1) {
		switch (c) {
		case 'h':
			r2mcp_help ();
			return 0;
		case 'c':
			r_list_append (cmds, (char *)opt.arg);
			break;
		case 'v':
			r2mcp_version ();
			return 0;
		case 'd':
			deco = opt.arg;
			break;
		case 'u':
			http_mode = true;
			baseurl = strdup (opt.arg);
			eprintf ("[R2MCP] HTTP r2pipe client mode enabled, baseurl=%s\n", baseurl);
			break;
		case 'l':
			logfile = strdup (opt.arg);
			break;
		case 's':
			sandbox = strdup (opt.arg);
			break;
		case 'n':
			loadplugins = true;
			break;
		case 'm':
			minimode = true;
			break;
		case 'p':
			permissive = true;
			break;
		case 'r':
			enable_run_command_tool = true;
			break;
		case 'R':
			readonly_mode = true;
			break;
		case 'i':
			ignore_analysis_level = true;
			break;
		case 't':
			list_tools = true;
			break;
		case 'T':
			dsl_tests = opt.arg;
			break;
		case 'S':
			if (opt.arg) {
				if (strspn (opt.arg, "0123456789") == strlen (opt.arg)) {
					svc_baseurl = r_str_newf ("http://localhost:%s", opt.arg);
				} else {
					svc_baseurl = strdup (opt.arg);
				}
			}
			break;
		case 'e':
			if (opt.arg) {
				if (!enabled_tools) {
					enabled_tools = r_list_newf (free);
				}
				r_list_append (enabled_tools, strdup (opt.arg));
			}
			break;
		default:
			R_LOG_ERROR ("Invalid flag -%c", c);
			return 1;
		}
	}
	ServerState ss = {
		.info = {
			.name = "Radare2 MCP Connector",
			.version = R2MCP_VERSION },
		.capabilities = { .tools = true, .prompts = true, .resources = true },
		.instructions = "Use this server to analyze binaries with radare2",
		.initialized = false,
		.minimode = minimode,
		.readonly_mode = readonly_mode,
		.permissive_tools = permissive,
		.enable_run_command_tool = enable_run_command_tool,
		.http_mode = http_mode,
		.baseurl = baseurl,
		.svc_baseurl = svc_baseurl,
		.sandbox = sandbox,
		.logfile = logfile,
		.ignore_analysis_level = ignore_analysis_level,
		.client_capabilities = NULL,
		.client_info = NULL,
		.enabled_tools = enabled_tools,
	};
	/* Enable logging */
	r2mcp_log_pub (&ss, "r2mcp starting");
#if R2__UNIX__
	setup_signals ();
#endif
	/* Initialize registries */
	tools_registry_init (&ss);
	if (list_tools) {
		/* Print tools and exit early */
		tools_print_table (&ss);
		return 0;
	}
	prompts_registry_init (&ss);
	/* Initialize r2 (unless running in HTTP client mode) */
	if (!ss.http_mode) {
		if (!r2mcp_state_init (&ss)) {
			R_LOG_ERROR ("Failed to initialize radare2");
			r2mcp_log_pub (&ss, "Failed to initialize radare2");
			return 1;
		}
		if (loadplugins) {
			r_core_loadlibs (ss.rstate.core, R_CORE_LOADLIBS_ALL, NULL);
			r_core_parse_radare2rc (ss.rstate.core);
		}
		if (deco) {
			if (!strcmp (deco, "decai")) {
				deco = "decai -d";
			}
			char *pdc = r_str_newf ("e cmd.pdc=%s", deco);
			eprintf ("[R2MCP] Using Decompiler: %s\n", pdc);
			r2mcp_cmd (&ss, pdc);
			free (pdc);
		}
	} else {
		r2mcp_log_pub (&ss, "HTTP r2pipe client mode active - skipping local r2 initialization");
	}
	/* If -T was provided, run DSL tests and exit */
	if (dsl_tests) {
		int r = r2mcp_run_dsl_tests (&ss, dsl_tests, NULL);
		/* Cleanup and return */
		tools_registry_fini (&ss);
		prompts_registry_fini (&ss);
		r2mcp_state_fini (&ss);
		free (ss.baseurl);
		free (ss.svc_baseurl);
		free (ss.sandbox);
		free (ss.logfile);
		if (ss.enabled_tools) {
			r_list_free (ss.enabled_tools);
		}
		return r == 0? 0: 2;
	}
	RListIter *iter;
	const char *cmd;
	r_list_foreach (cmds, iter, cmd) {
		r2mcp_cmd (&ss, cmd);
	}
	r_list_free (cmds);
	r2mcp_running_set (1);
	r2mcp_eventloop (&ss);
	tools_registry_fini (&ss);
	prompts_registry_fini (&ss);
	r2mcp_state_fini (&ss);
	/* Cleanup */
	free (ss.baseurl);
	free (ss.sandbox);
	free (ss.logfile);
	if (ss.enabled_tools) {
		r_list_free (ss.enabled_tools);
	}
	(void)0;
	return 0;
}
```
--------------------------------------------------------------------------------
/src/r2api.inc.c:
--------------------------------------------------------------------------------
```cpp
static void r2state_settings(RCore *core) {
	r_config_set_i (core->config, "scr.color", 0);
	r_config_set_b (core->config, "scr.utf8", false);
	r_config_set_b (core->config, "scr.interactive", false);
	r_config_set_b (core->config, "emu.str", true);
	r_config_set_b (core->config, "asm.bytes", false);
	r_config_set_b (core->config, "anal.strings", true);
	r_config_set_b (core->config, "asm.lines", false);
	r_config_set_b (core->config, "anal.hasnext", true); // TODO: optional
	r_config_set_b (core->config, "asm.lines.fcn", false);
	r_config_set_b (core->config, "asm.cmt.right", false);
	r_config_set_b (core->config, "scr.html", false);
	r_config_set_b (core->config, "scr.prompt", false);
	r_config_set_b (core->config, "scr.echo", false);
	r_config_set_b (core->config, "scr.flush", true);
	r_config_set_b (core->config, "scr.null", false);
	r_config_set_b (core->config, "scr.pipecolor", false);
	r_config_set_b (core->config, "scr.utf8", false);
	r_config_set_i (core->config, "scr.limit", 16768);
}
static bool logcb(void *user, int type, const char *origin, const char *msg) {
	if (type > R_LOG_LEVEL_WARN) {
		return false;
	}
	if (!msg || R_STR_ISEMPTY (origin)) {
		return true;
	}
	ServerState *ss = (ServerState *)user;
	if (ss->sb) {
		const char *typestr = r_log_level_tostring (type);
		// eprintf ("[%s] from=%s message=%s\n", typestr, origin, msg);
		r_strbuf_appendf (ss->sb, "[%s] %s\n", typestr, msg);
		// r_strbuf_appendf (ss->sb, "[%s] from=%s message=%s\n", typestr, origin, msg);
	}
	return true;
}
static void r2mcp_log_reset(ServerState *ss) {
	r_strbuf_free (ss->sb);
	ss->sb = r_strbuf_new ("");
}
static char *r2mcp_log_drain(ServerState *ss) {
	char *s = r_strbuf_drain (ss->sb);
	if (R_STR_ISNOTEMPTY (s)) {
		ss->sb = NULL;
		return s;
	}
	free (s);
	ss->sb = NULL;
	return NULL;
}
static inline void r2mcp_log(ServerState *ss, const char *x) {
	eprintf ("[R2MCP] %s\n", x);
#if R2MCP_DEBUG
	if (ss && ss->logfile && *ss->logfile) {
		r_file_dump (ss->logfile, (const ut8 *) (x), -1, true);
		r_file_dump (ss->logfile, (const ut8 *)"\n", -1, true);
	}
#endif
}
static char *r2_cmd_filter(const char *cmd, bool *changed) {
	char *res = r_str_trim_dup (cmd);
	char fchars[] = "|>`";
	*changed = false;
	if (*res == '!') {
		*changed = true;
		*res = 0;
	} else {
		char *ch = strstr (res, "$ (");
		if (ch) {
			*changed = true;
			*ch = 0;
		}
		for (ch = fchars; *ch; ch++) {
			char *p = strchr (res, *ch);
			if (p) {
				*changed = true;
				*p = 0;
			}
		}
	}
	return res;
}
#include "curl.inc.c"
static char *r2cmd_over_http(ServerState *ss, const char *cmd) {
	int rc = 0;
	char *res = curl_post_capture (ss->baseurl, cmd, &rc);
	if (rc != 0) {
		R_LOG_ERROR ("curl %d", rc);
		free (res);
		return NULL;
	}
	return res;
}
/* printf-like wrapper for r2mcp_cmd to avoid boilerplate */
char *r2mcp_cmdf(ServerState *ss, const char *fmt, ...) {
	if (!fmt) {
		return r2mcp_cmd (ss, "");
	}
	va_list ap;
	va_start (ap, fmt);
	va_list ap2;
	va_copy (ap2, ap);
	int n = vsnprintf (NULL, 0, fmt, ap2);
	va_end (ap2);
	if (n < 0) {
		va_end (ap);
		return NULL;
	}
	char *cmd = (char *)malloc ((size_t)n + 1);
	if (!cmd) {
		va_end (ap);
		return NULL;
	}
	vsnprintf (cmd, (size_t)n + 1, fmt, ap);
	va_end (ap);
	char *res = r2mcp_cmd (ss, cmd);
	free (cmd);
	return res;
}
static bool path_is_absolute(const char *p) {
	return p && p[0] == '/';
}
static bool path_contains_parent_ref(const char *p) {
	return p && strstr (p, "/../") != NULL;
}
static bool path_is_within_sandbox(const char *p, const char *sb) {
	if (!sb || !*sb) {
		return true;
	}
	size_t plen = strlen (p);
	size_t slen = strlen (sb);
	if (slen == 0 || slen > plen) {
		return false;
	}
	if (strncmp (p, sb, slen) != 0) {
		return false;
	}
	if (plen == slen) {
		return true; // exact match
	}
	// ensure boundary: next char must be '/'
	return p[slen] == '/';
}
static bool r2_open_file(ServerState *ss, const char *filepath) {
	R_LOG_INFO ("Attempting to open file: %s\n", filepath);
	// Security checks common to both local and HTTP modes
	if (!filepath || !*filepath) {
		R_LOG_ERROR ("Empty file path is not allowed");
		return false;
	}
	if (!path_is_absolute (filepath)) {
		R_LOG_ERROR ("Relative paths are not allowed. Use an absolute path");
		return false;
	}
	if (path_contains_parent_ref (filepath)) {
		R_LOG_ERROR ("Path traversal is not allowed (contains '/../')");
		return false;
	}
	if (ss && ss->sandbox && *ss->sandbox) {
		if (!path_is_within_sandbox (filepath, ss->sandbox)) {
			R_LOG_ERROR ("Access denied: path is outside of the sandbox");
			return false;
		}
	}
	/* In HTTP mode we do not touch the local r2 core. Just set the state
	 * so subsequent calls to r2mcp_cmd will be allowed (they will be handled
	 * by the HTTP helper).
	 */
	if (ss && ss->http_mode) {
		free (ss->rstate.current_file);
		ss->rstate.current_file = strdup (filepath);
		ss->rstate.file_opened = true;
		return true;
	}
	RCore *core = ss->rstate.core;
	if (!core && !r2mcp_state_init (ss)) {
		R_LOG_ERROR ("Failed to initialize r2 core\n");
		return false;
	}
	if (ss->rstate.file_opened) {
		R_LOG_INFO ("Closing previously opened file: %s", ss->rstate.current_file);
		r_core_cmd0 (core, "o-*");
		ss->rstate.file_opened = false;
		ss->rstate.current_file = NULL;
	}
	r_core_cmd0 (core, "e bin.relocs.apply=true");
	r_core_cmd0 (core, "e bin.cache=true");
	char *cmd = r_str_newf ("'o %s", filepath);
	R_LOG_INFO ("Running r2 command: %s", cmd);
	char *result = r_core_cmd_str (core, cmd);
	free (cmd);
	bool success = (result && strlen (result) > 0);
	free (result);
	if (!success) {
		R_LOG_INFO ("Trying alternative method to open file");
		RIODesc *fd = r_core_file_open (core, filepath, R_PERM_R, 0);
		if (fd) {
			r_core_bin_load (core, filepath, 0);
			R_LOG_INFO ("File opened using r_core_file_open");
			success = true;
		} else {
			R_LOG_ERROR ("Failed to open file: %s", filepath);
			return false;
		}
	}
	R_LOG_INFO ("Loading binary information");
	r_core_cmd0 (core, "ob");
	free (ss->rstate.current_file);
	ss->rstate.current_file = strdup (filepath);
	ss->rstate.file_opened = true;
	R_LOG_INFO ("File opened successfully: %s", filepath);
	return true;
}
static char *r2_analyze(ServerState *ss, int level) {
	if (ss && ss->http_mode) {
		/* In HTTP mode we won't run local analysis; return empty string. */
		return strdup ("");
	}
	RCore *core = ss->rstate.core;
	if (!core || !ss->rstate.file_opened) {
		return false;
	}
	const char *cmd = "aa";
	if (!ss->ignore_analysis_level) {
		switch (level) {
		case 1: cmd = "aac"; break;
		case 2: cmd = "aaa"; break;
		case 3: cmd = "aaaa"; break;
		case 4: cmd = "aaaaa"; break;
		}
	}
	r2mcp_log_reset (ss);
	r_core_cmd0 (core, cmd);
	return r2mcp_log_drain (ss);
}
```
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
```yaml
---
name: ci
env:
  R2V: 6.0.4
  ZIP_PREFIX: radare2-mcp-
on:
  push:
    branches:
      - main
    tags:
      - "[0-9]*"
  pull_request:
    branches:
      - main
jobs:
  r2git:
    runs-on: ubuntu-22.04
    strategy:
      fail-fast: false
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Installing radare2 from git
        run: |
          git clone --depth=1 https://github.com/radareorg/radare2
          cd radare2
          sys/install.sh /usr > /dev/null
      - name: Building radare2-mcp
        run: ./configure && make
      - name: Create Linux zip archive
        if: startsWith(github.ref, 'refs/tags/')
        run: |
          mkdir -p release
          FILENAME="${ZIP_PREFIX}${{ github.ref_name }}-linux-x64.zip"
          zip -j "release/$FILENAME" src/r2mcp
      - name: Upload Linux binary
        if: startsWith(github.ref, 'refs/tags/')
        uses: actions/upload-artifact@v4
        with:
          name: linux-x64-binary
          path: "release/${ZIP_PREFIX}${{ github.ref_name }}-linux-x64.zip"
  linux-deb:
    runs-on: ubuntu-22.04
    strategy:
      fail-fast: false
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Installing radare2 from deb
        run: |
          BASE_URL="https://github.com/radareorg/radare2/releases/download/${{env.R2V}}"
          wget "${BASE_URL}/radare2_${{env.R2V}}_amd64.deb"
          wget "${BASE_URL}/radare2-dev_${{env.R2V}}_amd64.deb"
          sudo dpkg -i *.deb
      - name: Building radare2-mcp
        run: ./configure && make
      - name: Building Debian package
        if: startsWith(github.ref, 'refs/tags/')
        run: make -C dist/debian
      - name: List Debian package files
        if: startsWith(github.ref, 'refs/tags/')
        run: ls -la dist/debian/*.deb
      - name: Upload Debian package
        if: startsWith(github.ref, 'refs/tags/')
        uses: actions/upload-artifact@v4
        with:
          name: debian-package
          path: dist/debian/*.deb
  macos:
    runs-on: macos-latest
    strategy:
      fail-fast: false
      matrix:
        arch: [x86_64, arm64]
    env:
      ARCH: ${{ matrix.arch }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Cloning radare2
        run: git clone --depth=1 --branch "${{env.R2V}}" https://github.com/radareorg/radare2
      - name: Building and installing radare2
        run: |
          cd radare2
          sys/install.sh > /dev/null
      - name: Building radare2-mcp
        run: ./configure && make
      - name: Check build output
        run: ls -la src/
      - name: Create macOS zip archive
        if: startsWith(github.ref, 'refs/tags/')
        run: |
          mkdir -p release
          if [ -f src/r2mcp ]; then
            FILENAME="${ZIP_PREFIX}${{ github.ref_name }}-macos-${{ matrix.arch }}.zip"
          zip -j "release/$FILENAME" src/r2mcp
          else
            echo "Binary src/r2mcp not found, checking for alternatives..."
            find . -name "r2mcp*" -type f
            exit 1
          fi
      - name: Upload macOS binary
        if: startsWith(github.ref, 'refs/tags/')
        uses: actions/upload-artifact@v4
        with:
          name: macos-${{ matrix.arch }}-binary
          path: "release/${ZIP_PREFIX}${{ github.ref_name }}-macos-${{ matrix.arch }}.zip"
  windows:
    runs-on: windows-latest
    strategy:
      fail-fast: false
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Set up Python (for Meson)
        uses: actions/setup-python@v4
        with:
          python-version: '3.x'
      - name: Install Meson
        shell: powershell
        run: |
          python -m pip install --upgrade pip
          pip install meson
      - name: Download and extract radare2 release
        shell: powershell
        run: |
          $R2V = '${{ env.R2V }}'
          $baseUrl = "https://github.com/radareorg/radare2/releases/download/$R2V"
          $url = "$baseUrl/radare2-$R2V-w64.zip"
          $dest = "$env:TEMP\radare2.zip"
          Write-Output "Downloading $url"
          Invoke-WebRequest -Uri $url -OutFile $dest -UseBasicParsing
          $prefix = 'C:\\radare2'
          if (Test-Path $prefix) { Remove-Item -Path $prefix -Recurse -Force }
          New-Item -ItemType Directory -Force -Path $prefix | Out-Null
          Expand-Archive -Path $dest -DestinationPath $prefix -Force
          # Some zips include a top-level folder; move its contents up if needed
          $inner = Get-ChildItem -Path $prefix | Where-Object { $_.PSIsContainer } | Select-Object -First 1
            if ($inner -and (Get-ChildItem -Path $inner.FullName |
                Measure-Object).Count -gt 0) {
              Get-ChildItem -Path $inner.FullName -Force | Move-Item -Destination $prefix -Force
            Remove-Item -Path $inner.FullName -Recurse -Force
          }
          Write-Output "Extracted radare2 contents to $prefix"
      - name: Configure Meson
        shell: powershell
        run: |
          $R2V = '${{ env.R2V }}'
          $prefix = 'C:\\radare2'
          Write-Output "Using radare2 prefix: $prefix"
          $posix = $prefix -replace '\\','/'
          Write-Output "Passing r2_prefix as: $posix"
          meson setup --reconfigure builddir --backend ninja `
                  --prefix="$posix" --buildtype=release -Dr2_prefix="$posix"
      - name: Compile
        shell: powershell
        run: |
          meson compile -C builddir -v
      - name: Create Windows zip archive
        if: startsWith(github.ref, 'refs/tags/')
        shell: powershell
        run: |
          New-Item -ItemType Directory -Force -Path output
          $src = ''
          if (Test-Path builddir/r2mcp.exe) {
            Copy-Item builddir/r2mcp.exe output/
            $src = 'builddir/r2mcp.exe'
          }
          if (Test-Path builddir/src/r2mcp.exe) {
            Copy-Item builddir/src/r2mcp.exe output/
            $src = 'builddir/src/r2mcp.exe'
          }
          if ($src -eq '') { Write-Output "Warning: r2mcp.exe not found after build" }
          Compress-Archive -Path output/* `
                  -DestinationPath r2mcp-windows.zip -Force
      - name: Upload Windows binary
        if: startsWith(github.ref, 'refs/tags/')
        uses: actions/upload-artifact@v4
        with:
          name: windows-binary
          path: r2mcp-windows.zip
  release:
    needs: [r2git, linux-deb, macos, windows]
    if: startsWith(github.ref, 'refs/tags/')
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Download all artifacts
        uses: actions/download-artifact@v4
        with:
          path: release-artifacts
      - name: List downloaded artifacts
        run: find release-artifacts -type f
      - name: Create GitHub Release
        uses: softprops/action-gh-release@v1
        with:
          files: |
            release-artifacts/**/*.zip
            release-artifacts/**/*.deb
          generate_release_notes: true
```
--------------------------------------------------------------------------------
/src/prompts.c:
--------------------------------------------------------------------------------
```cpp
#include "r2mcp.h"
#include "prompts.h"
static char *json_text_msg(const char *role, const char *text) {
	PJ *pj = pj_new ();
	pj_o (pj);
	pj_ks (pj, "role", role);
	pj_k (pj, "content");
	pj_a (pj);
	pj_o (pj);
	pj_ks (pj, "type", "text");
	pj_ks (pj, "text", text);
	pj_end (pj);
	pj_end (pj);
	pj_end (pj);
	return pj_drain (pj);
}
static char *json_messages_obj2(char *m1, char *m2) {
	RStrBuf *sb = r_strbuf_new ("{");
	r_strbuf_append (sb, "\"messages\":[");
	if (m1) {
		r_strbuf_append (sb, m1);
		if (m2) {
			r_strbuf_append (sb, ",");
		}
	}
	if (m2) {
		r_strbuf_append (sb, m2);
	}
	r_strbuf_append (sb, "]}");
	free (m1);
	free (m2);
	return r_strbuf_drain (sb);
}
// Utility to fetch string argument value
static const char *arg_str(RJson *arguments, const char *key, const char *dflt) {
	const char *s = r_json_get_str (arguments, key);
	return s? s: dflt;
}
// ---------- Prompt renderers ----------
// crackme solver
static char *render_crackme(const PromptSpec *spec, RJson *arguments) {
	(void)spec;
	const char *file_path = r_json_get_str (arguments, "file_path");
	const char *goal = arg_str (arguments, "goal", "Recover the correct input or bypass check");
	const char *sys =
		"You are an expert reverse engineer using radare2 via r2mcp.\n"
		"Goal: plan first, then execute minimal tool calls.\n"
		"General steps:\n"
		"1) Open the target binary and run lightweight analysis (analyze level 2).\n"
		"2) Identify main/entrypoints and functions referring to strcmp, strncmp, memcmp, crypto, or suspicious branches.\n"
		"3) Read/Decompile only the most relevant functions (avoid dumping huge outputs).\n"
		"4) Derive the key/logic and propose inputs or patches.\n"
		"5) Summarize findings and next actions.\n"
		"Prefer afl listing with addresses, selective pdc/pdf on key functions, and xrefs_to for checks.\n";
	RStrBuf *user = r_strbuf_new ("");
	r_strbuf_appendf (user, "Task: %s.\n", goal);
	if (file_path && *file_path) {
		r_strbuf_appendf (user, "Open file: %s (use tools/call open_file).\n", file_path);
	} else {
		r_strbuf_append (user, "Ask for or confirm file path if unknown.\n");
	}
	r_strbuf_append (user,
		"Plan your steps, then call: analyze (level=2), list_entrypoints, list_functions, list_imports, list_strings (filter optional).\n"
		"Use decompile_function or disassemble_function on candidate functions only.\n");
	char *m1 = json_text_msg ("system", sys);
	char *m2 = json_text_msg ("user", r_strbuf_get (user));
	char *out = json_messages_obj2 (m1, m2);
	r_strbuf_free (user);
	return out;
}
// find cryptographic material
static char *render_crypto(const PromptSpec *spec, RJson *arguments) {
	(void)spec;
	const char *hint = arg_str (arguments, "hint", "Look for keys, IVs, S-boxes, constants, PRNG seeds");
	const char *sys =
		"You are tasked with locating cryptographic material in a binary.\n"
		"Strategy:\n"
		"- List imports and strings to find crypto APIs/signatures.\n"
		"- Search for constants (AES S-box, SHA tables), base64 sets, or long random-looking blobs.\n"
		"- Inspect xrefs to functions handling buffers just before encryption/decryption.\n"
		"- Use selective decompilation and avoid dumping entire files.\n";
	RStrBuf *user = r_strbuf_new ("");
	r_strbuf_appendf (user, "Focus: %s.\n", hint);
	r_strbuf_append (user,
		"Use: list_imports, list_strings (filter to crypto keywords), list_functions (scan for suspicious names), and xrefs_to (address).\n"
		"If needed, disassemble/decompile only tight regions where material is assigned.\n");
	char *m1 = json_text_msg ("system", sys);
	char *m2 = json_text_msg ("user", r_strbuf_get (user));
	char *out = json_messages_obj2 (m1, m2);
	r_strbuf_free (user);
	return out;
}
// document assembly code for a function
static char *render_document_function(const PromptSpec *spec, RJson *arguments) {
	(void)spec;
	const char *address = r_json_get_str (arguments, "address");
	const char *depth = arg_str (arguments, "detail", "concise");
	const char *sys =
		"Produce a clear, structured explanation of a function’s behavior.\n"
		"Guidelines:\n"
		"- Summarize purpose, inputs/outputs, side effects.\n"
		"- Highlight algorithms, notable constants, error paths.\n"
		"- Provide a brief high-level pseudocode if helpful.\n";
	RStrBuf *user = r_strbuf_new ("");
	if (address && *address) {
		r_strbuf_appendf (user, "Target function address: %s.\n", address);
	} else {
		r_strbuf_append (user, "Ask for an address to document.\n");
	}
	r_strbuf_appendf (user,
		"Detail level: %s.\nUse: get_current_address (to verify), disassemble_function (address), decompile_function (address), get_function_prototype (address).\n",
		depth);
	char *m1 = json_text_msg ("system", sys);
	char *m2 = json_text_msg ("user", r_strbuf_get (user));
	char *out = json_messages_obj2 (m1, m2);
	r_strbuf_free (user);
	return out;
}
// find control-flow path between two addresses (for exploit dev or reachability)
static char *render_cfg_path(const PromptSpec *spec, RJson *arguments) {
	(void)spec;
	const char *src = r_json_get_str (arguments, "source_address");
	const char *dst = r_json_get_str (arguments, "target_address");
	const char *sys =
		"Find and explain a feasible control-flow path between two addresses.\n"
		"Approach:\n"
		"- Identify function boundaries for source/target.\n"
		"- Use xrefs_to and selective disassembly to traverse edges.\n"
		"- Summarize the path as a sequence of blocks with conditions.\n";
	RStrBuf *user = r_strbuf_new ("");
	r_strbuf_append (user, "Compute a path with minimal output.\n");
	if (src) {
		r_strbuf_appendf (user, "Source: %s. ", src);
	}
	if (dst) {
		r_strbuf_appendf (user, "Target: %s. ", dst);
	}
	r_strbuf_append (user, "Use: get_current_address, disassemble_function, disassemble, xrefs_to.\n");
	char *m1 = json_text_msg ("system", sys);
	char *m2 = json_text_msg ("user", r_strbuf_get (user));
	char *out = json_messages_obj2 (m1, m2);
	r_strbuf_free (user);
	return out;
}
// ---------- Registry ----------
static PromptArg ARGS_CRACKME[] = {
	{ "file_path", "Absolute path to target binary", false },
	{ "goal", "What success looks like (e.g., recover password)", false },
};
static PromptArg ARGS_DOCUMENT_FUNCTION[] = {
	{ "address", "Function start address to document", true },
	{ "detail", "Level of detail: concise|full", false },
};
static PromptArg ARGS_CFG_PATH[] = {
	{ "source_address", "Source address or block", true },
	{ "target_address", "Target address or block", true },
};
static PromptArg ARGS_CRYPTO[] = {
	{ "hint", "Extra context (e.g., suspected algorithm)", false },
};
static PromptSpec builtin_prompts[] = {
	{
		.name = "crackme_solver",
		.description = "Plan and solve a crackme using radare2 with minimal, targeted steps",
		.args = ARGS_CRACKME,
		.nargs = (int) (sizeof (ARGS_CRACKME) / sizeof (ARGS_CRACKME[0])),
		.render = render_crackme,
	},
	{
		.name = "find_crypto_material",
		.description = "Locate keys, IVs, S-boxes, and crypto constants",
		.args = ARGS_CRYPTO,
		.nargs = (int) (sizeof (ARGS_CRYPTO) / sizeof (ARGS_CRYPTO[0])),
		.render = render_crypto,
	},
	{
		.name = "document_function",
		.description = "Explain a function’s purpose, behavior, and pseudocode",
		.args = ARGS_DOCUMENT_FUNCTION,
		.nargs = (int) (sizeof (ARGS_DOCUMENT_FUNCTION) / sizeof (ARGS_DOCUMENT_FUNCTION[0])),
		.render = render_document_function,
	},
	{
		.name = "find_control_flow_path",
		.description = "Find a control-flow path between two addresses for reachability or exploit planning",
		.args = ARGS_CFG_PATH,
		.nargs = (int) (sizeof (ARGS_CFG_PATH) / sizeof (ARGS_CFG_PATH[0])),
		.render = render_cfg_path,
	},
};
typedef struct {
	RList *list; // of PromptSpec*(borrowed pointers to builtin entries)
} PromptRegistry;
void prompts_registry_init(ServerState *ss) {
	if (ss->prompts) {
		return;
	}
	PromptRegistry *reg = R_NEW0 (PromptRegistry);
	reg->list = r_list_new ();
	if (!reg->list) {
		free (reg);
		return;
	}
	// Append pointers to builtin prompts
	size_t n = sizeof (builtin_prompts) / sizeof (builtin_prompts[0]);
	for (size_t i = 0; i < n; i++) {
		r_list_append (reg->list, &builtin_prompts[i]);
	}
	ss->prompts = reg;
}
void prompts_registry_fini(ServerState *ss) {
	if (!ss || !ss->prompts) {
		return;
	}
	PromptRegistry *reg = (PromptRegistry *)ss->prompts;
	r_list_free (reg->list);
	free (reg);
	ss->prompts = NULL;
}
static PromptSpec *prompts_find(const ServerState *ss, const char *name) {
	if (!ss || !ss->prompts || !name) {
		return NULL;
	}
	PromptRegistry *reg = (PromptRegistry *)ss->prompts;
	RListIter *it;
	PromptSpec *p;
	r_list_foreach (reg->list, it, p) {
		if (!strcmp (p->name, name)) {
			return p;
		}
	}
	return NULL;
}
char *prompts_build_list_json(const ServerState *ss, const char *cursor, int page_size) {
	if (!ss || !ss->prompts) {
		PJ *pj = pj_new ();
		pj_o (pj);
		pj_k (pj, "prompts");
		pj_a (pj);
		pj_end (pj); // end array
		pj_end (pj); // end object
		return pj_drain (pj);
	}
	PromptRegistry *reg = (PromptRegistry *)ss->prompts;
	int start_index = 0;
	if (cursor) {
		start_index = atoi (cursor);
		if (start_index < 0) {
			start_index = 0;
		}
	}
	int total = r_list_length (reg->list);
	int end_index = start_index + page_size;
	if (end_index > total) {
		end_index = total;
	}
	PJ *pj = pj_new ();
	pj_o (pj);
	pj_k (pj, "prompts");
	pj_a (pj);
	RListIter *it;
	PromptSpec *p;
	int idx = 0;
	r_list_foreach (reg->list, it, p) {
		if (idx >= start_index && idx < end_index) {
			pj_o (pj);
			pj_ks (pj, "name", p->name);
			pj_ks (pj, "description", p->description? p->description: "");
			pj_k (pj, "arguments");
			pj_a (pj);
			for (int i = 0; i < p->nargs; i++) {
				pj_o (pj);
				pj_ks (pj, "name", p->args[i].name);
				pj_ks (pj, "description", p->args[i].description? p->args[i].description: "");
				pj_kb (pj, "required", p->args[i].required);
				pj_end (pj);
			}
			pj_end (pj); // end arguments array
			pj_end (pj); // end prompt object
		}
		idx++;
	}
	pj_end (pj); // end prompts array
	if (end_index < total) {
		char buf[32];
		snprintf (buf, sizeof (buf), "%d", end_index);
		pj_ks (pj, "nextCursor", buf);
	}
	pj_end (pj); // end root object
	return pj_drain (pj);
}
char *prompts_get_json(const ServerState *ss, const char *name, RJson *arguments) {
	PromptSpec *spec = prompts_find (ss, name);
	if (!spec) {
		return NULL;
	}
	return spec->render (spec, arguments);
}
```
--------------------------------------------------------------------------------
/src/r2mcp.c:
--------------------------------------------------------------------------------
```cpp
/* r2mcp - MIT - Copyright 2025 - pancake, dnakov */
#include <r_core.h>
#include <r_util/r_json.h>
#include <r_util/pj.h>
#include <r_util.h>
#include <r_util/r_print.h>
#include <stdio.h>
#include <stdarg.h>
#include <errno.h>
static void pj_append_rjson(PJ *pj, RJson *j) {
	if (!j) {
		pj_null (pj);
		return;
	}
	switch (j->type) {
	case R_JSON_NULL:
		pj_null (pj);
		break;
	case R_JSON_BOOLEAN:
		pj_b (pj, j->num.u_value);
		break;
	case R_JSON_INTEGER:
		pj_n (pj, j->num.s_value);
		break;
	case R_JSON_DOUBLE:
		pj_d (pj, j->num.dbl_value);
		break;
	case R_JSON_STRING:
		pj_s (pj, j->str_value);
		break;
	case R_JSON_ARRAY:
		pj_a (pj);
		RJson *child = j->children.first;
		while (child) {
			pj_append_rjson (pj, child);
			child = child->next;
		}
		pj_end (pj);
		break;
	case R_JSON_OBJECT:
		pj_o (pj);
		child = j->children.first;
		while (child) {
			pj_k (pj, child->key);
			pj_append_rjson (pj, child);
			child = child->next;
		}
		pj_end (pj);
		break;
	}
}
#if defined(R2__UNIX__)
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#elif defined(R2__WINDOWS__)
#include <windows.h>
#include <io.h>
#include <fcntl.h>
#include <signal.h>
#else
#error please define R2__WINDOWS__ or R2__UNIX__ for platform detection
#endif
#include "config.h"
#include "r2mcp.h"
#include "tools.h"
#include "prompts.h"
#define R2MCP_DEBUG 1
#ifndef R2MCP_VERSION
#warning R2MCP_VERSION is not defined
#define R2MCP_VERSION "1.1.0"
#endif
#define JSON_RPC_VERSION "2.0"
#define MCP_VERSION "2024-11-05"
#define READ_CHUNK_SIZE 32768
#define LATEST_PROTOCOL_VERSION "2024-11-05"
#include "utils.inc.c"
#include "r2api.inc.c"
static volatile sig_atomic_t running = 1;
void r2mcp_running_set(int value) {
	running = value? 1: 0;
}
// Local I/O mode helper (moved from utils.inc.c to avoid unused warnings in other TUs)
static void set_nonblocking_io(bool nonblocking) {
#if defined(R2__UNIX__)
	int flags = fcntl (STDIN_FILENO, F_GETFL, 0);
	if (nonblocking) {
		fcntl (STDIN_FILENO, F_SETFL, flags | O_NONBLOCK);
	} else {
		fcntl (STDIN_FILENO, F_SETFL, flags & ~O_NONBLOCK);
	}
	setvbuf (stdout, NULL, _IOLBF, 0);
#elif defined(R2__WINDOWS__)
	// Windows doesn't support POSIX fcntl/O_NONBLOCK on stdin reliably.
	// We set stdin mode to binary/text depending on requested mode and
	// keep line-buffered stdout. This is a best-effort no-op for nonblocking.
	if (nonblocking) {
		_setmode (_fileno (stdin), _O_BINARY);
	} else {
		_setmode (_fileno (stdin), _O_TEXT);
	}
	setvbuf (stdout, NULL, _IOLBF, 0);
#else
	(void)nonblocking;
#endif
}
/* Public wrappers to expose internal static helpers from r2api.inc.c */
bool r2mcp_state_init(ServerState *ss) {
	RCore *core = r_core_new ();
	if (!core) {
		R_LOG_ERROR ("Failed to initialize radare2 core");
		return false;
	}
	r2state_settings (core);
	ss->rstate.core = core;
	R_LOG_INFO ("Radare2 core initialized");
	r_log_add_callback (logcb, ss);
	return true;
}
void r2mcp_state_fini(ServerState *ss) {
	RCore *core = ss->rstate.core;
	if (core) {
		r_core_free (core);
		r_strbuf_free (ss->sb);
		ss->sb = NULL;
		ss->rstate.core = NULL;
		ss->rstate.file_opened = false;
		ss->rstate.current_file = NULL;
	}
}
char *r2mcp_cmd(ServerState *ss, const char *cmd) {
	if (ss && ss->http_mode) {
		return r2cmd_over_http (ss, cmd);
	}
	RCore *core = ss->rstate.core;
	if (!core || !ss->rstate.file_opened) {
		return strdup ("Use the open_file method before calling any other method");
	}
	bool changed = false;
	char *filteredCommand = r2_cmd_filter (cmd, &changed);
	if (changed) {
		r2mcp_log (ss, "command injection prevented");
	}
	r2mcp_log_reset (ss);
	char *res = r_core_cmd_str (core, filteredCommand);
	char *err = r2mcp_log_drain (ss);
	free (filteredCommand);
	// r2state_settings (core);
	if (err) {
		char *newres = r_str_newf ("%s<log>\n%s\n</log>\n", res, err);
		free (res);
		res = newres;
	}
	return res;
}
void r2mcp_log_pub(ServerState *ss, const char *msg) {
	r2mcp_log (ss, msg);
}
// New wrappers to expose functionality from r2api.inc.c to other modules
bool r2mcp_open_file(ServerState *ss, const char *filepath) {
	return r2_open_file (ss, filepath);
}
char *r2mcp_analyze(ServerState *ss, int level) {
	return r2_analyze (ss, level);
}
static bool check_client_capability(ServerState *ss, const char *capability) {
	if (ss->client_capabilities) {
		RJson *cap = (RJson *)r_json_get (ss->client_capabilities, capability);
		return cap != NULL;
	}
	return false;
}
static bool check_server_capability(ServerState *ss, const char *capability) {
	if (!strcmp (capability, "tools")) {
		return ss->capabilities.tools;
	}
	if (!strcmp (capability, "prompts")) {
		return ss->capabilities.prompts;
	}
	if (!strcmp (capability, "resources")) {
		return ss->capabilities.resources;
	}
	return false;
}
static bool assert_capability_for_method(ServerState *ss, const char *method, char **error) {
	if (!strcmp (method, "sampling/createMessage")) {
		if (!check_client_capability (ss, "sampling")) {
			*error = strdup ("Client does not support sampling");
			return false;
		}
	} else if (!strcmp (method, "roots/list")) {
		if (!check_client_capability (ss, "roots")) {
			*error = strdup ("Client does not support listing roots");
			return false;
		}
	}
	return true;
}
static bool assert_request_handler_capability(ServerState *ss, const char *method, char **error) {
	if (!strcmp (method, "sampling/createMessage")) {
		if (!check_server_capability (ss, "sampling")) {
			*error = strdup ("Server does not support sampling");
			return false;
		}
	} else if (r_str_startswith (method, "prompts/")) {
		if (!check_server_capability (ss, "prompts")) {
			*error = strdup ("Server does not support prompts");
			return false;
		}
	} else if (r_str_startswith (method, "tools/")) {
		if (!check_server_capability (ss, "tools")) {
			*error = strdup ("Server does not support tools");
			return false;
		}
	} else if (r_str_startswith (method, "resources/")) {
		if (!check_server_capability (ss, "resources")) {
			*error = strdup ("Server does not support resources");
			return false;
		}
	}
	return true;
}
// Helper function to create JSON-RPC error responses
// (moved to utils.inc.c earlier, keep this for compatibility if needed)
// static char *jsonrpc_error_response (int code, const char *message, const char *id, const char *uri) { ... }
static char *handle_initialize(ServerState *ss, RJson *params) {
	ss->client_capabilities = r_json_get (params, "capabilities");
	ss->client_info = r_json_get (params, "clientInfo");
	// Create a proper initialize response
	PJ *pj = pj_new ();
	pj_o (pj);
	pj_ks (pj, "protocolVersion", LATEST_PROTOCOL_VERSION);
	pj_k (pj, "serverInfo");
	pj_o (pj);
	pj_ks (pj, "name", ss->info.name);
	pj_ks (pj, "version", ss->info.version);
	pj_end (pj);
	// Capabilities need to be objects with specific structure, not booleans
	pj_k (pj, "capabilities");
	pj_o (pj);
	// Tools capability - needs to be an object
	pj_k (pj, "tools");
	pj_o (pj);
	pj_kb (pj, "listChanged", false);
	pj_end (pj);
	// Prompts capability - object with listChanged
	pj_k (pj, "prompts");
	pj_o (pj);
	pj_kb (pj, "listChanged", false);
	pj_end (pj);
	// Resources capability - empty object since we only support list
	pj_k (pj, "resources");
	pj_o (pj);
	pj_end (pj);
	// For any capability we don't support, don't include it at all
	// Don't add: roots, notifications, sampling
	pj_end (pj); // End capabilities
	if (ss->instructions) {
		pj_ks (pj, "instructions", ss->instructions);
	}
	pj_end (pj);
	ss->initialized = true;
	return pj_drain (pj);
}
// Create a proper success response
static char *jsonrpc_success_response(ServerState *ss, const char *result, const char *id) {
	PJ *pj = pj_new ();
	pj_o (pj);
	pj_ks (pj, "jsonrpc", "2.0");
	if (id) {
		// If id is a number string, treat it as a number
		char *endptr;
		long num_id = strtol (id, &endptr, 10);
		if (*id != '\0' && *endptr == '\0') {
			// It's a valid number
			pj_kn (pj, "id", num_id);
		} else {
			// It's a string
			pj_ks (pj, "id", id);
		}
	}
	pj_k (pj, "result");
	if (result) {
		pj_raw (pj, result);
	} else {
		pj_null (pj);
	}
	pj_end (pj);
	char *s = pj_drain (pj);
	r2mcp_log (ss, ">>>");
	r2mcp_log (ss, s);
	return s;
}
static char *handle_list_tools(ServerState *ss, RJson *params) {
	const char *cursor = r_json_get_str (params, "cursor");
	int page_size = 32;
	return tools_build_catalog_json (ss, cursor, page_size);
}
static char *handle_list_prompts(ServerState *ss, RJson *params) {
	const char *cursor = r_json_get_str (params, "cursor");
	int page_size = 32;
	return prompts_build_list_json (ss, cursor, page_size);
}
static char *handle_get_prompt(ServerState *ss, RJson *params) {
	const char *name = r_json_get_str (params, "name");
	if (!name) {
		return jsonrpc_error_response (-32602, "Missing required parameter: name", NULL, NULL);
	}
	RJson *args = (RJson *)r_json_get (params, "arguments");
	char *prompt = prompts_get_json (ss, name, args);
	if (!prompt) {
		return jsonrpc_error_response (-32602, "Unknown prompt name", NULL, NULL);
	}
	return prompt;
}
// Thin wrapper that delegates to the tools module. This keeps r2mcp.c small
// and moves the tool-specific logic into tools.c where it belongs.
static char *handle_call_tool(ServerState *ss, const char *tool_name, RJson *tool_args) {
	return tools_call (ss, tool_name, tool_args);
}
static char *handle_mcp_request(ServerState *ss, const char *method, RJson *params, const char *id) {
	char *error = NULL;
	char *result = NULL;
	if (!assert_capability_for_method (ss, method, &error) || !assert_request_handler_capability (ss, method, &error)) {
		char *response = jsonrpc_error_response (-32601, error, id, NULL);
		free (error);
		return response;
	}
	if (!strcmp (method, "initialize")) {
		result = handle_initialize (ss, params);
	} else if (!strcmp (method, "notifications/initialized")) {
		return NULL; // No response for notifications
	} else if (!strcmp (method, "ping")) {
		result = strdup ("{}");
	} else if (!strcmp (method, "tools/list") || !strcmp (method, "tool/list")) {
		result = handle_list_tools (ss, params);
	} else if (!strcmp (method, "tools/call") || !strcmp (method, "tool/call")) {
		const char *tool_name = r_json_get_str (params, "name");
		if (!tool_name) {
			tool_name = r_json_get_str (params, "tool");
		}
		RJson *tool_args = (RJson *)r_json_get (params, "arguments");
		if (!tool_args) {
			tool_args = (RJson *)r_json_get (params, "args");
		}
		if (ss->svc_baseurl) {
			PJ *pj = pj_new ();
			pj_o (pj);
			pj_ks (pj, "tool", tool_name);
			pj_k (pj, "arguments");
			pj_append_rjson (pj, tool_args);
			pj_k (pj, "available_tools");
			pj_a (pj);
			RListIter *iter;
			ToolSpec *ts;
			r_list_foreach (ss->tools, iter, ts) {
				pj_s (pj, ts->name);
			}
			pj_end (pj);
			pj_end (pj);
			char *req = pj_drain (pj);
			int rc;
			char *resp = curl_post_capture (ss->svc_baseurl, req, &rc);
			free (req);
			if (resp && rc == 0) {
				RJson *rj = r_json_parse (resp);
				free (resp);
				if (rj) {
					const char *err = r_json_get_str (rj, "error");
					if (err) {
						r_json_free (rj);
						return jsonrpc_error_response (-32000, err, id, NULL);
					}
					const char *r2cmd = r_json_get_str (rj, "r2cmd");
					if (r2cmd) {
						char *cmd_out = r2mcp_cmd (ss, r2cmd);
						char *res = jsonrpc_tooltext_response (cmd_out? cmd_out: "");
						free (cmd_out);
						r_json_free (rj);
						result = res;
					} else {
						const char *new_tool = r_json_get_str (rj, "tool");
						const RJson *new_args = r_json_get (rj, "arguments");
						if (new_tool && strcmp (new_tool, tool_name)) {
							tool_name = new_tool;
						}
						if (new_args) {
							tool_args = (RJson *)new_args;
						}
						result = handle_call_tool (ss, tool_name, tool_args);
						r_json_free (rj);
					}
				} else {
					result = handle_call_tool (ss, tool_name, tool_args);
				}
			} else {
				result = handle_call_tool (ss, tool_name, tool_args);
			}
		} else {
			result = handle_call_tool (ss, tool_name, tool_args);
		}
	} else if (!strcmp (method, "prompts/list") || !strcmp (method, "prompt/list")) {
		result = handle_list_prompts (ss, params);
	} else if (!strcmp (method, "prompts/get") || !strcmp (method, "prompt/get")) {
		result = handle_get_prompt (ss, params);
	} else if (!strcmp (method, "resources/list")) {
		result = strdup ("{\"resources\":[]}");
	} else if (!strcmp (method, "resources/templates/list")) {
		result = strdup ("{\"resourceTemplates\":[]}");
	} else {
		return jsonrpc_error_response (-32601, "Unknown method", id, NULL);
	}
	char *response = jsonrpc_success_response (ss, result, id);
	free (result);
	return response;
}
// Modified process_mcp_message to handle the protocol correctly
static void process_mcp_message(ServerState *ss, const char *msg) {
	r2mcp_log (ss, "<<<");
	r2mcp_log (ss, msg);
	RJson *request = r_json_parse ((char *)msg);
	if (!request) {
		R_LOG_ERROR ("Invalid JSON");
		return;
	}
	const char *method = r_json_get_str (request, "method");
	RJson *params = (RJson *)r_json_get (request, "params");
	RJson *id_json = (RJson *)r_json_get (request, "id");
	if (!method) {
		R_LOG_ERROR ("Invalid JSON-RPC message: missing method");
		r_json_free (request);
		return;
	}
	// Proper handling of notifications vs requests
	if (id_json) {
		// This is a request that requires a response
		const char *id = NULL;
		char id_buf[32] = { 0 };
		if (id_json->type == R_JSON_STRING) {
			id = id_json->str_value;
		} else if (id_json->type == R_JSON_INTEGER) {
			snprintf (id_buf, sizeof (id_buf), "%lld", (long long)id_json->num.u_value);
			id = id_buf;
		}
		char *response = handle_mcp_request (ss, method, params, id);
		if (response) {
			r2mcp_log (ss, ">>>");
			r2mcp_log (ss, response);
			// Ensure the response ends with a newline
			size_t resp_len = strlen (response);
			bool has_newline = (resp_len > 0 && response[resp_len - 1] == '\n');
			if (!has_newline) {
				write (STDOUT_FILENO, response, resp_len);
				write (STDOUT_FILENO, "\n", 1);
			} else {
				write (STDOUT_FILENO, response, resp_len);
			}
#if R2__UNIX__
			fsync (STDOUT_FILENO);
#endif
			free (response);
		}
	} else {
		// This is a notification, don't send a response
		// Just handle it internally
		if (!strcmp (method, "notifications/cancelled")) {
			r2mcp_log (ss, "Received cancelled notification");
		} else if (!strcmp (method, "notifications/initialized")) {
			r2mcp_log (ss, "Received initialized notification");
		} else {
			r2mcp_log (ss, "Received unknown notification");
		}
	}
	r_json_free (request);
}
// MCPO protocol-compliant direct mode loop
void r2mcp_eventloop(ServerState *ss) {
	r2mcp_log (ss, "Starting MCP direct mode (stdin/stdout)");
	// Use consistent unbuffered mode for stdout
	setvbuf (stdout, NULL, _IONBF, 0);
	// Set to blocking I/O for simplicity
	set_nonblocking_io (false);
	ReadBuffer *buffer = read_buffer_new ();
	char chunk[READ_CHUNK_SIZE];
	while (running) {
		// Read data from stdin
		ssize_t bytes_read = read (STDIN_FILENO, chunk, sizeof (chunk) - 1);
		if (bytes_read > 0) {
			// Append to our buffer
			read_buffer_append (buffer, chunk, bytes_read);
			// Try to process any complete messages
			char *msg;
			while ((msg = read_buffer_get_message (buffer)) != NULL) {
				r2mcp_log (ss, "Complete message received:");
				r2mcp_log (ss, msg);
				process_mcp_message (ss, msg);
				free (msg);
			}
		} else if (bytes_read == 0) {
			// EOF - stdin closed
			r2mcp_log (ss, "End of input stream - exiting");
			break;
		} else {
			// Error
			if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) {
				r2mcp_log (ss, "Read error");
				break;
			}
		}
	}
	read_buffer_free (buffer);
	r2mcp_log (ss, "Direct mode loop terminated");
}
```
--------------------------------------------------------------------------------
/src/tools.c:
--------------------------------------------------------------------------------
```cpp
#include <r_core.h>
#include "r2mcp.h"
#include "tools.h"
#include <r_util/pj.h>
#include "utils.inc.c" // bring in shared helpers like jsonrpc_tooltext_response
// Standardized error response helpers for consistent error handling
static inline char *jsonrpc_error_missing_param(const char *param_name) {
	char *msg = r_str_newf ("Missing required parameter: %s", param_name);
	char *err = jsonrpc_error_response (-32602, msg, NULL, NULL);
	free (msg);
	return err;
}
#if 0
static inline char *jsonrpc_error_invalid_param(const char *param_name, const char *reason) {
	char *msg = r_str_newf ("Invalid parameter '%s': %s", param_name, reason);
	char *err = jsonrpc_error_response (-32602, msg, NULL, NULL);
	free (msg);
	return err;
}
#endif
static inline char *jsonrpc_error_tool_not_allowed(const char *tool_name) {
	char *msg = r_str_newf ("Tool '%s' not available in current mode (use -p for permissive)", tool_name);
	char *err = jsonrpc_error_response (-32611, msg, NULL, NULL);
	free (msg);
	return err;
}
static inline char *jsonrpc_error_file_required(void) {
	return jsonrpc_error_response (-32611, "Use the open_file method before calling any other method", NULL, NULL);
}
// Parameter validation helpers
static inline bool validate_required_string_param(RJson *args, const char *param_name, const char **out_value) {
	const char *value = r_json_get_str (args, param_name);
	if (!value) {
		return false;
	}
	*out_value = value;
	return true;
}
static inline bool validate_address_param(RJson *args, const char *param_name, const char **out_address) {
	return validate_required_string_param (args, param_name, out_address);
}
static void pj_append_rjson(PJ *pj, RJson *j) {
	if (!j) {
		pj_null (pj);
		return;
	}
	switch (j->type) {
	case R_JSON_NULL:
		pj_null (pj);
		break;
	case R_JSON_BOOLEAN:
		pj_b (pj, j->num.u_value);
		break;
	case R_JSON_INTEGER:
		pj_n (pj, j->num.s_value);
		break;
	case R_JSON_DOUBLE:
		pj_d (pj, j->num.dbl_value);
		break;
	case R_JSON_STRING:
		pj_s (pj, j->str_value);
		break;
	case R_JSON_ARRAY:
		pj_a (pj);
		RJson *child = j->children.first;
		while (child) {
			pj_append_rjson (pj, child);
			child = child->next;
		}
		pj_end (pj);
		break;
	case R_JSON_OBJECT:
		pj_o (pj);
		child = j->children.first;
		while (child) {
			pj_k (pj, child->key);
			pj_append_rjson (pj, child);
			child = child->next;
		}
		pj_end (pj);
		break;
	}
}
// Check an optional whitelist of enabled tool names. If ss->enabled_tools is
// NULL, all tools are considered allowed. Otherwise only names present in the
// list are allowed.
static bool tool_allowed_by_whitelist(const ServerState *ss, const char *name) {
	if (!ss || !ss->enabled_tools) {
		return true;
	}
	RListIter *it;
	const char *s;
	r_list_foreach (ss->enabled_tools, it, s) {
		if (!strcmp (s, name)) {
			return true;
		}
	}
	return false;
}
static inline ToolMode current_mode(const ServerState *ss) {
	if (ss->http_mode) {
		return TOOL_MODE_HTTP;
	}
	if (ss->readonly_mode) {
		return TOOL_MODE_RO;
	}
	if (ss->minimode) {
		return TOOL_MODE_MINI;
	}
	return TOOL_MODE_NORMAL;
}
static ToolSpec *tool(const char *name, const char *desc, const char *schema, int modes) {
	ToolSpec *t = R_NEW0 (ToolSpec);
	t->name = name;
	t->description = desc;
	t->schema_json = schema;
	t->modes = modes;
	return t;
}
void tools_registry_init(ServerState *ss) {
	if (ss->tools) {
		return; // already initialized
	}
	ss->tools = r_list_newf (free);
	if (!ss->tools) {
		return;
	}
	// Modes convenience
	const int M_MINI = TOOL_MODE_MINI;
	const int M_HTTP = TOOL_MODE_HTTP;
	const int M_RO = TOOL_MODE_RO;
	// Normal mode: full set
	r_list_append (ss->tools, tool ("open_file", "Opens a binary file with radare2 for analysis <think>Call this tool before any other one from r2mcp. Use an absolute file_path</think>", "{\"type\":\"object\",\"properties\":{\"file_path\":{\"type\":\"string\",\"description\":\"Path to the file to open\"}},\"required\":[\"file_path\"]}", TOOL_MODE_NORMAL | M_MINI));
	if (ss->enable_run_command_tool) {
		r_list_append (ss->tools, tool ("run_javascript", "Executes JavaScript code using radare2's qjs runtime", "{\"type\":\"object\",\"properties\":{\"script\":{\"type\":\"string\",\"description\":\"The JavaScript code to execute\"}},\"required\":[\"script\"]}", TOOL_MODE_NORMAL | M_MINI | M_HTTP));
		r_list_append (ss->tools, tool ("run_command", "Executes a raw radare2 command directly", "{\"type\":\"object\",\"properties\":{\"command\":{\"type\":\"string\",\"description\":\"The radare2 command to execute\"}},\"required\":[\"command\"]}", TOOL_MODE_NORMAL | M_MINI | M_HTTP));
	}
	r_list_append (ss->tools, tool ("close_file", "Close the currently open file", "{\"type\":\"object\",\"properties\":{}}", TOOL_MODE_NORMAL));
	r_list_append (ss->tools, tool ("list_functions", "Lists all functions discovered during analysis", "{\"type\":\"object\",\"properties\":{\"only_named\":{\"type\":\"boolean\",\"description\":\"If true, only list functions with named symbols (excludes functions with numeric suffixes like sym.func.1000016c8)\"},\"filter\":{\"type\":\"string\",\"description\":\"Regular expression to filter the results\"}}}", TOOL_MODE_NORMAL | M_MINI | M_HTTP | M_RO));
	r_list_append (ss->tools, tool ("list_functions_tree", "Lists functions and successors (aflmu)", "{\"type\":\"object\",\"properties\":{}}", TOOL_MODE_NORMAL | M_MINI | M_HTTP | M_RO));
	r_list_append (ss->tools, tool ("list_libraries", "Lists all shared libraries linked to the binary", "{\"type\":\"object\",\"properties\":{}}", TOOL_MODE_NORMAL | M_MINI | M_HTTP | M_RO));
	r_list_append (ss->tools, tool ("list_imports", "Lists imported symbols (note: use list_symbols for addresses with sym.imp. prefix)", "{\"type\":\"object\",\"properties\":{\"filter\":{\"type\":\"string\",\"description\":\"Regular expression to filter the results\"}}}", TOOL_MODE_NORMAL | M_MINI | M_HTTP | M_RO));
	r_list_append (ss->tools, tool ("list_sections", "Displays memory sections and segments from the binary", "{\"type\":\"object\",\"properties\":{}}", TOOL_MODE_NORMAL | M_RO));
	r_list_append (ss->tools, tool ("show_function_details", "Displays detailed information about the current function", "{\"type\":\"object\",\"properties\":{}}", TOOL_MODE_NORMAL | M_RO));
	r_list_append (ss->tools, tool ("get_current_address", "Shows the current position and function name", "{\"type\":\"object\",\"properties\":{}}", TOOL_MODE_NORMAL | M_RO));
	r_list_append (ss->tools, tool ("show_headers", "Displays binary headers and file information", "{\"type\":\"object\",\"properties\":{}}", TOOL_MODE_NORMAL | M_MINI | M_HTTP | M_RO));
	r_list_append (ss->tools, tool ("list_symbols", "Shows all symbols (functions, variables, imports) with addresses", "{\"type\":\"object\",\"properties\":{\"filter\":{\"type\":\"string\",\"description\":\"Regular expression to filter the results\"}}}", TOOL_MODE_NORMAL | M_MINI | M_HTTP | M_RO));
	r_list_append (ss->tools, tool ("list_entrypoints", "Displays program entrypoints, constructors and main function", "{\"type\":\"object\",\"properties\":{}}", TOOL_MODE_NORMAL | M_MINI | M_HTTP | M_RO));
	r_list_append (ss->tools, tool ("list_methods", "Lists all methods belonging to the specified class", "{\"type\":\"object\",\"properties\":{\"classname\":{\"type\":\"string\",\"description\":\"Name of the class to list methods for\"}},\"required\":[\"classname\"]}", TOOL_MODE_NORMAL | M_RO));
	r_list_append (ss->tools, tool ("list_classes", "Lists class names from various languages (C++, ObjC, Swift, Java, Dalvik)", "{\"type\":\"object\",\"properties\":{\"filter\":{\"type\":\"string\",\"description\":\"Regular expression to filter the results\"}}}", TOOL_MODE_NORMAL | M_RO));
	r_list_append (ss->tools, tool ("list_decompilers", "Shows all available decompiler backends", "{\"type\":\"object\",\"properties\":{}}", TOOL_MODE_NORMAL | M_RO));
	r_list_append (ss->tools, tool ("rename_function", "Renames the function at the specified address", "{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"description\":\"New function name\"},\"address\":{\"type\":\"string\",\"description\":\"Address of the function to rename\"}},\"required\":[\"name\",\"address\"]}", TOOL_MODE_NORMAL));
	r_list_append (ss->tools, tool ("rename_flag", "Renames a local variable or data reference within the specified address", "{\"type\":\"object\",\"properties\":{\"address\":{\"type\":\"string\",\"description\":\"Address of the flag containing the variable or data reference\"},\"name\":{\"type\":\"string\",\"description\":\"Current variable name or data reference\"},\"new_name\":{\"type\":\"string\",\"description\":\"New variable name or data reference\"}},\"required\":[\"address\",\"name\",\"new_name\"]}", TOOL_MODE_NORMAL | M_HTTP));
	r_list_append (ss->tools, tool ("use_decompiler", "Selects which decompiler backend to use (default: pdc)", "{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"description\":\"Name of the decompiler\"}},\"required\":[\"name\"]}", TOOL_MODE_NORMAL));
	r_list_append (ss->tools, tool ("get_function_prototype", "Retrieves the function signature at the specified address", "{\"type\":\"object\",\"properties\":{\"address\":{\"type\":\"string\",\"description\":\"Address of the function\"}},\"required\":[\"address\"]}", TOOL_MODE_NORMAL | M_RO));
	r_list_append (ss->tools, tool ("set_function_prototype", "Sets the function signature (return type, name, arguments)", "{\"type\":\"object\",\"properties\":{\"address\":{\"type\":\"string\",\"description\":\"Address of the function\"},\"prototype\":{\"type\":\"string\",\"description\":\"Function signature in C-like syntax\"}},\"required\":[\"address\",\"prototype\"]}", TOOL_MODE_NORMAL));
	r_list_append (ss->tools, tool ("set_comment", "Adds a comment at the specified address", "{\"type\":\"object\",\"properties\":{\"address\":{\"type\":\"string\",\"description\":\"Address to put the comment in\"},\"message\":{\"type\":\"string\",\"description\":\"Comment text to use\"}},\"required\":[\"address\",\"message\"]}", TOOL_MODE_NORMAL | M_HTTP));
	r_list_append (ss->tools, tool ("list_strings", "Lists strings from data sections with optional regex filter", "{\"type\":\"object\",\"properties\":{\"filter\":{\"type\":\"string\",\"description\":\"Regular expression to filter the results\"},\"cursor\":{\"type\":\"string\",\"description\":\"Cursor for pagination (line number to start from)\"},\"page_size\":{\"type\":\"integer\",\"description\":\"Number of lines per page (default: 1000, max: 10000)\"}}}", TOOL_MODE_NORMAL | M_MINI | M_HTTP | M_RO));
	r_list_append (ss->tools, tool ("list_all_strings", "Scans the entire binary for strings with optional regex filter", "{\"type\":\"object\",\"properties\":{\"filter\":{\"type\":\"string\",\"description\":\"Regular expression to filter the results\"},\"cursor\":{\"type\":\"string\",\"description\":\"Cursor for pagination (line number to start from)\"},\"page_size\":{\"type\":\"integer\",\"description\":\"Number of lines per page (default: 1000, max: 10000)\"}}}", TOOL_MODE_NORMAL | M_RO));
	r_list_append (ss->tools, tool ("analyze", "Runs binary analysis with optional depth level", "{\"type\":\"object\",\"properties\":{\"level\":{\"type\":\"number\",\"description\":\"Analysis level (0-4, higher is more thorough)\"}},\"required\":[]}", TOOL_MODE_NORMAL | M_MINI | M_HTTP));
	r_list_append (ss->tools, tool ("xrefs_to", "Finds all code references to the specified address", "{\"type\":\"object\",\"properties\":{\"address\":{\"type\":\"string\",\"description\":\"Address to check for cross-references\"}},\"required\":[\"address\"]}", TOOL_MODE_NORMAL | M_MINI | M_HTTP | M_RO));
	r_list_append (ss->tools, tool ("decompile_function", "Show C-like pseudocode of the function in the given address. <think>Use this to inspect the code in a function, do not run multiple times in the same offset</think>", "{\"type\":\"object\",\"properties\":{\"address\":{\"type\":\"string\",\"description\":\"Address of the function to decompile\"},\"cursor\":{\"type\":\"string\",\"description\":\"Cursor for pagination (line number to start from)\"},\"page_size\":{\"type\":\"integer\",\"description\":\"Number of lines per page (default: 1000, max: 10000)\"}},\"required\":[\"address\"]}", TOOL_MODE_NORMAL | M_MINI | M_HTTP | M_RO));
	r_list_append (ss->tools, tool ("list_files", "Lists files in the specified path using radare2's ls -q command. Files ending with / are directories, otherwise they are files.", "{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Path to list files from\"}},\"required\":[\"path\"]}", TOOL_MODE_NORMAL | M_MINI | M_HTTP | M_RO));
	r_list_append (ss->tools, tool ("disassemble_function", "Shows assembly listing of the function at the specified address", "{\"type\":\"object\",\"properties\":{\"address\":{\"type\":\"string\",\"description\":\"Address of the function to disassemble\"},\"cursor\":{\"type\":\"string\",\"description\":\"Cursor for pagination (line number to start from)\"},\"page_size\":{\"type\":\"integer\",\"description\":\"Number of lines per page (default: 1000, max: 10000)\"}},\"required\":[\"address\"]}", TOOL_MODE_NORMAL | M_RO));
	r_list_append (ss->tools, tool ("disassemble", "Disassembles a specific number of instructions from an address <think>Use this tool to inspect a portion of memory as code without depending on function analysis boundaries. Use this tool when functions are large and you are only interested on few instructions</think>", "{\"type\":\"object\",\"properties\":{\"address\":{\"type\":\"string\",\"description\":\"Address to start disassembly\"},\"num_instructions\":{\"type\":\"integer\",\"description\":\"Number of instructions to disassemble (default: 10)\"}},\"required\":[\"address\"]}", TOOL_MODE_NORMAL | M_RO));
	r_list_append (ss->tools, tool ("calculate", "Evaluate a math expression using core->num (r_num_math). Usecases: do proper 64-bit math, resolve addresses for flag names/symbols, and avoid hallucinated results.", "{\"type\":\"object\",\"properties\":{\"expression\":{\"type\":\"string\",\"description\":\"Math expression to evaluate (eg. 0x100 + sym.flag - 4)\"}},\"required\":[\"expression\"]}", TOOL_MODE_NORMAL | M_MINI | M_RO));
}
void tools_registry_fini(ServerState *ss) {
	if (ss && ss->tools) {
		r_list_free (ss->tools);
		ss->tools = NULL;
	}
}
static bool tool_matches_mode(const ToolSpec *t, ToolMode mode) {
	return (t->modes & mode) != 0;
}
static RList *tools_filtered_for_mode(const ServerState *ss) {
	ToolMode mode = current_mode (ss);
	RList *out = r_list_new ();
	if (!out) {
		return NULL;
	}
	RListIter *it;
	ToolSpec *t;
	r_list_foreach (ss->tools, it, t) {
		if (tool_matches_mode (t, mode) && tool_allowed_by_whitelist (ss, t->name)) {
			r_list_append (out, t); // reference only
		}
	}
	return out;
}
bool tools_is_tool_allowed(const ServerState *ss, const char *name) {
	if (ss->permissive_tools) {
		return true;
	}
	if (!ss->tools || !name) {
		return false;
	}
	ToolMode mode = current_mode (ss);
	RListIter *it;
	ToolSpec *t;
	r_list_foreach (ss->tools, it, t) {
		if (!strcmp (t->name, name)) {
			if (!tool_allowed_by_whitelist (ss, name)) {
				return false;
			}
			return tool_matches_mode (t, mode);
		}
	}
	return false;
}
char *tools_build_catalog_json(const ServerState *ss, const char *cursor, int page_size) {
	if (!ss->tools) {
		return strdup ("{\"tools\":[]}");
	}
	int start_index = 0;
	if (cursor) {
		start_index = atoi (cursor);
		if (start_index < 0) {
			start_index = 0;
		}
	}
	RList *list = tools_filtered_for_mode (ss);
	if (!list) {
		return strdup ("{\"tools\":[]}");
	}
	int total_tools = r_list_length (list);
	int end_index = start_index + page_size;
	if (end_index > total_tools) {
		end_index = total_tools;
	}
	RStrBuf *sb = r_strbuf_new ("");
	r_strbuf_append (sb, "{\"tools\":[");
	int idx = 0;
	int out_count = 0;
	RListIter *it;
	ToolSpec *t;
	r_list_foreach (list, it, t) {
		if (idx >= start_index && idx < end_index) {
			if (out_count > 0) {
				r_strbuf_append (sb, ",");
			}
			r_strbuf_appendf (sb,
				"{\"name\":\"%s\",\"description\":\"%s\",\"inputSchema\":%s}",
				t->name, t->description, t->schema_json);
			out_count++;
		}
		idx++;
		if (idx >= end_index) {
			// keep looping for correctness of idx but we could break
		}
	}
	r_strbuf_append (sb, "]");
	if (end_index < total_tools) {
		r_strbuf_appendf (sb, ",\"nextCursor\":\"%d\"", end_index);
	}
	r_strbuf_append (sb, "}");
	r_list_free (list);
	return r_strbuf_drain (sb);
}
void tools_print_table(const ServerState *ss) {
	if (!ss || !ss->tools) {
		R_LOG_ERROR ("No tools registered");
		return;
	}
	RTable *table = r_table_new ("tools");
	if (!table) {
		R_LOG_ERROR ("Failed to allocate table");
		return;
	}
	RTableColumnType *s = r_table_type ("string");
	if (!s) {
		R_LOG_WARN ("Table string type unavailable");
		r_table_free (table);
		return;
	}
	r_table_add_column (table, s, "name", 0);
	r_table_add_column (table, s, "modes", 0);
	r_table_add_column (table, s, "description", 0);
	RListIter *it;
	ToolSpec *t;
	r_list_foreach (ss->tools, it, t) {
		char modes_buf[8];
		int p = 0;
		if (t->modes & TOOL_MODE_MINI) {
			modes_buf[p++] = 'M';
		}
		if (t->modes & TOOL_MODE_HTTP) {
			modes_buf[p++] = 'H';
		}
		if (t->modes & TOOL_MODE_RO) {
			modes_buf[p++] = 'R';
		}
		if (t->modes & TOOL_MODE_NORMAL) {
			modes_buf[p++] = 'N';
		}
		modes_buf[p] = '\0';
		const char *desc = t->description? t->description: "";
		r_table_add_rowf (table, "sss", t->name, modes_buf, desc);
	}
	char *table_str = r_table_tostring (table);
	if (table_str) {
		printf ("%s\n", table_str);
		free (table_str);
	}
	r_table_free (table);
}
// Filter lines in `input` by `pattern` regex. Returns a newly allocated string.
static char *filter_lines_by_regex(const char *input, const char *pattern) {
	const char *src = input? input: "";
	if (!pattern || !*pattern) {
		return strdup (src);
	}
	RStrBuf *sb = r_strbuf_new ("");
	RRegex rx;
	int re_flags = r_regex_flags ("e");
	if (r_regex_init (&rx, pattern, re_flags) != 0) {
		r_strbuf_appendf (sb, "Invalid regex used in filter parameter, try a simpler expression");
		return r_strbuf_drain (sb);
	}
	const char *line_begin = src;
	const char *p = src;
	for (;;) {
		if (*p == '\n' || *p == '\0') {
			size_t len = (size_t) (p - line_begin);
			char *line = (char *)malloc (len + 1);
			if (!line) {
				break;
			}
			memcpy (line, line_begin, len);
			line[len] = '\0';
			if (r_regex_exec (&rx, line, 0, 0, 0) == 0) {
				r_strbuf_appendf (sb, "%s\n", line);
			}
			free (line);
			if (*p == '\0') {
				break;
			}
			p++;
			line_begin = p;
			continue;
		}
		p++;
	}
	r_regex_fini (&rx);
	return r_strbuf_drain (sb);
}
static char *filter_named_functions_only(const char *input) {
	const char *src = input? input: "";
	RStrBuf *sb = r_strbuf_new ("");
	const char *line_begin = src;
	const char *p = src;
	for (;;) {
		if (*p == '\n' || *p == '\0') {
			size_t len = (size_t) (p - line_begin);
			char *line = (char *)malloc (len + 1);
			if (!line) {
				break;
			}
			memcpy (line, line_begin, len);
			line[len] = '\0';
			bool is_named = true;
			const char *last_dot = r_str_lchr (line, '.');
			if (last_dot && last_dot[1]) {
				if (isdigit (last_dot[1])) {
					is_named = false;
				}
			}
			if (is_named) {
				r_strbuf_appendf (sb, "%s\n", line);
			}
			free (line);
			if (*p == '\0') {
				break;
			}
			p++;
			line_begin = p;
			continue;
		}
		p++;
	}
	return r_strbuf_drain (sb);
}
// Main dispatcher that handles tool calls. Returns heap-allocated JSON
// string representing the tool "result" (caller must free it).
char *tools_call(ServerState *ss, const char *tool_name, RJson *tool_args) {
	RJson nil = { 0 };
	if (!tool_args) {
		tool_args = &nil;
	}
	if (!tool_name) {
		return jsonrpc_error_missing_param ("name");
	}
	// Enforce tool availability per mode unless permissive is enabled
	if (!tools_is_tool_allowed (ss, tool_name)) {
		return jsonrpc_error_tool_not_allowed (tool_name);
	}
	// Supervisor control check
	if (ss->svc_baseurl) {
		PJ *pj = pj_new ();
		pj_o (pj);
		pj_ks (pj, "tool", tool_name);
		pj_k (pj, "arguments");
		pj_append_rjson (pj, tool_args);
		pj_k (pj, "available_tools");
		pj_a (pj);
		RListIter *iter;
		ToolSpec *ts;
		r_list_foreach (ss->tools, iter, ts) {
			pj_s (pj, ts->name);
		}
		pj_end (pj);
		pj_end (pj);
		char *req = pj_drain (pj);
		int rc;
		char *resp = curl_post_capture (ss->svc_baseurl, req, &rc);
		free (req);
		if (resp && rc == 0) {
			RJson *rj = r_json_parse (resp);
			free (resp);
			if (rj) {
				const char *err = r_json_get_str (rj, "error");
				if (err) {
					r_json_free (rj);
					return jsonrpc_error_response (-32000, err, NULL, NULL);
				}
				const char *r2cmd = r_json_get_str (rj, "r2cmd");
				if (r2cmd) {
					char *cmd_out = r2mcp_cmd (ss, r2cmd);
					char *res = jsonrpc_tooltext_response (cmd_out? cmd_out: "");
					free (cmd_out);
					// If the original tool was open_file, assume the r2cmd opened the file
					if (!strcmp (tool_name, "open_file")) {
						const char *filepath = r_json_get_str (tool_args, "file_path");
						if (filepath) {
							free (ss->rstate.current_file);
							ss->rstate.current_file = strdup (filepath);
							ss->rstate.file_opened = true;
						}
					}
					r_json_free (rj);
					return res;
				}
				const char *new_tool = r_json_get_str (rj, "tool");
				const RJson *new_args = r_json_get (rj, "arguments");
				if (new_tool && strcmp (new_tool, tool_name)) {
					tool_name = strdup (new_tool);
				}
				if (new_args) {
					tool_args = (RJson *)new_args;
				}
				// r_json_free (rj); // Keep alive for tool_args
			}
		}
	}
	// Special-case: open_file
	if (!strcmp (tool_name, "open_file")) {
		if (ss->http_mode) {
			char *res = r2mcp_cmd (ss, "i");
			char *foo = r_str_newf ("File was already opened, this are the details:\n%s", res);
			char *out = jsonrpc_tooltext_response (foo);
			free (res);
			free (foo);
			return out;
		}
		const char *filepath;
		if (!validate_required_string_param (tool_args, "file_path", &filepath)) {
			return jsonrpc_error_missing_param ("file_path");
		}
		char *filteredpath = strdup (filepath);
		r_str_replace_ch (filteredpath, '`', 0, true);
		bool success = r2mcp_open_file (ss, filteredpath);
		free (filteredpath);
		return jsonrpc_tooltext_response (success? "File opened successfully.": "Failed to open file.");
	}
	if (!ss->http_mode && !ss->rstate.file_opened) {
		return jsonrpc_error_file_required ();
	}
	// Map simple tools to commands or handlers
	if (!strcmp (tool_name, "list_methods")) {
		const char *classname;
		if (!validate_required_string_param (tool_args, "classname", &classname)) {
			return jsonrpc_error_missing_param ("classname");
		}
		char *res = r2mcp_cmdf (ss, "'ic %s", classname);
		char *o = jsonrpc_tooltext_response (res);
		free (res);
		return o;
	}
	if (!strcmp (tool_name, "list_files")) {
		const char *path;
		if (!validate_required_string_param (tool_args, "path", &path)) {
			return jsonrpc_error_missing_param ("path");
		}
		// Security checks
		if (!path || path[0] != '/') {
			return jsonrpc_error_response (-32603, "Relative paths are not allowed. Use an absolute path", NULL, NULL);
		}
		if (strstr (path, "/../") != NULL) {
			return jsonrpc_error_response (-32603, "Path traversal is not allowed (contains '/../')", NULL, NULL);
		}
		if (ss->sandbox && *ss->sandbox) {
			size_t plen = strlen (path);
			size_t slen = strlen (ss->sandbox);
			if (slen == 0 || slen > plen || strncmp (path, ss->sandbox, slen) != 0 ||
				(plen > slen && path[slen] != '/')) {
				return jsonrpc_error_response (-32603, "Access denied: path is outside of the sandbox", NULL, NULL);
			}
		}
		char *cmd = r_str_newf ("ls -q %s", path);
		char *res = r2mcp_cmd (ss, cmd);
		free (cmd);
		char *o = jsonrpc_tooltext_response (res);
		free (res);
		return o;
	}
	if (!strcmp (tool_name, "list_classes")) {
		const char *filter = r_json_get_str (tool_args, "filter");
		char *res = r2mcp_cmd (ss, "icqq");
		if (R_STR_ISNOTEMPTY (filter)) {
			char *r = filter_lines_by_regex (res, filter);
			free (res);
			res = r;
		}
		char *o = jsonrpc_tooltext_response (res);
		free (res);
		return o;
	}
	if (!strcmp (tool_name, "list_decompilers")) {
		char *res = r2mcp_cmd (ss, "e cmd.pdc=?");
		char *o = jsonrpc_tooltext_response (res);
		free (res);
		return o;
	}
	if (!strcmp (tool_name, "list_functions")) {
		const RJson *only_named_parameter = r_json_get (tool_args, "only_named");
		bool only_named = false;
		if (only_named_parameter) {
			if (only_named_parameter->type == R_JSON_BOOLEAN) {
				only_named = only_named_parameter->num.u_value;
			}
		}
		const char *filter = r_json_get_str (tool_args, "filter");
		char *res = r2mcp_cmd (ss, "afl,addr/cols/name");
		r_str_trim (res);
		if (R_STR_ISEMPTY (res)) {
			free (res);
			free (r2mcp_cmd (ss, "aaa"));
			res = r2mcp_cmd (ss, "afl,addr/cols/name");
			r_str_trim (res);
			if (R_STR_ISEMPTY (res)) {
				free (res);
				res = strdup ("No functions found. Run the analysis first.");
			}
		}
		// Apply filtering if only_named is true
		if (only_named && res && !R_STR_ISEMPTY (res)) {
			char *filtered = filter_named_functions_only (res);
			if (filtered) {
				free (res);
				res = filtered;
			}
		}
		// Apply regex filter if provided
		if (R_STR_ISNOTEMPTY (filter) && res && !R_STR_ISEMPTY (res)) {
			char *r = filter_lines_by_regex (res, filter);
			free (res);
			res = r;
		}
		char *o = jsonrpc_tooltext_response (res);
		free (res);
		return o;
	}
	if (!strcmp (tool_name, "list_functions_tree")) {
		char *res = r2mcp_cmd (ss, "aflmu");
		r_str_trim (res);
		char *o = jsonrpc_tooltext_response (res);
		free (res);
		return o;
	}
	if (!strcmp (tool_name, "list_imports")) {
		const char *filter = r_json_get_str (tool_args, "filter");
		char *res = r2mcp_cmd (ss, "iiq");
		if (R_STR_ISNOTEMPTY (filter)) {
			char *r = filter_lines_by_regex (res, filter);
			free (res);
			res = r;
		}
		char *o = jsonrpc_tooltext_response (res);
		free (res);
		return o;
	}
	if (!strcmp (tool_name, "list_sections")) {
		char *res = r2mcp_cmd (ss, "iS;iSS");
		char *o = jsonrpc_tooltext_response (res);
		free (res);
		return o;
	}
	if (!strcmp (tool_name, "show_headers")) {
		char *res = r2mcp_cmd (ss, "i;iH");
		char *o = jsonrpc_tooltext_response (res);
		free (res);
		return o;
	}
	if (!strcmp (tool_name, "show_function_details")) {
		char *res = r2mcp_cmd (ss, "afi");
		char *o = jsonrpc_tooltext_response (res);
		free (res);
		return o;
	}
	if (!strcmp (tool_name, "get_current_address")) {
		char *res = r2mcp_cmd (ss, "s;fd");
		char *o = jsonrpc_tooltext_response (res);
		free (res);
		return o;
	}
	if (!strcmp (tool_name, "list_symbols")) {
		const char *filter = r_json_get_str (tool_args, "filter");
		char *res = r2mcp_cmd (ss, "isq~!func.,!imp.");
		if (R_STR_ISNOTEMPTY (filter)) {
			char *r = filter_lines_by_regex (res, filter);
			free (res);
			res = r;
		}
		char *o = jsonrpc_tooltext_response (res);
		free (res);
		return o;
	}
	if (!strcmp (tool_name, "list_entrypoints")) {
		char *res = r2mcp_cmd (ss, "ies");
		char *o = jsonrpc_tooltext_response_lines (res);
		free (res);
		return o;
	}
	if (!strcmp (tool_name, "list_libraries")) {
		char *res = r2mcp_cmd (ss, "ilq");
		char *o = jsonrpc_tooltext_response (res);
		free (res);
		return o;
	}
	if (!strcmp (tool_name, "calculate")) {
		const char *expression;
		if (!validate_required_string_param (tool_args, "expression", &expression)) {
			return jsonrpc_error_missing_param ("expression");
		}
		if (!ss->rstate.core || !ss->rstate.core->num) {
			return jsonrpc_error_response (-32611, "Core or number parser unavailable (open a file first)", NULL, NULL);
		}
		RCore *core = ss->rstate.core;
		ut64 result = r_num_math (core->num, expression);
		char *numstr = r_str_newf ("0x%" PFMT64x, (ut64)result);
		char *resp = jsonrpc_tooltext_response (numstr);
		free (numstr);
		return resp;
	}
	if (!strcmp (tool_name, "close_file")) {
		if (ss->http_mode) {
			return jsonrpc_tooltext_response ("In r2pipe mode we won't close the file.");
		}
		if (ss->rstate.core) {
			free (r2mcp_cmd (ss, "o-*"));
			ss->rstate.file_opened = false;
			free (ss->rstate.current_file);
			ss->rstate.current_file = NULL;
		}
		return jsonrpc_tooltext_response ("File closed successfully.");
	}
	if (!strcmp (tool_name, "set_comment")) {
		const char *address, *message;
		if (!validate_address_param (tool_args, "address", &address) ||
			!validate_required_string_param (tool_args, "message", &message)) {
			return jsonrpc_error_missing_param ("address and message");
		}
		char *cmd_cc = r_str_newf ("'@%s'CC %s", address, message);
		char *tmpres_cc = r2mcp_cmd (ss, cmd_cc);
		free (tmpres_cc);
		free (cmd_cc);
		return strdup ("ok");
	}
	if (!strcmp (tool_name, "set_function_prototype")) {
		const char *address, *prototype;
		if (!validate_address_param (tool_args, "address", &address) ||
			!validate_required_string_param (tool_args, "prototype", &prototype)) {
			return jsonrpc_error_missing_param ("address and prototype");
		}
		char *cmd_afs = r_str_newf ("'@%s'afs %s", address, prototype);
		char *tmpres_afs = r2mcp_cmd (ss, cmd_afs);
		free (tmpres_afs);
		free (cmd_afs);
		return strdup ("ok");
	}
	if (!strcmp (tool_name, "get_function_prototype")) {
		const char *address;
		if (!validate_address_param (tool_args, "address", &address)) {
			return jsonrpc_error_missing_param ("address");
		}
		char *s = r_str_newf ("'@%s'afs", address);
		char *res = r2mcp_cmd (ss, s);
		free (s);
		return res;
	}
	if (!strcmp (tool_name, "list_strings") || !strcmp (tool_name, "list_all_strings")) {
		const char *filter = r_json_get_str (tool_args, "filter");
		const char *cursor = r_json_get_str (tool_args, "cursor");
		int page_size = (int)r_json_get_num (tool_args, "page_size");
		if (page_size <= 0) {
			page_size = R2MCP_DEFAULT_PAGE_SIZE;
		}
		if (page_size > R2MCP_MAX_PAGE_SIZE) {
			page_size = R2MCP_MAX_PAGE_SIZE;
		}
		char *result = r2mcp_cmd (ss, (!strcmp (tool_name, "list_strings")? "izqq": "izzzqq"));
		if (R_STR_ISNOTEMPTY (filter)) {
			char *r = filter_lines_by_regex (result, filter);
			free (result);
			result = r;
		}
		if (!strcmp (tool_name, "list_all_strings") && R_STR_ISEMPTY (result)) {
			free (result);
			result = r_str_newf ("Error: No strings with regex %s", filter);
		}
		bool has_more = false;
		char *next_cursor = NULL;
		char *paginated = paginate_text_by_lines (result, cursor, page_size, &has_more, &next_cursor);
		free (result);
		char *response = jsonrpc_tooltext_response_paginated (paginated, has_more, next_cursor);
		free (paginated);
		free (next_cursor);
		return response;
	}
	if (!strcmp (tool_name, "analyze")) {
		const int level = (int)r_json_get_num (tool_args, "level");
		char *err = r2mcp_analyze (ss, level);
		char *result = r2mcp_cmd (ss, "aflc");
		char *errstr;
		if (R_STR_ISNOTEMPTY (err)) {
			errstr = r_str_newf ("\n\n<log>\n%s\n</log>\n", err);
		} else {
			errstr = strdup ("");
		}
		char *text = r_str_newf ("Analysis completed with level %d.\nFound %d functions.%s", level, atoi (result), errstr);
		char *response = jsonrpc_tooltext_response (text);
		free (err);
		free (errstr);
		free (result);
		free (text);
		return response;
	}
	if (!strcmp (tool_name, "disassemble")) {
		const char *address;
		if (!validate_address_param (tool_args, "address", &address)) {
			return jsonrpc_error_missing_param ("address");
		}
		RJson *num_instr_json = (RJson *)r_json_get (tool_args, "num_instructions");
		int num_instructions = 10;
		if (num_instr_json && num_instr_json->type == R_JSON_INTEGER) {
			num_instructions = (int)num_instr_json->num.u_value;
		}
		char *disasm = r2mcp_cmdf (ss, "'@%s'pd %d", address, num_instructions);
		char *response = jsonrpc_tooltext_response (disasm);
		free (disasm);
		return response;
	}
	if (!strcmp (tool_name, "use_decompiler")) {
		const char *deco;
		if (!validate_required_string_param (tool_args, "name", &deco)) {
			return jsonrpc_error_missing_param ("name");
		}
		char *decompilersAvailable = r2mcp_cmd (ss, "e cmd.pdc=?");
		const char *response = "ok";
		if (strstr (deco, "ghidra")) {
			if (strstr (decompilersAvailable, "pdg")) {
				free (r2mcp_cmd (ss, "-e cmd.pdc=pdg"));
			} else {
				response = "This decompiler is not available";
			}
		} else if (strstr (deco, "decai")) {
			if (strstr (decompilersAvailable, "decai")) {
				free (r2mcp_cmd (ss, "-e cmd.pdc=decai -d"));
			} else {
				response = "This decompiler is not available";
			}
		} else if (strstr (deco, "r2dec")) {
			if (strstr (decompilersAvailable, "pdd")) {
				free (r2mcp_cmd (ss, "-e cmd.pdc=pdd"));
			} else {
				response = "This decompiler is not available";
			}
		} else {
			response = "Unknown decompiler";
		}
		free (decompilersAvailable);
		return jsonrpc_tooltext_response (response);
	}
	if (!strcmp (tool_name, "xrefs_to")) {
		const char *address;
		if (!validate_address_param (tool_args, "address", &address)) {
			return jsonrpc_error_missing_param ("address");
		}
		char *disasm = r2mcp_cmdf (ss, "'@%s'axt", address);
		char *response = jsonrpc_tooltext_response (disasm);
		free (disasm);
		return response;
	}
	if (!strcmp (tool_name, "disassemble_function")) {
		const char *address;
		if (!validate_address_param (tool_args, "address", &address)) {
			return jsonrpc_error_missing_param ("address");
		}
		const char *cursor = r_json_get_str (tool_args, "cursor");
		int page_size = (int)r_json_get_num (tool_args, "page_size");
		if (page_size <= 0) {
			page_size = R2MCP_DEFAULT_PAGE_SIZE;
		}
		if (page_size > R2MCP_MAX_PAGE_SIZE) {
			page_size = R2MCP_MAX_PAGE_SIZE;
		}
		char *disasm = r2mcp_cmdf (ss, "'@%s'pdf", address);
		bool has_more = false;
		char *next_cursor = NULL;
		char *paginated = paginate_text_by_lines (disasm, cursor, page_size, &has_more, &next_cursor);
		free (disasm);
		char *response = jsonrpc_tooltext_response_paginated (paginated, has_more, next_cursor);
		free (paginated);
		free (next_cursor);
		return response;
	}
	if (!strcmp (tool_name, "rename_flag")) {
		const char *address, *name, *new_name;
		if (!validate_address_param (tool_args, "address", &address) ||
			!validate_required_string_param (tool_args, "name", &name) ||
			!validate_required_string_param (tool_args, "new_name", &new_name)) {
			return jsonrpc_error_missing_param ("address, name, and new_name");
		}
		char *remove_res = r2mcp_cmdf (ss, "'@%s'fr %s %s", address, name, new_name);
		if (R_STR_ISNOTEMPTY (remove_res)) {
			char *response = jsonrpc_tooltext_response (remove_res);
			free (remove_res);
			return response;
		}
		free (remove_res);
		return jsonrpc_tooltext_response ("ok");
	}
	if (!strcmp (tool_name, "rename_function")) {
		const char *address, *name;
		if (!validate_address_param (tool_args, "address", &address) ||
			!validate_required_string_param (tool_args, "name", &name)) {
			return jsonrpc_error_missing_param ("address and name");
		}
		free (r2mcp_cmdf (ss, "'@%s'afn %s", address, name));
		return jsonrpc_tooltext_response ("ok");
	}
	if (!strcmp (tool_name, "decompile_function")) {
		const char *address;
		if (!validate_address_param (tool_args, "address", &address)) {
			return jsonrpc_error_missing_param ("address");
		}
		const char *cursor = r_json_get_str (tool_args, "cursor");
		int page_size = (int)r_json_get_num (tool_args, "page_size");
		if (page_size <= 0) {
			page_size = R2MCP_DEFAULT_PAGE_SIZE;
		}
		if (page_size > R2MCP_MAX_PAGE_SIZE) {
			page_size = R2MCP_MAX_PAGE_SIZE;
		}
		char *disasm = r2mcp_cmdf (ss, "'@%s'pdc", address);
		bool has_more = false;
		char *next_cursor = NULL;
		char *paginated = paginate_text_by_lines (disasm, cursor, page_size, &has_more, &next_cursor);
		free (disasm);
		char *response = jsonrpc_tooltext_response_paginated (paginated, has_more, next_cursor);
		free (paginated);
		free (next_cursor);
		return response;
	}
	if (ss->enable_run_command_tool) {
		if (!strcmp (tool_name, "run_command")) {
			const char *command;
			if (!validate_required_string_param (tool_args, "command", &command)) {
				return jsonrpc_error_missing_param ("command");
			}
			char *res = r2mcp_cmd (ss, command);
			char *o = jsonrpc_tooltext_response (res);
			free (res);
			return o;
		}
		if (!strcmp (tool_name, "run_javascript")) {
			const char *script;
			if (!validate_required_string_param (tool_args, "script", &script)) {
				return jsonrpc_error_missing_param ("script");
			}
			char *encoded = r_base64_encode_dyn ((const ut8 *)script, strlen (script));
			if (!encoded) {
				return jsonrpc_error_response (-32603, "Failed to encode script", NULL, NULL);
			}
			char *cmd = r_str_newf ("js base64:%s", encoded);
			char *res = r2mcp_cmd (ss, cmd);
			char *o = jsonrpc_tooltext_response (res);
			free (res);
			free (cmd);
			free (encoded);
			return o;
		}
	}
	char *tmp = r_str_newf ("Unknown tool: %s", tool_name);
	char *err = jsonrpc_error_response (-32602, tmp, NULL, NULL);
	free (tmp);
	return err;
}
```