├── r2mcp.png ├── src ├── config.h.acr ├── readbuffer.h ├── jsonrpc.h ├── prompts │ ├── document_function.r2ai.md │ ├── find_control_flow_path.r2ai.md │ ├── find_crypto_material.r2ai.md │ └── crackme_solver.r2ai.md ├── prompts.h ├── tools.h ├── plugin.c ├── Makefile ├── readbuffer.c ├── utils.inc.c ├── r2mcp.h ├── jsonrpc.c ├── dsltest.c ├── curl.inc.c ├── main.c ├── r2api.inc.c ├── prompts.c ├── r2mcp.c └── tools.c ├── meson_options.txt ├── autogen.sh ├── dist ├── scripts │ ├── indent.pl │ └── indent.py ├── debian │ ├── DESCR │ ├── CONFIG │ ├── Makefile │ ├── build.sh │ └── deb.mk └── docker │ ├── Dockerfile │ └── dockcross ├── .gitignore ├── Makefile ├── configure.acr ├── INSTALL.md ├── test.sh ├── svc ├── Makefile ├── README.md └── r2mcp-svc.c ├── LICENSE ├── TODO.md ├── config.h.w64 ├── README.md ├── meson.build ├── AGENTS.md ├── .github └── workflows │ └── ci.yml └── configure /r2mcp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radareorg/radare2-mcp/main/r2mcp.png -------------------------------------------------------------------------------- /src/config.h.acr: -------------------------------------------------------------------------------- 1 | #ifndef R2MCP_CONFIG_H 2 | #define R2MCP_CONFIG_H 1 3 | 4 | #undef R2MCP_VERSION 5 | #define R2MCP_VERSION "@R2MCP_VERSION@" 6 | 7 | #endif 8 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option('r2_prefix', 2 | type : 'string', 3 | value : '', 4 | description : 'Path prefix where radare2 is installed (include/ and lib/)', 5 | ) 6 | 7 | -------------------------------------------------------------------------------- /autogen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | [ -z "${EDITOR}" ] && EDITOR=vim 3 | acr -p 4 | V=`./configure -qV | cut -d - -f -1` 5 | meson rewrite kwargs set project / version "$V" 6 | ${EDITOR} src/r2mcp.h 7 | ${EDITOR} src/r2mcp.c 8 | -------------------------------------------------------------------------------- /dist/scripts/indent.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | use strict; 3 | use warnings; 4 | 5 | while (<>) { 6 | s/^ +/\t/g; 7 | if (/^[A-Za-z0-9]/) { 8 | s/\s*\(/(/g; 9 | } else { 10 | s/\s*\(/ (/g; 11 | } 12 | print; 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | r2mcp 3 | src/r2mcp 4 | src/config.h 5 | src/Makefile 6 | svc/r2mcp-svc 7 | src/r2mcp 8 | third_party 9 | .cache 10 | ***/*.dSYM 11 | ***/*.dylib 12 | ***/*.dll 13 | ***/*.swp 14 | ***/*.so 15 | ***/*.o 16 | -------------------------------------------------------------------------------- /dist/debian/DESCR: -------------------------------------------------------------------------------- 1 | Model Context Protocol server for radare2 2 | AI Agents with MCP support can start the r2mcp to open binaries with radare2, analyze, solve crackmes, generate security reports, find vulnerabilities and anything you can express with human language without the need to know how to use radare2 3 | -------------------------------------------------------------------------------- /dist/debian/CONFIG: -------------------------------------------------------------------------------- 1 | PACKAGE=r2mcp 2 | VERSION=$(shell ../../configure -qV) 3 | DEPENDS=radare2 4 | SECTION=user/shell 5 | PRIORITY=optional 6 | MAINTAINER=pancake 7 | # arch 8 | UNAMEM=$(shell uname -m) 9 | ifeq ($(UNAMEM),x86_64) 10 | ARCH=amd64 11 | else 12 | ARCH=arm64 13 | endif 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SRC_TARGETS=all clean install user-install uninstall user-uninstall 2 | 3 | $(SRC_TARGETS): src/Makefile 4 | $(MAKE) -C src $@ 5 | $(MAKE) -C svc $@ 6 | 7 | format fmt indent: 8 | clang-format-radare2 src/*.c svc/*.c 9 | #clang-format -i src/*.c svc/*.c 10 | 11 | src/Makefile: 12 | ./configure 13 | 14 | doc: 15 | cat INSTALL.md 16 | 17 | .PHONY: $(SRC_TARGETS) help doc 18 | -------------------------------------------------------------------------------- /configure.acr: -------------------------------------------------------------------------------- 1 | PKGNAME r2mcp 2 | VERSION 1.4.2 3 | CONTACT pancake ; pancake@nopcode.org 4 | 5 | LANG_C! 6 | USE_PKGCONFIG! 7 | 8 | PKGCFG! R2_CFLAGS R2_LDFLAGS r_core 9 | CHKPRG! R2 r2 10 | 11 | EXEC! R2_PREFIX r2 -H R2_PREFIX ; 12 | EXEC! R2_USER_PLUGINS r2 -H R2_USER_PLUGINS ; 13 | EXEC! R2_LIBR_PLUGINS r2 -H R2_LIBR_PLUGINS ; 14 | 15 | R2MCP_VERSION = $VERSION ; 16 | 17 | SUBDIRS src/config.h ; 18 | 19 | REPORT VERSION R2_PREFIX PREFIX PKGCONFIG ; 20 | -------------------------------------------------------------------------------- /src/readbuffer.h: -------------------------------------------------------------------------------- 1 | #ifndef R2MCP_READBUFFER_H 2 | #define R2MCP_READBUFFER_H 1 3 | 4 | #include 5 | #include 6 | 7 | #define BUFFER_SIZE 65536 8 | typedef struct { 9 | char *data; 10 | size_t size; 11 | size_t capacity; 12 | } ReadBuffer; 13 | 14 | ReadBuffer *read_buffer_new(void); 15 | void read_buffer_free(ReadBuffer *buf); 16 | void read_buffer_append(ReadBuffer *buf, const char *data, size_t len); 17 | char *read_buffer_get_message(ReadBuffer *buf); 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | Add the following JSON in your claude's config: 2 | 3 | - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` 4 | - Windows: `%APPDATA%\Claude\claude_desktop_config.json` 5 | - Linux: `~/.config/Claude/claude_desktop_config.json` 6 | 7 | ```json 8 | { 9 | "mcpServers": { 10 | "radare2": { 11 | "command": "r2pm", 12 | "args": ["-r", "r2mcp"] 13 | } 14 | } 15 | } 16 | ``` 17 | 18 | To use r2mcp with OpenWebUI and local models run the mcp-server proxy like this: 19 | 20 | ```bash 21 | pip install mcpo 22 | mcpo -- r2pm -r r2mcp 23 | ``` 24 | -------------------------------------------------------------------------------- /src/jsonrpc.h: -------------------------------------------------------------------------------- 1 | #ifndef JSONRPC_H 2 | #define JSONRPC_H 3 | 4 | #include 5 | #include "r2mcp.h" 6 | 7 | char *jsonrpc_tooltext_response(const char *text); 8 | char *jsonrpc_tooltext_response_paginated(const char *text, bool has_more, const char *next_cursor); 9 | char *jsonrpc_tooltext_response_lines(const char *text); 10 | char *jsonrpc_error_response(int code, const char *message, const char *id, const char *uri); 11 | char *jsonrpc_success_response(ServerState *ss, const char *result, const char *id); 12 | char *jsonrpc_error_missing_param(const char *param_name); 13 | char *jsonrpc_error_tool_not_allowed(const char *tool_name); 14 | char *jsonrpc_error_file_required(void); 15 | 16 | #endif -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | echo "== Build ==" 5 | make -C src -j > /dev/null 6 | 7 | BIN="src/r2mcp" 8 | 9 | echo "== List tools ==" 10 | ${BIN} -t | sed -n '1,10p' 11 | 12 | echo "== DSL: open_file + listFunctions (no -p) ==" 13 | ${BIN} -T 'open_file file_path="/bin/ls"; list_functions only_named=true; close_file' | sed -n '1,8p' 14 | 15 | echo "== DSL: unquoted value and multiple tools ==" 16 | ${BIN} -T 'open_file file_path=/bin/ls; show_headers; get_current_address; close_file' | sed -n '1,12p' 17 | 18 | echo "== DSL: ensure error when missing open_file ==" 19 | set +e 20 | ${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" 21 | set -e 22 | 23 | echo "== OK ==" 24 | 25 | -------------------------------------------------------------------------------- /dist/debian/Makefile: -------------------------------------------------------------------------------- 1 | include ./CONFIG 2 | 3 | DEPENDS= 4 | CROSSARCH=x64 5 | PWD=$(shell pwd) 6 | PACKAGE_DIR?=${PWD} 7 | 8 | DOCKCROSS=$(PWD)/../docker/dockcross 9 | R2PLUGDIR=$(shell r2 -H R2_LIBR_PLUGINS) 10 | 11 | all: root 12 | $(MAKE) build 13 | 14 | local: data 15 | $(MAKE) -C ../.. install DESTDIR=$(PWD)/root 16 | $(MAKE) build 17 | 18 | data $(shell pwd)/data: 19 | sudo rm -rf control data 20 | ${MAKE} clean 21 | rm -f data 22 | mkdir -p data 23 | cp -rf root/* data 24 | 25 | build: data 26 | ${MAKE} control 27 | ${MAKE} deb 28 | 29 | root: 30 | cd ../.. && $(DOCKCROSS) --image dockcross/linux-$(CROSSARCH) \ 31 | bash -c 'DESTDIR=/work/dist/debian/root sh dist/debian/build.sh' 32 | 33 | purge: clean 34 | rm -rf root 35 | docker rmi dockcross/linux-x64 36 | 37 | summary: 38 | echo $(VERSION) 39 | 40 | include deb.mk 41 | -------------------------------------------------------------------------------- /src/prompts/document_function.r2ai.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: 'Explain a function's purpose, behavior, and pseudocode' 3 | version: 0.1 4 | author: r2mcp 5 | args: 6 | - name: address 7 | description: 'Function start address to document' 8 | required: true 9 | - name: detail 10 | description: 'Level of detail: concise|full' 11 | required: false 12 | user_template: | 13 | Target function address: {address}. 14 | Detail level: {detail}. 15 | Use: get_current_address (to verify), disassemble_function (address), decompile_function (address), get_function_prototype (address). 16 | --- 17 | Produce a clear, structured explanation of a function's behavior. 18 | Guidelines: 19 | - Summarize purpose, inputs/outputs, side effects. 20 | - Highlight algorithms, notable constants, error paths. 21 | - Provide a brief high-level pseudocode if helpful. -------------------------------------------------------------------------------- /src/prompts/find_control_flow_path.r2ai.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: 'Find a control-flow path between two addresses for reachability or exploit planning' 3 | version: 0.1 4 | author: r2mcp 5 | args: 6 | - name: source_address 7 | description: 'Source address or block' 8 | required: true 9 | - name: target_address 10 | description: 'Target address or block' 11 | required: true 12 | user_template: | 13 | Compute a path with minimal output. 14 | Source: {source_address}. Target: {target_address}. Use: get_current_address, disassemble_function, disassemble, xrefs_to. 15 | --- 16 | Find and explain a feasible control-flow path between two addresses. 17 | Approach: 18 | - Identify function boundaries for source/target. 19 | - Use xrefs_to and selective disassembly to traverse edges. 20 | - Summarize the path as a sequence of blocks with conditions. -------------------------------------------------------------------------------- /src/prompts/find_crypto_material.r2ai.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: 'Locate keys, IVs, S-boxes, and crypto constants' 3 | version: 0.1 4 | author: r2mcp 5 | args: 6 | - name: hint 7 | description: 'Extra context (e.g., suspected algorithm)' 8 | required: false 9 | user_template: | 10 | Focus: {hint}. 11 | Use: list_imports, list_strings (filter to crypto keywords), list_functions (scan for suspicious names), and xrefs_to (address). 12 | If needed, disassemble/decompile only tight regions where material is assigned. 13 | --- 14 | You are tasked with locating cryptographic material in a binary. 15 | Strategy: 16 | - List imports and strings to find crypto APIs/signatures. 17 | - Search for constants (AES S-box, SHA tables), base64 sets, or long random-looking blobs. 18 | - Inspect xrefs to functions handling buffers just before encryption/decryption. 19 | - Use selective decompilation and avoid dumping entire files. -------------------------------------------------------------------------------- /svc/Makefile: -------------------------------------------------------------------------------- 1 | CC ?= gcc 2 | PKGCONFIG = pkg-config 3 | R2_CFLAGS = $(shell $(PKGCONFIG) --cflags r_util) 4 | R2_LDFLAGS = $(shell $(PKGCONFIG) --libs r_util r_socket r_cons) 5 | R2PM_BINDIR=$(shell r2pm -H R2PM_BINDIR) 6 | INSTALL_DIR ?= install -d 7 | INSTALL_PROGRAM ?= install -m 755 8 | PREFIX ?= /usr/local 9 | 10 | TARGET = r2mcp-svc 11 | 12 | all: $(TARGET) 13 | 14 | $(TARGET): r2mcp-svc.c 15 | $(CC) $(R2_CFLAGS) -Wall -Wextra -o $@ $< $(R2_LDFLAGS) 16 | 17 | clean: 18 | rm -f $(TARGET) 19 | 20 | install: all 21 | $(INSTALL_DIR) $(DESTDIR)/$(PREFIX)/bin 22 | $(INSTALL_PROGRAM) $(TARGET) $(DESTDIR)/$(PREFIX)/bin/$(TARGET) 23 | 24 | uninstall: 25 | rm -f $(DESTDIR)/$(PREFIX)/bin/$(TARGET) 26 | 27 | user-install: 28 | $(INSTALL_DIR) $(DESTDIR)/$(R2PM_BINDIR) 29 | $(INSTALL_PROGRAM) $(TARGET) $(DESTDIR)/$(R2PM_BINDIR)/$(TARGET) 30 | 31 | user-uninstall: 32 | rm -f $(DESTDIR)/$(R2PM_BINDIR)/$(TARGET) 33 | 34 | .PHONY: clean install uninstall 35 | -------------------------------------------------------------------------------- /src/prompts.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "r2mcp.h" 5 | 6 | typedef struct { 7 | const char *name; 8 | const char *description; 9 | bool required; 10 | } PromptArg; 11 | 12 | typedef struct PromptSpec { 13 | const char *name; 14 | const char *description; 15 | PromptArg *args; 16 | int nargs; 17 | // Render returns a JSON object string with { messages: [...] } 18 | char *(*render)(const struct PromptSpec *spec, RJson *arguments); 19 | void *render_data; 20 | } PromptSpec; 21 | 22 | // Initialize and shutdown the prompts registry stored in ServerState 23 | void prompts_registry_init(ServerState *ss); 24 | void prompts_registry_fini(ServerState *ss); 25 | 26 | // Build list JSON for prompts with optional pagination 27 | char *prompts_build_list_json(const ServerState *ss, const char *cursor, int page_size); 28 | 29 | // Resolve a prompt by name and arguments, returning a JSON object string 30 | char *prompts_get_json(const ServerState *ss, const char *name, RJson *arguments); 31 | 32 | -------------------------------------------------------------------------------- /dist/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest AS builder 2 | 3 | # Install build dependencies 4 | RUN apk add --no-cache \ 5 | build-base \ 6 | git \ 7 | pkgconfig \ 8 | python3 \ 9 | libc-dev \ 10 | file-dev \ 11 | libzip-dev \ 12 | openssl-dev \ 13 | linux-headers \ 14 | meson \ 15 | ninja 16 | 17 | WORKDIR /build 18 | # Install radare2 (for build only) 19 | RUN git clone --depth 1 https://github.com/radareorg/radare2.git && \ 20 | cd radare2 && \ 21 | ./sys/install.sh && \ 22 | cd .. 23 | 24 | RUN git clone --depth 1 https://github.com/radareorg/radare2-mcp.git && \ 25 | cd radare2-mcp && \ 26 | ./configure && \ 27 | make && \ 28 | cp -f src/r2mcp /usr/local/bin/ 29 | 30 | # Create /data volume for binary analysis 31 | WORKDIR /data 32 | VOLUME ["/data"] 33 | 34 | # Environment setup for r2mcp 35 | ENV R2MCP_DEBUG=0 36 | 37 | # Simply set the entrypoint 38 | ENTRYPOINT ["/usr/local/bin/r2mcp"] 39 | 40 | # Instructions for users: 41 | # IMPORTANT: Always run with `-i` flag to keep stdin open: 42 | # docker run -i --rm r2mcp 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 dnakov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dist/debian/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | type fakeroot > /dev/null 2>&1 4 | if [ $? = 0 ]; then 5 | FAKEROOT=fakeroot 6 | else 7 | FAKEROOT=sudo 8 | fi 9 | 10 | r2 -qv 11 | if [ $? != 0 ]; then 12 | echo "Cannot find radare2, building with sys/debian.sh from git.." 13 | # git clone --depth=1 git@github.com:radareorg/radare2 r2 || exit 1 14 | wget -c https://github.com/radareorg/radare2/archive/master.zip 15 | sudo apt-get update 16 | sudo apt-get -y install git g++ make pkg-config flex bison unzip patch || exit 1 17 | unzip -l master.zip 18 | unzip master.zip || exit 1 19 | mv radare2-master r2 20 | ( cd r2 && sys/debian.sh ) # make -C r2/dist/debian 21 | sudo dpkg -i r2/dist/debian/*/*.deb || exit 1 22 | fi 23 | [ -z "${DESTDIR}" ] && DESTDIR="/work/dist/debian/root" 24 | 25 | RV=`r2 -qv` 26 | [ -z "${RV}" ] && RV=`r2/configure -qV` 27 | 28 | R2_LIBR_PLUGINS="`r2 -H R2_LIBR_PLUGINS`" 29 | [ -z "${R2_LIBR_PLUGINS}" ] && R2_LIBR_PLUGINS=/usr/lib/radare2 30 | 31 | export CFLAGS=-O2 32 | make R2_PLUGDIR=${R2_LIBR_PLUGINS} DESTDIR=${DESTDIR} 33 | 34 | ./configure --prefix=/usr || exit 1 35 | make -j4 || exit 1 36 | strip --strip-unneeded r2mcp 37 | ${FAKEROOT} make install DESTDIR="${DESTDIR}" 38 | -------------------------------------------------------------------------------- /dist/scripts/indent.py: -------------------------------------------------------------------------------- 1 | import re 2 | import subprocess 3 | 4 | # Step 1: Format the file in-place using clang-format 5 | subprocess.run(["clang-format", "-i", "r2mcp.c"], check=True) 6 | 7 | def is_function(s): 8 | return s and not s[0].isspace() 9 | 10 | def is_control_structure(s): 11 | return s in {"if", "for", "while", "switch", "catch", "return"} 12 | 13 | def fix_line(line): 14 | # Skip lines that are empty or only whitespace 15 | if not line.strip(): 16 | return line 17 | 18 | # Match function calls like: foo(bar) => foo (bar) 19 | # Avoid if/for/while/catch/return and function *definitions* 20 | pattern = r'\b([a-zA-Z_]\w*)\(' 21 | 22 | def replacer(match): 23 | name = match.group(1) 24 | if is_control_structure(name) or is_function(line): 25 | return match.group(0) # No change 26 | return f'{name} (' 27 | 28 | return re.sub(pattern, replacer, line) 29 | 30 | # Step 2: Read the file, transform it, and write it back 31 | with open("r2mcp.c", "r", encoding="utf-8") as f: 32 | lines = f.readlines() 33 | 34 | with open("r2mcp.c", "w", encoding="utf-8") as f: 35 | for line in lines: 36 | f.write(fix_line(line)) 37 | -------------------------------------------------------------------------------- /src/prompts/crackme_solver.r2ai.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: 'Plan and solve a crackme using radare2 with minimal, targeted steps' 3 | version: 0.1 4 | author: r2mcp 5 | args: 6 | - name: file_path 7 | description: 'Absolute path to target binary' 8 | required: false 9 | - name: goal 10 | description: 'What success looks like (e.g., recover password)' 11 | required: false 12 | user_template: | 13 | Task: {goal}. 14 | {if file_path}Open file: {file_path} (use tools/call open_file).{/if} 15 | {else}Ask for or confirm file path if unknown.{/else} 16 | Plan your steps, then call: analyze (level=2), list_entrypoints, list_functions, list_imports, list_strings (filter optional). 17 | Use decompile_function or disassemble_function on candidate functions only. 18 | --- 19 | You are an expert reverse engineer using radare2 via r2mcp. 20 | Goal: plan first, then execute minimal tool calls. 21 | General steps: 22 | 1) Open the target binary and run lightweight analysis (analyze level 2). 23 | 2) Identify main/entrypoints and functions referring to strcmp, strncmp, memcmp, crypto, or suspicious branches. 24 | 3) Read/Decompile only the most relevant functions (avoid dumping huge outputs). 25 | 4) Derive the key/logic and propose inputs or patches. 26 | 5) Summarize findings and next actions. 27 | Prefer afl listing with addresses, selective pdc/pdf on key functions, and xrefs_to for checks. -------------------------------------------------------------------------------- /src/tools.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "r2mcp.h" 5 | 6 | typedef enum { 7 | TOOL_MODE_MINI = 1 << 0, 8 | TOOL_MODE_HTTP = 1 << 1, 9 | TOOL_MODE_NORMAL = 1 << 2, 10 | TOOL_MODE_RO = 1 << 3, 11 | } ToolMode; 12 | 13 | // Tool handler signature: returns heap-allocated JSON string describing 14 | // the tool result (typically jsonrpc_tooltext_response() content or other 15 | // structured JSON). Caller must free the returned string. 16 | typedef char *(*ToolHandler)(ServerState *ss, RJson *args); 17 | 18 | typedef struct { 19 | const char *name; 20 | const char *description; 21 | const char *schema_json; 22 | int modes; // bitmask of ToolMode 23 | ToolHandler func; 24 | } ToolSpec; 25 | 26 | extern ToolSpec tool_specs[]; 27 | 28 | // Tool flags (for future use). For now, we use one to require an open file. 29 | #define TOOL_FLAG_REQUIRES_OPENFILE (1 << 0) 30 | 31 | 32 | 33 | // Build catalog JSON for the current server mode with optional pagination 34 | char *tools_build_catalog_json(const ServerState *ss, const char *cursor, int page_size); 35 | 36 | // Check if a tool is allowed for the current mode (honors permissive flag) 37 | bool tools_is_tool_allowed(const ServerState *ss, const char *name); 38 | 39 | // Call a tool by name; returns heap-allocated JSON (tool "result") or 40 | // a JSON error result if the tool is unavailable or arguments are invalid. 41 | // The returned string must be freed by the caller. 42 | char *tools_call(ServerState *ss, const char *tool_name, RJson *args); 43 | 44 | // Print a human friendly table of available tools for the current mode 45 | // Columns: name | mini-mode (yes/no) | description 46 | void tools_print_table(const ServerState *ss); 47 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | Ideas and future plans for r2mcp 4 | 5 | * Support multiple sessions 6 | * The r2copilot-mcp supports having multiple clients using the same mcp at the same time 7 | * This requires a hard change in the logic because we need to pass session identifiers and handle multiple instancs of core. so better dont do it until core is fully refactored to be thread safe 8 | * Extensions/Plugins 9 | * Let the user load an .r2.js script or yaml file to define new tools or prompts 10 | * Support loading custom plugins written in C or other languages 11 | * Resources and Templates 12 | * Strings, symbols, relocs, imports, libraries, .. 13 | * Reversing context with user comments and project memory 14 | * Prompts in filesystem instead of hardcoded inside the executable 15 | * Easier to maintain, user-customizable prompts for different analysis scenarios 16 | * Support templates (yaml definition for parameters?) to make prompts more concise 17 | * Automated report generation 18 | * Advanced Analysis Tools 19 | * Find path between two points in the program 20 | * Progressive analysis and avoid analyzing twice (Optimized analysis for large binaries) 21 | * Support loading and unloading multiple files 22 | * Projects Support 23 | * Function signature matching and library identification 24 | * Caching of analysis results 25 | * Export analysis results to various formats (JSON, XML, GraphML) 26 | * Import external analysis data 27 | * Debugging Integration (Requires providing permissions to do it) 28 | * Support for emulation and native debugging 29 | * Spawn or Attach to running processes 30 | * Step-through debugging with breakpoints 31 | * Memory inspection and modification 32 | * Register state analysis 33 | * User Interface and Testing Q&A 34 | * Documentation and tutorials 35 | * User-contributed scripts and templates 36 | * Comprehensive test suite for all tools 37 | -------------------------------------------------------------------------------- /src/plugin.c: -------------------------------------------------------------------------------- 1 | /* r2mcp - MIT - Copyright 2025 - pancake */ 2 | 3 | #define R_LOG_ORIGIN "core.r2mcp" 4 | 5 | #include 6 | #include "r2mcp.h" 7 | #include "tools.h" 8 | 9 | int r2mcp_run_dsl_tests(ServerState *ss, const char *dsl, RCore *core); 10 | 11 | typedef struct r2mcp_data_t { 12 | ServerState *ss; 13 | } R2mcpData; 14 | 15 | static bool r2mcp_call(RCorePluginSession *cps, const char *input) { 16 | RCore *core = cps->core; 17 | R2mcpData *data = cps->data; 18 | 19 | if (!r_str_startswith (input, "r2mcp")) { 20 | return false; 21 | } 22 | 23 | // Skip "r2mcp" command name 24 | const char *args = r_str_trim_head_ro (input + strlen ("r2mcp")); 25 | 26 | // Initialize server state if not already done 27 | if (!data->ss) { 28 | data->ss = R_NEW0 (ServerState); 29 | // Initialize the tools registry 30 | // Set up the core reference 31 | data->ss->rstate.core = core; 32 | data->ss->rstate.file_opened = true; // We're already in r2 with a file 33 | } 34 | 35 | if (R_STR_ISEMPTY (args)) { 36 | tools_print_table (data->ss); 37 | } else { 38 | if (r2mcp_run_dsl_tests (data->ss, args, core) != 0) { 39 | R_LOG_ERROR ("Error executing r2mcp command"); 40 | } 41 | } 42 | 43 | return true; 44 | } 45 | 46 | static bool r2mcp_init(RCorePluginSession *cps) { 47 | R2mcpData *data = R_NEW0 (R2mcpData); 48 | cps->data = data; 49 | return true; 50 | } 51 | 52 | static bool r2mcp_fini(RCorePluginSession *cps) { 53 | R2mcpData *data = cps->data; 54 | if (data) { 55 | if (data->ss) { 56 | free (data->ss); 57 | } 58 | free (data); 59 | } 60 | return true; 61 | } 62 | 63 | // PLUGIN Definition Info 64 | RCorePlugin r_core_plugin_r2mcp = { 65 | .meta = { 66 | .name = "core-r2mcp", 67 | .desc = "r2mcp command integration for radare2", 68 | .author = "pancake", 69 | .license = "MIT", 70 | }, 71 | .call = r2mcp_call, 72 | .init = r2mcp_init, 73 | .fini = r2mcp_fini, 74 | }; 75 | 76 | #ifndef R2_PLUGIN_INCORE 77 | R_API RLibStruct radare_plugin = { 78 | .type = R_LIB_TYPE_CORE, 79 | .data = &r_core_plugin_r2mcp, 80 | .version = R2_VERSION, 81 | .abiversion = R2_ABIVERSION 82 | }; 83 | #endif 84 | -------------------------------------------------------------------------------- /src/Makefile: -------------------------------------------------------------------------------- 1 | CC?= gcc 2 | TARGET = r2mcp 3 | CFLAGS = -Wall -Wextra -g 4 | PKGCONFIG = pkg-config 5 | INSTALL_DIR?=install -d 6 | R2_LIBEXT=$(shell r2 -H R2_LIBEXT) 7 | INSTALL_PROGRAM?=install -m 755 8 | PREFIX?=/usr/local 9 | R2PM_BINDIR?=$(shell r2pm -H R2PM_BINDIR) 10 | R2PM_PLUGDIR?=$(shell r2pm -H R2PM_PLUGDIR) 11 | 12 | SRC = main.c 13 | SRC += r2mcp.c 14 | SRC += readbuffer.c 15 | SRC += tools.c 16 | SRC += dsltest.c 17 | SRC += prompts.c 18 | SRC += plugin.c 19 | SRC += jsonrpc.c 20 | INC += utils.inc.c 21 | INC += r2api.inc.c 22 | OBJS:=$(subst .c,.o,$(SRC)) 23 | 24 | # Detect OS-specific settings 25 | UNAME_S := $(shell uname -s) 26 | ifeq ($(UNAME_S),Darwin) 27 | # macOS specific include paths 28 | CFLAGS += -I/usr/local/include/libr -I/usr/local/include 29 | CFLAGS += -I$(shell brew --prefix)/include 30 | endif 31 | 32 | # Get compiler flags from pkg-config 33 | R2_CFLAGS = $(shell $(PKGCONFIG) --cflags r_core) 34 | R2_LDFLAGS = $(shell $(PKGCONFIG) --libs r_core) 35 | 36 | CFLAGS += $(R2_CFLAGS) 37 | LDFLAGS = $(R2_LDFLAGS) 38 | 39 | .PHONY: all clean check_deps help install uninstall user-install user-uninstall 40 | 41 | LIBTARGET=libcore_$(TARGET).$(R2_LIBEXT) 42 | all: check_deps $(TARGET) $(LIBTARGET) 43 | 44 | check_deps: 45 | @echo "Checking dependencies..." 46 | @which $(PKGCONFIG) > /dev/null || (echo "pkg-config not found. Please install it."; exit 1) 47 | @$(PKGCONFIG) --exists r_core || (echo "radare2 development files not found."; exit 1) 48 | @echo "✓ All dependencies are satisfied." 49 | 50 | $(TARGET): $(OBJS) $(INC) 51 | @echo "Building R2 MCP server..." 52 | $(CC) $(CFLAGS) -o $@ $(OBJS) $(LDFLAGS) 53 | @echo "✓ Server built successfully." 54 | 55 | $(LIBTARGET): $(OBJS) $(INC) 56 | $(CC) $(CFLAGS) -o $(LIBTARGET) $(OBJS) $(LDFLAGS) 57 | 58 | clean: 59 | rm -f $(TARGET) $(OBJS) 60 | -rm -rf r2mcp.dSYM 61 | 62 | mrproper: clean 63 | rm -f Makefile 64 | 65 | install: all 66 | $(INSTALL_DIR) $(DESTDIR)/$(PREFIX)/bin 67 | $(INSTALL_PROGRAM) $(TARGET) $(DESTDIR)/$(PREFIX)/bin/r2mcp 68 | 69 | uninstall: 70 | rm -f $(DESTDIR)/$(PREFIX)/bin/r2mcp 71 | 72 | user-install: all 73 | $(INSTALL_DIR) $(R2PM_BINDIR) 74 | $(INSTALL_PROGRAM) $(TARGET) $(R2PM_BINDIR)/r2mcp 75 | $(INSTALL_PROGRAM) $(LIBTARGET) $(R2PM_PLUGDIR) 76 | 77 | doc: 78 | cat INSTALL.md 79 | 80 | user-uninstall: 81 | rm -f $(R2_BINDIR)/bin/r2mcp 82 | 83 | indent fmt: 84 | clang-format-radare2 *.c 85 | 86 | help: 87 | @echo "Available targets:" 88 | @echo " all - Build the server (default)" 89 | @echo " check_deps - Check for required dependencies" 90 | @echo " clean - Remove built binaries" 91 | @echo " install - Install the server to /usr/local/bin" 92 | @echo " uninstall - Install the server to /usr/local/bin" 93 | @echo " user-install - Install the server to /usr/local/bin" 94 | @echo " user-uninstall - Install the server to /usr/local/bin" 95 | @echo " help - Display this help message" 96 | @echo 97 | @echo "Usage: make [target]" 98 | -------------------------------------------------------------------------------- /svc/README.md: -------------------------------------------------------------------------------- 1 | # SuperVisor Console for r2mcp 2 | 3 | ## Overview 4 | 5 | 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. 6 | 7 | ## Purpose 8 | 9 | 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. 10 | 11 | ## How It Works 12 | 13 | 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: 14 | 15 | - **Accept**: Allow the tool call to proceed as requested 16 | - **Reject**: Block the tool call entirely 17 | - **Modify**: Alter the query or parameters before execution 18 | - **Respond with Error**: Return a custom error message 19 | - **Provide Different Instructions**: Substitute alternative instructions 20 | 21 | ## Integration with R2 MCP 22 | 23 | The SBC integrates seamlessly with R2 MCP through a simple flag-based mechanism: 24 | 25 | - Run R2 MCP with a specific flag specifying the SBC host and port 26 | - R2 MCP will then execute tool calls against the target SBC URL 27 | - The SBC service receives these connections and prompts the user via command-line interface for the desired action 28 | 29 | ## Control Protocol 30 | 31 | The supervision control is implemented using JSON over HTTP, ensuring compatibility and ease of integration. 32 | 33 | ## User Interface 34 | 35 | The SBC provides a command-line tool interface for user interaction and decision-making. 36 | 37 | ## Default Behavior 38 | 39 | By default, R2 MCP operates normally without supervision. When the SBC flag is provided: 40 | 41 | - R2 MCP attempts to connect to the specified SBC endpoint 42 | - If connection fails, R2 MCP falls back to normal operation (no supervision) 43 | - If connection succeeds, R2 MCP waits for SBC responses before proceeding with tool executions 44 | 45 | This design ensures that supervision is optional and doesn't break existing workflows when the SBC is unavailable. 46 | 47 | ## Building and Running 48 | 49 | To build the R2 MCP-SBC tool: 50 | 51 | ```bash 52 | make 53 | ``` 54 | 55 | This will create the `r2mcp-svc` executable. 56 | 57 | To run the SBC server on a specific port: 58 | 59 | ```bash 60 | ./r2mcp-svc 61 | ``` 62 | 63 | For example: 64 | 65 | ```bash 66 | ./r2mcp-svc 8080 67 | ``` 68 | 69 | 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. 70 | 71 | ## Integration with R2 MCP 72 | 73 | In R2 MCP, use the supervision flag to specify the SBC endpoint: 74 | 75 | ``` 76 | r2mcp --supervise http://localhost:8080 77 | ``` 78 | 79 | If the SBC is unreachable, R2 MCP will operate normally without supervision. 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/readbuffer.c: -------------------------------------------------------------------------------- 1 | /* r2mcp - MIT - Copyright 2025 - pancake, dnakov */ 2 | 3 | #include 4 | #include 5 | #include "readbuffer.h" 6 | 7 | ReadBuffer *read_buffer_new(void) { 8 | ReadBuffer *buf = R_NEW (ReadBuffer); 9 | buf->data = malloc (BUFFER_SIZE); 10 | buf->size = 0; 11 | buf->capacity = BUFFER_SIZE; 12 | return buf; 13 | } 14 | 15 | void read_buffer_free(ReadBuffer *buf) { 16 | if (buf) { 17 | free (buf->data); 18 | free (buf); 19 | } 20 | } 21 | 22 | void read_buffer_append(ReadBuffer *buf, const char *data, size_t len) { 23 | if (buf->size + len > buf->capacity) { 24 | size_t new_capacity = buf->capacity * 2; 25 | if (new_capacity < buf->size + len) { 26 | new_capacity = buf->size + len; 27 | } 28 | char *new_data = realloc (buf->data, new_capacity); 29 | if (!new_data) { 30 | R_LOG_ERROR ("Failed to resize buffer"); 31 | return; 32 | } 33 | buf->data = new_data; 34 | buf->capacity = new_capacity; 35 | } 36 | memcpy (buf->data + buf->size, data, len); 37 | buf->size += len; 38 | } 39 | 40 | // Extract a complete JSON message from the buffer, respecting string quoting. 41 | // Returns a heap-allocated message string or NULL if no complete message is available. 42 | char *read_buffer_get_message(ReadBuffer *buf) { 43 | if (buf->size == 0) { 44 | return NULL; 45 | } 46 | 47 | // Ensure the buffer is null-terminated for safety 48 | if (buf->capacity <= buf->size + 1) { 49 | buf->capacity = buf->size + 2; 50 | char *new_data = realloc (buf->data, buf->capacity); 51 | if (!new_data) { 52 | return NULL; 53 | } 54 | buf->data = new_data; 55 | } 56 | buf->data[buf->size] = '\0'; 57 | 58 | int brace_count = 0; 59 | int start_pos = -1; 60 | bool in_string = false; 61 | bool escape_next = false; 62 | size_t i; 63 | 64 | for (i = 0; i < buf->size; i++) { 65 | const char c = buf->data[i]; 66 | 67 | // Handle escape sequences inside strings 68 | if (escape_next) { 69 | escape_next = false; 70 | continue; 71 | } 72 | 73 | if (in_string) { 74 | if (c == '\\') { 75 | escape_next = true; 76 | } else if (c == '"') { 77 | in_string = false; 78 | } 79 | continue; 80 | } 81 | 82 | // Outside of a string 83 | if (c == '"') { 84 | in_string = true; 85 | continue; 86 | } 87 | 88 | if (start_pos == -1) { 89 | if (c == '{') { 90 | start_pos = i; 91 | brace_count = 1; 92 | } 93 | continue; 94 | } 95 | 96 | if (c == '{') { 97 | brace_count++; 98 | } else if (c == '}') { 99 | brace_count--; 100 | if (brace_count == 0) { 101 | // Complete message from start_pos to i (inclusive) 102 | size_t msg_len = i - start_pos + 1; 103 | char *msg = malloc (msg_len + 1); 104 | memcpy (msg, buf->data + start_pos, msg_len); 105 | msg[msg_len] = '\0'; 106 | 107 | // Shift remaining data to the front 108 | size_t remaining = buf->size - (i + 1); 109 | if (remaining > 0) { 110 | memmove (buf->data, buf->data + i + 1, remaining); 111 | } 112 | buf->size = remaining; 113 | return msg; 114 | } 115 | } 116 | } 117 | 118 | return NULL; 119 | } 120 | -------------------------------------------------------------------------------- /src/utils.inc.c: -------------------------------------------------------------------------------- 1 | /* r2mcp - MIT - Copyright 2025 - pancake */ 2 | 3 | #if R2_VERSION_NUMBER < 50909 4 | static st64 r_json_get_num(const RJson *json, const char *key) { 5 | R_RETURN_VAL_IF_FAIL (json && key, 0); 6 | const RJson *field = r_json_get (json, key); 7 | if (field) { 8 | switch (field->type) { 9 | case R_JSON_STRING: 10 | return r_num_get (NULL, field->str_value); 11 | case R_JSON_INTEGER: 12 | return field->num.s_value; 13 | case R_JSON_BOOLEAN: 14 | return field->num.u_value; 15 | case R_JSON_DOUBLE: 16 | return (int)field->num.dbl_value; 17 | } 18 | } 19 | return 0; 20 | } 21 | 22 | static const char *r_json_get_str(const RJson *json, const char *key) { 23 | R_RETURN_VAL_IF_FAIL (json && key, NULL); 24 | const RJson *field = r_json_get (json, key); 25 | if (!field || field->type != R_JSON_STRING) { 26 | return NULL; 27 | } 28 | return field->str_value; 29 | } 30 | 31 | #endif 32 | 33 | // Helper to paginate text by lines 34 | static inline char *paginate_text_by_lines(char *text, const char *cursor, int page_size, bool *has_more, char **next_cursor) { 35 | if (!text) { 36 | if (has_more) { 37 | *has_more = false; 38 | } 39 | if (next_cursor) { 40 | *next_cursor = NULL; 41 | } 42 | return strdup (""); 43 | } 44 | RList *lines = r_str_split_list (text, "\n", 0); 45 | if (!lines) { 46 | return NULL; 47 | } 48 | int total_lines = r_list_length (lines); 49 | int start_line = 0; 50 | if (cursor) { 51 | start_line = atoi (cursor); 52 | if (start_line < 0) { 53 | start_line = 0; 54 | } 55 | } 56 | int end_line = start_line + page_size; 57 | if (end_line > total_lines) { 58 | end_line = total_lines; 59 | } 60 | RStrBuf *sb = r_strbuf_new (""); 61 | int idx = 0; 62 | RListIter *it; 63 | char *line; 64 | r_list_foreach (lines, it, line) { 65 | if (idx >= start_line && idx < end_line) { 66 | if (r_strbuf_length (sb) > 0) { 67 | r_strbuf_append (sb, "\n"); 68 | } 69 | r_strbuf_append (sb, line); 70 | } 71 | idx++; 72 | } 73 | r_list_free (lines); 74 | char *result = r_strbuf_drain (sb); 75 | if (has_more) { 76 | *has_more = end_line < total_lines; 77 | } 78 | if (next_cursor) { 79 | if (end_line < total_lines) { 80 | *next_cursor = r_str_newf ("%d", end_line); 81 | } else { 82 | *next_cursor = NULL; 83 | } 84 | } 85 | return result; 86 | } 87 | 88 | static void pj_append_rjson(PJ *pj, RJson *j) { 89 | if (!j) { 90 | pj_null (pj); 91 | return; 92 | } 93 | switch (j->type) { 94 | case R_JSON_NULL: 95 | pj_null (pj); 96 | break; 97 | case R_JSON_BOOLEAN: 98 | pj_b (pj, j->num.u_value); 99 | break; 100 | case R_JSON_INTEGER: 101 | pj_n (pj, j->num.s_value); 102 | break; 103 | case R_JSON_DOUBLE: 104 | pj_d (pj, j->num.dbl_value); 105 | break; 106 | case R_JSON_STRING: 107 | pj_s (pj, j->str_value); 108 | break; 109 | case R_JSON_ARRAY: 110 | pj_a (pj); 111 | RJson *child = j->children.first; 112 | while (child) { 113 | pj_append_rjson (pj, child); 114 | child = child->next; 115 | } 116 | pj_end (pj); 117 | break; 118 | case R_JSON_OBJECT: 119 | pj_o (pj); 120 | child = j->children.first; 121 | while (child) { 122 | pj_k (pj, child->key); 123 | pj_append_rjson (pj, child); 124 | child = child->next; 125 | } 126 | pj_end (pj); 127 | break; 128 | } 129 | } 130 | 131 | // Intentionally no generic require_str_param helper; callers validate params inline 132 | -------------------------------------------------------------------------------- /src/r2mcp.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "readbuffer.h" 7 | 8 | /* Version fallback if not provided by build */ 9 | #ifndef R2MCP_VERSION 10 | #define R2MCP_VERSION "1.5.2" 11 | #endif 12 | 13 | /* Pagination limits for tool responses */ 14 | #define R2MCP_DEFAULT_PAGE_SIZE 1000 15 | #define R2MCP_MAX_PAGE_SIZE 10000 16 | 17 | typedef struct { 18 | const char *name; 19 | const char *version; 20 | } ServerInfo; 21 | 22 | typedef struct { 23 | bool tools; 24 | bool prompts; 25 | bool resources; 26 | } ServerCapabilities; 27 | 28 | typedef struct { 29 | RCore *core; 30 | bool file_opened; 31 | char *current_file; 32 | } RadareState; 33 | 34 | typedef struct { 35 | ServerInfo info; 36 | ServerCapabilities capabilities; 37 | const char *instructions; 38 | bool initialized; 39 | bool minimode; 40 | bool permissive_tools; // allow calling tools not exposed for current mode 41 | bool enable_run_command_tool; 42 | /* When true operate in read-only mode: only expose non-mutating tools */ 43 | bool readonly_mode; 44 | /* When true, operate in HTTP r2pipe client mode and do NOT use r2 C APIs */ 45 | bool http_mode; 46 | /* Base URL of the remote r2 webserver (if http_mode is true) */ 47 | char *baseurl; 48 | /* Base URL of the supervisor control service (if set) */ 49 | char *svc_baseurl; 50 | /* Optional sandbox path. When set, only allow opening files under this dir */ 51 | char *sandbox; 52 | /* Optional path to append debug logs when set via -l */ 53 | char *logfile; 54 | /* When true, ignore the analysis level specified in analyze calls */ 55 | bool ignore_analysis_level; 56 | const RJson *client_capabilities; 57 | const RJson *client_info; 58 | RadareState rstate; 59 | RStrBuf *sb; 60 | /* Optional whitelist of tool names enabled via command line -e options. 61 | * When non-NULL, only tools whose name appears in this list will be 62 | * registered in the runtime tools registry. Items are heap-allocated 63 | * strings and the list should be created with `r_list_newf(free)`. 64 | */ 65 | RList *enabled_tools; 66 | void *prompts; // registry of PromptSpec* (RList*), opaque here 67 | } ServerState; 68 | 69 | /* Entry point wrapper implemented in r2mcp.c */ 70 | int r2mcp_main(int argc, const char **argv); 71 | 72 | /* Exposed helpers implemented in r2mcp.c */ 73 | void setup_signals(void); 74 | void r2mcp_eventloop(ServerState *ss); 75 | void r2mcp_help(void); 76 | void r2mcp_version(void); 77 | void r2mcp_running_set(int value); 78 | 79 | /* Public wrappers for internal r2 helpers (implemented in r2mcp.c) */ 80 | bool r2mcp_state_init(ServerState *ss); 81 | void r2mcp_state_fini(ServerState *ss); 82 | char *r2mcp_cmd(ServerState *ss, const char *cmd); 83 | char *r2mcp_cmdf(ServerState *ss, const char *fmt, ...); 84 | void r2mcp_log_pub(ServerState *ss, const char *msg); 85 | 86 | // Additional public wrappers exposed so other modules (eg. tools.c) can use 87 | // functionality implemented in r2api.inc.c. These simply forward to the 88 | // internal static helpers so we keep the original separation. 89 | bool r2mcp_open_file(ServerState *ss, const char *filepath); 90 | char *r2mcp_analyze(ServerState *ss, int level); 91 | 92 | // Run a small domain-specific language (DSL) used for testing tools from the 93 | // command-line. The DSL describes a sequence of tool calls with arguments and 94 | // prints their results. If core is provided, output goes to r2 console, else stdout. 95 | // Returns 0 on success, non-zero on failure. 96 | int r2mcp_run_dsl_tests(ServerState *ss, const char *dsl, RCore *core); 97 | 98 | // HTTP POST helper for svc communication 99 | char *curl_post_capture(const char *url, const char *msg, int *exit_code_out); 100 | -------------------------------------------------------------------------------- /src/jsonrpc.c: -------------------------------------------------------------------------------- 1 | /* r2mcp - MIT - Copyright 2025 - pancake, dnakov */ 2 | 3 | #include "jsonrpc.h" 4 | 5 | // Helper function to create a simple text tool result 6 | char *jsonrpc_tooltext_response(const char *text) { 7 | PJ *pj = pj_new (); 8 | pj_o (pj); 9 | pj_k (pj, "content"); 10 | pj_a (pj); 11 | pj_o (pj); 12 | pj_ks (pj, "type", "text"); 13 | pj_ks (pj, "text", text); 14 | pj_end (pj); 15 | pj_end (pj); 16 | pj_end (pj); 17 | return pj_drain (pj); 18 | } 19 | 20 | // Helper function to create a paginated text tool result 21 | char *jsonrpc_tooltext_response_paginated(const char *text, bool has_more, const char *next_cursor) { 22 | PJ *pj = pj_new (); 23 | pj_o (pj); 24 | pj_k (pj, "content"); 25 | pj_a (pj); 26 | pj_o (pj); 27 | pj_ks (pj, "type", "text"); 28 | pj_ks (pj, "text", text); 29 | pj_end (pj); 30 | pj_end (pj); 31 | if (has_more || next_cursor) { 32 | pj_k (pj, "pagination"); 33 | pj_o (pj); 34 | if (has_more) { 35 | pj_kb (pj, "hasMore", true); 36 | } 37 | if (next_cursor) { 38 | pj_ks (pj, "nextCursor", next_cursor); 39 | } 40 | pj_end (pj); 41 | } 42 | pj_end (pj); 43 | return pj_drain (pj); 44 | } 45 | 46 | // Render tool output as a JSON array of lines for frontend filtering compatibility 47 | char *jsonrpc_tooltext_response_lines(const char *text) { 48 | PJ *pj = pj_new (); 49 | pj_o (pj); 50 | pj_k (pj, "content"); 51 | pj_a (pj); 52 | if (text) { 53 | RList *lines = r_str_split_list ((char *)text, "\n", 0); 54 | if (lines) { 55 | RListIter *it; 56 | char *line; 57 | r_list_foreach (lines, it, line) { 58 | pj_s (pj, line); 59 | } 60 | r_list_free (lines); 61 | } 62 | } 63 | pj_end (pj); 64 | pj_end (pj); 65 | return pj_drain (pj); 66 | } 67 | 68 | // JSON-RPC error response builder. Returns heap-allocated JSON string (caller frees). 69 | char *jsonrpc_error_response(int code, const char *message, const char *id, const char *uri) { 70 | PJ *pj = pj_new (); 71 | pj_o (pj); 72 | pj_ks (pj, "jsonrpc", "2.0"); 73 | if (id) { 74 | pj_ks (pj, "id", id); 75 | } 76 | pj_k (pj, "error"); 77 | pj_o (pj); 78 | pj_ki (pj, "code", code); 79 | pj_ks (pj, "message", message); 80 | if (uri) { 81 | pj_k (pj, "data"); 82 | pj_o (pj); 83 | pj_ks (pj, "uri", uri); 84 | pj_end (pj); 85 | } 86 | pj_end (pj); 87 | pj_end (pj); 88 | return pj_drain (pj); 89 | } 90 | 91 | // Create a proper success response 92 | char *jsonrpc_success_response(ServerState *ss, const char *result, const char *id) { 93 | (void)ss; // unused now, kept for API consistency 94 | PJ *pj = pj_new (); 95 | pj_o (pj); 96 | pj_ks (pj, "jsonrpc", "2.0"); 97 | 98 | if (id) { 99 | // If id is a number string, treat it as a number 100 | char *endptr; 101 | long num_id = strtol (id, &endptr, 10); 102 | if (*id != '\0' && *endptr == '\0') { 103 | // It's a valid number 104 | pj_kn (pj, "id", num_id); 105 | } else { 106 | // It's a string 107 | pj_ks (pj, "id", id); 108 | } 109 | } 110 | 111 | pj_k (pj, "result"); 112 | if (result) { 113 | pj_raw (pj, result); 114 | } else { 115 | pj_null (pj); 116 | } 117 | 118 | pj_end (pj); 119 | return pj_drain (pj); 120 | } 121 | 122 | // Standardized error response helpers for consistent error handling 123 | char *jsonrpc_error_missing_param(const char *param_name) { 124 | char *msg = r_str_newf ("Missing required parameter: %s", param_name); 125 | char *err = jsonrpc_error_response (-32602, msg, NULL, NULL); 126 | free (msg); 127 | return err; 128 | } 129 | 130 | char *jsonrpc_error_tool_not_allowed(const char *tool_name) { 131 | char *msg = r_str_newf ("Tool '%s' not available in current mode (use -p for permissive)", tool_name); 132 | char *err = jsonrpc_error_response (-32611, msg, NULL, NULL); 133 | free (msg); 134 | return err; 135 | } 136 | 137 | char *jsonrpc_error_file_required(void) { 138 | return jsonrpc_error_response (-32611, "Use the open_file method before calling any other method", NULL, NULL); 139 | } 140 | -------------------------------------------------------------------------------- /dist/debian/deb.mk: -------------------------------------------------------------------------------- 1 | # Create .deb without using dpkg tools. 2 | # 3 | # Author: Tim Wegener 4 | # 5 | # Use 'include deb_hand.mak' after defining the user variables in a local 6 | # makefile. 7 | # 8 | # The 'data' rule must be customised in the local make file. 9 | # This rule should make a 'data' directory containing the full file 10 | # layout of the installed package. 11 | # 12 | # This makefile will create a debian-binary file a control directory and a 13 | # a build directory in the current directory. 14 | # Do 'make clobber' to remove these generated files. 15 | # 16 | # Destination: 17 | # PACKAGE_DIR - directory where package (and support files) will be built 18 | # defaults to the current directory 19 | # 20 | # Sources: 21 | # SOURCE_DIR - directory containing files to be packaged 22 | # ICON_SOURCE - 26x26 icon file for maemo 23 | # DESCR - description with summary on first line 24 | # preinst, postinst, prerm, postrm - optional control shell scripts 25 | 26 | # These fields are used to build the control file: 27 | # PACKAGE = 28 | # VERSION = 29 | # ARCH = 30 | # SECTION = 31 | # PRIORITY = 32 | # MAINTAINER = 33 | # DEPENDS = 34 | # 35 | # SOURCE_DIR = 36 | # ICON_SOURCE = 37 | # (ICON_SOURCE is optional) 38 | 39 | # *** NO USER CHANGES REQUIRED BEYOND THIS POINT *** 40 | ifeq ($(shell uname),Darwin) 41 | MD5SUM=md5 42 | else 43 | MD5SUM=md5sum 44 | endif 45 | 46 | GAWK=awk 47 | PACKAGE_DIR=$(shell pwd) 48 | CONTROL_EXTRAS ?= ${wildcard preinst postinst prerm postrm} 49 | 50 | ${PACKAGE_DIR}/control: ${PACKAGE_DIR}/data ${CONTROL_EXTRAS} DESCR \ 51 | ${ICON_SOURCE} 52 | #rm -rf $@ 53 | mkdir -p $@ 54 | ifneq (${CONTROL_EXTRAS},) 55 | cp ${CONTROL_EXTRAS} $@ 56 | endif 57 | # Make control file. 58 | echo "Package: ${PACKAGE}" > $@/control 59 | echo "Version: ${VERSION}" >> $@/control 60 | echo "Section: ${SECTION}" >> $@/control 61 | echo "Priority: ${PRIORITY}" >> $@/control 62 | echo "Architecture: ${ARCH}" >> $@/control 63 | ifneq (${REPLACES},) 64 | echo "Replaces: ${REPLACES}" >> $@/control 65 | endif 66 | ifneq (${DEPENDS},) 67 | echo "Depends: ${DEPENDS}" >> $@/control 68 | endif 69 | echo "Installed-Size: ${shell du -s ${PACKAGE_DIR}/data|cut -f1}" \ 70 | >> $@/control 71 | echo "Maintainer: ${MAINTAINER}" >> $@/control 72 | printf "Description:" >> $@/control 73 | cat DESCR | ${GAWK} '{print " "$$0;}' >> $@/control 74 | #ifneq (${ICON_SOURCE},) 75 | # echo "Maemo-Icon-26:" >> $@/control 76 | # base64 ${ICON_SOURCE} | ${GAWK} '{print " "$$0;}' >> $@/control 77 | #endif 78 | # Make md5sums. 79 | cd ${PACKAGE_DIR}/data && find . -type f -exec ${MD5SUM} {} \; \ 80 | | sed -e 's| \./||' \ 81 | > $@/md5sums 82 | 83 | ${PACKAGE_DIR}/debian-binary: 84 | echo "2.0" > $@ 85 | 86 | ${PACKAGE_DIR}/clean: 87 | rm -rf ${PACKAGE_DIR}/data ${PACKAGE_DIR}/control ${PACKAGE_DIR}/build *.deb 88 | 89 | ${PACKAGE_DIR}/build: ${PACKAGE_DIR}/debian-binary ${PACKAGE_DIR}/control \ 90 | ${PACKAGE_DIR}/data 91 | rm -rf $@ 92 | mkdir $@ 93 | cp ${PACKAGE_DIR}/debian-binary $@/ 94 | cd ${PACKAGE_DIR}/control && tar cJvf $@/control.tar.xz * 95 | cd ${PACKAGE_DIR}/data && \ 96 | COPY_EXTENDED_ATTRIBUTES_DISABLE=true \ 97 | COPYFILE_DISABLE=true \ 98 | tar cpJvf $@/data.tar.xz * 99 | 100 | # Convert GNU ar to BSD ar that debian requires. 101 | # Note: Order of files within ar archive is important! 102 | ${PACKAGE_DIR}/${PACKAGE}_${VERSION}_${ARCH}.deb: ${PACKAGE_DIR}/build 103 | ar -rc $@ $ $@fail 105 | #rm -f $@tmp 106 | #mv $@fail $@ 107 | 108 | .PHONY: data 109 | data: ${PACKAGE_DIR}/data 110 | 111 | .PHONY: control 112 | control: ${PACKAGE_DIR}/control 113 | 114 | .PHONY: build 115 | build: ${PACKAGE_DIR}/build 116 | 117 | .PHONY: clean 118 | clean: ${PACKAGE_DIR}/clean $(EXTRA_CLEAN) 119 | rm -f debian-binary 120 | 121 | .PHONY: deb 122 | deb: ${PACKAGE_DIR}/${PACKAGE}_${VERSION}_${ARCH}.deb 123 | 124 | 125 | clobber:: 126 | rm -rf ${PACKAGE_DIR}/debian_binary ${PACKAGE_DIR}/control \ 127 | ${PACKAGE_DIR}/data ${PACKAGE_DIR}/build 128 | 129 | push: 130 | scp *.deb radare.org:/srv/http/radareorg/cydia/debs 131 | 132 | mrproper: clean 133 | rm -rf root 134 | -------------------------------------------------------------------------------- /config.h.w64: -------------------------------------------------------------------------------- 1 | #ifndef CONFIG_H 2 | #define CONFIG_H 3 | 4 | /* Windows-specific configuration for r2mcp */ 5 | 6 | /* Version information */ 7 | #define R2MCP_VERSION "1.2.0" 8 | #define R2MCP_VERSION_MAJOR 1 9 | #define R2MCP_VERSION_MINOR 2 10 | #define R2MCP_VERSION_PATCH 0 11 | 12 | /* Platform detection */ 13 | #define WINDOWS 1 14 | #define WIN32 1 15 | #define _WIN32 1 16 | #define _WIN64 1 17 | 18 | /* Compiler-specific definitions */ 19 | #ifdef _MSC_VER 20 | #define HAVE_MSC_VER 1 21 | #endif 22 | 23 | /* Standard C library features */ 24 | #define HAVE_STDLIB_H 1 25 | #define HAVE_STDIO_H 1 26 | #define HAVE_STRING_H 1 27 | #define HAVE_STRINGS_H 1 28 | #define HAVE_UNISTD_H 1 29 | #define HAVE_SYS_TYPES_H 1 30 | #define HAVE_SYS_STAT_H 1 31 | #define HAVE_FCNTL_H 1 32 | #define HAVE_ERRNO_H 1 33 | #define HAVE_SIGNAL_H 1 34 | 35 | /* Windows-specific headers */ 36 | #define HAVE_WINDOWS_H 1 37 | #define HAVE_WINSOCK2_H 1 38 | #define HAVE_WS2TCPIP_H 1 39 | 40 | /* Function availability */ 41 | #define HAVE_MEMCPY 1 42 | #define HAVE_MEMSET 1 43 | #define HAVE_STRCPY 1 44 | #define HAVE_STRNCPY 1 45 | #define HAVE_STRCAT 1 46 | #define HAVE_STRNCAT 1 47 | #define HAVE_STRCMP 1 48 | #define HAVE_STRNCMP 1 49 | #define HAVE_STRLEN 1 50 | #define HAVE_STRDUP 1 51 | #define HAVE_STRCHR 1 52 | #define HAVE_STRRCHR 1 53 | #define HAVE_STRSTR 1 54 | #define HAVE_STRTOK 1 55 | #define HAVE_SPRINTF 1 56 | #define HAVE_SNPRINTF 1 57 | #define HAVE_VSNPRINTF 1 58 | #define HAVE_ATOI 1 59 | #define HAVE_ATOL 1 60 | #define HAVE_STRTOL 1 61 | #define HAVE_STRTOUL 1 62 | #define HAVE_MALLOC 1 63 | #define HAVE_CALLOC 1 64 | #define HAVE_REALLOC 1 65 | #define HAVE_FREE 1 66 | 67 | /* File I/O */ 68 | #define HAVE_OPEN 1 69 | #define HAVE_CLOSE 1 70 | #define HAVE_READ 1 71 | #define HAVE_WRITE 1 72 | #define HAVE_LSEEK 1 73 | #define HAVE_UNLINK 1 74 | #define HAVE_RENAME 1 75 | #define HAVE_MKDIR 1 76 | #define HAVE_RMDIR 1 77 | #define HAVE_ACCESS 1 78 | #define HAVE_STAT 1 79 | 80 | /* Process control */ 81 | #undef HAVE_FORK 82 | #define HAVE_FORK 0 83 | #define HAVE_EXEC 0 84 | #define HAVE_WAITPID 0 85 | #define HAVE_KILL 0 86 | #define HAVE_GETPID 1 87 | #define HAVE_GETPPID 0 88 | 89 | /* Network */ 90 | #define HAVE_SOCKET 1 91 | #define HAVE_CONNECT 1 92 | #define HAVE_BIND 1 93 | #define HAVE_LISTEN 1 94 | #define HAVE_ACCEPT 1 95 | #define HAVE_SEND 1 96 | #define HAVE_RECV 1 97 | #define HAVE_SELECT 1 98 | #define HAVE_POLL 1 99 | 100 | /* Threading */ 101 | #define HAVE_PTHREAD_H 0 102 | #define HAVE_THREAD_H 1 103 | 104 | /* Math */ 105 | #define HAVE_MATH_H 1 106 | #define HAVE_FLOOR 1 107 | #define HAVE_CEIL 1 108 | #define HAVE_ROUND 1 109 | #define HAVE_SQRT 1 110 | #define HAVE_POW 1 111 | #define HAVE_LOG 1 112 | #define HAVE_LOG10 1 113 | #define HAVE_SIN 1 114 | #define HAVE_COS 1 115 | #define HAVE_TAN 1 116 | #define HAVE_ASIN 1 117 | #define HAVE_ACOS 1 118 | #define HAVE_ATAN 1 119 | #define HAVE_ATAN2 1 120 | 121 | /* Time */ 122 | #define HAVE_TIME_H 1 123 | #define HAVE_SYS_TIME_H 0 124 | #define HAVE_TIME 1 125 | #define HAVE_GETTIMEOFDAY 0 126 | #define HAVE_CLOCK_GETTIME 0 127 | 128 | /* Random */ 129 | #define HAVE_RAND 1 130 | #define HAVE_SRAND 1 131 | #define HAVE_RANDOM 0 132 | #define HAVE_SRANDOM 0 133 | 134 | /* Environment */ 135 | #define HAVE_GETENV 1 136 | #define HAVE_SETENV 0 137 | #define HAVE_UNSETENV 0 138 | #define HAVE_PUTENV 1 139 | 140 | /* Signal handling */ 141 | #define HAVE_SIGNAL 1 142 | #define HAVE_SIGACTION 0 143 | #define HAVE_SIGPROCMASK 0 144 | 145 | /* Directory operations */ 146 | #define HAVE_OPENDIR 0 147 | #define HAVE_READDIR 0 148 | #define HAVE_CLOSEDIR 0 149 | #define HAVE_REWINDDIR 0 150 | 151 | /* Windows-specific features */ 152 | #define HAVE_GETPROCESSID 1 153 | #define HAVE_GETCURRENTTHREADID 1 154 | #define HAVE_GETCURRENTPROCESSID 1 155 | 156 | /* Large file support */ 157 | #define HAVE_LARGEFILE_SUPPORT 1 158 | #define _FILE_OFFSET_BITS 64 159 | 160 | /* Endianness */ 161 | #define WORDS_BIGENDIAN 0 162 | #define WORDS_LITTLEENDIAN 1 163 | 164 | /* Architecture */ 165 | #define ARCH_X86 1 166 | #define ARCH_X86_64 1 167 | #define ARCH_AMD64 1 168 | 169 | /* Build configuration */ 170 | #define DEBUG 0 171 | #define NDEBUG 1 172 | #define RELEASE 1 173 | 174 | /* Feature flags */ 175 | #define HAVE_DYNAMIC_LOADING 1 176 | #define HAVE_SHARED_LIBRARIES 1 177 | #define HAVE_PLUGINS 1 178 | 179 | /* Windows API version */ 180 | #define WINVER 0x0601 181 | #define _WIN32_WINNT 0x0601 182 | #define _WIN32_IE 0x0800 183 | 184 | /* Unicode support */ 185 | #define UNICODE 1 186 | #define _UNICODE 1 187 | 188 | /* Security */ 189 | #define HAVE_SECURE_CRT 1 190 | 191 | /* Compiler warnings */ 192 | #ifdef _MSC_VER 193 | #pragma warning(disable: 4996) /* deprecated functions */ 194 | #pragma warning(disable: 4101) /* unreferenced local variable */ 195 | #pragma warning(disable: 4244) /* conversion from 'type1' to 'type2' */ 196 | #pragma warning(disable: 4267) /* conversion from 'size_t' to 'type' */ 197 | #endif 198 | 199 | #endif /* CONFIG_H */ 200 | -------------------------------------------------------------------------------- /src/dsltest.c: -------------------------------------------------------------------------------- 1 | /* r2mcp - MIT - Copyright 2025 - pancake, dnakov */ 2 | 3 | #include "r2mcp.h" 4 | // Simple DSL runner for invoking tools from the CLI for testing. 5 | // DSL grammar (very small): 6 | // program := stmt (';' stmt)* 7 | // stmt := TOOLNAME (WS key=val)* 8 | // Values may be quoted with double-quotes. Keys/values do not support 9 | // nested structures. Example: 10 | // open_file file_path="/bin/ls"; list_functions only_named=true; close_file 11 | char *tools_call(ServerState *ss, const char *tool_name, RJson *tool_args); 12 | // Parse a single statement of the form: toolName [key=val ...] 13 | // Returns 0 on success. 14 | static int run_statement(ServerState *ss, char *stmt, RCore *core) { 15 | r_str_trim (stmt); 16 | if (R_STR_ISEMPTY (stmt)) { 17 | return 0; 18 | } 19 | // extract tool name (first token up to whitespace) 20 | char *p = stmt; 21 | while (*p && !isspace ((unsigned char)*p)) { 22 | p++; 23 | } 24 | char saved = *p; 25 | if (saved) { 26 | *p++ = '\0'; 27 | } 28 | const char *tool = stmt; 29 | RStrBuf *sb = r_strbuf_new ("{"); 30 | bool first = true; 31 | // parse key=val tokens 32 | while (*p) { 33 | p = (char *)r_str_trim_head_ro (p); 34 | if (!*p) { 35 | break; 36 | } 37 | // key 38 | char *key = p; 39 | // advance to '=' or whitespace 40 | while (*p && *p != '=' && !isspace ((ut8)*p)) { 41 | p++; 42 | } 43 | if (!*p || *p != '=') { 44 | // no key=value, treat rest as error or ignore 45 | break; 46 | } 47 | *p++ = '\0'; 48 | // value 49 | char *val = p; 50 | if (*p == '"') { 51 | p++; 52 | val = p; 53 | while (*p && *p != '"') { 54 | if (*p == '\\' && p[1]) { 55 | p += 2; 56 | } else { 57 | p++; 58 | } 59 | } 60 | if (*p == '"') { 61 | *p++ = '\0'; 62 | } 63 | } else { 64 | p = (char *)r_str_trim_head_ro (p); 65 | if (*p) { 66 | *p++ = '\0'; 67 | } 68 | } 69 | // append to JSON 70 | if (!first) { 71 | r_strbuf_append (sb, ","); 72 | } 73 | first = false; 74 | // determine if val is a bare true/false or number 75 | bool bare = false; 76 | if (!strcmp (val, "true") || !strcmp (val, "false")) { 77 | bare = true; 78 | } else { 79 | // check if integer 80 | char *q = (char *)val; 81 | bool digits = true; 82 | if (*q == '-' || *q == '+') { 83 | q++; 84 | } 85 | while (*q) { 86 | if (!isdigit ((ut8)*q)) { 87 | digits = false; 88 | break; 89 | } 90 | q++; 91 | } 92 | if (digits && *val) { 93 | bare = true; 94 | } 95 | } 96 | if (bare) { 97 | r_strbuf_appendf (sb, "\"%s\":%s", key, val); 98 | } else { 99 | char *esc = strdup (val); // r_str_escape_json (val, -1); 100 | r_strbuf_appendf (sb, "\"%s\":\"%s\"", key, esc); 101 | free (esc); 102 | } 103 | } 104 | // no need to restore the separator char 105 | r_strbuf_append (sb, "}"); 106 | char *jsonbuf = r_strbuf_drain (sb); 107 | // debug: built args JSON (left commented intentionally) 108 | // printf ("[DSL] args json: %s\n", jsonbuf); 109 | RJson *args = NULL; 110 | if (strlen (jsonbuf) > 2) { 111 | // parse it (parser does not take ownership; keep jsonbuf alive until free) 112 | args = r_json_parse (jsonbuf); 113 | if (!args) { 114 | printf ("[DSL] Failed to parse arguments for tool %s\n", tool); 115 | free (jsonbuf); 116 | return -1; 117 | } 118 | } 119 | // call the tool 120 | char *res = tools_call (ss, tool, args); 121 | if (args) { 122 | r_json_free (args); 123 | } 124 | if (res) { 125 | if (core) { 126 | // Extract text from JSON response 127 | const char *text_start = strstr (res, "\"text\":\""); 128 | if (text_start) { 129 | text_start += 8; // skip "text":" 130 | const char *text_end = strstr (text_start, "\""); 131 | if (text_end) { 132 | char *text = r_str_ndup (text_start, text_end - text_start); 133 | r_str_replace_in (text, -1, "\\n", "\n", true); 134 | r_cons_printf (core->cons, "%s\n", text); 135 | free (text); 136 | } else { 137 | r_cons_printf (core->cons, "(malformed text in response)\n"); 138 | } 139 | } else { 140 | r_cons_printf (core->cons, "(no text in response)\n"); 141 | } 142 | } else { 143 | printf ("[DSL] %s -> %s\n", tool, res); 144 | } 145 | free (res); 146 | } else { 147 | if (core) { 148 | r_cons_printf (core->cons, "(no result)\n"); 149 | } else { 150 | printf ("[DSL] %s -> (no result)\n", tool); 151 | } 152 | } 153 | free (jsonbuf); 154 | return 0; 155 | } 156 | 157 | int r2mcp_run_dsl_tests(ServerState *ss, const char *dsl, RCore *core) { 158 | R_RETURN_VAL_IF_FAIL (dsl, 1); 159 | char *copy = strdup (dsl); 160 | char *cur = copy; 161 | char *semi; 162 | int rc = 0; 163 | while ((semi = strchr (cur, ';')) != NULL) { 164 | *semi = '\0'; 165 | if (run_statement (ss, cur, core) != 0) { 166 | rc = 1; 167 | } 168 | cur = semi + 1; 169 | } 170 | if (*cur) { 171 | if (run_statement (ss, cur, core) != 0) { 172 | rc = 1; 173 | } 174 | } 175 | free (copy); 176 | return rc; 177 | } 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Radare2 MCP Server 2 | 3 | [![ci](https://github.com/radareorg/radare2-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/radareorg/radare2-mcp/actions/workflows/ci.yml) 4 | [![radare2](https://img.shields.io/badge/radare2-6.0.4-green)](https://github.com/radareorg/radare2) 5 | 6 | r2mcp logo 7 | 8 | An MCP server to use **radare2** with AI agents such as OpenCode, Mai, VSCode, Claude, CLION, ... 9 | 10 | ## Features 11 | 12 | This implementation provides: 13 | 14 | - 💻 Fully written in C using the native r2 APIs 15 | - 🧩 Works from the CLI, as an r2 plugin and as an MCP server 16 | - 🔍 Seamless binary analysis with radare2 17 | - 🔗 Connect to any local or remote r2/iaito session via r2pipe 18 | - 🔒 Supports readonly mode, sandbox lock and restrict tools 19 | - 🔩 Fine grained tools configuration 20 | - 🔁 Direct stdin/stdout communication model 21 | - 🛠️ Optional raw access to run r2 commands or r2js scripts 22 | 23 | ## Installation 24 | 25 | Screenshot_2025-03-22_at_5 34 47_PM 26 | Screenshot_2025-03-22_at_5 36 17_PM 27 | 28 | ### Using r2pm 29 | 30 | The simplest way to install the package is by using `r2pm`: 31 | 32 | ```bash 33 | $ r2pm -Uci r2mcp 34 | ``` 35 | 36 | 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. 37 | 38 | ```bash 39 | $ r2pm -r r2mcp 40 | ``` 41 | 42 | That's the common mcpServer JSON configuration file: 43 | 44 | ```json 45 | { 46 | "mcpServers": { 47 | "radare2": { 48 | "command": "r2pm", 49 | "args": ["-r", "r2mcp"] 50 | } 51 | } 52 | } 53 | ``` 54 | 55 | ### Using Docker 56 | 57 | Alternatively, you can build the Docker image: 58 | 59 | ```bash 60 | docker build -t r2mcp . 61 | ``` 62 | 63 | Update your MCP client configuration file (see below) to use the Docker image to use: 64 | 65 | - `"command": "docker"` 66 | - `"args": ["run", "--rm", "-i", "-v", "/tmp/data:/data", "r2mcp"]`. 67 | 68 | ## Configuration 69 | 70 | ### Claude Desktop Integration 71 | 72 | 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: 73 | 74 | 1. Locate your Claude Desktop configuration file: 75 | 76 | - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` 77 | - Windows: `%APPDATA%\Claude\claude_desktop_config.json` 78 | 79 | 2. Add the following to your configuration file: 80 | 81 | ```json 82 | { 83 | "mcpServers": { 84 | "radare2": { 85 | "command": "r2pm", 86 | "args": ["-r", "r2mcp"] 87 | } 88 | } 89 | } 90 | ``` 91 | 92 | ## VS Code Integration 93 | 94 | 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)): 95 | 96 | 1. Open the Command Palette with `CMD + Shift + P` (macOS) or `Ctrl + Shift + P` (Windows/Linux). 97 | 2. Search for and select `Copilot: Open User Configuration` (typically found in `~/Library/Application Support/Code/User/mcp.json` in macOS). 98 | 3. Add the following to your configuration file: 99 | 100 | ```json 101 | { 102 | "servers": { 103 | "radare2": { 104 | "type": "stdio", 105 | "command": "r2pm", 106 | "args": ["-r", "r2mcp"] 107 | } 108 | }, 109 | "inputs": [] 110 | } 111 | ``` 112 | 113 | ## Zed Integration 114 | 115 | You can use r2mcp with Zed as well by [adding it to your configuration](https://zed.dev/docs/ai/mcp): 116 | 117 | 1. Open the command palette: `CMD + Shift + P` (macOS) or `Ctrl + Shift + P` (Windows/Linux). 118 | 2. Search of `agent: open configuration` or search of `settings`. 119 | 3. Add your server as such: 120 | 121 | ```json 122 | "context_servers": { 123 | "r2-mcp-server": { 124 | "source": "custom", 125 | "command": "r2pm", 126 | "args": ["-r", "r2mcp"], 127 | "env": {} 128 | } 129 | } 130 | ``` 131 | Note: you will need another LLM agent, such as Claude, Gemini or else to be able to use it. 132 | 133 | ## For Developers 134 | 135 | ### Build from Source 136 | 137 | #### Linux/macOS 138 | 139 | To test the server locally, you can build and install it with make: 140 | 141 | ```bash 142 | make install 143 | ``` 144 | 145 | This will compile the server and place the `r2mcp` binary in `/usr/local/bin` on macOS. 146 | 147 | #### Windows 148 | 149 | For Windows, just use meson and ninja like it's done in the CI: 150 | 151 | ```cmd 152 | meson b 153 | ninja -C b 154 | ``` 155 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'r2mcp', 3 | 'c', 4 | version: '1.5.2', 5 | default_options: ['warning_level=2', 'c_std=c11'] 6 | ) 7 | 8 | cc = meson.get_compiler('c') 9 | 10 | r_core_dep = dependency('r_core', method: 'pkg-config', required: false) 11 | 12 | r2_libdirs = [] 13 | r2_incdirs = [] 14 | 15 | if not r_core_dep.found() 16 | is_windows_like = host_machine.system() == 'windows' or cc.get_id() == 'msvc' or cc.get_id() == 'clang-cl' 17 | 18 | if is_windows_like 19 | r2libdirs = [] 20 | if get_option('r2_prefix') != '' 21 | p = get_option('r2_prefix') 22 | r2libdirs += [join_paths(p, 'lib'), join_paths(p, 'radare2', 'lib')] 23 | endif 24 | r2libdirs += [meson.current_source_dir() + '/radare2/lib', 'C:/radare2/lib'] 25 | 26 | rcore_lib = cc.find_library('r_core', dirs: r2libdirs, required: false) 27 | if not rcore_lib.found() 28 | rcore_lib = cc.find_library('libr_core', dirs: r2libdirs, required: false) 29 | endif 30 | 31 | if rcore_lib.found() 32 | incdirs = [] 33 | if get_option('r2_prefix') != '' 34 | p = get_option('r2_prefix') 35 | incdirs += [join_paths(p, 'include', 'libr'), join_paths(p, 'include')] 36 | endif 37 | rcore_inc = include_directories(incdirs) 38 | r_core_dep = declare_dependency(dependencies: [rcore_lib], include_directories: rcore_inc) 39 | r2_libdirs = r2libdirs 40 | r2_incdirs = incdirs 41 | else 42 | error('Windows dependency lookup for r_core failed: ensure radare2 is installed and pass -Dr2_prefix= (e.g. C:/radare2)') 43 | endif 44 | else 45 | r2_prefixes = [] 46 | if get_option('r2_prefix') != '' 47 | r2_prefixes += [get_option('r2_prefix')] 48 | endif 49 | 50 | found = false 51 | foreach p : r2_prefixes 52 | libdirs = [ 53 | join_paths(p, 'lib'), 54 | join_paths(p, 'lib64'), 55 | join_paths(p, 'radare2', 'lib'), 56 | join_paths(p, 'radare2', 'lib64'), 57 | join_paths(p, 'mingw64', 'lib'), 58 | ] 59 | incdirs = [ 60 | join_paths(p, 'include'), 61 | join_paths(p, 'radare2', 'include'), 62 | join_paths(p, 'include', 'radare2'), 63 | ] 64 | extra_libdirs = libdirs + [join_paths(p, 'bin')] 65 | candidate_names = ['r_core', 'r_core-6', 'r_core6', 'r_core64', 'libr_core'] 66 | foreach name : candidate_names 67 | rcore_lib = cc.find_library(name, dirs: extra_libdirs, required: false) 68 | if rcore_lib.found() 69 | rcore_inc = include_directories(incdirs + [join_paths(p, 'include', 'libr')]) 70 | r_core_dep = declare_dependency(dependencies: [rcore_lib], include_directories: rcore_inc) 71 | r2_libdirs = extra_libdirs 72 | r2_incdirs = incdirs + [join_paths(p, 'include', 'libr')] 73 | found = true 74 | break 75 | endif 76 | endforeach 77 | if found 78 | break 79 | endif 80 | endforeach 81 | 82 | if not found 83 | error('Dependency lookup for r_core failed: install radare2 development files or provide pkg-config paths or set the meson option -Dr2_prefix=') 84 | endif 85 | endif 86 | endif 87 | 88 | r_deps = { 89 | 'r_util': ['r_util', 'libr_util'], 90 | 'r_cons': ['r_cons', 'libr_cons'], 91 | 'r_config': ['r_config', 'libr_config'], 92 | 'r_socket': ['r_socket', 'libr_socket'] 93 | } 94 | 95 | foreach dep_name, lib_names : r_deps 96 | dep_var = dependency(dep_name, method: 'pkg-config', required: false) 97 | if not dep_var.found() and r2_libdirs.length() > 0 98 | foreach name : lib_names 99 | lib = cc.find_library(name, dirs: r2_libdirs, required: false) 100 | if lib.found() 101 | inc = include_directories(r2_incdirs) 102 | dep_var = declare_dependency(dependencies: [lib], include_directories: inc) 103 | break 104 | endif 105 | endforeach 106 | endif 107 | if not dep_var.found() 108 | warning('@0@ dependency not found via pkg-config or same paths as r_core'.format(dep_name)) 109 | endif 110 | set_variable(dep_name + '_dep', dep_var) 111 | endforeach 112 | 113 | # duplicated files between srcs and plugins, also "srcs" is not clear who will use that 114 | srcs = [ 115 | 'src/main.c', 116 | 'src/r2mcp.c', 117 | 'src/readbuffer.c', 118 | 'src/tools.c', 119 | 'src/dsltest.c', 120 | 'src/prompts.c', 121 | 'src/jsonrpc.c', 122 | ] 123 | 124 | plugin_srcs = [ 125 | 'src/plugin.c', 126 | 'src/r2mcp.c', 127 | 'src/readbuffer.c', 128 | 'src/tools.c', 129 | 'src/dsltest.c', 130 | 'src/prompts.c', 131 | 'src/jsonrpc.c', 132 | ] 133 | 134 | if host_machine.system() == 'windows' 135 | configure_file( 136 | input: 'config.h.w64', 137 | output: 'config.h', 138 | copy: true 139 | ) 140 | 141 | link_args = [] 142 | c_args = [] 143 | if cc.get_id() == 'msvc' or cc.get_id() == 'clang-cl' 144 | link_args += ['/SUBSYSTEM:CONSOLE', '/DEFAULTLIB:setupapi.lib'] 145 | endif 146 | else 147 | conf_data = configuration_data() 148 | conf_data.set('R2MCP_VERSION', meson.project_version()) 149 | configure_file( 150 | input: 'src/config.h.acr', 151 | output: 'config.h', 152 | configuration: conf_data 153 | ) 154 | link_args = [] 155 | c_args = ['-D_GNU_SOURCE', '-D_POSIX_C_SOURCE=200809L'] 156 | endif 157 | 158 | executable( 159 | 'r2mcp', 160 | srcs, 161 | dependencies: [r_core_dep, r_util_dep, r_cons_dep, r_config_dep], 162 | c_args: c_args, 163 | link_args: link_args, 164 | install: true, 165 | ) 166 | 167 | plugin_lib = shared_library( 168 | 'core_r2mcp', 169 | plugin_srcs, 170 | dependencies: [r_core_dep, r_util_dep, r_cons_dep, r_config_dep], 171 | c_args: c_args, 172 | link_args: link_args, 173 | install: false, 174 | ) 175 | 176 | executable( 177 | 'r2mcp-svc', 178 | 'svc/r2mcp-svc.c', 179 | dependencies: [r_socket_dep, r_util_dep, r_cons_dep], 180 | c_args: c_args, 181 | link_args: link_args, 182 | install: true, 183 | ) 184 | -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | # Agentic Coding Guidelines for the r2mcp (radare2 MCP) server 2 | 3 | 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/`. 4 | 5 | **Scope** 6 | - 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. 7 | 8 | **Repository layout (important files)** 9 | - `src/main.c` - program entry, CLI parsing, signal setup and high-level program lifecycle. 10 | - `src/r2mcp.c` - main server machinery: JSON-RPC handling, event loop, dispatch to `tools` and `prompts` registries. 11 | - `src/tools.c`, `src/prompts.c` - registries and implementations for tools and prompts. 12 | - `src/readbuffer.c` - framed message reader used by the MCP direct mode loop. 13 | - `src/r2api.inc.c`, `src/utils.inc.c` - implementation fragments included into `r2mcp.c`. These are not separate compilation units. 14 | - `src/r2mcp.h`, `src/tools.h`, `src/readbuffer.h`, `src/prompts.h` - public headers for the modules above. 15 | 16 | Coding style and rules (project-specific) 17 | - Indentation: use **TABS** for indentation (project convention). 18 | - Function calls: include a space before the parenthesis, e.g. `foo ()`. 19 | - Always use braces `{}` for conditionals and loops, even if a single statement. 20 | - `case` labels in `switch` statements must be aligned at the same column as other cases. 21 | - Define loop variables before the `for` statement (older C style used in this codebase). 22 | - Prefer `!strcmp ()` instead of `strcmp () == 0`. 23 | - Use `R_RETURN_*` macros in public APIs (functions exported as `R_API`) to declare preconditions and avoid returning invalid values. 24 | 25 | Memory and ownership 26 | - `R_NEW`/`R_NEW0` macros in this project are assumed never to return NULL; code can rely on that. 27 | - Do not check for NULL before calling `free` or other `*_free` helpers (the codebase follows this convention). 28 | - `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. 29 | - When using `r_strbuf_free`, `r_core_free`, `r_list_free` or similar, pass only previously-initialized objects; do not NULL-check before freeing. 30 | 31 | Build and test 32 | - 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. 33 | - The primary output binary is `src/r2mcp`. Run `src/r2mcp -t` to list available tools and `src/r2mcp -h` for CLI help. 34 | - Use `make -C src -j > /dev/null` when you want quieter output during iterative development. 35 | 36 | Guidelines for editing the code 37 | - Keep changes minimal and narrowly scoped; prefer fixing the root cause. 38 | - When adding new tools or commands, implement a `?` subcommand to print help for that tool. 39 | - Prefer using `r_str_newf` for formatted strings instead of manual `malloc` + `snprintf`. 40 | - 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`. 41 | - Use `r_str_pad2` to construct repeated-character strings when needed. 42 | - When introducing new public APIs, follow the `R_API` and `R_RETURN_*` conventions already present in the repo. 43 | 44 | Working with `*.inc.c` files 45 | - 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. 46 | 47 | Logging and diagnostics 48 | - 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. 49 | 50 | JSON and protocol handling 51 | - 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. 52 | - 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. 53 | - Distinguish between notifications (no `id` field) and requests (have `id`). Notifications must not produce a response. 54 | 55 | Signals and event loop 56 | - `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. 57 | - 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. 58 | 59 | Incidental tips 60 | - When making changes that affect only `src/` files, run `make -C src -j` from the repo root to recompile only `src/`. 61 | - Avoid adding new dependencies. This project expects to build against existing radare2 headers (`r_core.h`, `r_util/*`). 62 | - When adding tests or additional tooling, prefer placing small test drivers under `b/` (repo already uses `b/` for auxiliary build/test files). 63 | 64 | Checklist before submitting a patch 65 | - 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`. 66 | - Ensure all new public APIs use `R_RETURN_*` where appropriate. 67 | - Follow TAB indentation and other style rules above. 68 | 69 | 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. 70 | -------------------------------------------------------------------------------- /dist/docker/dockcross: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | DEFAULT_DOCKCROSS_IMAGE=dockcross/linux-armv5 4 | 5 | #------------------------------------------------------------------------------ 6 | # Helpers 7 | # 8 | err() { 9 | echo >&2 ERROR: $@\\n 10 | } 11 | 12 | die() { 13 | err $@ 14 | exit 1 15 | } 16 | 17 | has() { 18 | # eg. has command update 19 | local kind=$1 20 | local name=$2 21 | 22 | type -t $kind:$name | grep -q function 23 | } 24 | 25 | #------------------------------------------------------------------------------ 26 | # Command handlers 27 | # 28 | command_update_image() { 29 | docker pull $FINAL_IMAGE 30 | } 31 | 32 | help_update_image() { 33 | echo "Pull the latest $FINAL_IMAGE ." 34 | } 35 | 36 | command_update_script() { 37 | if [ docker run $FINAL_IMAGE | cmp -s $0 ]; then 38 | echo "$0 is up to date" 39 | else 40 | printf "Updating $0 '... '" 41 | docker run $FINAL_IMAGE > $0 && echo ok 42 | fi 43 | } 44 | 45 | command_update() { 46 | command_update_image 47 | command_update_script 48 | } 49 | 50 | help_update() { 51 | echo "Pull the latest $FINAL_IMAGE, and then update $0 from that." 52 | } 53 | 54 | command_help() { 55 | if [ $# != 0 ]; then 56 | if ! has command $1; then 57 | err \"$1\" is not an dockcross command 58 | command:help 59 | elif ! has help $1; then 60 | err No help found for \"$1\" 61 | else 62 | help:$1 63 | fi 64 | else 65 | cat >&2 < 84 | ENDHELP 85 | exit 1 86 | fi 87 | } 88 | 89 | #------------------------------------------------------------------------------ 90 | # Option processing 91 | # 92 | special_update_command='' 93 | while [ $# != 0 ]; do 94 | case $1 in 95 | 96 | --) 97 | break 98 | ;; 99 | 100 | --args|-a) 101 | ARG_ARGS="$2" 102 | shift 2 103 | ;; 104 | 105 | --config|-c) 106 | ARG_CONFIG="$2" 107 | shift 2 108 | ;; 109 | 110 | --image|-i) 111 | ARG_IMAGE="$2" 112 | shift 2 113 | ;; 114 | update|update-image|update-script) 115 | special_update_command=$1 116 | break 117 | ;; 118 | -*) 119 | err Unknown option \"$1\" 120 | command:help 121 | exit 122 | ;; 123 | 124 | *) 125 | break 126 | ;; 127 | 128 | esac 129 | done 130 | 131 | # The precedence for options is: 132 | # 1. command-line arguments 133 | # 2. environment variables 134 | # 3. defaults 135 | 136 | # Source the config file if it exists 137 | DEFAULT_DOCKCROSS_CONFIG=~/.dockcross 138 | FINAL_CONFIG=${ARG_CONFIG-${DOCKCROSS_CONFIG-$DEFAULT_DOCKCROSS_CONFIG}} 139 | 140 | [ -f "$FINAL_CONFIG" ] && source "$FINAL_CONFIG" 141 | 142 | # Set the docker image 143 | FINAL_IMAGE=${ARG_IMAGE-${DOCKCROSS_IMAGE-$DEFAULT_DOCKCROSS_IMAGE}} 144 | 145 | # Handle special update command 146 | if [ "$special_update_command" != "" ]; then 147 | case $special_update_command in 148 | 149 | update) 150 | command:update 151 | exit $? 152 | ;; 153 | 154 | update-image) 155 | command:update-image 156 | exit $? 157 | ;; 158 | 159 | update-script) 160 | command:update-script 161 | exit $? 162 | ;; 163 | 164 | esac 165 | fi 166 | 167 | # Set the docker run extra args (if any) 168 | FINAL_ARGS=${ARG_ARGS-${DOCKCROSS_ARGS}} 169 | 170 | # Bash on Ubuntu on Windows 171 | UBUNTU_ON_WINDOWS=$([ -e /proc/version ] && grep -l Microsoft /proc/version || echo "") 172 | # MSYS, Git Bash, etc. 173 | MSYS=$([ -e /proc/version ] && grep -l MINGW /proc/version || echo "") 174 | 175 | if [ -z "$UBUNTU_ON_WINDOWS" -a -z "$MSYS" ]; then 176 | USER_IDS="-e BUILDER_UID=$( id -u ) -e BUILDER_GID=$( id -g ) -e BUILDER_USER=$( id -un ) -e BUILDER_GROUP=$( id -gn )" 177 | fi 178 | 179 | # Change the PWD when working in Docker on Windows 180 | if [ -n "$UBUNTU_ON_WINDOWS" ]; then 181 | HOST_PWD=$PWD 182 | HOST_PWD=${HOST_PWD/\/mnt\//} 183 | HOST_PWD=${HOST_PWD/\//:\/} 184 | elif [ -n "$MSYS" ]; then 185 | HOST_PWD=$PWD 186 | HOST_PWD=${HOST_PWD/\//} 187 | HOST_PWD=${HOST_PWD/\//:\/} 188 | else 189 | HOST_PWD=$PWD 190 | fi 191 | 192 | #------------------------------------------------------------------------------ 193 | # Now, finally, run the command in a container 194 | # 195 | tty -s && TTY_ARGS=-ti || TTY_ARGS= 196 | CONTAINER_NAME=dockcross_$RANDOM 197 | docker run $TTY_ARGS --name $CONTAINER_NAME \ 198 | -v "$HOST_PWD":/work \ 199 | $USER_IDS \ 200 | $FINAL_ARGS \ 201 | $FINAL_IMAGE "$@" 202 | run_exit_code=$? 203 | 204 | # Attempt to delete container 205 | rm_output=$(docker rm -f $CONTAINER_NAME 2>&1) 206 | rm_exit_code=$? 207 | if [ $rm_exit_code != 0 ]; then 208 | if [ "$CIRCLECI" == "true" ] && [ $rm_output == *"Driver btrfs failed to remove"* ]; then 209 | : # Ignore error because of https://circleci.com/docs/docker-btrfs-error/ 210 | else 211 | echo "$rm_output" 212 | exit $rm_exit_code 213 | fi 214 | fi 215 | 216 | exit $run_exit_code 217 | 218 | ################################################################################ 219 | # 220 | # This image is not intended to be run manually. 221 | # 222 | # To create a dockcross helper script for the 223 | # dockcross/linux-armv7 image, run: 224 | # 225 | # docker run --rm dockcross/linux-armv7 > dockcross-linux-armv7 226 | # chmod +x dockcross-linux-armv7 227 | # 228 | # You may then wish to move the dockcross script to your PATH. 229 | # 230 | ################################################################################ 231 | -------------------------------------------------------------------------------- /src/curl.inc.c: -------------------------------------------------------------------------------- 1 | /* r2mcp - MIT - Copyright 2025 - pancake, dnakov */ 2 | 3 | #define _POSIX_C_SOURCE 200809L 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #if defined(R2__WINDOWS__) 10 | #include 11 | #include 12 | #include 13 | #elif defined(R2__UNIX__) 14 | #include 15 | #include 16 | #include 17 | #else 18 | #error please define R2__WINDOWS__ or R2__UNIX__ for platform detection 19 | #endif 20 | 21 | /** 22 | * Execute: curl -sS -d "" 23 | * Capture stdout (HTTP response body) and return it as a malloc'd NUL-terminated string. 24 | * 25 | * @param url Target URL (non-NULL) 26 | * @param msg Message for -d (non-NULL), e.g. "key=value" or JSON string 27 | * @param exit_code_out Optional: receives curl's exit code (0 on success). Pass NULL if not needed. 28 | * 29 | * @return malloc'd buffer with the response (caller must free), or NULL on error. 30 | * On error, *exit_code_out (if provided) is set to a negative number when possible. 31 | */ 32 | char *curl_post_capture(const char *url, const char *msg, int *exit_code_out) { 33 | if (exit_code_out) { 34 | *exit_code_out = -1; 35 | } 36 | if (!url || !msg) { 37 | errno = EINVAL; 38 | return NULL; 39 | } 40 | char *buf = NULL; 41 | size_t len = 0; 42 | int exit_code = -1; 43 | #if R2__WINDOWS__ 44 | SECURITY_ATTRIBUTES sa; 45 | sa.nLength = sizeof (SECURITY_ATTRIBUTES); 46 | sa.lpSecurityDescriptor = NULL; 47 | sa.bInheritHandle = TRUE; 48 | 49 | HANDLE read_h = NULL, write_h = NULL; 50 | if (!CreatePipe (&read_h, &write_h, &sa, 0)) { 51 | return NULL; 52 | } 53 | // Ensure the read handle is not inherited 54 | SetHandleInformation (read_h, HANDLE_FLAG_INHERIT, 0); 55 | 56 | char *escaped_msg = r_str_escape_sh (msg); 57 | char *escaped_url = r_str_escape_sh (url); 58 | char *cmd = r_str_newf ("curl -sS -d \"%s\" \"%s\"", escaped_msg, escaped_url); 59 | free (escaped_msg); 60 | free (escaped_url); 61 | 62 | STARTUPINFOA si; 63 | PROCESS_INFORMATION pi; 64 | ZeroMemory (&si, sizeof (si)); 65 | si.cb = sizeof (si); 66 | si.hStdOutput = write_h; 67 | si.hStdError = GetStdHandle (STD_ERROR_HANDLE); 68 | si.dwFlags |= STARTF_USESTDHANDLES; 69 | 70 | ZeroMemory (&pi, sizeof (pi)); 71 | 72 | // Create process 73 | BOOL ok = CreateProcessA (NULL, cmd, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi); 74 | free (cmd); 75 | // Close the write handle in parent after creating the child 76 | CloseHandle (write_h); 77 | 78 | if (!ok) { 79 | CloseHandle (read_h); 80 | return NULL; 81 | } 82 | 83 | // Read child's stdout 84 | size_t cap = 8192; 85 | buf = malloc (cap); 86 | if (!buf) { 87 | CloseHandle (read_h); 88 | CloseHandle (pi.hProcess); 89 | CloseHandle (pi.hThread); 90 | WaitForSingleObject (pi.hProcess, INFINITE); 91 | return NULL; 92 | } 93 | 94 | for (;;) { 95 | if (len + 4096 + 1 > cap) { 96 | size_t ncap = cap * 2; 97 | char *tmp = realloc (buf, ncap); 98 | if (!tmp) { 99 | R_FREE (buf); 100 | break; 101 | } 102 | buf = tmp; 103 | cap = ncap; 104 | } 105 | DWORD nread = 0; 106 | BOOL r = ReadFile (read_h, buf + len, 4096, &nread, NULL); 107 | if (r && nread > 0) { 108 | len += (size_t)nread; 109 | continue; 110 | } 111 | if (!r) { 112 | DWORD err = GetLastError (); 113 | if (err == ERROR_BROKEN_PIPE) { 114 | break; // EOF 115 | } 116 | R_FREE (buf); 117 | break; 118 | } 119 | // r == TRUE but nread == 0 -> EOF 120 | break; 121 | } 122 | 123 | CloseHandle (read_h); 124 | 125 | // Wait for process and get exit code 126 | WaitForSingleObject (pi.hProcess, INFINITE); 127 | DWORD exitcode = 0; 128 | if (!GetExitCodeProcess (pi.hProcess, &exitcode)) { 129 | exitcode = (DWORD)-1; 130 | } 131 | exit_code = (int)exitcode; 132 | CloseHandle (pi.hProcess); 133 | CloseHandle (pi.hThread); 134 | #elif R2__UNIX__ 135 | int pipefd[2]; 136 | if (pipe (pipefd) == -1) { 137 | return NULL; 138 | } 139 | 140 | pid_t pid = fork (); 141 | if (pid == -1) { 142 | int e = errno; 143 | close (pipefd[0]); 144 | close (pipefd[1]); 145 | errno = e; 146 | return NULL; 147 | } 148 | 149 | if (pid == 0) { 150 | // Child: stdout -> pipe write end 151 | // stderr unchanged (so errors still show on parent stderr because of -sS) 152 | if (dup2 (pipefd[1], STDOUT_FILENO) == -1) { 153 | _exit (127); 154 | } 155 | 156 | close (pipefd[0]); 157 | close (pipefd[1]); 158 | 159 | // Build argv; no shell is involved (safe for spaces/quotes in msg/url). 160 | // Note: if msg starts with '@', curl treats it as a file. If that's unwanted, 161 | // use "--data-raw" instead of "-d". 162 | char *const argv[] = { 163 | "curl", 164 | "-sS", 165 | "-d", (char *)msg, 166 | (char *)url, 167 | NULL 168 | }; 169 | 170 | execvp ("curl", argv); 171 | // If exec fails: 172 | _exit (127); 173 | } 174 | 175 | // Parent 176 | close (pipefd[1]); // we read from pipefd[0] 177 | 178 | // Read child's stdout fully into a dynamic buffer 179 | size_t cap = 8192; 180 | buf = malloc (cap); 181 | if (!buf) { 182 | close (pipefd[0]); 183 | // Reap child to avoid a zombie 184 | int st; 185 | waitpid (pid, &st, 0); 186 | return NULL; 187 | } 188 | 189 | for (;;) { 190 | if (len + 4096 + 1 > cap) { 191 | size_t ncap = cap * 2; 192 | char *tmp = realloc (buf, ncap); 193 | if (!tmp) { 194 | R_FREE (buf); 195 | break; 196 | } 197 | buf = tmp; 198 | cap = ncap; 199 | } 200 | ssize_t n = read (pipefd[0], buf + len, 4096); 201 | if (n > 0) { 202 | len += (size_t)n; 203 | } else if (n == 0) { 204 | break; // EOF 205 | } else if (errno != EINTR) { 206 | free (buf); 207 | buf = NULL; // read error 208 | break; 209 | } 210 | } 211 | close (pipefd[0]); 212 | // Reap curl 213 | int status = 0; 214 | if (waitpid (pid, &status, 0) != -1) { 215 | if (WIFEXITED (status)) { 216 | exit_code = WEXITSTATUS (status); 217 | } else if (WIFSIGNALED (status)) { 218 | exit_code = 128 + WTERMSIG (status); 219 | } 220 | } 221 | #else 222 | #error unsupported platform for curl_post_capture 223 | #endif 224 | if (exit_code_out) { 225 | *exit_code_out = exit_code; 226 | } 227 | if (!buf) { 228 | return NULL; 229 | } 230 | // NUL-terminate (even if empty) 231 | buf[len] = '\0'; 232 | return buf; 233 | } 234 | -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | /* r2mcp - MIT - Copyright 2025 - pancake, dnakov */ 2 | 3 | #include 4 | 5 | #include "tools.h" 6 | #include "prompts.h" 7 | 8 | #if R2__UNIX__ 9 | #include 10 | /* Signal handling moved from r2mcp.c */ 11 | static void signal_handler(int signum) { 12 | const char msg[] = "\nInterrupt received, shutting down...\n"; 13 | (void) write (STDERR_FILENO, msg, sizeof (msg) - 1); 14 | r2mcp_running_set (0); 15 | signal (signum, SIG_DFL); 16 | } 17 | void setup_signals(void) { 18 | struct sigaction sa = { 0 }; 19 | sa.sa_flags = 0; 20 | sa.sa_handler = signal_handler; 21 | sigemptyset (&sa.sa_mask); 22 | sigaction (SIGINT, &sa, NULL); 23 | sigaction (SIGTERM, &sa, NULL); 24 | sigaction (SIGHUP, &sa, NULL); 25 | signal (SIGPIPE, SIG_IGN); 26 | } 27 | 28 | #endif 29 | /* Help and version moved from r2mcp.c */ 30 | void r2mcp_help(void) { 31 | const char help_text[] = 32 | "Usage: r2mcp [-flags]\n" 33 | " -c [cmd] run those commands before entering the mcp loop\n" 34 | " -d [pdc] select a different decompiler (pdc by default)\n" 35 | " -u [url] use remote r2 webserver base URL (HTTP r2pipe client mode)\n" 36 | " -l [file] append debug logs to this file\n" 37 | " -s [dir] enable sandbox mode; only allow files under [dir]\n" 38 | " -e [tool] enable only the specified tool (repeatable)\n" 39 | " -h show this help\n" 40 | " -r enable the dangerous runCommand tool\n" 41 | " -R enable read-only mode (expose only non-mutating tools)\n" 42 | " -m expose minimum amount of tools\n" 43 | " -t list available tools and exit\n" 44 | " -T [tests] run DSL tests and exit\n" 45 | " -p permissive tools: allow calling non-listed tools\n" 46 | " -n do not load any plugin or radare2rc\n" 47 | " -i ignore analysis level specified in analyze calls\n" 48 | " -S [url] enable supervisor control; connect to svc at [url]\n" 49 | " -v show version\n"; 50 | printf ("%s", help_text); 51 | } 52 | 53 | void r2mcp_version(void) { 54 | printf ("%s\n", R2MCP_VERSION); 55 | } 56 | 57 | /* Program entry point wrapper */ 58 | int main(int argc, const char **argv) { 59 | return r2mcp_main (argc, argv); 60 | } 61 | /* Moved from r2mcp.c to isolate main concerns here */ 62 | int r2mcp_main(int argc, const char **argv) { 63 | bool minimode = false; 64 | bool enable_run_command_tool = false; 65 | bool readonly_mode = false; 66 | bool list_tools = false; 67 | RList *cmds = r_list_newf (free); 68 | /* Whitelist of enabled tool names (populated via repeated -e flags) */ 69 | RList *enabled_tools = NULL; 70 | bool loadplugins = true; 71 | const char *deco = NULL; 72 | bool http_mode = false; 73 | bool permissive = false; 74 | char *baseurl = NULL; 75 | char *svc_baseurl = NULL; 76 | char *sandbox = NULL; 77 | char *logfile = NULL; 78 | bool ignore_analysis_level = false; 79 | const char *dsl_tests = NULL; 80 | RGetopt opt; 81 | r_getopt_init (&opt, argc, argv, "hmvpd:nc:u:l:s:rite:RT:S:"); 82 | int c; 83 | while ((c = r_getopt_next (&opt)) != -1) { 84 | switch (c) { 85 | case 'h': 86 | r2mcp_help (); 87 | return 0; 88 | case 'c': 89 | r_list_append (cmds, strdup (opt.arg)); 90 | break; 91 | case 'v': 92 | r2mcp_version (); 93 | return 0; 94 | case 'd': 95 | deco = opt.arg; 96 | break; 97 | case 'u': 98 | http_mode = true; 99 | baseurl = strdup (opt.arg); 100 | R_LOG_INFO ("[R2MCP] HTTP r2pipe client mode enabled, baseurl=%s", baseurl); 101 | break; 102 | case 'l': 103 | logfile = strdup (opt.arg); 104 | break; 105 | case 's': 106 | sandbox = strdup (opt.arg); 107 | break; 108 | case 'n': 109 | loadplugins = false; 110 | break; 111 | case 'm': 112 | minimode = true; 113 | break; 114 | case 'p': 115 | permissive = true; 116 | break; 117 | case 'r': 118 | enable_run_command_tool = true; 119 | break; 120 | case 'R': 121 | readonly_mode = true; 122 | break; 123 | case 'i': 124 | ignore_analysis_level = true; 125 | break; 126 | case 't': 127 | list_tools = true; 128 | break; 129 | case 'T': 130 | dsl_tests = opt.arg; 131 | break; 132 | case 'S': 133 | if (opt.arg) { 134 | if (strspn (opt.arg, "0123456789") == strlen (opt.arg)) { 135 | svc_baseurl = r_str_newf ("http://localhost:%s", opt.arg); 136 | } else { 137 | svc_baseurl = strdup (opt.arg); 138 | } 139 | } 140 | break; 141 | case 'e': 142 | if (opt.arg) { 143 | if (!enabled_tools) { 144 | enabled_tools = r_list_newf (free); 145 | } 146 | r_list_append (enabled_tools, strdup (opt.arg)); 147 | } 148 | break; 149 | default: 150 | R_LOG_ERROR ("Invalid flag -%c", c); 151 | return 1; 152 | } 153 | } 154 | ServerState ss = { 155 | .info = { 156 | .name = "Radare2 MCP Connector", 157 | .version = R2MCP_VERSION }, 158 | .capabilities = { .tools = true, .prompts = true, .resources = true }, 159 | .instructions = "Use this server to analyze binaries with radare2", 160 | .initialized = false, 161 | .minimode = minimode, 162 | .readonly_mode = readonly_mode, 163 | .permissive_tools = permissive, 164 | .enable_run_command_tool = enable_run_command_tool, 165 | .http_mode = http_mode, 166 | .baseurl = baseurl, 167 | .svc_baseurl = svc_baseurl, 168 | .sandbox = sandbox, 169 | .logfile = logfile, 170 | .ignore_analysis_level = ignore_analysis_level, 171 | .client_capabilities = NULL, 172 | .client_info = NULL, 173 | .enabled_tools = enabled_tools, 174 | }; 175 | /* Enable logging */ 176 | r2mcp_log_pub (&ss, "r2mcp starting"); 177 | #if R2__UNIX__ 178 | setup_signals (); 179 | #endif 180 | /* Initialize registries */ 181 | if (list_tools) { 182 | /* Print tools and exit early */ 183 | tools_print_table (&ss); 184 | return 0; 185 | } 186 | prompts_registry_init (&ss); 187 | /* Initialize r2 (unless running in HTTP client mode) */ 188 | if (!ss.http_mode) { 189 | if (!r2mcp_state_init (&ss)) { 190 | R_LOG_ERROR ("Failed to initialize radare2"); 191 | r2mcp_log_pub (&ss, "Failed to initialize radare2"); 192 | return 1; 193 | } 194 | if (loadplugins) { 195 | r_core_loadlibs (ss.rstate.core, R_CORE_LOADLIBS_ALL, NULL); 196 | r_core_parse_radare2rc (ss.rstate.core); 197 | } 198 | if (deco) { 199 | if (!strcmp (deco, "decai")) { 200 | deco = "decai -d"; 201 | } 202 | char *pdc = r_str_newf ("e cmd.pdc=%s", deco); 203 | R_LOG_INFO ("[R2MCP] Using Decompiler: %s", pdc); 204 | r2mcp_cmd (&ss, pdc); 205 | free (pdc); 206 | } 207 | } else { 208 | r2mcp_log_pub (&ss, "HTTP r2pipe client mode active - skipping local r2 initialization"); 209 | } 210 | /* If -T was provided, run DSL tests and exit */ 211 | if (dsl_tests) { 212 | int r = r2mcp_run_dsl_tests (&ss, dsl_tests, NULL); 213 | /* Cleanup and return */ 214 | prompts_registry_fini (&ss); 215 | r2mcp_state_fini (&ss); 216 | free (ss.baseurl); 217 | free (ss.svc_baseurl); 218 | free (ss.sandbox); 219 | free (ss.logfile); 220 | if (ss.enabled_tools) { 221 | r_list_free (ss.enabled_tools); 222 | } 223 | return r == 0? 0: 2; 224 | } 225 | RListIter *iter; 226 | const char *cmd; 227 | r_list_foreach (cmds, iter, cmd) { 228 | r2mcp_cmd (&ss, cmd); 229 | } 230 | r_list_free (cmds); 231 | r2mcp_running_set (1); 232 | r2mcp_eventloop (&ss); 233 | prompts_registry_fini (&ss); 234 | r2mcp_state_fini (&ss); 235 | /* Cleanup */ 236 | free (ss.baseurl); 237 | free (ss.sandbox); 238 | free (ss.logfile); 239 | if (ss.enabled_tools) { 240 | r_list_free (ss.enabled_tools); 241 | } 242 | (void)0; 243 | return 0; 244 | } 245 | -------------------------------------------------------------------------------- /svc/r2mcp-svc.c: -------------------------------------------------------------------------------- 1 | /* r2mcp - MIT - Copyright 2025 - pancake, dnakov */ 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | typedef struct { 13 | bool yolo_mode; 14 | bool quit; 15 | bool single_request; 16 | int port; 17 | RCons *cons; 18 | } R2McpSvcContext; 19 | 20 | static bool parse_args(int argc, char **argv, R2McpSvcContext *ctx) { 21 | int i = 1; 22 | for (; i < argc; i++) { 23 | const char *arg = argv[i]; 24 | if (!strcmp (arg, "-y")) { 25 | ctx->yolo_mode = true; 26 | } else if (!strcmp (arg, "-q")) { 27 | ctx->single_request = true; 28 | } else if (r_str_startswith (arg, "-")) { 29 | R_LOG_INFO ("Unknown flag %s", arg); 30 | } else { 31 | break; 32 | } 33 | } 34 | if (i != argc - 1) { 35 | R_LOG_ERROR ("Usage: %s [-y] [-q] ", argv[0]); 36 | return false; 37 | } 38 | ctx->port = atoi (argv[i]); 39 | if (ctx->port <= 0) { 40 | R_LOG_ERROR ("Invalid port"); 41 | return false; 42 | } 43 | return true; 44 | } 45 | 46 | static RSocket *setup_server(int port) { 47 | RSocket *server = r_socket_new (false); 48 | if (!server) { 49 | R_LOG_ERROR ("Cannot create socket"); 50 | return NULL; 51 | } 52 | char port_str[16]; 53 | sprintf (port_str, "%d", port); 54 | if (!r_socket_listen (server, port_str, NULL)) { 55 | R_LOG_ERROR ("Cannot listen on port %s", port_str); 56 | r_socket_free (server); 57 | return NULL; 58 | } 59 | R_LOG_INFO (Color_GREEN "🚀 R2MCP Supervisor waiting for requests on port %d" Color_RESET, port); 60 | return server; 61 | } 62 | 63 | static char *handle_modify(R2McpSvcContext *ctx, char *data) { 64 | r_cons_printf (ctx->cons, Color_MAGENTA "✏️" 65 | " Enter new tool name:" Color_RESET " "); 66 | r_cons_flush (ctx->cons); 67 | const char *line = r_line_readline (ctx->cons); 68 | if (!line) { 69 | return strdup ("{\"error\":\"input failed\"}"); 70 | } 71 | char *new_tool = strdup (line); 72 | char *escaped_new_tool = r_str_replace (strdup (new_tool), "\\", "\\\\", 1); 73 | escaped_new_tool = r_str_replace (escaped_new_tool, "\"", "\\\"", 1); 74 | char *tool_key = "\"tool\":\""; 75 | char *pos = strstr (data, tool_key); 76 | if (!pos) { 77 | free (escaped_new_tool); 78 | free (new_tool); 79 | return strdup ("{\"error\":\"no tool field\"}"); 80 | } 81 | char *start = pos + strlen (tool_key); 82 | char *end = strchr (start, '"'); 83 | if (!end) { 84 | free (escaped_new_tool); 85 | free (new_tool); 86 | return strdup ("{\"error\":\"invalid json\"}"); 87 | } 88 | size_t prefix_len = start - data; 89 | char *response_body = r_str_newf ("%.*s%s%s", (int)prefix_len, data, escaped_new_tool, end); 90 | free (escaped_new_tool); 91 | free (new_tool); 92 | return response_body; 93 | } 94 | 95 | static char *handle_r2cmd(R2McpSvcContext *ctx) { 96 | r_cons_printf (ctx->cons, Color_CYAN "🖥️" 97 | " Enter r2 command:" Color_RESET " "); 98 | r_cons_flush (ctx->cons); 99 | const char *line = r_line_readline (ctx->cons); 100 | if (!line) { 101 | return strdup ("{\"error\":\"input failed\"}"); 102 | } 103 | char *r2cmd = strdup (line); 104 | char *escaped = r_str_replace (strdup (r2cmd), "\"", "\\\"", 1); 105 | char *response_body = r_str_newf ("{\"r2cmd\":\"%s\"}", escaped); 106 | free (escaped); 107 | free (r2cmd); 108 | return response_body; 109 | } 110 | 111 | static char *show_menu_and_get_response(char *data, const char *tool, R2McpSvcContext *ctx) { 112 | r_cons_printf (ctx->cons, "\n" Color_YELLOW 113 | "╔══════════════════════════════════════╗\n" 114 | "║ " Color_CYAN "🔧 Tool Call Request " Color_YELLOW "║\n" 115 | "╚══════════════════════════════════════╝\n" 116 | Color_GREEN "Tool: %s\n" 117 | Color_BLUE "Request: %s\n\n" 118 | "Available Actions:\n" 119 | Color_GREEN "1. ✅ Accept\n" 120 | Color_RED "2. ❌ Reject\n" 121 | Color_YELLOW "3. ⚡ Accept all (YOLO mode)\n" 122 | Color_MAGENTA "4. Modify tool\n" 123 | Color_CYAN "5. 🖥️ Run r2 command\n" 124 | Color_RED "6. Quit server\n\n" Color_RESET 125 | "❓ Your choice: ", tool? tool: "unknown", data); 126 | r_cons_flush (ctx->cons); 127 | 128 | const char *line = r_line_readline (ctx->cons); 129 | if (!line) { 130 | return strdup ("{\"error\":\"input failed\"}"); 131 | } 132 | char *input = strdup (line); 133 | int choice = atoi (input); 134 | free (input); 135 | switch (choice) { 136 | case 1: // Accept 137 | return strdup (data); 138 | case 2: // Reject 139 | return strdup ("{\"error\":\"rejected by user\"}"); 140 | case 3: // YOLO 141 | ctx->yolo_mode = true; 142 | return strdup (data); 143 | case 4: // Modify 144 | return handle_modify (ctx, data); 145 | case 5: // Run r2 command 146 | return handle_r2cmd (ctx); 147 | case 6: // Quit 148 | ctx->quit = true; 149 | return strdup ("{\"error\":\"server quit\"}"); 150 | default: 151 | return strdup ("{\"error\":\"invalid choice\"}"); 152 | } 153 | } 154 | 155 | static void handle_request(RSocket *server, R2McpSvcContext *ctx) { 156 | RSocketHTTPOptions so = { 0 }; 157 | so.timeout = 3; 158 | fflush (stderr); 159 | RSocketHTTPRequest *rs = r_socket_http_accept (server, &so); 160 | if (!rs) { 161 | return; 162 | } 163 | 164 | // Only accept POST requests 165 | if (strcmp (rs->method, "POST")) { 166 | char *response_body = r_str_newf ("{\"error\":\"Method not allowed\"}"); 167 | r_socket_http_response (rs, 405, response_body, 0, "Content-Type: application/json\r\n"); 168 | free (response_body); 169 | r_socket_http_free (rs); 170 | return; 171 | } 172 | 173 | if (!rs->data) { 174 | char *response_body = r_str_newf ("{\"error\":\"No data\"}"); 175 | r_socket_http_response (rs, 400, response_body, 0, "Content-Type: application/json\r\n"); 176 | free (response_body); 177 | r_socket_http_free (rs); 178 | return; 179 | } 180 | 181 | // Parse JSON 182 | char *body_copy = strdup ((char *)rs->data); // r_json_parse modifies the string 183 | RJson *j = r_json_parse (body_copy); 184 | if (!j) { 185 | free (body_copy); 186 | char *response_body = r_str_newf ("{\"error\":\"Invalid JSON\"}"); 187 | r_socket_http_response (rs, 400, response_body, 0, "Content-Type: application/json\r\n"); 188 | free (response_body); 189 | r_socket_http_free (rs); 190 | return; 191 | } 192 | 193 | const char *tool = r_json_get_str (j, "tool"); 194 | char *response_body = NULL; 195 | 196 | if (ctx->yolo_mode) { 197 | // Auto accept 198 | r_cons_printf (ctx->cons, Color_YELLOW "⚡" 199 | " YOLO: Received message:" Color_RESET " %s\n", 200 | (char *)rs->data); 201 | r_cons_printf (ctx->cons, Color_YELLOW "⚡" 202 | " YOLO: Tool executed:" Color_RESET " %s\n", 203 | tool? tool: "unknown"); 204 | r_cons_flush (ctx->cons); 205 | response_body = strdup ((char *)rs->data); 206 | } else { 207 | response_body = show_menu_and_get_response ((char *)rs->data, tool, ctx); 208 | } 209 | 210 | if (response_body) { 211 | r_socket_http_response (rs, 200, response_body, 0, "Content-Type: application/json\r\n"); 212 | free (response_body); 213 | } 214 | 215 | r_json_free (j); 216 | free (body_copy); 217 | r_socket_http_free (rs); 218 | if (ctx->single_request) { 219 | ctx->quit = true; 220 | } 221 | } 222 | 223 | int main(int argc, char **argv) { 224 | R2McpSvcContext ctx = { 0 }; 225 | ctx.cons = r_cons_new (); 226 | if (parse_args (argc, argv, &ctx)) { 227 | RSocket *server = setup_server (ctx.port); 228 | if (server) { 229 | while (!ctx.quit) { 230 | handle_request (server, &ctx); 231 | } 232 | r_socket_free (server); 233 | } 234 | } 235 | r_cons_free (ctx.cons); 236 | return 0; 237 | } 238 | -------------------------------------------------------------------------------- /src/r2api.inc.c: -------------------------------------------------------------------------------- 1 | /* r2mcp - MIT - Copyright 2025 - pancake, dnakov */ 2 | 3 | static void r2state_settings(RCore *core) { 4 | r_config_set_i (core->config, "scr.color", 0); 5 | r_config_set_b (core->config, "scr.utf8", false); 6 | r_config_set_b (core->config, "scr.interactive", false); 7 | r_config_set_b (core->config, "emu.str", true); 8 | r_config_set_b (core->config, "asm.bytes", false); 9 | r_config_set_b (core->config, "anal.strings", true); 10 | r_config_set_b (core->config, "asm.lines", false); 11 | r_config_set_b (core->config, "anal.hasnext", true); // TODO: optional 12 | r_config_set_b (core->config, "asm.lines.fcn", false); 13 | r_config_set_b (core->config, "asm.cmt.right", false); 14 | r_config_set_b (core->config, "scr.html", false); 15 | r_config_set_b (core->config, "scr.prompt", false); 16 | r_config_set_b (core->config, "scr.echo", false); 17 | r_config_set_b (core->config, "scr.flush", true); 18 | r_config_set_b (core->config, "scr.null", false); 19 | r_config_set_b (core->config, "scr.pipecolor", false); 20 | r_config_set_b (core->config, "scr.utf8", false); 21 | r_config_set_i (core->config, "scr.limit", 16768); 22 | } 23 | 24 | static bool logcb(void *user, int type, const char *origin, const char *msg) { 25 | if (type > R_LOG_LEVEL_WARN) { 26 | return false; 27 | } 28 | if (!msg || R_STR_ISEMPTY (origin)) { 29 | return true; 30 | } 31 | ServerState *ss = (ServerState *)user; 32 | if (ss->sb) { 33 | const char *typestr = r_log_level_tostring (type); 34 | // R_LOG_INFO ("[%s] from=%s message=%s\n", typestr, origin, msg); 35 | r_strbuf_appendf (ss->sb, "[%s] %s\n", typestr, msg); 36 | // r_strbuf_appendf (ss->sb, "[%s] from=%s message=%s\n", typestr, origin, msg); 37 | } 38 | return true; 39 | } 40 | 41 | static void r2mcp_log_reset(ServerState *ss) { 42 | r_strbuf_free (ss->sb); 43 | ss->sb = r_strbuf_new (""); 44 | } 45 | 46 | static char *r2mcp_log_drain(ServerState *ss) { 47 | char *s = r_strbuf_drain (ss->sb); 48 | if (R_STR_ISNOTEMPTY (s)) { 49 | ss->sb = NULL; 50 | return s; 51 | } 52 | free (s); 53 | ss->sb = NULL; 54 | return NULL; 55 | } 56 | 57 | static inline void r2mcp_log(ServerState *ss, const char *x) { 58 | R_LOG_INFO ("[R2MCP] %s", x); 59 | #if R2MCP_DEBUG 60 | if (ss && ss->logfile && *ss->logfile) { 61 | r_file_dump (ss->logfile, (const ut8 *) (x), -1, true); 62 | r_file_dump (ss->logfile, (const ut8 *)"\n", -1, true); 63 | } 64 | #endif 65 | } 66 | 67 | static char *r2_cmd_filter(const char *cmd, bool *changed) { 68 | char *res = r_str_trim_dup (cmd); 69 | char fchars[] = "|>`"; 70 | *changed = false; 71 | if (*res == '!') { 72 | *changed = true; 73 | *res = 0; 74 | } else { 75 | char *ch = strstr (res, "$ ("); 76 | if (ch) { 77 | *changed = true; 78 | *ch = 0; 79 | } 80 | for (ch = fchars; *ch; ch++) { 81 | char *p = strchr (res, *ch); 82 | if (p) { 83 | *changed = true; 84 | *p = 0; 85 | } 86 | } 87 | } 88 | return res; 89 | } 90 | 91 | #include "curl.inc.c" 92 | 93 | static char *r2cmd_over_http(ServerState *ss, const char *cmd) { 94 | int rc = 0; 95 | char *res = curl_post_capture (ss->baseurl, cmd, &rc); 96 | if (rc != 0) { 97 | R_LOG_ERROR ("curl %d", rc); 98 | free (res); 99 | return NULL; 100 | } 101 | return res; 102 | } 103 | 104 | /* printf-like wrapper for r2mcp_cmd to avoid boilerplate */ 105 | char *r2mcp_cmdf(ServerState *ss, const char *fmt, ...) { 106 | if (!fmt) { 107 | return r2mcp_cmd (ss, ""); 108 | } 109 | va_list ap; 110 | va_start (ap, fmt); 111 | va_list ap2; 112 | va_copy (ap2, ap); 113 | int n = vsnprintf (NULL, 0, fmt, ap2); 114 | va_end (ap2); 115 | if (n < 0) { 116 | va_end (ap); 117 | return NULL; 118 | } 119 | char *cmd = (char *)malloc ((size_t)n + 1); 120 | if (!cmd) { 121 | va_end (ap); 122 | return NULL; 123 | } 124 | vsnprintf (cmd, (size_t)n + 1, fmt, ap); 125 | va_end (ap); 126 | char *res = r2mcp_cmd (ss, cmd); 127 | free (cmd); 128 | return res; 129 | } 130 | 131 | static bool path_is_absolute(const char *p) { 132 | return p && p[0] == '/'; 133 | } 134 | 135 | static bool path_contains_parent_ref(const char *p) { 136 | return p && strstr (p, "/../") != NULL; 137 | } 138 | 139 | static bool path_is_within_sandbox(const char *p, const char *sb) { 140 | if (!sb || !*sb) { 141 | return true; 142 | } 143 | size_t plen = strlen (p); 144 | size_t slen = strlen (sb); 145 | if (slen == 0 || slen > plen) { 146 | return false; 147 | } 148 | if (strncmp (p, sb, slen) != 0) { 149 | return false; 150 | } 151 | if (plen == slen) { 152 | return true; // exact match 153 | } 154 | // ensure boundary: next char must be '/' 155 | return p[slen] == '/'; 156 | } 157 | 158 | static bool r2_open_file(ServerState *ss, const char *filepath) { 159 | R_LOG_INFO ("Attempting to open file: %s\n", filepath); 160 | 161 | // Security checks common to both local and HTTP modes 162 | if (!filepath || !*filepath) { 163 | R_LOG_ERROR ("Empty file path is not allowed"); 164 | return false; 165 | } 166 | if (!path_is_absolute (filepath)) { 167 | R_LOG_ERROR ("Relative paths are not allowed. Use an absolute path"); 168 | return false; 169 | } 170 | if (path_contains_parent_ref (filepath)) { 171 | R_LOG_ERROR ("Path traversal is not allowed (contains '/../')"); 172 | return false; 173 | } 174 | if (ss && ss->sandbox && *ss->sandbox) { 175 | if (!path_is_within_sandbox (filepath, ss->sandbox)) { 176 | R_LOG_ERROR ("Access denied: path is outside of the sandbox"); 177 | return false; 178 | } 179 | } 180 | /* In HTTP mode we do not touch the local r2 core. Just set the state 181 | * so subsequent calls to r2mcp_cmd will be allowed (they will be handled 182 | * by the HTTP helper). 183 | */ 184 | if (ss && ss->http_mode) { 185 | free (ss->rstate.current_file); 186 | ss->rstate.current_file = strdup (filepath); 187 | ss->rstate.file_opened = true; 188 | return true; 189 | } 190 | 191 | RCore *core = ss->rstate.core; 192 | if (!core && !r2mcp_state_init (ss)) { 193 | R_LOG_ERROR ("Failed to initialize r2 core\n"); 194 | return false; 195 | } 196 | 197 | if (ss->rstate.file_opened) { 198 | R_LOG_INFO ("Closing previously opened file: %s", ss->rstate.current_file); 199 | r_core_cmd0 (core, "o-*"); 200 | ss->rstate.file_opened = false; 201 | free (ss->rstate.current_file); 202 | ss->rstate.current_file = NULL; 203 | } 204 | 205 | r_core_cmd0 (core, "e bin.relocs.apply=true"); 206 | r_core_cmd0 (core, "e bin.cache=true"); 207 | 208 | char *cmd = r_str_newf ("'o %s", filepath); 209 | R_LOG_INFO ("Running r2 command: %s", cmd); 210 | char *result = r_core_cmd_str (core, cmd); 211 | free (cmd); 212 | bool success = (result && strlen (result) > 0); 213 | free (result); 214 | 215 | if (!success) { 216 | R_LOG_INFO ("Trying alternative method to open file"); 217 | RIODesc *fd = r_core_file_open (core, filepath, R_PERM_R, 0); 218 | if (fd) { 219 | r_core_bin_load (core, filepath, 0); 220 | R_LOG_INFO ("File opened using r_core_file_open"); 221 | success = true; 222 | } else { 223 | R_LOG_ERROR ("Failed to open file: %s", filepath); 224 | return false; 225 | } 226 | } 227 | 228 | R_LOG_INFO ("Loading binary information"); 229 | r_core_cmd0 (core, "ob"); 230 | 231 | free (ss->rstate.current_file); 232 | ss->rstate.current_file = strdup (filepath); 233 | ss->rstate.file_opened = true; 234 | R_LOG_INFO ("File opened successfully: %s", filepath); 235 | 236 | return true; 237 | } 238 | 239 | static char *r2_analyze(ServerState *ss, int level) { 240 | if (ss && ss->http_mode) { 241 | /* In HTTP mode we won't run local analysis; return empty string. */ 242 | return strdup (""); 243 | } 244 | RCore *core = ss->rstate.core; 245 | if (!core || !ss->rstate.file_opened) { 246 | return NULL; 247 | } 248 | const char *cmd = "aa"; 249 | if (!ss->ignore_analysis_level) { 250 | switch (level) { 251 | case 1: cmd = "aac"; break; 252 | case 2: cmd = "aaa"; break; 253 | case 3: cmd = "aaaa"; break; 254 | case 4: cmd = "aaaaa"; break; 255 | } 256 | } 257 | r2mcp_log_reset (ss); 258 | r_core_cmd0 (core, cmd); 259 | return r2mcp_log_drain (ss); 260 | } 261 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: ci 3 | 4 | env: 5 | R2V: 6.0.4 6 | ZIP_PREFIX: radare2-mcp- 7 | 8 | on: 9 | push: 10 | branches: 11 | - main 12 | tags: 13 | - "[0-9]*" 14 | pull_request: 15 | branches: 16 | - main 17 | 18 | jobs: 19 | r2git: 20 | runs-on: ubuntu-22.04 21 | strategy: 22 | fail-fast: false 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | - name: Installing radare2 from git 27 | run: | 28 | git clone --depth=1 https://github.com/radareorg/radare2 29 | cd radare2 30 | sys/install.sh /usr > /dev/null 31 | - name: Building radare2-mcp 32 | run: ./configure && make 33 | - name: Create Linux zip archive 34 | if: startsWith(github.ref, 'refs/tags/') 35 | run: | 36 | mkdir -p release 37 | FILENAME="${ZIP_PREFIX}${{ github.ref_name }}-linux-x64.zip" 38 | zip -j "release/$FILENAME" src/r2mcp 39 | - name: Upload Linux binary 40 | if: startsWith(github.ref, 'refs/tags/') 41 | uses: actions/upload-artifact@v4 42 | with: 43 | name: linux-x64-binary 44 | path: "release/${ZIP_PREFIX}${{ github.ref_name }}-linux-x64.zip" 45 | 46 | linux-deb: 47 | runs-on: ubuntu-22.04 48 | strategy: 49 | fail-fast: false 50 | steps: 51 | - name: Checkout 52 | uses: actions/checkout@v4 53 | - name: Installing radare2 from deb 54 | run: | 55 | BASE_URL="https://github.com/radareorg/radare2/releases/download/${{env.R2V}}" 56 | wget "${BASE_URL}/radare2_${{env.R2V}}_amd64.deb" 57 | wget "${BASE_URL}/radare2-dev_${{env.R2V}}_amd64.deb" 58 | sudo dpkg -i *.deb 59 | - name: Building radare2-mcp 60 | run: ./configure && make 61 | - name: Building Debian package 62 | if: startsWith(github.ref, 'refs/tags/') 63 | run: make -C dist/debian 64 | - name: List Debian package files 65 | if: startsWith(github.ref, 'refs/tags/') 66 | run: ls -la dist/debian/*.deb 67 | - name: Upload Debian package 68 | if: startsWith(github.ref, 'refs/tags/') 69 | uses: actions/upload-artifact@v4 70 | with: 71 | name: debian-package 72 | path: dist/debian/*.deb 73 | 74 | linux-meson: 75 | runs-on: ubuntu-22.04 76 | strategy: 77 | fail-fast: false 78 | steps: 79 | - name: Checkout 80 | uses: actions/checkout@v4 81 | - name: Installing radare2 from deb 82 | run: | 83 | BASE_URL="https://github.com/radareorg/radare2/releases/download/${{env.R2V}}" 84 | wget "${BASE_URL}/radare2_${{env.R2V}}_amd64.deb" 85 | wget "${BASE_URL}/radare2-dev_${{env.R2V}}_amd64.deb" 86 | sudo dpkg -i *.deb 87 | - name: Install Meson and Ninja 88 | run: sudo apt update && sudo apt install -y meson ninja-build 89 | - name: Configure Meson 90 | run: meson setup builddir --prefix=/usr --buildtype=release -Dr2_prefix=/usr 91 | - name: Compile 92 | run: meson compile -C builddir 93 | 94 | macos: 95 | runs-on: macos-latest 96 | strategy: 97 | fail-fast: false 98 | matrix: 99 | arch: [x86_64, arm64] 100 | env: 101 | ARCH: ${{ matrix.arch }} 102 | steps: 103 | - name: Checkout 104 | uses: actions/checkout@v4 105 | - name: Cloning radare2 106 | run: git clone --depth=1 --branch "${{env.R2V}}" https://github.com/radareorg/radare2 107 | - name: Building and installing radare2 108 | run: | 109 | cd radare2 110 | sys/install.sh > /dev/null 111 | - name: Building radare2-mcp 112 | run: ./configure && make 113 | - name: Check build output 114 | run: ls -la src/ 115 | - name: Create macOS zip archive 116 | if: startsWith(github.ref, 'refs/tags/') 117 | run: | 118 | mkdir -p release 119 | if [ -f src/r2mcp ]; then 120 | FILENAME="${ZIP_PREFIX}${{ github.ref_name }}-macos-${{ matrix.arch }}.zip" 121 | zip -j "release/$FILENAME" src/r2mcp 122 | else 123 | echo "Binary src/r2mcp not found, checking for alternatives..." 124 | find . -name "r2mcp*" -type f 125 | exit 1 126 | fi 127 | - name: Upload macOS binary 128 | if: startsWith(github.ref, 'refs/tags/') 129 | uses: actions/upload-artifact@v4 130 | with: 131 | name: macos-${{ matrix.arch }}-binary 132 | path: "release/${ZIP_PREFIX}${{ github.ref_name }}-macos-${{ matrix.arch }}.zip" 133 | 134 | windows: 135 | runs-on: windows-latest 136 | strategy: 137 | fail-fast: false 138 | steps: 139 | - name: Checkout 140 | uses: actions/checkout@v4 141 | 142 | - name: Set up Python (for Meson) 143 | uses: actions/setup-python@v4 144 | with: 145 | python-version: '3.x' 146 | 147 | - name: Install Meson 148 | shell: powershell 149 | run: | 150 | python -m pip install --upgrade pip 151 | pip install meson 152 | 153 | - name: Download and extract radare2 release 154 | shell: powershell 155 | run: | 156 | $R2V = '${{ env.R2V }}' 157 | $baseUrl = "https://github.com/radareorg/radare2/releases/download/$R2V" 158 | $url = "$baseUrl/radare2-$R2V-w64.zip" 159 | $dest = "$env:TEMP\radare2.zip" 160 | Write-Output "Downloading $url" 161 | Invoke-WebRequest -Uri $url -OutFile $dest -UseBasicParsing 162 | $prefix = 'C:\\radare2' 163 | if (Test-Path $prefix) { Remove-Item -Path $prefix -Recurse -Force } 164 | New-Item -ItemType Directory -Force -Path $prefix | Out-Null 165 | Expand-Archive -Path $dest -DestinationPath $prefix -Force 166 | # Some zips include a top-level folder; move its contents up if needed 167 | $inner = Get-ChildItem -Path $prefix | Where-Object { $_.PSIsContainer } | Select-Object -First 1 168 | if ($inner -and (Get-ChildItem -Path $inner.FullName | 169 | Measure-Object).Count -gt 0) { 170 | Get-ChildItem -Path $inner.FullName -Force | Move-Item -Destination $prefix -Force 171 | Remove-Item -Path $inner.FullName -Recurse -Force 172 | } 173 | Write-Output "Extracted radare2 contents to $prefix" 174 | 175 | - name: Configure Meson 176 | shell: powershell 177 | run: | 178 | $R2V = '${{ env.R2V }}' 179 | $prefix = 'C:\\radare2' 180 | Write-Output "Using radare2 prefix: $prefix" 181 | $posix = $prefix -replace '\\','/' 182 | Write-Output "Passing r2_prefix as: $posix" 183 | meson setup --reconfigure builddir --backend ninja ` 184 | --prefix="$posix" --buildtype=release -Dr2_prefix="$posix" 185 | 186 | - name: Compile 187 | shell: powershell 188 | run: | 189 | meson compile -C builddir -v 190 | 191 | - name: Create Windows zip archive 192 | if: startsWith(github.ref, 'refs/tags/') 193 | shell: powershell 194 | run: | 195 | New-Item -ItemType Directory -Force -Path output 196 | $src = '' 197 | if (Test-Path builddir/r2mcp.exe) { 198 | Copy-Item builddir/r2mcp.exe output/ 199 | $src = 'builddir/r2mcp.exe' 200 | } 201 | if (Test-Path builddir/src/r2mcp.exe) { 202 | Copy-Item builddir/src/r2mcp.exe output/ 203 | $src = 'builddir/src/r2mcp.exe' 204 | } 205 | if ($src -eq '') { Write-Output "Warning: r2mcp.exe not found after build" } 206 | Compress-Archive -Path output/* ` 207 | -DestinationPath r2mcp-windows.zip -Force 208 | 209 | - name: Upload Windows binary 210 | if: startsWith(github.ref, 'refs/tags/') 211 | uses: actions/upload-artifact@v4 212 | with: 213 | name: windows-binary 214 | path: r2mcp-windows.zip 215 | 216 | release: 217 | needs: [r2git, linux-deb, linux-meson, macos, windows] 218 | if: startsWith(github.ref, 'refs/tags/') 219 | runs-on: ubuntu-latest 220 | permissions: 221 | contents: write 222 | steps: 223 | - name: Checkout 224 | uses: actions/checkout@v4 225 | 226 | - name: Download all artifacts 227 | uses: actions/download-artifact@v4 228 | with: 229 | path: release-artifacts 230 | 231 | - name: List downloaded artifacts 232 | run: find release-artifacts -type f 233 | 234 | - name: Create GitHub Release 235 | uses: softprops/action-gh-release@v1 236 | with: 237 | files: | 238 | release-artifacts/**/*.zip 239 | release-artifacts/**/*.deb 240 | generate_release_notes: true 241 | -------------------------------------------------------------------------------- /src/prompts.c: -------------------------------------------------------------------------------- 1 | /* r2mcp - MIT - Copyright 2025 - pancake, dnakov */ 2 | 3 | #include "r2mcp.h" 4 | #include "prompts.h" 5 | #include 6 | 7 | static char *expand_home(const char *path) { 8 | if (path[0] == '~') { 9 | char *home = getenv ("HOME"); 10 | if (home) { 11 | return r_str_newf ("%s%s", home, path + 1); 12 | } 13 | } 14 | return strdup (path); 15 | } 16 | 17 | static RList *list_files(const char *path) { 18 | DIR *dir = opendir (path); 19 | if (!dir) { 20 | return NULL; 21 | } 22 | RList *list = r_list_newf (free); 23 | struct dirent *entry; 24 | while ((entry = readdir (dir))) { 25 | if (strcmp (entry->d_name, ".") && strcmp (entry->d_name, "..")) { 26 | r_list_append (list, strdup (entry->d_name)); 27 | } 28 | } 29 | closedir (dir); 30 | return list; 31 | } 32 | 33 | typedef struct { 34 | char *name; 35 | char *description; 36 | char *required; 37 | } ParsedArg; 38 | 39 | typedef struct { 40 | char *name; 41 | char *description; 42 | char *content; 43 | char *user_template; 44 | RList *args; 45 | } ParsedPrompt; 46 | 47 | static char *json_text_msg(const char *role, const char *text) { 48 | PJ *pj = pj_new (); 49 | pj_o (pj); 50 | pj_ks (pj, "role", role); 51 | pj_k (pj, "content"); 52 | pj_a (pj); 53 | pj_o (pj); 54 | pj_ks (pj, "type", "text"); 55 | pj_ks (pj, "text", text); 56 | pj_end (pj); 57 | pj_end (pj); 58 | pj_end (pj); 59 | return pj_drain (pj); 60 | } 61 | 62 | static char *json_messages_obj2(char *m1, char *m2) { 63 | RStrBuf *sb = r_strbuf_new ("{"); 64 | r_strbuf_append (sb, "\"messages\":["); 65 | if (m1) { 66 | r_strbuf_append (sb, m1); 67 | if (m2) { 68 | r_strbuf_append (sb, ","); 69 | } 70 | } 71 | if (m2) { 72 | r_strbuf_append (sb, m2); 73 | } 74 | r_strbuf_append (sb, "]}"); 75 | free (m1); 76 | free (m2); 77 | return r_strbuf_drain (sb); 78 | } 79 | 80 | static char *expand_template(const char *template, RJson *arguments) { 81 | RStrBuf *sb = r_strbuf_new (""); 82 | const char *p = template; 83 | while (*p) { 84 | if (*p == '{') { 85 | if (r_str_startswith (p, "{if ")) { 86 | p += 4; 87 | const char *arg_start = p; 88 | char *end = strchr (p, '}'); 89 | if (end) { 90 | char *arg = r_str_ndup (arg_start, end - arg_start); 91 | const char *val = r_json_get_str (arguments, arg); 92 | p = end + 1; 93 | const char *if_content = p; 94 | const char *else_pos = strstr (p, "{else}"); 95 | const char *endif_pos = strstr (p, "{/if}"); 96 | if (val && *val) { 97 | const char *end_content = else_pos? else_pos: endif_pos; 98 | if (end_content) { 99 | r_strbuf_append_n (sb, if_content, end_content - if_content); 100 | p = end_content + (else_pos? 6: 5); 101 | } 102 | } else { 103 | if (else_pos) { 104 | const char *else_content = else_pos + 6; 105 | const char *end_content = endif_pos; 106 | if (end_content) { 107 | r_strbuf_append_n (sb, else_content, end_content - else_content); 108 | p = end_content + 5; 109 | } 110 | } else { 111 | if (endif_pos) { 112 | p = endif_pos + 5; 113 | } 114 | } 115 | } 116 | free (arg); 117 | } 118 | } else { 119 | char *end = strchr (p, '}'); 120 | if (end) { 121 | char *arg = r_str_ndup (p + 1, end - p - 1); 122 | const char *val = r_json_get_str (arguments, arg); 123 | if (val) { 124 | r_strbuf_append (sb, val); 125 | } 126 | p = end + 1; 127 | free (arg); 128 | } else { 129 | r_strbuf_appendf (sb, "%c", *p); 130 | p++; 131 | } 132 | } 133 | } else { 134 | r_strbuf_appendf (sb, "%c", *p); 135 | p++; 136 | } 137 | } 138 | return r_strbuf_drain (sb); 139 | } 140 | 141 | static char *render_loaded(const PromptSpec *spec, RJson *arguments) { 142 | ParsedPrompt *pp = (ParsedPrompt *)spec->render_data; 143 | char *user = NULL; 144 | if (pp->user_template) { 145 | user = expand_template (pp->user_template, arguments); 146 | } 147 | char *m1 = json_text_msg ("system", pp->content); 148 | char *m2 = user? json_text_msg ("user", user): NULL; 149 | char *out = json_messages_obj2 (m1, m2); 150 | free (user); 151 | return out; 152 | } 153 | 154 | static ParsedPrompt *parse_r2ai_md(const char *path) { 155 | size_t size; 156 | char *data = r_file_slurp (path, &size); 157 | if (!data) { 158 | return NULL; 159 | } 160 | ParsedPrompt *pp = R_NEW0 (ParsedPrompt); 161 | const char *basename = r_file_basename (path); 162 | char *name_dup = strdup (basename); 163 | char *dot = strstr (name_dup, ".r2ai.md"); 164 | if (dot) { 165 | *dot = 0; 166 | } 167 | pp->name = name_dup; 168 | char *p = data; 169 | if (size < 4 || strncmp (p, "---\n", 4)) { 170 | goto fail; 171 | } 172 | p += 4; 173 | char *end = strstr (p, "\n---\n"); 174 | if (!end) { 175 | goto fail; 176 | } 177 | *end = 0; 178 | char *front = p; 179 | p = end + 5; 180 | pp->content = r_str_trim_dup (p); 181 | char *front_copy = strdup (front); 182 | char *line = strtok (front_copy, "\n"); 183 | int in_args = 0; 184 | RList *args = NULL; 185 | while (line) { 186 | char *trimmed_line = r_str_trim_dup (line); 187 | if (!*trimmed_line) { 188 | free (trimmed_line); 189 | line = strtok (NULL, "\n"); 190 | continue; 191 | } 192 | if (!strcmp (trimmed_line, "args:")) { 193 | in_args = 1; 194 | args = r_list_newf (free); 195 | free (trimmed_line); 196 | line = strtok (NULL, "\n"); 197 | continue; 198 | } 199 | if (in_args) { 200 | if (trimmed_line[0] == '-') { 201 | char *arg_line = trimmed_line + 1; 202 | char *trimmed_arg = r_str_trim_dup (arg_line); 203 | ParsedArg *arg = R_NEW0 (ParsedArg); 204 | char *colon = strchr (trimmed_arg, ':'); 205 | if (colon) { 206 | *colon = 0; 207 | char *key = r_str_trim_dup (trimmed_arg); 208 | char *value = r_str_trim_dup (colon + 1); 209 | if (!strcmp (key, "name")) { 210 | arg->name = value; 211 | } else if (!strcmp (key, "description")) { 212 | arg->description = value; 213 | } else if (!strcmp (key, "required")) { 214 | arg->required = value; 215 | } else { 216 | free (value); 217 | } 218 | free (key); 219 | } 220 | free (trimmed_arg); 221 | if (arg->name) { 222 | r_list_append (args, arg); 223 | } else { 224 | free (arg); 225 | } 226 | } else { 227 | in_args = 0; 228 | } 229 | } 230 | if (!in_args) { 231 | char *colon = strchr (trimmed_line, ':'); 232 | if (colon) { 233 | *colon = 0; 234 | char *key = r_str_trim_dup (trimmed_line); 235 | char *value = r_str_trim_dup (colon + 1); 236 | if (!strcmp (key, "description")) { 237 | pp->description = value; 238 | } else if (!strcmp (key, "user_template")) { 239 | if (value[0] == '|') { 240 | RStrBuf *sb = r_strbuf_new (""); 241 | free (value); 242 | line = strtok (NULL, "\n"); 243 | while (line) { 244 | char *trimmed = r_str_trim_dup (line); 245 | if (!*trimmed) { 246 | free (trimmed); 247 | line = strtok (NULL, "\n"); 248 | continue; 249 | } 250 | r_strbuf_append (sb, trimmed); 251 | r_strbuf_append (sb, "\n"); 252 | free (trimmed); 253 | line = strtok (NULL, "\n"); 254 | } 255 | pp->user_template = r_strbuf_drain (sb); 256 | } else { 257 | free (value); 258 | } 259 | } else { 260 | free (value); 261 | } 262 | free (key); 263 | } 264 | } 265 | free (trimmed_line); 266 | line = strtok (NULL, "\n"); 267 | } 268 | pp->args = args; 269 | free (front_copy); 270 | free (data); 271 | return pp; 272 | fail: 273 | free (pp->name); 274 | free (pp->description); 275 | free (pp->content); 276 | free (pp->user_template); 277 | r_list_free (pp->args); 278 | free (pp); 279 | free (data); 280 | return NULL; 281 | } 282 | 283 | // ---------- Registry ---------- 284 | 285 | typedef struct { 286 | RList *list; // of PromptSpec*(borrowed pointers to builtin entries) 287 | } PromptRegistry; 288 | 289 | void prompts_registry_init(ServerState *ss) { 290 | if (ss->prompts) { 291 | return; 292 | } 293 | PromptRegistry *reg = R_NEW0 (PromptRegistry); 294 | reg->list = r_list_new (); 295 | if (!reg->list) { 296 | free (reg); 297 | return; 298 | } 299 | // Load prompts from directories 300 | char *dirs[] = { "prompts", "~/.config/r2ai/prompts", NULL }; 301 | for (char **dir = dirs; *dir; dir++) { 302 | char *path = expand_home (*dir); 303 | if (!path) { 304 | continue; 305 | } 306 | RList *files = list_files (path); 307 | if (files) { 308 | RListIter *it; 309 | char *file; 310 | r_list_foreach (files, it, file) { 311 | if (r_str_endswith (file, ".r2ai.md")) { 312 | char *full_path = r_str_newf ("%s/%s", path, file); 313 | ParsedPrompt *pp = parse_r2ai_md (full_path); 314 | if (pp) { 315 | PromptSpec *spec = R_NEW (PromptSpec); 316 | spec->name = pp->name; 317 | spec->description = pp->description; 318 | spec->nargs = pp->args ? r_list_length (pp->args) : 0; 319 | spec->args = calloc (spec->nargs + 1, sizeof (PromptArg)); 320 | int i = 0; 321 | RListIter *ait; 322 | ParsedArg *pa; 323 | r_list_foreach (pp->args, ait, pa) { 324 | spec->args[i].name = pa->name; 325 | spec->args[i].description = pa->description; 326 | spec->args[i].required = !strcmp (pa->required, "true"); 327 | i++; 328 | } 329 | spec->render = render_loaded; 330 | spec->render_data = pp; 331 | r_list_append (reg->list, spec); 332 | } 333 | free (full_path); 334 | } 335 | } 336 | r_list_free (files); 337 | } 338 | free (path); 339 | } 340 | ss->prompts = reg; 341 | } 342 | 343 | void prompts_registry_fini(ServerState *ss) { 344 | if (!ss || !ss->prompts) { 345 | return; 346 | } 347 | PromptRegistry *reg = (PromptRegistry *)ss->prompts; 348 | r_list_free (reg->list); 349 | free (reg); 350 | ss->prompts = NULL; 351 | } 352 | 353 | static PromptSpec *prompts_find(const ServerState *ss, const char *name) { 354 | if (!ss || !ss->prompts || !name) { 355 | return NULL; 356 | } 357 | PromptRegistry *reg = (PromptRegistry *)ss->prompts; 358 | RListIter *it; 359 | PromptSpec *p; 360 | r_list_foreach (reg->list, it, p) { 361 | if (!strcmp (p->name, name)) { 362 | return p; 363 | } 364 | } 365 | return NULL; 366 | } 367 | 368 | char *prompts_build_list_json(const ServerState *ss, const char *cursor, int page_size) { 369 | if (!ss || !ss->prompts) { 370 | PJ *pj = pj_new (); 371 | pj_o (pj); 372 | pj_k (pj, "prompts"); 373 | pj_a (pj); 374 | pj_end (pj); // end array 375 | pj_end (pj); // end object 376 | return pj_drain (pj); 377 | } 378 | 379 | PromptRegistry *reg = (PromptRegistry *)ss->prompts; 380 | 381 | int start_index = 0; 382 | if (cursor) { 383 | start_index = atoi (cursor); 384 | if (start_index < 0) { 385 | start_index = 0; 386 | } 387 | } 388 | int total = r_list_length (reg->list); 389 | int end_index = start_index + page_size; 390 | if (end_index > total) { 391 | end_index = total; 392 | } 393 | 394 | PJ *pj = pj_new (); 395 | pj_o (pj); 396 | pj_k (pj, "prompts"); 397 | pj_a (pj); 398 | 399 | RListIter *it; 400 | PromptSpec *p; 401 | int idx = 0; 402 | r_list_foreach (reg->list, it, p) { 403 | if (idx >= start_index && idx < end_index) { 404 | pj_o (pj); 405 | pj_ks (pj, "name", p->name); 406 | pj_ks (pj, "description", p->description? p->description: ""); 407 | pj_k (pj, "arguments"); 408 | pj_a (pj); 409 | for (int i = 0; i < p->nargs; i++) { 410 | pj_o (pj); 411 | pj_ks (pj, "name", p->args[i].name); 412 | pj_ks (pj, "description", p->args[i].description? p->args[i].description: ""); 413 | pj_kb (pj, "required", p->args[i].required); 414 | pj_end (pj); 415 | } 416 | pj_end (pj); // end arguments array 417 | pj_end (pj); // end prompt object 418 | } 419 | idx++; 420 | } 421 | 422 | pj_end (pj); // end prompts array 423 | if (end_index < total) { 424 | char buf[32]; 425 | snprintf (buf, sizeof (buf), "%d", end_index); 426 | pj_ks (pj, "nextCursor", buf); 427 | } 428 | pj_end (pj); // end root object 429 | return pj_drain (pj); 430 | } 431 | 432 | char *prompts_get_json(const ServerState *ss, const char *name, RJson *arguments) { 433 | PromptSpec *spec = prompts_find (ss, name); 434 | if (!spec) { 435 | return NULL; 436 | } 437 | return spec->render (spec, arguments); 438 | } 439 | -------------------------------------------------------------------------------- /configure: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This script was automatically generated by ACR v2.2.4 3 | # @author: pancake 4 | # @url: http://www.nopcode.org 5 | # @repo: git clone https://github.com/radare/acr 6 | 7 | [ -z "${AWK}" ] && AWK=awk 8 | do_remove() { 9 | if [ "${ACR_RMFILES}" ]; then 10 | printf "cleaning temporally files... " 11 | rm -f ${ACR_RMFILES} 12 | echo "done" 13 | fi 14 | } 15 | control_c() { 16 | printf "\n\n^C control-c : script execution interrupted.\n" 17 | do_remove 18 | exit 1 19 | } 20 | trap control_c 2 21 | split_host() { 22 | S="$" 23 | while : ; do 24 | ENVWORDS="${ENVWORDS} $1_CPU $1_OS" 25 | STR=`eval "echo ${S}$1"` 26 | SPLIT_CPU="`echo "$STR" | cut -d - -f 1`" 27 | SPLIT_OS="`echo "$STR" | $AWK -F - '{ 28 | if ($2=="unknown"){ 29 | if (NF<3) { print $2; } else { print $3; } 30 | } else { 31 | if ($2=="linux") { print $2; } else 32 | if (NF<3) { print $2; } else { print $3; } 33 | } 34 | }'`" 35 | eval "$1_CPU=\"$SPLIT_CPU\"" 36 | eval "$1_OS=\"$SPLIT_OS\"" 37 | shift 38 | [ -z "$1" ] && break 39 | done 40 | } 41 | 42 | QUIET=0 43 | be_quiet() { 44 | QUIET=1 45 | } 46 | 47 | VPATH=`dirname ${0}` 48 | if [ "${VPATH}" = "." ]; then 49 | WODIS=current 50 | else 51 | if [ "${VPATH}" = "${PWD}" ]; then 52 | VPATH=. 53 | WODIS=current 54 | else 55 | WODIS=crosspath 56 | CURDIR=$PWD 57 | cd $VPATH 58 | VPATH="${PWD}/" 59 | cd $CURDIR 60 | fi 61 | fi 62 | 63 | guess_os() { 64 | if [ -e "${VPATH}/config.guess" ]; then 65 | sh ${VPATH}/config.guess 66 | return 67 | fi 68 | CPU="`uname -m | tr 'A-Z' 'a-z' | sed -e 's, ,,g' | cut -d - -f 1`" 69 | OS="`uname -s|tr A-Z a-z`" 70 | uname -r | grep -qE "(Microsoft|WSL)" 2>/dev/null && OS="wsl" 71 | GNU="`uname --help 2>&1 | grep gnu`" 72 | [ "${GNU}" ] && OS="${OS}-gnu" 73 | # normalize CPU 74 | case "${CPU}" in 75 | ppc*|powermac*|powermacintosh*|powerpc*) 76 | if echo "${CPU}" | grep -q '64'; then 77 | CPU=powerpc64 78 | else 79 | CPU=powerpc 80 | fi 81 | ;; 82 | *) ;; 83 | esac 84 | VENDOR="unknown" 85 | if [ "${OS}" = "darwin" ]; then 86 | VENDOR=apple 87 | fi 88 | echo "${CPU}-${VENDOR}-${OS}" 89 | } 90 | 91 | SEARCHPATH="/usr /usr/local /usr/pkg /sw" 92 | 93 | : ${PREFIX:=/usr/local} 94 | CROSSBUILD=0 95 | BUILD=`guess_os` 96 | HOST="${BUILD}" 97 | TARGET="${HOST}" 98 | ETCDIR="/etc" 99 | SYSCONFDIR="" 100 | DESCRIPTION="" 101 | 102 | create_environ() { 103 | : ${EPREFIX:="${PREFIX}"} 104 | : ${SPREFIX:="${PREFIX}"} 105 | : ${BINDIR:="${SPREFIX}/bin"} 106 | : ${SBINDIR:="${PREFIX}/sbin"} 107 | : ${ETCDIR:="${SPREFIX}/etc"} 108 | : ${LIBDIR:="${SPREFIX}/lib"} 109 | : ${PKGCFG_LIBDIR:='${exec_prefix}/lib'} 110 | : ${PKGCFG_INCDIR:='${prefix}/include'} 111 | : ${LIBEXECDIR:="${SPREFIX}/libexec"} 112 | : ${INCLUDEDIR:="${SPREFIX}/include"} 113 | : ${DATADIR:="${SPREFIX}/share"} 114 | : ${INFODIR:="${DATADIR}/info"} 115 | : ${MANDIR:="${DATADIR}/man"} 116 | : ${DOCDIR:="${DATADIR}/doc/r2mcp"} 117 | : ${LOCALSTATEDIR:="${SPREFIX}/var"} 118 | for A in `echo ${PATH} | sed -e 's,:, ,g'` ; do 119 | [ -e "$A"/ginstall ] && : ${INSTALL:="$A"/ginstall} && break 120 | [ -e "$A"/install ] && : ${INSTALL:="$A"/install} && break 121 | done 122 | : ${INSTALL_DIR:=${INSTALL} -d} 123 | : ${INSTALL_DATA:=${INSTALL} -m 644} 124 | : ${INSTALL_SCRIPT:=${INSTALL} -m 755} 125 | : ${INSTALL_PROGRAM:=${INSTALL} -m 755} 126 | : ${INSTALL_PROGRAM_STRIP:=${INSTALL} -m 755 -s} 127 | : ${INSTALL_MAN:=${INSTALL} -m 444} 128 | : ${INSTALL_LIB:=${INSTALL} -m 755 -c} 129 | PKGNAME='r2mcp' ; VERSION='1.4.2' ; VERSION_MAJOR=1; VERSION_MINOR=4; VERSION_PATCH=2; VERSION_NUMBER=10402; CONTACT_MAIL="pancake@nopcode.org" ; CONTACT_NAME="pancake" ; CONTACT="pancake " ; 130 | } 131 | 132 | show_usage() { 133 | cat < if you have libraries in a 181 | nonstandard directory 182 | CPPFLAGS C/C++ preprocessor flags, e.g. -I if you have 183 | headers in a nonstandard directory 184 | CPP C preprocessor 185 | " 186 | printf " 187 | Report bugs to: pancake " 188 | echo "" 189 | exit 0 190 | } 191 | 192 | take_environ() { 193 | : ${SH:=/bin/sh} 194 | : ${CPP:=cpp} 195 | : ${CC:=gcc} 196 | : ${PREFIX:=/usr/local/} 197 | } 198 | 199 | ochof() { 200 | [ "$QUIET" = 1 ] && return 201 | printf "$*" 202 | } 203 | 204 | ocho() { 205 | [ "$QUIET" = 1 ] && return 206 | echo "$*" 207 | } 208 | 209 | show_version() { 210 | if [ "$QUIET" = 1 ]; then 211 | echo "1.4.2" 212 | exit 0 213 | fi 214 | echo "r2mcp-1.4.2 configuration script done with acr v2.2.4. 215 | The 'Free Software Foundation' message is only for autodetection. 216 | Originally written by pancake ." 217 | exit 0 218 | } 219 | 220 | parse_options() { 221 | flag=`echo $1| cut -d = -f 1` 222 | value=`echo $1| $AWK 'BEGIN{FS="=";}{print $2}'` 223 | flag2=`echo $flag|cut -f2- -d -| sed -e 's,-,_,g' -e 's,^_,,g'|tr '[a-z]' '[A-Z]'` 224 | 225 | if [ "${TARGET_OS}" = "darwin" ]; then 226 | LIBPATH=-Wl,-install_name, 227 | else 228 | LIBPATH=-Wl,-R 229 | fi 230 | 231 | case $flag in 232 | -h|--help|--hel|--h|--he|-help) 233 | show_usage ; ;; 234 | -qV|-quiet-version|--quiet-version) 235 | be_quiet 236 | show_version ; ;; 237 | -q|-quiet|--quiet) 238 | be_quiet ; ;; 239 | -V|-version|--version) 240 | show_version ; ;; 241 | -r|--r|--report) 242 | echo "PKGNAME: r2mcp" 243 | echo "VERSION: 1.4.2" 244 | echo "AUTHOR: pancake" 245 | echo "EMAIL: pancake@nopcode.org" 246 | echo "LANGS: c" 247 | echo "PKGCONFIG: r_core" 248 | exit 0 249 | ;; 250 | --cache-file) 251 | # ignored: acr have no cache 252 | ;; 253 | --build) 254 | BUILD="$value"; split_host BUILD ; ;; 255 | --host) 256 | CROSSBUILD=1 # XXX 257 | HOST="$value"; split_host HOST ; ;; 258 | --target) 259 | TARGET="$value"; split_host TARGET ; ;; 260 | --prefix) 261 | PREFIX="$value"; ;; 262 | --exec-prefix) 263 | EPREFIX="$value"; ;; 264 | --sandbox|--sprefix) 265 | SPREFIX="$value"; ;; 266 | --bindir) 267 | BINDIR="$value"; ;; 268 | --sbindir) 269 | SBINDIR="$value"; ;; 270 | --libexecdir) 271 | LIBEXECDIR="$value"; ;; 272 | --docdir) 273 | DOCDIR="$value"; ;; 274 | --datadir) 275 | DATADIR="$value"; ;; 276 | --sysconfdir) 277 | SYSCONFDIR="$value" 278 | ETCDIR="$value"; ;; 279 | --etcdir) 280 | SYSCONFDIR="$value" 281 | ETCDIR="$value"; ;; 282 | --sharedstatedir) 283 | SHAREDSTATEDIR="$value"; ;; 284 | --localstatedir) 285 | LOCALSTATEDIR="$value"; ;; 286 | --libdir) 287 | LIBDIR="$value" 288 | PKGCFG_LIBDIR="$value"; ;; 289 | --libpath) 290 | LDFLAGS="${LDFLAGS} ${LIBPATH}$value"; ;; 291 | --includedir) 292 | PKGCFG_INCDIR="$value" 293 | INCLUDEDIR="$value"; CFLAGS="${CFLAGS} -I$value"; ;; 294 | --infodir) 295 | INFODIR="$value"; ;; 296 | --mandir) 297 | MANDIR="$value"; ;; 298 | 299 | *) if [ "$value" ]; then eval "`echo $flag2=$value`" ; 300 | else echo ; echo "WARNING: Unknown flag '$flag'." >&2 ; echo ; fi ;; 301 | esac 302 | } 303 | 304 | # MAIN # 305 | take_environ 306 | split_host BUILD HOST TARGET 307 | [ -z "$ACRHOOK" ] && ACRHOOK=./configure.hook 308 | [ -e "$ACRHOOK" ] && . ${ACRHOOK} 309 | 310 | while : ; do 311 | [ -z "$1" ] && break 312 | parse_options "$1" 313 | shift 314 | done 315 | 316 | ENVWORDS="MANDIR DESCRIPTION INFODIR LIBDIR INCLUDEDIR LOCALSTATEDIR ETCDIR SYSCONFDIR DATADIR DOCDIR LIBEXECDIR SBINDIR BINDIR EPREFIX PREFIX SPREFIX TARGET HOST BUILD INSTALL INSTALL_LIB INSTALL_MAN INSTALL_PROGRAM INSTALL_PROGRAM_STRIP INSTALL_DIR INSTALL_SCRIPT INSTALL_DATA HOST_OS HOST_CPU BUILD_OS BUILD_CPU TARGET_OS TARGET_CPU VERSION VERSION_MAJOR VERSION_MINOR VERSION_PATCH VERSION_NUMBER PKGCFG_LIBDIR PKGCFG_INCDIR PKGNAME VPATH CONTACT CONTACT_NAME CONTACT_MAIL CC CFLAGS CPPFLAGS LDFLAGS HAVE_LANG_C PKGCONFIG R2_CFLAGS R2_LDFLAGS HAVE_PKGCFG_R_CORE HAVE_R2 R2 R2_PREFIX R2_USER_PLUGINS R2_LIBR_PLUGINS R2MCP_VERSION" 317 | 318 | create_environ 319 | 320 | ocho "checking build system type... ${BUILD}" 321 | ocho "checking host system type... ${HOST}" 322 | ocho "checking target system type... ${TARGET}" 323 | [ "${CROSSBUILD}" = 1 ] && echo "using crosscompilation mode." 324 | 325 | #split_host BUILD HOST TARGET 326 | [ -n "${prefix}" ] && PREFIX="${prefix}" 327 | ocho "checking for working directories... ${WODIS}" 328 | ocho "using prefix '${PREFIX}'" 329 | ACR_RMFILES=" test.c a.out a.exe" 330 | 331 | COMPILER=CC 332 | ochof "checking for c compiler... " 333 | HAVE_LANG_C=1 334 | if [ "${CROSSBUILD}" = 1 ]; then 335 | (command -v ${HOST}-${CC} >/dev/null 2>&1) 336 | if [ $? = 0 ]; then CC="${HOST}-${CC}"; fi 337 | fi 338 | echo "int main(int argc, char **argv){return 0;}" > test.c 339 | (exec ${CC} -o a.out ${CFLAGS} ${CPPFLAGS} ${LDFLAGS} test.c >/dev/null 2>&1) 340 | if [ $? = 0 ]; then echo ${CC}; else 341 | echo no ; HAVE_LANG_C=0 342 | do_remove 343 | echo "ERROR: ${CC} cannot create executables" >&2 ; 344 | exit 1 ; fi 345 | ochof "checking for pkg-config... " 346 | ENVWORDS="${ENVWORDS} HAVE_PKGCONFIG" 347 | if [ -n "$PKGCONFIG" ]; then 348 | if [ -x "$PKGCONFIG" ]; then 349 | _HAVE_PKGCONFIG="${PKGCONFIG}" 350 | else 351 | ocho "PKGCONFIG env is not executable" 352 | PKGCONFIG="" 353 | fi 354 | else 355 | IFS=: 356 | for A in ${PATH} ; do 357 | if [ -x "${A}/pkg-config" ]; then 358 | PKGCONFIG="${A}/pkg-config" 359 | elif [ -x "${A}/pkgconf" ]; then 360 | PKGCONFIG="${A}/pkgconf" 361 | else 362 | continue 363 | fi 364 | break 365 | done 366 | unset IFS 367 | fi 368 | if [ -n "${PKGCONFIG}" ]; then 369 | ocho ${PKGCONFIG} 370 | HAVE_PKGCONFIG=1 371 | else 372 | HAVE_PKGCONFIG=0 373 | echo "Cannot find pkg-config or pkgconf in PATH" >&2 374 | exit 1 375 | ocho no; fi 376 | if [ -z "${PKGCONFIG}" ]; then pkg-config --version >/dev/null 2>&1 ; if [ 0 = 0 ]; then PKGCONFIG=pkg-config ; else PKGCONFIG=pkgconf ; fi; fi 377 | type ${PKGCONFIG} > /dev/null 2>&1 || echo "ERROR: Cannot find valid PKGCONFIG, pkg-config or pkgconf in PATH" 378 | echo 'Using PKGCONFIG: '${PKGCONFIG} 379 | printf 'checking pkg-config flags for r_core... ' 380 | tmp=`${PKGCONFIG} --cflags r_core 2>/dev/null` 381 | if [ $? != 0 ]; then echo no ; HAVE_PKGCFG_R_CORE=0; 382 | R2_CFLAGS='';R2_LDFLAGS=''; 383 | echo 'This package is required' 384 | exit 1 385 | else 386 | R2_CFLAGS=$tmp; 387 | tmp=`${PKGCONFIG} --libs r_core 2>/dev/null` 388 | if [ $? = 0 ]; then 389 | echo yes; HAVE_PKGCFG_R_CORE=1; 390 | R2_LDFLAGS=$tmp; fi; fi 391 | ochof "checking for r2... " 392 | if [ -x "${R2}" ]; then 393 | FIND=${R2} 394 | else 395 | FIND="" 396 | IFS=: 397 | for A in ${PATH} ; do 398 | if [ -x "${A}/r2" ]; then 399 | FIND="${A}/r2" 400 | break; 401 | fi 402 | done 403 | unset IFS 404 | fi 405 | if [ -n "${FIND}" ]; then 406 | ocho ${FIND}; 407 | HAVE_R2=1 408 | R2=${FIND} 409 | else 410 | HAVE_R2=0 411 | R2=r2 412 | if [ "" = 1 ]; then 413 | echo "Cannot find r2" >&2 414 | else 415 | echo no 416 | fi 417 | echo "error: This program is required." >&2 418 | exit 1 ; fi 419 | printf "checking exec r2 -H R2_PREFIX... " 420 | R2_PREFIX="`r2 -H R2_PREFIX 2>/dev/null`" 421 | if [ $? = 0 ]; then 422 | echo "ok" 423 | else 424 | R2_PREFIX="" 425 | echo "not found" 426 | echo 'required'; exit 1 427 | fi 428 | printf "checking exec r2 -H R2_USER_PLUGINS... " 429 | R2_USER_PLUGINS="`r2 -H R2_USER_PLUGINS 2>/dev/null`" 430 | if [ $? = 0 ]; then 431 | echo "ok" 432 | else 433 | R2_USER_PLUGINS="" 434 | echo "not found" 435 | echo 'required'; exit 1 436 | fi 437 | printf "checking exec r2 -H R2_LIBR_PLUGINS... " 438 | R2_LIBR_PLUGINS="`r2 -H R2_LIBR_PLUGINS 2>/dev/null`" 439 | if [ $? = 0 ]; then 440 | echo "ok" 441 | else 442 | R2_LIBR_PLUGINS="" 443 | echo "not found" 444 | echo 'required'; exit 1 445 | fi 446 | R2MCP_VERSION="$VERSION" 447 | SEDFLAGS=" -e '" 448 | COUNT=0 449 | for A in ${ENVWORDS} ; do 450 | [ "${A}" = VPATH ] && continue 451 | [ "${A}" = srcdir ] && continue 452 | eval "VAR=\$${A}" 453 | case "" in 454 | mingw*|cygwin*|msys*) 455 | VAR="`echo ${VAR} | sed -e 's/\\/\\\\/g' -e 's/\,/\\\,/g'`" 456 | ;; 457 | *) 458 | VAR="`echo ${VAR} | sed -e 's/\,/\\\,/g'`" 459 | ;; 460 | esac 461 | [ $COUNT = 10 ] && COUNT=0 && SEDFLAGS="${SEDFLAGS}' -e '" 462 | COUNT=$(($COUNT+1)) 463 | SEDFLAGS="${SEDFLAGS}s,@${A}@,${VAR},g;" 464 | done 465 | SEDFLAGS="${SEDFLAGS}'" 466 | for A in src/config.h ; do # SUBDIRS 467 | if [ -f "${VPATH}/${A}.acr" ]; then 468 | SD_TARGET=${A} 469 | else 470 | if [ -d "${VPATH}/${A}" ]; then 471 | SD_TARGET=${A}/Makefile 472 | mkdir -p ${A} 473 | else 474 | echo "ERROR: Cannot find ${VPATH}/${A}.acr" >&2 475 | exit 1 476 | fi 477 | fi 478 | ocho "creating ${SD_TARGET}" 479 | [ "${VPATH}" != '.' ] && mkdir -p $(echo ${A} | sed -e "s,/`basename ${A}`$,,g") 480 | cat ${VPATH}/${SD_TARGET}.acr | \ 481 | eval sed -e "s,@VPATH@,${VPATH}/${A},g" ${SEDFLAGS} > ${SD_TARGET}.tmp 482 | 483 | for A in ${ENVWORDS}; do 484 | VALUE=`eval echo "$"${A}` 485 | if [ "$VALUE" = 0 ]; then ## FALSE 486 | MARK="##${A}##" 487 | if [ -n "`grep \"${MARK}\" ${SD_TARGET}.tmp`" ]; then 488 | mv ${SD_TARGET}.tmp ${SD_TARGET}.tmp2 489 | cat ${SD_TARGET}.tmp2 | MARK=$MARK $AWK 'BEGIN{a=0;}{if($1==ENVIRON["MARK"]){if(a)a=0;else a=1}else{if(!a)print;}}' > ${SD_TARGET}.tmp 490 | fi 491 | fi 492 | done 493 | mv ${SD_TARGET}.tmp ${SD_TARGET} && rm -f ${SD_TARGET}.tmp2 494 | if [ ! $? = 0 ]; then echo Cannot write target file ; control_c ; fi 495 | done 496 | 497 | do_remove 498 | if [ "$QUIET" = 0 ]; then 499 | echo 500 | echo "Final report:" 501 | for A in VERSION R2_PREFIX PREFIX PKGCONFIG ; do 502 | eval VAL="\$${A}" 503 | [ -z "${VAL}" ] && VAL="\"\"" 504 | echo " - ${A} = ${VAL}" 505 | done 506 | fi 507 | -------------------------------------------------------------------------------- /src/r2mcp.c: -------------------------------------------------------------------------------- 1 | /* r2mcp - MIT - Copyright 2025 - pancake, dnakov */ 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "config.h" 14 | #include "jsonrpc.h" 15 | 16 | #if defined(R2__UNIX__) 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #elif defined(R2__WINDOWS__) 23 | #include 24 | #include 25 | #include 26 | #include 27 | #else 28 | #error please define R2__WINDOWS__ or R2__UNIX__ for platform detection 29 | #endif 30 | #include "r2mcp.h" 31 | #include "tools.h" 32 | #include "prompts.h" 33 | 34 | #define R2MCP_DEBUG 1 35 | 36 | #ifndef R2MCP_VERSION 37 | #warning R2MCP_VERSION is not defined 38 | #define R2MCP_VERSION "1.5.2" 39 | #endif 40 | 41 | #define JSON_RPC_VERSION "2.0" 42 | #define MCP_VERSION "2024-11-05" 43 | #define READ_CHUNK_SIZE 32768 44 | #define LATEST_PROTOCOL_VERSION "2024-11-05" 45 | 46 | #include "utils.inc.c" 47 | #include "r2api.inc.c" 48 | 49 | static volatile sig_atomic_t running = 1; 50 | void r2mcp_running_set(int value) { 51 | running = value? 1: 0; 52 | } 53 | 54 | // Local I/O mode helper (moved from utils.inc.c to avoid unused warnings in other TUs) 55 | static void set_nonblocking_io(bool nonblocking) { 56 | #if defined(R2__UNIX__) 57 | int flags = fcntl (STDIN_FILENO, F_GETFL, 0); 58 | if (nonblocking) { 59 | fcntl (STDIN_FILENO, F_SETFL, flags | O_NONBLOCK); 60 | } else { 61 | fcntl (STDIN_FILENO, F_SETFL, flags & ~O_NONBLOCK); 62 | } 63 | setvbuf (stdout, NULL, _IOLBF, 0); 64 | #elif defined(R2__WINDOWS__) 65 | // Windows doesn't support POSIX fcntl/O_NONBLOCK on stdin reliably. 66 | // We set stdin mode to binary/text depending on requested mode and 67 | // keep line-buffered stdout. This is a best-effort no-op for nonblocking. 68 | if (nonblocking) { 69 | _setmode (_fileno (stdin), _O_BINARY); 70 | } else { 71 | _setmode (_fileno (stdin), _O_TEXT); 72 | } 73 | setvbuf (stdout, NULL, _IOLBF, 0); 74 | #else 75 | (void)nonblocking; 76 | #endif 77 | } 78 | 79 | /* Public wrappers to expose internal static helpers from r2api.inc.c */ 80 | bool r2mcp_state_init(ServerState *ss) { 81 | RCore *core = r_core_new (); 82 | if (!core) { 83 | R_LOG_ERROR ("Failed to initialize radare2 core"); 84 | return false; 85 | } 86 | 87 | r2state_settings (core); 88 | ss->rstate.core = core; 89 | 90 | R_LOG_INFO ("Radare2 core initialized"); 91 | r_log_add_callback (logcb, ss); 92 | return true; 93 | } 94 | 95 | void r2mcp_state_fini(ServerState *ss) { 96 | RCore *core = ss->rstate.core; 97 | if (core) { 98 | r_core_free (core); 99 | r_strbuf_free (ss->sb); 100 | ss->sb = NULL; 101 | ss->rstate.core = NULL; 102 | ss->rstate.file_opened = false; 103 | ss->rstate.current_file = NULL; 104 | } 105 | } 106 | 107 | char *r2mcp_cmd(ServerState *ss, const char *cmd) { 108 | if (ss && ss->http_mode) { 109 | char *res = r2cmd_over_http (ss, cmd); 110 | if (!res) { 111 | return strdup ("HTTP request failed"); 112 | } 113 | return res; 114 | } 115 | RCore *core = ss->rstate.core; 116 | if (!core || !ss->rstate.file_opened) { 117 | return strdup ("Use the open_file method before calling any other method"); 118 | } 119 | bool changed = false; 120 | char *filteredCommand = r2_cmd_filter (cmd, &changed); 121 | if (changed) { 122 | r2mcp_log (ss, "command injection prevented"); 123 | } 124 | r2mcp_log_reset (ss); 125 | char *res = r_core_cmd_str (core, filteredCommand); 126 | char *err = r2mcp_log_drain (ss); 127 | free (filteredCommand); 128 | // r2state_settings (core); 129 | if (err) { 130 | char *newres = r_str_newf ("%s\n%s\n\n", res, err); 131 | free (res); 132 | res = newres; 133 | } 134 | return res; 135 | } 136 | 137 | void r2mcp_log_pub(ServerState *ss, const char *msg) { 138 | r2mcp_log (ss, msg); 139 | } 140 | 141 | // New wrappers to expose functionality from r2api.inc.c to other modules 142 | bool r2mcp_open_file(ServerState *ss, const char *filepath) { 143 | return r2_open_file (ss, filepath); 144 | } 145 | char *r2mcp_analyze(ServerState *ss, int level) { 146 | return r2_analyze (ss, level); 147 | } 148 | 149 | static bool check_client_capability(ServerState *ss, const char *capability) { 150 | if (ss->client_capabilities) { 151 | RJson *cap = (RJson *)r_json_get (ss->client_capabilities, capability); 152 | return cap != NULL; 153 | } 154 | return false; 155 | } 156 | 157 | static bool check_server_capability(ServerState *ss, const char *capability) { 158 | if (!strcmp (capability, "tools")) { 159 | return ss->capabilities.tools; 160 | } 161 | if (!strcmp (capability, "prompts")) { 162 | return ss->capabilities.prompts; 163 | } 164 | if (!strcmp (capability, "resources")) { 165 | return ss->capabilities.resources; 166 | } 167 | return false; 168 | } 169 | 170 | static bool assert_capability_for_method(ServerState *ss, const char *method, char **error) { 171 | if (!strcmp (method, "sampling/createMessage")) { 172 | if (!check_client_capability (ss, "sampling")) { 173 | *error = strdup ("Client does not support sampling"); 174 | return false; 175 | } 176 | } else if (!strcmp (method, "roots/list")) { 177 | if (!check_client_capability (ss, "roots")) { 178 | *error = strdup ("Client does not support listing roots"); 179 | return false; 180 | } 181 | } 182 | return true; 183 | } 184 | 185 | static bool assert_request_handler_capability(ServerState *ss, const char *method, char **error) { 186 | if (!strcmp (method, "sampling/createMessage")) { 187 | if (!check_server_capability (ss, "sampling")) { 188 | *error = strdup ("Server does not support sampling"); 189 | return false; 190 | } 191 | } else if (r_str_startswith (method, "prompts/")) { 192 | if (!check_server_capability (ss, "prompts")) { 193 | *error = strdup ("Server does not support prompts"); 194 | return false; 195 | } 196 | } else if (r_str_startswith (method, "tools/")) { 197 | if (!check_server_capability (ss, "tools")) { 198 | *error = strdup ("Server does not support tools"); 199 | return false; 200 | } 201 | } else if (r_str_startswith (method, "resources/")) { 202 | if (!check_server_capability (ss, "resources")) { 203 | *error = strdup ("Server does not support resources"); 204 | return false; 205 | } 206 | } 207 | return true; 208 | } 209 | 210 | // Helper function to create JSON-RPC error responses 211 | // (moved to utils.inc.c earlier, keep this for compatibility if needed) 212 | // static char *jsonrpc_error_response (int code, const char *message, const char *id, const char *uri) { ... } 213 | 214 | static char *handle_initialize(ServerState *ss, RJson *params) { 215 | ss->client_capabilities = r_json_get (params, "capabilities"); 216 | ss->client_info = r_json_get (params, "clientInfo"); 217 | 218 | // Create a proper initialize response 219 | PJ *pj = pj_new (); 220 | pj_o (pj); 221 | pj_ks (pj, "protocolVersion", LATEST_PROTOCOL_VERSION); 222 | 223 | pj_k (pj, "serverInfo"); 224 | pj_o (pj); 225 | pj_ks (pj, "name", ss->info.name); 226 | pj_ks (pj, "version", ss->info.version); 227 | pj_end (pj); 228 | 229 | // Capabilities need to be objects with specific structure, not booleans 230 | pj_k (pj, "capabilities"); 231 | pj_o (pj); 232 | 233 | // Tools capability - needs to be an object 234 | pj_k (pj, "tools"); 235 | pj_o (pj); 236 | pj_kb (pj, "listChanged", false); 237 | pj_end (pj); 238 | 239 | // Prompts capability - object with listChanged 240 | pj_k (pj, "prompts"); 241 | pj_o (pj); 242 | pj_kb (pj, "listChanged", false); 243 | pj_end (pj); 244 | 245 | // Resources capability - empty object since we only support list 246 | pj_k (pj, "resources"); 247 | pj_o (pj); 248 | pj_end (pj); 249 | 250 | // For any capability we don't support, don't include it at all 251 | // Don't add: roots, notifications, sampling 252 | 253 | pj_end (pj); // End capabilities 254 | 255 | if (ss->instructions) { 256 | pj_ks (pj, "instructions", ss->instructions); 257 | } 258 | 259 | pj_end (pj); 260 | 261 | ss->initialized = true; 262 | return pj_drain (pj); 263 | } 264 | 265 | static char *handle_list_tools(ServerState *ss, RJson *params) { 266 | const char *cursor = r_json_get_str (params, "cursor"); 267 | int page_size = 32; 268 | return tools_build_catalog_json (ss, cursor, page_size); 269 | } 270 | 271 | static char *handle_list_prompts(ServerState *ss, RJson *params) { 272 | const char *cursor = r_json_get_str (params, "cursor"); 273 | int page_size = 32; 274 | return prompts_build_list_json (ss, cursor, page_size); 275 | } 276 | 277 | static char *handle_get_prompt(ServerState *ss, RJson *params) { 278 | const char *name = r_json_get_str (params, "name"); 279 | if (!name) { 280 | return jsonrpc_error_response (-32602, "Missing required parameter: name", NULL, NULL); 281 | } 282 | RJson *args = (RJson *)r_json_get (params, "arguments"); 283 | char *prompt = prompts_get_json (ss, name, args); 284 | if (!prompt) { 285 | return jsonrpc_error_response (-32602, "Unknown prompt name", NULL, NULL); 286 | } 287 | return prompt; 288 | } 289 | 290 | // Thin wrapper that delegates to the tools module. This keeps r2mcp.c small 291 | // and moves the tool-specific logic into tools.c where it belongs. 292 | static char *handle_call_tool(ServerState *ss, const char *tool_name, RJson *tool_args) { 293 | return tools_call (ss, tool_name, tool_args); 294 | } 295 | 296 | static char *handle_mcp_request(ServerState *ss, const char *method, RJson *params, const char *id) { 297 | char *error = NULL; 298 | char *result = NULL; 299 | 300 | if (!assert_capability_for_method (ss, method, &error) || !assert_request_handler_capability (ss, method, &error)) { 301 | char *response = jsonrpc_error_response (-32601, error, id, NULL); 302 | free (error); 303 | return response; 304 | } 305 | 306 | if (!strcmp (method, "initialize")) { 307 | result = handle_initialize (ss, params); 308 | } else if (!strcmp (method, "notifications/initialized")) { 309 | return NULL; // No response for notifications 310 | } else if (!strcmp (method, "ping")) { 311 | result = strdup ("{}"); 312 | 313 | } else if (!strcmp (method, "tools/list") || !strcmp (method, "tool/list")) { 314 | result = handle_list_tools (ss, params); 315 | } else if (!strcmp (method, "tools/call") || !strcmp (method, "tool/call")) { 316 | const char *tool_name = r_json_get_str (params, "name"); 317 | if (!tool_name) { 318 | tool_name = r_json_get_str (params, "tool"); 319 | } 320 | RJson *tool_args = (RJson *)r_json_get (params, "arguments"); 321 | if (!tool_args) { 322 | tool_args = (RJson *)r_json_get (params, "args"); 323 | } 324 | if (ss->svc_baseurl) { 325 | PJ *pj = pj_new (); 326 | pj_o (pj); 327 | pj_ks (pj, "tool", tool_name); 328 | pj_k (pj, "arguments"); 329 | pj_append_rjson (pj, tool_args); 330 | pj_k (pj, "available_tools"); 331 | pj_a (pj); 332 | for (size_t i = 0; tool_specs[i].name; i++) { 333 | pj_s (pj, tool_specs[i].name); 334 | } 335 | pj_end (pj); 336 | pj_end (pj); 337 | char *req = pj_drain (pj); 338 | int rc; 339 | char *resp = curl_post_capture (ss->svc_baseurl, req, &rc); 340 | free (req); 341 | if (resp && rc == 0) { 342 | RJson *rj = r_json_parse (resp); 343 | free (resp); 344 | if (rj) { 345 | const char *err = r_json_get_str (rj, "error"); 346 | if (err) { 347 | r_json_free (rj); 348 | return jsonrpc_error_response (-32000, err, id, NULL); 349 | } 350 | const char *r2cmd = r_json_get_str (rj, "r2cmd"); 351 | if (r2cmd) { 352 | char *cmd_out = r2mcp_cmd (ss, r2cmd); 353 | char *res = jsonrpc_tooltext_response (cmd_out? cmd_out: ""); 354 | free (cmd_out); 355 | r_json_free (rj); 356 | result = res; 357 | } else { 358 | const char *new_tool = r_json_get_str (rj, "tool"); 359 | const RJson *new_args = r_json_get (rj, "arguments"); 360 | if (new_tool && strcmp (new_tool, tool_name)) { 361 | tool_name = new_tool; 362 | } 363 | if (new_args) { 364 | tool_args = (RJson *)new_args; 365 | } 366 | result = handle_call_tool (ss, tool_name, tool_args); 367 | r_json_free (rj); 368 | } 369 | } else { 370 | result = handle_call_tool (ss, tool_name, tool_args); 371 | } 372 | } else { 373 | result = handle_call_tool (ss, tool_name, tool_args); 374 | } 375 | } else { 376 | result = handle_call_tool (ss, tool_name, tool_args); 377 | } 378 | } else if (!strcmp (method, "prompts/list") || !strcmp (method, "prompt/list")) { 379 | result = handle_list_prompts (ss, params); 380 | } else if (!strcmp (method, "prompts/get") || !strcmp (method, "prompt/get")) { 381 | result = handle_get_prompt (ss, params); 382 | } else if (!strcmp (method, "resources/list")) { 383 | result = strdup ("{\"resources\":[]}"); 384 | } else if (!strcmp (method, "resources/templates/list")) { 385 | result = strdup ("{\"resourceTemplates\":[]}"); 386 | } else { 387 | return jsonrpc_error_response (-32601, "Unknown method", id, NULL); 388 | } 389 | 390 | char *response = jsonrpc_success_response (ss, result, id); 391 | free (result); 392 | return response; 393 | } 394 | 395 | // Send a JSON-RPC response to stdout with proper framing 396 | static void send_response(ServerState *ss, const char *response) { 397 | if (!response) { 398 | return; 399 | } 400 | r2mcp_log (ss, ">>>"); 401 | r2mcp_log (ss, response); 402 | size_t len = strlen (response); 403 | (void) write (STDOUT_FILENO, response, len); 404 | if (len == 0 || response[len - 1] != '\n') { 405 | (void) write (STDOUT_FILENO, "\n", 1); 406 | } 407 | #if R2__UNIX__ 408 | fsync (STDOUT_FILENO); 409 | #endif 410 | } 411 | 412 | // Process a JSON-RPC message from the client 413 | static void process_mcp_message(ServerState *ss, const char *msg) { 414 | r2mcp_log (ss, "<<<"); 415 | r2mcp_log (ss, msg); 416 | 417 | RJson *request = r_json_parse ((char *)msg); 418 | if (!request) { 419 | char *err = jsonrpc_error_response (-32700, "Parse error: invalid JSON", NULL, NULL); 420 | send_response (ss, err); 421 | free (err); 422 | return; 423 | } 424 | 425 | RJson *id_json = (RJson *)r_json_get (request, "id"); 426 | const char *method = r_json_get_str (request, "method"); 427 | RJson *params = (RJson *)r_json_get (request, "params"); 428 | 429 | // Extract id for error responses (may be NULL for notifications) 430 | const char *id = NULL; 431 | char id_buf[32] = { 0 }; 432 | if (id_json) { 433 | if (id_json->type == R_JSON_STRING) { 434 | id = id_json->str_value; 435 | } else if (id_json->type == R_JSON_INTEGER) { 436 | snprintf (id_buf, sizeof (id_buf), "%lld", (long long)id_json->num.u_value); 437 | id = id_buf; 438 | } 439 | } 440 | 441 | if (!method) { 442 | // Per JSON-RPC 2.0, missing method is an invalid request 443 | char *err = jsonrpc_error_response (-32600, "Invalid Request: missing method", id, NULL); 444 | send_response (ss, err); 445 | free (err); 446 | r_json_free (request); 447 | return; 448 | } 449 | 450 | if (id_json) { 451 | // Request: requires a response 452 | char *response = handle_mcp_request (ss, method, params, id); 453 | if (response) { 454 | send_response (ss, response); 455 | free (response); 456 | } 457 | } else { 458 | // Notification: no response 459 | if (!strcmp (method, "notifications/cancelled")) { 460 | r2mcp_log (ss, "Received cancelled notification"); 461 | } else if (!strcmp (method, "notifications/initialized")) { 462 | r2mcp_log (ss, "Received initialized notification"); 463 | } else { 464 | r2mcp_log (ss, "Received unknown notification"); 465 | } 466 | } 467 | 468 | r_json_free (request); 469 | } 470 | 471 | // MCPO protocol-compliant direct mode loop 472 | void r2mcp_eventloop(ServerState *ss) { 473 | r2mcp_log (ss, "Starting MCP direct mode (stdin/stdout)"); 474 | 475 | // Use consistent unbuffered mode for stdout 476 | setvbuf (stdout, NULL, _IONBF, 0); 477 | 478 | // Set to blocking I/O for simplicity 479 | set_nonblocking_io (false); 480 | 481 | ReadBuffer *buffer = read_buffer_new (); 482 | char chunk[READ_CHUNK_SIZE]; 483 | 484 | while (running) { 485 | // Read data from stdin 486 | ssize_t bytes_read = read (STDIN_FILENO, chunk, sizeof (chunk) - 1); 487 | 488 | if (bytes_read > 0) { 489 | // Append to our buffer 490 | read_buffer_append (buffer, chunk, bytes_read); 491 | 492 | // Try to process any complete messages 493 | char *msg; 494 | while ((msg = read_buffer_get_message (buffer)) != NULL) { 495 | r2mcp_log (ss, "Complete message received:"); 496 | r2mcp_log (ss, msg); 497 | process_mcp_message (ss, msg); 498 | free (msg); 499 | } 500 | } else if (bytes_read == 0) { 501 | // EOF - stdin closed 502 | r2mcp_log (ss, "End of input stream - exiting"); 503 | break; 504 | } else { 505 | // Error 506 | if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) { 507 | r2mcp_log (ss, "Read error"); 508 | break; 509 | } 510 | } 511 | } 512 | 513 | read_buffer_free (buffer); 514 | r2mcp_log (ss, "Direct mode loop terminated"); 515 | } 516 | -------------------------------------------------------------------------------- /src/tools.c: -------------------------------------------------------------------------------- 1 | /* r2mcp - MIT - Copyright 2025 - pancake */ 2 | 3 | #include 4 | #include "r2mcp.h" 5 | #include "tools.h" 6 | #include 7 | #include "utils.inc.c" 8 | #include "jsonrpc.h" 9 | 10 | typedef char *(*ToolFunc)(ServerState *ss, RJson *tool_args); 11 | 12 | typedef struct { 13 | const char *name; 14 | ToolFunc func; 15 | } ToolEntry; 16 | 17 | extern ToolSpec tool_specs[]; 18 | 19 | // Parameter validation helpers 20 | static inline bool validate_required_string_param(RJson *args, const char *param_name, const char **out_value) { 21 | const char *value = r_json_get_str (args, param_name); 22 | if (value) { 23 | *out_value = value; 24 | return true; 25 | } 26 | return false; 27 | } 28 | 29 | static bool validate_address_param(RJson *args, const char *param_name, const char **out_address) { 30 | return validate_required_string_param (args, param_name, out_address); 31 | } 32 | 33 | // Helper to wrap command result in JSON response and free the result 34 | static char *tool_cmd_response(char *res) { 35 | char *response = jsonrpc_tooltext_response (res); 36 | free (res); 37 | return response; 38 | } 39 | 40 | // Check an optional whitelist of enabled tool names. If ss->enabled_tools is 41 | // NULL, all tools are considered allowed. Otherwise only names present in the 42 | // list are allowed. 43 | static bool tool_allowed_by_whitelist(const ServerState *ss, const char *name) { 44 | if (!ss || !ss->enabled_tools) { 45 | return true; 46 | } 47 | RListIter *it; 48 | const char *s; 49 | r_list_foreach (ss->enabled_tools, it, s) { 50 | if (!strcmp (s, name)) { 51 | return true; 52 | } 53 | } 54 | return false; 55 | } 56 | 57 | static inline ToolMode current_mode(const ServerState *ss) { 58 | if (ss->http_mode) { 59 | return TOOL_MODE_HTTP; 60 | } 61 | if (ss->readonly_mode) { 62 | return TOOL_MODE_RO; 63 | } 64 | if (ss->minimode) { 65 | return TOOL_MODE_MINI; 66 | } 67 | return TOOL_MODE_NORMAL; 68 | } 69 | 70 | static bool tool_matches_mode(const ToolSpec *t, ToolMode mode) { 71 | return (t->modes & mode) != 0; 72 | } 73 | 74 | static RList *tools_filtered_for_mode(const ServerState *ss) { 75 | ToolMode mode = current_mode (ss); 76 | RList *out = r_list_new (); 77 | if (!out) { 78 | return NULL; 79 | } 80 | for (size_t i = 0; tool_specs[i].name; i++) { 81 | ToolSpec *t = &tool_specs[i]; 82 | if (tool_matches_mode (t, mode) && tool_allowed_by_whitelist (ss, t->name)) { 83 | r_list_append (out, t); // reference only 84 | } 85 | } 86 | return out; 87 | } 88 | 89 | bool tools_is_tool_allowed(const ServerState *ss, const char *name) { 90 | if (ss->permissive_tools) { 91 | return true; 92 | } 93 | if (!name) { 94 | return false; 95 | } 96 | ToolMode mode = current_mode (ss); 97 | for (size_t i = 0; tool_specs[i].name; i++) { 98 | ToolSpec *t = &tool_specs[i]; 99 | if (!strcmp (t->name, name)) { 100 | if (!tool_allowed_by_whitelist (ss, name)) { 101 | return false; 102 | } 103 | return tool_matches_mode (t, mode); 104 | } 105 | } 106 | return false; 107 | } 108 | 109 | char *tools_build_catalog_json(const ServerState *ss, const char *cursor, int page_size) { 110 | 111 | int start_index = 0; 112 | if (cursor) { 113 | start_index = atoi (cursor); 114 | if (start_index < 0) { 115 | start_index = 0; 116 | } 117 | } 118 | 119 | RList *list = tools_filtered_for_mode (ss); 120 | if (!list) { 121 | return strdup ("{\"tools\":[]}"); 122 | } 123 | int total_tools = r_list_length (list); 124 | int end_index = start_index + page_size; 125 | if (end_index > total_tools) { 126 | end_index = total_tools; 127 | } 128 | 129 | RStrBuf *sb = r_strbuf_new (""); 130 | r_strbuf_append (sb, "{\"tools\":["); 131 | 132 | int idx = 0; 133 | int out_count = 0; 134 | RListIter *it; 135 | ToolSpec *t; 136 | r_list_foreach (list, it, t) { 137 | if (idx >= start_index && idx < end_index) { 138 | if (out_count > 0) { 139 | r_strbuf_append (sb, ","); 140 | } 141 | r_strbuf_appendf (sb, 142 | "{\"name\":\"%s\",\"description\":\"%s\",\"inputSchema\":%s}", 143 | t->name, t->description, t->schema_json); 144 | out_count++; 145 | } 146 | idx++; 147 | if (idx >= end_index) { 148 | // keep looping for correctness of idx but we could break 149 | } 150 | } 151 | 152 | r_strbuf_append (sb, "]"); 153 | if (end_index < total_tools) { 154 | r_strbuf_appendf (sb, ",\"nextCursor\":\"%d\"", end_index); 155 | } 156 | r_strbuf_append (sb, "}"); 157 | 158 | r_list_free (list); 159 | return r_strbuf_drain (sb); 160 | } 161 | 162 | void tools_print_table(const ServerState *ss) { 163 | RTable *table = r_table_new ("tools"); 164 | if (!table) { 165 | R_LOG_ERROR ("Failed to allocate table"); 166 | return; 167 | } 168 | 169 | RTableColumnType *s = r_table_type ("string"); 170 | if (!s) { 171 | R_LOG_WARN ("Table string type unavailable"); 172 | r_table_free (table); 173 | return; 174 | } 175 | 176 | r_table_add_column (table, s, "name", 0); 177 | r_table_add_column (table, s, "modes", 0); 178 | r_table_add_column (table, s, "description", 0); 179 | 180 | for (size_t i = 0; tool_specs[i].name; i++) { 181 | ToolSpec *t = &tool_specs[i]; 182 | if (!tool_allowed_by_whitelist (ss, t->name)) { 183 | continue; 184 | } 185 | char modes_buf[8]; 186 | int p = 0; 187 | if (t->modes & TOOL_MODE_MINI) { 188 | modes_buf[p++] = 'M'; 189 | } 190 | if (t->modes & TOOL_MODE_HTTP) { 191 | modes_buf[p++] = 'H'; 192 | } 193 | if (t->modes & TOOL_MODE_RO) { 194 | modes_buf[p++] = 'R'; 195 | } 196 | if (t->modes & TOOL_MODE_NORMAL) { 197 | modes_buf[p++] = 'N'; 198 | } 199 | modes_buf[p] = '\0'; 200 | const char *desc = t->description? t->description: ""; 201 | r_table_add_rowf (table, "sss", t->name, modes_buf, desc); 202 | } 203 | 204 | char *table_str = r_table_tostring (table); 205 | if (table_str) { 206 | printf ("%s\n", table_str); 207 | free (table_str); 208 | } 209 | r_table_free (table); 210 | } 211 | 212 | // Filter lines in `input` by `pattern` regex. Returns a newly allocated string. 213 | static char *filter_lines_by_regex(const char *input, const char *pattern) { 214 | const char *src = input? input: ""; 215 | if (!pattern || !*pattern) { 216 | return strdup (src); 217 | } 218 | RStrBuf *sb = r_strbuf_new (""); 219 | RRegex rx; 220 | int re_flags = r_regex_flags ("e"); 221 | if (r_regex_init (&rx, pattern, re_flags) != 0) { 222 | r_strbuf_appendf (sb, "Invalid regex used in filter parameter, try a simpler expression"); 223 | return r_strbuf_drain (sb); 224 | } 225 | const char *line_begin = src; 226 | const char *p = src; 227 | for (;;) { 228 | if (*p == '\n' || *p == '\0') { 229 | size_t len = (size_t) (p - line_begin); 230 | char *line = (char *)malloc (len + 1); 231 | if (!line) { 232 | break; 233 | } 234 | memcpy (line, line_begin, len); 235 | line[len] = '\0'; 236 | if (r_regex_exec (&rx, line, 0, 0, 0) == 0) { 237 | r_strbuf_appendf (sb, "%s\n", line); 238 | } 239 | free (line); 240 | if (*p == '\0') { 241 | break; 242 | } 243 | p++; 244 | line_begin = p; 245 | continue; 246 | } 247 | p++; 248 | } 249 | r_regex_fini (&rx); 250 | return r_strbuf_drain (sb); 251 | } 252 | 253 | static char *filter_named_functions_only(const char *input) { 254 | const char *src = input? input: ""; 255 | RStrBuf *sb = r_strbuf_new (""); 256 | const char *line_begin = src; 257 | const char *p = src; 258 | for (;;) { 259 | if (*p == '\n' || *p == '\0') { 260 | size_t len = (size_t) (p - line_begin); 261 | char *line = (char *)malloc (len + 1); 262 | if (!line) { 263 | break; 264 | } 265 | memcpy (line, line_begin, len); 266 | line[len] = '\0'; 267 | bool is_named = true; 268 | const char *last_dot = r_str_lchr (line, '.'); 269 | if (last_dot && last_dot[1]) { 270 | if (isdigit (last_dot[1])) { 271 | is_named = false; 272 | } 273 | } 274 | if (is_named) { 275 | r_strbuf_appendf (sb, "%s\n", line); 276 | } 277 | free (line); 278 | if (*p == '\0') { 279 | break; 280 | } 281 | p++; 282 | line_begin = p; 283 | continue; 284 | } 285 | p++; 286 | } 287 | return r_strbuf_drain (sb); 288 | } 289 | 290 | static char *tool_close_file(ServerState *ss, RJson *tool_args) { 291 | (void)tool_args; 292 | if (ss->http_mode) { 293 | return jsonrpc_tooltext_response ("In r2pipe mode we won't close the file."); 294 | } 295 | if (ss->rstate.core) { 296 | free (r2mcp_cmd (ss, "o-*")); 297 | ss->rstate.file_opened = false; 298 | free (ss->rstate.current_file); 299 | ss->rstate.current_file = NULL; 300 | } 301 | return jsonrpc_tooltext_response ("File closed successfully."); 302 | } 303 | 304 | static char *tool_list_functions(ServerState *ss, RJson *tool_args) { 305 | const RJson *only_named_parameter = r_json_get (tool_args, "only_named"); 306 | bool only_named = false; 307 | if (only_named_parameter) { 308 | if (only_named_parameter->type == R_JSON_BOOLEAN) { 309 | only_named = only_named_parameter->num.u_value; 310 | } 311 | } 312 | const char *filter = r_json_get_str (tool_args, "filter"); 313 | char *res = r2mcp_cmd (ss, "afl,addr/cols/name"); 314 | r_str_trim (res); 315 | if (R_STR_ISEMPTY (res)) { 316 | free (res); 317 | free (r2mcp_cmd (ss, "aaa")); 318 | res = r2mcp_cmd (ss, "afl,addr/cols/name"); 319 | r_str_trim (res); 320 | if (R_STR_ISEMPTY (res)) { 321 | free (res); 322 | res = strdup ("No functions found. Run the analysis first."); 323 | } 324 | } 325 | // Apply filtering if only_named is true 326 | if (only_named && R_STR_ISNOTEMPTY (res)) { 327 | char *filtered = filter_named_functions_only (res); 328 | if (filtered) { 329 | free (res); 330 | res = filtered; 331 | } 332 | } 333 | // Apply regex filter if provided 334 | if (R_STR_ISNOTEMPTY (filter) && R_STR_ISNOTEMPTY (res)) { 335 | char *r = filter_lines_by_regex (res, filter); 336 | free (res); 337 | res = r; 338 | } 339 | return tool_cmd_response (res); 340 | } 341 | 342 | static char *tool_list_files(ServerState *ss, RJson *tool_args) { 343 | const char *path; 344 | if (!validate_required_string_param (tool_args, "path", &path)) { 345 | return jsonrpc_error_missing_param ("path"); 346 | } 347 | 348 | // Security checks 349 | if (!path || path[0] != '/') { 350 | return jsonrpc_error_response (-32603, "Relative paths are not allowed. Use an absolute path", NULL, NULL); 351 | } 352 | if (strstr (path, "/../") != NULL) { 353 | return jsonrpc_error_response (-32603, "Path traversal is not allowed (contains '/../')", NULL, NULL); 354 | } 355 | if (ss->sandbox && *ss->sandbox) { 356 | size_t plen = strlen (path); 357 | size_t slen = strlen (ss->sandbox); 358 | if (slen == 0 || slen > plen || strncmp (path, ss->sandbox, slen) != 0 || 359 | (plen > slen && path[slen] != '/')) { 360 | return jsonrpc_error_response (-32603, "Access denied: path is outside of the sandbox", NULL, NULL); 361 | } 362 | } 363 | 364 | char *cmd = r_str_newf ("ls -q %s", path); 365 | char *res = r2mcp_cmd (ss, cmd); 366 | free (cmd); 367 | return tool_cmd_response (res); 368 | } 369 | 370 | static char *tool_list_classes(ServerState *ss, RJson *tool_args) { 371 | const char *filter = r_json_get_str (tool_args, "filter"); 372 | char *res = r2mcp_cmd (ss, "icqq"); 373 | 374 | if (R_STR_ISNOTEMPTY (filter)) { 375 | 376 | char *r = filter_lines_by_regex (res, filter); 377 | 378 | free (res); 379 | 380 | res = r; 381 | } 382 | 383 | return tool_cmd_response (res); 384 | } 385 | 386 | static char *tool_list_methods(ServerState *ss, RJson *tool_args) { 387 | const char *classname; 388 | if (!validate_required_string_param (tool_args, "classname", &classname)) { 389 | return jsonrpc_error_missing_param ("classname"); 390 | } 391 | return tool_cmd_response (r2mcp_cmdf (ss, "'ic %s", classname)); 392 | } 393 | 394 | static char *tool_list_decompilers(ServerState *ss, RJson *tool_args) { 395 | (void)tool_args; 396 | return tool_cmd_response (r2mcp_cmd (ss, "e cmd.pdc=?")); 397 | } 398 | 399 | static char *tool_list_functions_tree(ServerState *ss, RJson *tool_args) { 400 | (void)tool_args; 401 | char *res = r2mcp_cmd (ss, "aflmu"); 402 | 403 | r_str_trim (res); 404 | 405 | return tool_cmd_response (res); 406 | } 407 | 408 | static char *tool_list_imports(ServerState *ss, RJson *tool_args) { 409 | const char *filter = r_json_get_str (tool_args, "filter"); 410 | char *res = r2mcp_cmd (ss, "iiq"); 411 | 412 | if (R_STR_ISNOTEMPTY (filter)) { 413 | 414 | char *r = filter_lines_by_regex (res, filter); 415 | 416 | free (res); 417 | 418 | res = r; 419 | } 420 | 421 | return tool_cmd_response (res); 422 | } 423 | 424 | static char *tool_list_sections(ServerState *ss, RJson *tool_args) { 425 | (void)tool_args; 426 | return tool_cmd_response (r2mcp_cmd (ss, "iS;iSS")); 427 | } 428 | 429 | static char *tool_show_headers(ServerState *ss, RJson *tool_args) { 430 | (void)tool_args; 431 | return tool_cmd_response (r2mcp_cmd (ss, "i;iH")); 432 | } 433 | 434 | static char *tool_show_function_details(ServerState *ss, RJson *tool_args) { 435 | (void)tool_args; 436 | return tool_cmd_response (r2mcp_cmd (ss, "afi")); 437 | } 438 | 439 | static char *tool_get_current_address(ServerState *ss, RJson *tool_args) { 440 | (void)tool_args; 441 | return tool_cmd_response (r2mcp_cmd (ss, "s;fd")); 442 | } 443 | 444 | static char *tool_list_symbols(ServerState *ss, RJson *tool_args) { 445 | const char *filter = r_json_get_str (tool_args, "filter"); 446 | char *res = r2mcp_cmd (ss, "isq~!func.,!imp."); 447 | 448 | if (R_STR_ISNOTEMPTY (filter)) { 449 | 450 | char *r = filter_lines_by_regex (res, filter); 451 | 452 | free (res); 453 | 454 | res = r; 455 | } 456 | 457 | return tool_cmd_response (res); 458 | } 459 | 460 | static char *tool_list_entrypoints(ServerState *ss, RJson *tool_args) { 461 | (void)tool_args; 462 | char *res = r2mcp_cmd (ss, "ies"); 463 | char *o = jsonrpc_tooltext_response_lines (res); 464 | free (res); 465 | return o; 466 | } 467 | 468 | static char *tool_list_libraries(ServerState *ss, RJson *tool_args) { 469 | (void)tool_args; 470 | return tool_cmd_response (r2mcp_cmd (ss, "ilq")); 471 | } 472 | 473 | static char *tool_calculate(ServerState *ss, RJson *tool_args) { 474 | const char *expression; 475 | if (!validate_required_string_param (tool_args, "expression", &expression)) { 476 | return jsonrpc_error_missing_param ("expression"); 477 | } 478 | if (!ss->rstate.core || !ss->rstate.core->num) { 479 | return jsonrpc_error_response (-32611, "Core or number parser unavailable (open a file first)", NULL, NULL); 480 | } 481 | RCore *core = ss->rstate.core; 482 | ut64 calc_result = r_num_math (core->num, expression); 483 | char *numstr = r_str_newf ("0x%" PFMT64x, (ut64)calc_result); 484 | char *resp = jsonrpc_tooltext_response (numstr); 485 | free (numstr); 486 | return resp; 487 | } 488 | 489 | static char *tool_set_comment(ServerState *ss, RJson *tool_args) { 490 | const char *address, *message; 491 | if (!validate_address_param (tool_args, "address", &address) || 492 | !validate_required_string_param (tool_args, "message", &message)) { 493 | return jsonrpc_error_missing_param ("address and message"); 494 | } 495 | 496 | char *cmd_cc = r_str_newf ("'@%s'CC %s", address, message); 497 | char *tmpres_cc = r2mcp_cmd (ss, cmd_cc); 498 | free (tmpres_cc); 499 | free (cmd_cc); 500 | return strdup ("ok"); 501 | } 502 | 503 | static char *tool_set_function_prototype(ServerState *ss, RJson *tool_args) { 504 | const char *address, *prototype; 505 | if (!validate_address_param (tool_args, "address", &address) || 506 | !validate_required_string_param (tool_args, "prototype", &prototype)) { 507 | return jsonrpc_error_missing_param ("address and prototype"); 508 | } 509 | char *cmd_afs = r_str_newf ("'@%s'afs %s", address, prototype); 510 | char *tmpres_afs = r2mcp_cmd (ss, cmd_afs); 511 | free (tmpres_afs); 512 | free (cmd_afs); 513 | return strdup ("ok"); 514 | } 515 | 516 | static char *tool_get_function_prototype(ServerState *ss, RJson *tool_args) { 517 | const char *address; 518 | if (!validate_address_param (tool_args, "address", &address)) { 519 | return jsonrpc_error_missing_param ("address"); 520 | } 521 | char *s = r_str_newf ("'@%s'afs", address); 522 | char *res = r2mcp_cmd (ss, s); 523 | free (s); 524 | return res; 525 | } 526 | 527 | static char *tool_list_strings(ServerState *ss, RJson *tool_args) { 528 | const char *filter = r_json_get_str (tool_args, "filter"); 529 | const char *cursor = r_json_get_str (tool_args, "cursor"); 530 | int page_size = (int)r_json_get_num (tool_args, "page_size"); 531 | if (page_size <= 0) { 532 | page_size = R2MCP_DEFAULT_PAGE_SIZE; 533 | } 534 | if (page_size > R2MCP_MAX_PAGE_SIZE) { 535 | page_size = R2MCP_MAX_PAGE_SIZE; 536 | } 537 | 538 | char *cmd_result = r2mcp_cmd (ss, "izqq"); 539 | if (R_STR_ISNOTEMPTY (filter)) { 540 | char *r = filter_lines_by_regex (cmd_result, filter); 541 | free (cmd_result); 542 | cmd_result = r; 543 | } 544 | bool has_more = false; 545 | char *next_cursor = NULL; 546 | char *paginated = paginate_text_by_lines (cmd_result, cursor, page_size, &has_more, &next_cursor); 547 | free (cmd_result); 548 | char *response = jsonrpc_tooltext_response_paginated (paginated, has_more, next_cursor); 549 | free (paginated); 550 | free (next_cursor); 551 | return response; 552 | } 553 | 554 | static char *tool_list_all_strings(ServerState *ss, RJson *tool_args) { 555 | const char *filter = r_json_get_str (tool_args, "filter"); 556 | const char *cursor = r_json_get_str (tool_args, "cursor"); 557 | int page_size = (int)r_json_get_num (tool_args, "page_size"); 558 | if (page_size <= 0) { 559 | page_size = R2MCP_DEFAULT_PAGE_SIZE; 560 | } 561 | if (page_size > R2MCP_MAX_PAGE_SIZE) { 562 | page_size = R2MCP_MAX_PAGE_SIZE; 563 | } 564 | 565 | char *cmd_result = r2mcp_cmd (ss, "izzzqq"); 566 | if (R_STR_ISNOTEMPTY (filter)) { 567 | char *r = filter_lines_by_regex (cmd_result, filter); 568 | free (cmd_result); 569 | cmd_result = r; 570 | } 571 | if (R_STR_ISEMPTY (cmd_result)) { 572 | free (cmd_result); 573 | cmd_result = r_str_newf ("Error: No strings with regex %s", filter); 574 | } 575 | bool has_more = false; 576 | char *next_cursor = NULL; 577 | char *paginated = paginate_text_by_lines (cmd_result, cursor, page_size, &has_more, &next_cursor); 578 | free (cmd_result); 579 | char *response = jsonrpc_tooltext_response_paginated (paginated, has_more, next_cursor); 580 | free (paginated); 581 | free (next_cursor); 582 | return response; 583 | } 584 | 585 | static char *tool_analyze(ServerState *ss, RJson *tool_args) { 586 | const int level = (int)r_json_get_num (tool_args, "level"); 587 | char *err = r2mcp_analyze (ss, level); 588 | char *cmd_result = r2mcp_cmd (ss, "aflc"); 589 | char *errstr; 590 | if (R_STR_ISNOTEMPTY (err)) { 591 | errstr = r_str_newf ("\n\n\n%s\n\n", err); 592 | } else { 593 | errstr = strdup (""); 594 | } 595 | char *text = r_str_newf ("Analysis completed with level %d.\nFound %d functions.%s", level, atoi (cmd_result), errstr); 596 | char *response = jsonrpc_tooltext_response (text); 597 | free (err); 598 | free (errstr); 599 | free (cmd_result); 600 | free (text); 601 | return response; 602 | } 603 | 604 | static char *tool_disassemble(ServerState *ss, RJson *tool_args) { 605 | const char *address; 606 | if (!validate_address_param (tool_args, "address", &address)) { 607 | return jsonrpc_error_missing_param ("address"); 608 | } 609 | 610 | RJson *num_instr_json = (RJson *)r_json_get (tool_args, "num_instructions"); 611 | int num_instructions = 10; 612 | if (num_instr_json && num_instr_json->type == R_JSON_INTEGER) { 613 | num_instructions = (int)num_instr_json->num.u_value; 614 | } 615 | 616 | return tool_cmd_response (r2mcp_cmdf (ss, "'@%s'pd %d", address, num_instructions)); 617 | } 618 | 619 | static char *tool_use_decompiler(ServerState *ss, RJson *tool_args) { 620 | const char *deco; 621 | if (!validate_required_string_param (tool_args, "name", &deco)) { 622 | return jsonrpc_error_missing_param ("name"); 623 | } 624 | char *decompilersAvailable = r2mcp_cmd (ss, "e cmd.pdc=?"); 625 | const char *response = "ok"; 626 | if (strstr (deco, "ghidra")) { 627 | if (strstr (decompilersAvailable, "pdg")) { 628 | free (r2mcp_cmd (ss, "-e cmd.pdc=pdg")); 629 | } else { 630 | response = "This decompiler is not available"; 631 | } 632 | } else if (strstr (deco, "decai")) { 633 | if (strstr (decompilersAvailable, "decai")) { 634 | free (r2mcp_cmd (ss, "-e cmd.pdc=decai -d")); 635 | } else { 636 | response = "This decompiler is not available"; 637 | } 638 | } else if (strstr (deco, "r2dec")) { 639 | if (strstr (decompilersAvailable, "pdd")) { 640 | free (r2mcp_cmd (ss, "-e cmd.pdc=pdd")); 641 | } else { 642 | response = "This decompiler is not available"; 643 | } 644 | } else { 645 | response = "Unknown decompiler"; 646 | } 647 | free (decompilersAvailable); 648 | return jsonrpc_tooltext_response (response); 649 | } 650 | 651 | static char *tool_xrefs_to(ServerState *ss, RJson *tool_args) { 652 | const char *address; 653 | if (!validate_address_param (tool_args, "address", &address)) { 654 | return jsonrpc_error_missing_param ("address"); 655 | } 656 | return tool_cmd_response (r2mcp_cmdf (ss, "'@%s'axt", address)); 657 | } 658 | 659 | static char *tool_disassemble_function(ServerState *ss, RJson *tool_args) { 660 | const char *address; 661 | if (!validate_address_param (tool_args, "address", &address)) { 662 | return jsonrpc_error_missing_param ("address"); 663 | } 664 | const char *cursor = r_json_get_str (tool_args, "cursor"); 665 | int page_size = (int)r_json_get_num (tool_args, "page_size"); 666 | if (page_size <= 0) { 667 | page_size = R2MCP_DEFAULT_PAGE_SIZE; 668 | } 669 | if (page_size > R2MCP_MAX_PAGE_SIZE) { 670 | page_size = R2MCP_MAX_PAGE_SIZE; 671 | } 672 | char *disasm = r2mcp_cmdf (ss, "'@%s'pdf", address); 673 | bool has_more = false; 674 | char *next_cursor = NULL; 675 | char *paginated = paginate_text_by_lines (disasm, cursor, page_size, &has_more, &next_cursor); 676 | free (disasm); 677 | char *response = jsonrpc_tooltext_response_paginated (paginated, has_more, next_cursor); 678 | free (paginated); 679 | free (next_cursor); 680 | return response; 681 | } 682 | 683 | static char *tool_rename_flag(ServerState *ss, RJson *tool_args) { 684 | const char *address, *name, *new_name; 685 | if (!validate_address_param (tool_args, "address", &address) || 686 | !validate_required_string_param (tool_args, "name", &name) || 687 | !validate_required_string_param (tool_args, "new_name", &new_name)) { 688 | return jsonrpc_error_missing_param ("address, name, and new_name"); 689 | } 690 | char *remove_res = r2mcp_cmdf (ss, "'@%s'fr %s %s", address, name, new_name); 691 | 692 | if (R_STR_ISNOTEMPTY (remove_res)) { 693 | 694 | return tool_cmd_response (remove_res); 695 | } 696 | 697 | free (remove_res); 698 | 699 | return jsonrpc_tooltext_response ("ok"); 700 | } 701 | 702 | static char *tool_rename_function(ServerState *ss, RJson *tool_args) { 703 | const char *address, *name; 704 | if (!validate_address_param (tool_args, "address", &address) || 705 | !validate_required_string_param (tool_args, "name", &name)) { 706 | return jsonrpc_error_missing_param ("address and name"); 707 | } 708 | free (r2mcp_cmdf (ss, "'@%s'afn %s", address, name)); 709 | return jsonrpc_tooltext_response ("ok"); 710 | } 711 | 712 | static char *tool_decompile_function(ServerState *ss, RJson *tool_args) { 713 | const char *address; 714 | if (!validate_address_param (tool_args, "address", &address)) { 715 | return jsonrpc_error_missing_param ("address"); 716 | } 717 | const char *cursor = r_json_get_str (tool_args, "cursor"); 718 | int page_size = (int)r_json_get_num (tool_args, "page_size"); 719 | if (page_size <= 0) { 720 | page_size = R2MCP_DEFAULT_PAGE_SIZE; 721 | } 722 | if (page_size > R2MCP_MAX_PAGE_SIZE) { 723 | page_size = R2MCP_MAX_PAGE_SIZE; 724 | } 725 | char *disasm = r2mcp_cmdf (ss, "'@%s'pdc", address); 726 | bool has_more = false; 727 | char *next_cursor = NULL; 728 | char *paginated = paginate_text_by_lines (disasm, cursor, page_size, &has_more, &next_cursor); 729 | free (disasm); 730 | char *response = jsonrpc_tooltext_response_paginated (paginated, has_more, next_cursor); 731 | free (paginated); 732 | free (next_cursor); 733 | return response; 734 | } 735 | 736 | static char *tool_run_command(ServerState *ss, RJson *tool_args) { 737 | const char *command; 738 | if (!validate_required_string_param (tool_args, "command", &command)) { 739 | return jsonrpc_error_missing_param ("command"); 740 | } 741 | return tool_cmd_response (r2mcp_cmd (ss, command)); 742 | } 743 | 744 | static char *tool_run_javascript(ServerState *ss, RJson *tool_args) { 745 | const char *script; 746 | if (!validate_required_string_param (tool_args, "script", &script)) { 747 | return jsonrpc_error_missing_param ("script"); 748 | } 749 | char *encoded = r_base64_encode_dyn ((const ut8 *)script, strlen (script)); 750 | 751 | if (!encoded) { 752 | 753 | return jsonrpc_error_response (-32603, "Failed to encode script", NULL, NULL); 754 | } 755 | 756 | char *cmd = r_str_newf ("js base64:%s", encoded); 757 | 758 | char *res = r2mcp_cmd (ss, cmd); 759 | 760 | free (cmd); 761 | 762 | free (encoded); 763 | 764 | return tool_cmd_response (res); 765 | } 766 | 767 | static char *tool_list_sessions(ServerState *ss, RJson *tool_args) { 768 | (void)tool_args; 769 | (void)ss; 770 | // r2agent command doesn't require an open file, run it directly 771 | char *res = NULL; 772 | if (ss && ss->http_mode) { 773 | // In HTTP mode, we can't run r2agent locally, return empty result 774 | res = strdup ("[]"); 775 | } else { 776 | // Run r2agent command directly using system 777 | FILE *fp = popen ("r2agent -Lj 2>/dev/null", "r"); 778 | if (!fp) { 779 | res = strdup ("[]"); 780 | } else { 781 | char buffer[4096]; 782 | RStrBuf *sb = r_strbuf_new (""); 783 | while (fgets (buffer, sizeof (buffer), fp)) { 784 | r_strbuf_append (sb, buffer); 785 | } 786 | pclose (fp); 787 | res = r_strbuf_drain (sb); 788 | if (R_STR_ISEMPTY (res)) { 789 | free (res); 790 | res = strdup ("[]"); 791 | } 792 | } 793 | } 794 | return tool_cmd_response (res); 795 | } 796 | 797 | static char *tool_open_session(ServerState *ss, RJson *tool_args) { 798 | const char *url; 799 | if (!validate_required_string_param (tool_args, "url", &url)) { 800 | return jsonrpc_error_missing_param ("url"); 801 | } 802 | 803 | // Store the current baseurl if we're not already in HTTP mode 804 | char *old_baseurl = NULL; 805 | bool old_http_mode = ss->http_mode; 806 | if (!ss->http_mode && ss->baseurl) { 807 | old_baseurl = strdup (ss->baseurl); 808 | } 809 | 810 | // Set up HTTP mode for this session 811 | ss->http_mode = true; 812 | free (ss->baseurl); 813 | ss->baseurl = strdup (url); 814 | 815 | // Test the connection by running a simple command 816 | char *test_result = r2mcp_cmd (ss, "i"); 817 | if (!test_result || strstr (test_result, "HTTP request failed")) { 818 | // Restore previous state if connection failed 819 | ss->http_mode = old_http_mode; 820 | free (ss->baseurl); 821 | ss->baseurl = old_baseurl; 822 | free (test_result); 823 | 824 | char *error_msg = r_str_newf ("Failed to connect to URL: %s", url); 825 | char *error_resp = jsonrpc_error_response (-32603, error_msg, NULL, NULL); 826 | free (error_msg); 827 | return error_resp; 828 | } 829 | 830 | free (test_result); 831 | free (old_baseurl); 832 | 833 | char *success_msg = r_str_newf ("Successfully connected to remote r2 instance at %s", url); 834 | char *response = jsonrpc_tooltext_response (success_msg); 835 | free (success_msg); 836 | return response; 837 | } 838 | 839 | static char *tool_close_session(ServerState *ss, RJson *tool_args) { 840 | (void)tool_args; 841 | 842 | if (!ss->http_mode) { 843 | return jsonrpc_tooltext_response ("No active remote session to close."); 844 | } 845 | 846 | // Clear the HTTP mode and baseurl 847 | ss->http_mode = false; 848 | free (ss->baseurl); 849 | ss->baseurl = NULL; 850 | 851 | return jsonrpc_tooltext_response ("Remote session closed successfully."); 852 | } 853 | 854 | static char *check_supervisor_permission(ServerState *ss, const char *tool_name, RJson *tool_args, char **new_tool_name_out, RJson **new_tool_args_out, RJson **parsed_json_out) { 855 | if (!ss->svc_baseurl) { 856 | return NULL; 857 | } 858 | PJ *pj = pj_new (); 859 | pj_o (pj); 860 | pj_ks (pj, "tool", tool_name); 861 | pj_k (pj, "arguments"); 862 | pj_append_rjson (pj, tool_args); 863 | pj_k (pj, "available_tools"); 864 | pj_a (pj); 865 | for (size_t i = 0; tool_specs[i].name; i++) { 866 | pj_s (pj, tool_specs[i].name); 867 | } 868 | pj_end (pj); 869 | pj_end (pj); 870 | char *req = pj_drain (pj); 871 | int rc; 872 | char *resp = curl_post_capture (ss->svc_baseurl, req, &rc); 873 | free (req); 874 | if (!resp || rc != 0) { 875 | free (resp); 876 | return NULL; 877 | } 878 | *parsed_json_out = r_json_parse (resp); 879 | free (resp); 880 | if (!*parsed_json_out) { 881 | return NULL; 882 | } 883 | const char *err = r_json_get_str (*parsed_json_out, "error"); 884 | if (err) { 885 | char *error_resp = jsonrpc_error_response (-32000, err, NULL, NULL); 886 | r_json_free (*parsed_json_out); 887 | *parsed_json_out = NULL; 888 | return error_resp; 889 | } 890 | const char *r2cmd = r_json_get_str (*parsed_json_out, "r2cmd"); 891 | if (r2cmd) { 892 | char *cmd_out = r2mcp_cmd (ss, r2cmd); 893 | char *res = jsonrpc_tooltext_response (cmd_out? cmd_out: ""); 894 | free (cmd_out); 895 | if (!strcmp (tool_name, "open_file")) { 896 | const char *filepath = r_json_get_str (tool_args, "file_path"); 897 | if (filepath) { 898 | free (ss->rstate.current_file); 899 | ss->rstate.current_file = strdup (filepath); 900 | ss->rstate.file_opened = true; 901 | } 902 | } 903 | r_json_free (*parsed_json_out); 904 | *parsed_json_out = NULL; 905 | return res; 906 | } 907 | const char *new_tool = r_json_get_str (*parsed_json_out, "tool"); 908 | if (new_tool && strcmp (new_tool, tool_name)) { 909 | *new_tool_name_out = strdup (new_tool); 910 | } 911 | const RJson *new_args = r_json_get (*parsed_json_out, "arguments"); 912 | if (new_args) { 913 | *new_tool_args_out = (RJson *)new_args; 914 | } else { 915 | r_json_free (*parsed_json_out); 916 | *parsed_json_out = NULL; 917 | } 918 | return NULL; 919 | } 920 | 921 | // Main dispatcher that handles tool calls. Returns heap-allocated JSON 922 | // string representing the tool "result" (caller must free it). 923 | char *tools_call(ServerState *ss, const char *tool_name, RJson *tool_args) { 924 | RJson nil = { 0 }; 925 | if (!tool_args) { 926 | tool_args = &nil; 927 | } 928 | char *result = NULL; 929 | char *allocated_tool_name = NULL; 930 | RJson *parsed_json = NULL; 931 | if (!tool_name) { 932 | result = jsonrpc_error_missing_param ("name"); 933 | goto cleanup; 934 | } 935 | // Enforce tool availability per mode unless permissive is enabled 936 | if (!tools_is_tool_allowed (ss, tool_name)) { 937 | result = jsonrpc_error_tool_not_allowed (tool_name); 938 | goto cleanup; 939 | } 940 | 941 | // Supervisor control check 942 | char *supervisor_override = check_supervisor_permission (ss, tool_name, tool_args, &allocated_tool_name, &tool_args, &parsed_json); 943 | if (supervisor_override) { 944 | result = supervisor_override; 945 | goto cleanup; 946 | } 947 | if (allocated_tool_name) { 948 | tool_name = allocated_tool_name; 949 | } 950 | 951 | // Special-case: open_file 952 | if (!strcmp (tool_name, "open_file")) { 953 | if (ss->http_mode) { 954 | char *res = r2mcp_cmd (ss, "i"); 955 | char *foo = r_str_newf ("File was already opened, this are the details:\n%s", res); 956 | char *out = jsonrpc_tooltext_response (foo); 957 | free (res); 958 | free (foo); 959 | result = out; 960 | goto cleanup; 961 | } 962 | const char *filepath; 963 | if (!validate_required_string_param (tool_args, "file_path", &filepath)) { 964 | result = jsonrpc_error_missing_param ("file_path"); 965 | goto cleanup; 966 | } 967 | 968 | char *filteredpath = strdup (filepath); 969 | r_str_replace_ch (filteredpath, '`', 0, true); 970 | bool success = r2mcp_open_file (ss, filteredpath); 971 | free (filteredpath); 972 | result = jsonrpc_tooltext_response (success? "File opened successfully.": "Failed to open file."); 973 | goto cleanup; 974 | } 975 | 976 | // Special-case: open_session 977 | if (!strcmp (tool_name, "open_session")) { 978 | result = tool_open_session (ss, tool_args); 979 | goto cleanup; 980 | } 981 | 982 | // Special-case: list_sessions 983 | if (!strcmp (tool_name, "list_sessions")) { 984 | result = tool_list_sessions (ss, tool_args); 985 | goto cleanup; 986 | } 987 | 988 | if (!ss->http_mode && !ss->rstate.file_opened) { 989 | result = jsonrpc_error_file_required (); 990 | goto cleanup; 991 | } 992 | 993 | // Dispatch to tool functions 994 | for (size_t i = 0; tool_specs[i].name; i++) { 995 | ToolSpec *t = &tool_specs[i]; 996 | if (!strcmp (tool_name, t->name)) { 997 | result = t->func (ss, tool_args); 998 | goto cleanup; 999 | } 1000 | } 1001 | 1002 | char *tmp = r_str_newf ("Unknown tool: %s", tool_name); 1003 | char *err = jsonrpc_error_response (-32602, tmp, NULL, NULL); 1004 | free (tmp); 1005 | result = err; 1006 | goto cleanup; 1007 | 1008 | cleanup: 1009 | free (allocated_tool_name); 1010 | r_json_free (parsed_json); 1011 | return result; 1012 | } 1013 | ToolSpec tool_specs[] = { 1014 | { "open_file", "Opens a binary file with radare2 for analysis Call this tool before any other one from r2mcp. Use an absolute file_path", "{\"type\":\"object\",\"properties\":{\"file_path\":{\"type\":\"string\",\"description\":\"Path to the file to open\"}},\"required\":[\"file_path\"]}", TOOL_MODE_NORMAL | TOOL_MODE_MINI, NULL }, 1015 | { "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 | TOOL_MODE_MINI | TOOL_MODE_HTTP, tool_run_javascript }, 1016 | { "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 | TOOL_MODE_MINI | TOOL_MODE_HTTP, tool_run_command }, 1017 | { "list_sessions", "Lists available r2agent sessions in JSON format", "{\"type\":\"object\",\"properties\":{}}", TOOL_MODE_NORMAL | TOOL_MODE_MINI | TOOL_MODE_HTTP | TOOL_MODE_RO, tool_list_sessions }, 1018 | { "open_session", "Connects to a remote r2 instance using r2pipe API", "{\"type\":\"object\",\"properties\":{\"url\":{\"type\":\"string\",\"description\":\"URL of the remote r2 instance to connect to\"}},\"required\":[\"url\"]}", TOOL_MODE_NORMAL | TOOL_MODE_MINI | TOOL_MODE_HTTP, tool_open_session }, 1019 | { "close_session", "Close the currently open remote session", "{\"type\":\"object\",\"properties\":{}}", TOOL_MODE_NORMAL | TOOL_MODE_MINI | TOOL_MODE_HTTP, tool_close_session }, 1020 | { "close_file", "Close the currently open file", "{\"type\":\"object\",\"properties\":{}}", TOOL_MODE_NORMAL, tool_close_file }, 1021 | { "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 | TOOL_MODE_MINI | TOOL_MODE_HTTP | TOOL_MODE_RO, tool_list_functions }, 1022 | { "list_functions_tree", "Lists functions and successors (aflmu)", "{\"type\":\"object\",\"properties\":{}}", TOOL_MODE_NORMAL | TOOL_MODE_MINI | TOOL_MODE_HTTP | TOOL_MODE_RO, tool_list_functions_tree }, 1023 | { "list_libraries", "Lists all shared libraries linked to the binary", "{\"type\":\"object\",\"properties\":{}}", TOOL_MODE_NORMAL | TOOL_MODE_MINI | TOOL_MODE_HTTP | TOOL_MODE_RO, tool_list_libraries }, 1024 | { "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 | TOOL_MODE_MINI | TOOL_MODE_HTTP | TOOL_MODE_RO, tool_list_imports }, 1025 | { "list_sections", "Displays memory sections and segments from the binary", "{\"type\":\"object\",\"properties\":{}}", TOOL_MODE_NORMAL | TOOL_MODE_RO, tool_list_sections }, 1026 | { "show_function_details", "Displays detailed information about the current function", "{\"type\":\"object\",\"properties\":{}}", TOOL_MODE_NORMAL | TOOL_MODE_RO, tool_show_function_details }, 1027 | { "get_current_address", "Shows the current position and function name", "{\"type\":\"object\",\"properties\":{}}", TOOL_MODE_NORMAL | TOOL_MODE_RO, tool_get_current_address }, 1028 | { "show_headers", "Displays binary headers and file information", "{\"type\":\"object\",\"properties\":{}}", TOOL_MODE_NORMAL | TOOL_MODE_MINI | TOOL_MODE_HTTP | TOOL_MODE_RO, tool_show_headers }, 1029 | { "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 | TOOL_MODE_MINI | TOOL_MODE_HTTP | TOOL_MODE_RO, tool_list_symbols }, 1030 | { "list_entrypoints", "Displays program entrypoints, constructors and main function", "{\"type\":\"object\",\"properties\":{}}", TOOL_MODE_NORMAL | TOOL_MODE_MINI | TOOL_MODE_HTTP | TOOL_MODE_RO, tool_list_entrypoints }, 1031 | { "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 | TOOL_MODE_RO, tool_list_methods }, 1032 | { "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 | TOOL_MODE_RO, tool_list_classes }, 1033 | { "list_decompilers", "Shows all available decompiler backends", "{\"type\":\"object\",\"properties\":{}}", TOOL_MODE_NORMAL | TOOL_MODE_RO, tool_list_decompilers }, 1034 | { "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, tool_rename_function }, 1035 | { "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 | TOOL_MODE_HTTP, tool_rename_flag }, 1036 | { "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, tool_use_decompiler }, 1037 | { "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 | TOOL_MODE_RO, tool_get_function_prototype }, 1038 | { "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, tool_set_function_prototype }, 1039 | { "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 | TOOL_MODE_HTTP, tool_set_comment }, 1040 | { "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 | TOOL_MODE_MINI | TOOL_MODE_HTTP | TOOL_MODE_RO, tool_list_strings }, 1041 | { "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 | TOOL_MODE_RO, tool_list_all_strings }, 1042 | { "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 | TOOL_MODE_MINI | TOOL_MODE_HTTP, tool_analyze }, 1043 | { "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 | TOOL_MODE_MINI | TOOL_MODE_HTTP | TOOL_MODE_RO, tool_xrefs_to }, 1044 | { "decompile_function", "Show C-like pseudocode of the function in the given address. Use this to inspect the code in a function, do not run multiple times in the same offset", "{\"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 | TOOL_MODE_MINI | TOOL_MODE_HTTP | TOOL_MODE_RO, tool_decompile_function }, 1045 | { "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 | TOOL_MODE_MINI | TOOL_MODE_HTTP | TOOL_MODE_RO, tool_list_files }, 1046 | { "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 | TOOL_MODE_RO, tool_disassemble_function }, 1047 | { "disassemble", "Disassembles a specific number of instructions from an address 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", "{\"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 | TOOL_MODE_RO, tool_disassemble }, 1048 | { "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 | TOOL_MODE_MINI | TOOL_MODE_RO, tool_calculate }, 1049 | { NULL, NULL, NULL, 0, NULL } 1050 | }; 1051 | --------------------------------------------------------------------------------