feat: rea reverse-engineering skill + per-server MCP HTTP request timeout (#37)

feat: rea reverse-engineering skill + per-server MCP HTTP request timeout
This commit is contained in:
BaaRai
2026-10-11 19:55:54 +02:00
committed by GitHub
9 changed files with 167 additions and 21 deletions
+21 -16
View File
@@ -812,20 +812,21 @@ func (d *DB) ListPromptVersions(agentID int64) ([]PromptVersion, error) {
// ---------- MCP servers ----------
type MCPServer struct {
ID int64 `json:"id"`
Name string `json:"name"`
Transport string `json:"transport"`
Command string `json:"command,omitempty"`
Args json.RawMessage `json:"args"`
Env json.RawMessage `json:"env"`
URL string `json:"url,omitempty"`
Enabled bool `json:"enabled"`
Insecure bool `json:"insecure"` // http: skip TLS cert verification (self-signed servers, issue #108)
Tools []string `json:"tools,omitempty"` // cached tool names (mcp_tools_cache)
ID int64 `json:"id"`
Name string `json:"name"`
Transport string `json:"transport"`
Command string `json:"command,omitempty"`
Args json.RawMessage `json:"args"`
Env json.RawMessage `json:"env"`
URL string `json:"url,omitempty"`
Enabled bool `json:"enabled"`
Insecure bool `json:"insecure"` // http: skip TLS cert verification (self-signed servers, issue #108)
TimeoutSeconds int `json:"timeout_seconds"` // http: per-request timeout in seconds; 0 = default 120
Tools []string `json:"tools,omitempty"` // cached tool names (mcp_tools_cache)
}
func (d *DB) ListMCP() ([]*MCPServer, error) {
rows, err := d.Query(`SELECT id,name,transport,COALESCE(command,''),args,env,COALESCE(url,''),enabled,insecure FROM mcp_servers ORDER BY id`)
rows, err := d.Query(`SELECT id,name,transport,COALESCE(command,''),args,env,COALESCE(url,''),enabled,insecure,COALESCE(timeout_seconds,120) FROM mcp_servers ORDER BY id`)
if err != nil {
return nil, err
}
@@ -833,7 +834,7 @@ func (d *DB) ListMCP() ([]*MCPServer, error) {
for rows.Next() {
var m MCPServer
var args, env []byte
if err := rows.Scan(&m.ID, &m.Name, &m.Transport, &m.Command, &args, &env, &m.URL, &m.Enabled, &m.Insecure); err != nil {
if err := rows.Scan(&m.ID, &m.Name, &m.Transport, &m.Command, &args, &env, &m.URL, &m.Enabled, &m.Insecure, &m.TimeoutSeconds); err != nil {
rows.Close()
return nil, err
}
@@ -921,14 +922,18 @@ func (d *DB) SaveMCP(m *MCPServer) (int64, error) {
if env == "" {
env = "{}"
}
timeout := m.TimeoutSeconds
if timeout <= 0 {
timeout = 120
}
if m.ID == 0 {
var id int64
err := d.QueryRow(`INSERT INTO mcp_servers(name,transport,command,args,env,url,enabled,insecure) VALUES ($1,$2,NULLIF($3,''),$4,$5,NULLIF($6,''),$7,$8) RETURNING id`,
m.Name, m.Transport, m.Command, args, env, m.URL, m.Enabled, m.Insecure).Scan(&id)
err := d.QueryRow(`INSERT INTO mcp_servers(name,transport,command,args,env,url,enabled,insecure,timeout_seconds) VALUES ($1,$2,NULLIF($3,''),$4,$5,NULLIF($6,''),$7,$8,$9) RETURNING id`,
m.Name, m.Transport, m.Command, args, env, m.URL, m.Enabled, m.Insecure, timeout).Scan(&id)
return id, err
}
_, err := d.Exec(`UPDATE mcp_servers SET name=$1,transport=$2,command=NULLIF($3,''),args=$4,env=$5,url=NULLIF($6,''),enabled=$7,insecure=$8 WHERE id=$9`,
m.Name, m.Transport, m.Command, args, env, m.URL, m.Enabled, m.Insecure, m.ID)
_, err := d.Exec(`UPDATE mcp_servers SET name=$1,transport=$2,command=NULLIF($3,''),args=$4,env=$5,url=NULLIF($6,''),enabled=$7,insecure=$8,timeout_seconds=$9 WHERE id=$10`,
m.Name, m.Transport, m.Command, args, env, m.URL, m.Enabled, m.Insecure, timeout, m.ID)
return m.ID, err
}
+4
View File
@@ -859,6 +859,10 @@ ALTER TABLE mcp_servers ADD CONSTRAINT mcp_servers_transport_check
CHECK (transport IN ('stdio','http','sse'));
-- Backfill column for old databases (schema.sql runs Exec at every startup).
ALTER TABLE mcp_servers ADD COLUMN IF NOT EXISTS insecure BOOLEAN NOT NULL DEFAULT false;
-- Per-server request timeout for remote MCP transports, in seconds (default 120). Analysis
-- servers whose tools legitimately run minutes (e.g. binary reverse engineering) need room
-- beyond the default without forcing every other server to wait as long.
ALTER TABLE mcp_servers ADD COLUMN IF NOT EXISTS timeout_seconds INT NOT NULL DEFAULT 120;
DROP TRIGGER IF EXISTS trg_mcp_upd ON mcp_servers;
CREATE TRIGGER trg_mcp_upd BEFORE UPDATE ON mcp_servers
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
+9 -2
View File
@@ -96,12 +96,19 @@ func normalizeHeaders(headers map[string]string) (map[string]string, error) {
// headers are sent on every request (Authorization, custom API keys, …).
// When insecure is true, TLS certificate verification is skipped so servers that
// present a self-signed certificate can still be reached (issue #108).
func New(ctx context.Context, server, url string, headers map[string]string, insecure bool) (*Client, error) {
// timeoutSeconds bounds each request (handshake, tools/list, one tools/call); 0
// falls back to the 120s default. Analysis servers with minutes-long tool calls
// raise it per server.
func New(ctx context.Context, server, url string, headers map[string]string, insecure bool, timeoutSeconds int) (*Client, error) {
cleanHeaders, err := normalizeHeaders(headers)
if err != nil {
return nil, err
}
hc := &http.Client{Timeout: 120 * time.Second}
timeout := 120 * time.Second
if timeoutSeconds > 0 {
timeout = time.Duration(timeoutSeconds) * time.Second
}
hc := &http.Client{Timeout: timeout}
if insecure {
hc.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
}
+75
View File
@@ -0,0 +1,75 @@
package mcphttp
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
)
// A Streamable HTTP server whose slow_call tool takes 3s, so the client's
// per-request timeout can be observed: 1s cuts the call off, 10s lets it finish.
func TestStreamableClientRequestTimeout(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req rpcRequest
_ = json.NewDecoder(r.Body).Decode(&req)
w.Header().Set("Content-Type", "application/json")
switch req.Method {
case "initialize":
fmt.Fprintf(w, `{"jsonrpc":"2.0","id":%d,"result":{"protocolVersion":"2025-06-18","capabilities":{},"serverInfo":{"name":"t","version":"0"}}}`, req.ID)
case "notifications/initialized":
w.WriteHeader(http.StatusAccepted)
case "tools/list":
fmt.Fprintf(w, `{"jsonrpc":"2.0","id":%d,"result":{"tools":[{"name":"slow_call","description":"slow","inputSchema":{"type":"object"}}]}}`, req.ID)
case "tools/call":
time.Sleep(3 * time.Second)
fmt.Fprintf(w, `{"jsonrpc":"2.0","id":%d,"result":{"content":[{"type":"text","text":"done"}]}}`, req.ID)
default:
http.NotFound(w, r)
}
}))
defer srv.Close()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// timeout_seconds=1: the slow call is cut off well before the server answers.
c, err := New(ctx, "t", srv.URL, nil, false, 1)
if err != nil {
t.Fatalf("connect: %v", err)
}
if _, err := c.Tools(ctx); err != nil {
t.Fatalf("tools/list: %v", err)
}
start := time.Now()
if _, err := c.Call(ctx, "slow_call", nil); err == nil {
t.Fatal("expected the 1s-timeout call to fail")
} else if elapsed := time.Since(start); elapsed > 2*time.Second {
t.Fatalf("call not cut off by the timeout: %v", elapsed)
}
_ = c.Close()
// timeout_seconds=10: the same slow call completes.
c2, err := New(ctx, "t", srv.URL, nil, false, 10)
if err != nil {
t.Fatalf("connect: %v", err)
}
got, err := c2.Call(ctx, "slow_call", nil)
if err != nil || got != "done" {
t.Fatalf("Call() = %q, %v", got, err)
}
_ = c2.Close()
// timeout_seconds=0: falls back to the 120s default (call completes).
c3, err := New(ctx, "t", srv.URL, nil, false, 0)
if err != nil {
t.Fatalf("connect: %v", err)
}
if got, err := c3.Call(ctx, "slow_call", nil); err != nil || got != "done" {
t.Fatalf("default-timeout Call() = %q, %v", got, err)
}
_ = c3.Close()
}
+1 -1
View File
@@ -33,7 +33,7 @@ func connectMCP(ctx context.Context, m *db.MCPServer) (mcpClient, error) {
return nil, fmt.Errorf("http transport is missing a URL")
}
// env map doubles as HTTP headers (e.g. Authorization).
return mcphttp.New(ctx, m.Name, m.URL, jsonStrMap(m.Env), m.Insecure)
return mcphttp.New(ctx, m.Name, m.URL, jsonStrMap(m.Env), m.Insecure, m.TimeoutSeconds)
case "sse":
if m.URL == "" {
return nil, fmt.Errorf("sse transport is missing a URL")
+2 -2
View File
@@ -55,7 +55,7 @@ func (s *Server) scopeSentryClient(ctx context.Context) (*mcphttp.Client, error)
if m.URL == "" {
return nil, fmt.Errorf("data source %s has no URL configured; configure it first", scopeSentryMCPName)
}
return mcphttp.New(ctx, m.Name, m.URL, jsonStrMap(m.Env), m.Insecure)
return mcphttp.New(ctx, m.Name, m.URL, jsonStrMap(m.Env), m.Insecure, m.TimeoutSeconds)
}
// envHasValue reports whether the env/header map has any non-empty value
@@ -102,7 +102,7 @@ func (s *Server) syncSSStatus(w http.ResponseWriter, r *http.Request) {
if configured && m.Enabled {
ctx, cancel := context.WithTimeout(r.Context(), 12*time.Second)
defer cancel()
if cl, cerr := mcphttp.New(ctx, m.Name, m.URL, jsonStrMap(m.Env), m.Insecure); cerr == nil {
if cl, cerr := mcphttp.New(ctx, m.Name, m.URL, jsonStrMap(m.Env), m.Insecure, m.TimeoutSeconds); cerr == nil {
if _, terr := cl.Tools(ctx); terr == nil {
resp["reachable"] = true
}
+39
View File
@@ -0,0 +1,39 @@
---
name: rea
description: "Reverse-engineer binaries and application packages (ELF, PE, Mach-O, APK, IPA, HAP, WASM, ZIP/JAR containers, JavaScript bundles) through the rea MCP server running on the host. Invoke when a task needs static analysis of a compiled or packaged target: embedded secrets, hardcoded endpoints, native library review, string and constant extraction, managed code structure, or deobfuscation."
mcps: rea
---
# rea: binary and application reverse engineering
The rea MCP server runs on the host machine, not inside this container. Its tools are named `mcp__rea__<tool>`. After this skill loads, their names appear as deferred tools: fetch each tool's schema with SearchExtraTools (for example `select:mcp__rea__open_binary`, or keywords like `rea strings`), then invoke it with ExecuteExtraTool. Use the tool that directly answers the question; do not enumerate the whole catalog.
## 1. File paths: container view vs host view
rea opens files on the HOST filesystem, not inside the container. Only files under the bind-mounted data directory are visible to both sides; the exact host prefix is deployment-specific (check the compose file, or ask the operator via task chat when unknown).
Rules:
- Save or download every target file under the container data directory (for example `/app/data/tasks/<task_id>/rea/`) so both sides can see it. Files elsewhere in the container are invisible to rea.
- Always pass the host-side path in rea tool parameters. If open_binary reports the path as missing, you passed a container path: retry with the host-side equivalent of the same mount.
- Verify the file exists on your side (ls) before opening it with rea.
## 2. Session lifecycle
1. `open_binary` with `path` (host path). For native binaries (ELF, PE, Mach-O) set `provider_id: "ghidra"`; if the error lists candidate provider ids, retry with one of them. Packages and bundles (APK, IPA, HAP, zip, JAR, wasm, JavaScript) need no provider.
2. Run analysis tools (`search_strings`, `list_strings`, `search_procedures`, `inspect_native_*`, `project_managed_application_graph`, family-specific graph tools, ...). Results are Evidence objects: they carry an evidence id, a normalized result, and a limitations section. Record anything task-relevant as facts in the knowledge graph and cite the evidence id in findings.
3. `close_binary` when done. One open target per session; close before opening another. Never leave a binary open at the end of a run.
## 3. What to use it for (authorized targets only)
- Client-side artifacts fetched from an in-scope target: mobile apps (APK, IPA, HAP), desktop binaries, Electron app archives, obfuscated JavaScript bundles, wasm modules.
- Embedded secrets and configuration: API keys, tokens, endpoints, debug flags, test accounts (`search_strings` with patterns like `key`, `token`, `secret`, `password`, `http`, base64 blobs).
- Fingerprinting: sha256 and component inventories identify the exact build and its third-party libraries.
- Deep dives: strings and constants, imports and exports, procedures, managed type graphs, native API usage.
## 4. Practical notes
- Prefer scoped queries (one string pattern, one procedure) over whole-binary deep passes; some analyses take minutes. If the server is configured with a short request timeout, budget accordingly.
- Large results are truncated to the session output limit with spill to disk; narrow the query instead of paging through huge output.
- If a call fails with `capability_unavailable`, read the message: it usually lists the exact candidate provider ids or the remediation to apply.
- rea reads files only; it never executes the target.
- rea cannot see container-only paths; do not try to open files outside the shared data mount.
+15
View File
@@ -32,6 +32,7 @@ type FormState = {
url: string;
env: string;
insecure: boolean;
timeoutSeconds: string;
};
const emptyForm: FormState = {
name: "",
@@ -41,6 +42,7 @@ const emptyForm: FormState = {
url: "",
env: "",
insecure: false,
timeoutSeconds: "",
};
export default function MCPPage() {
@@ -117,6 +119,7 @@ export default function MCPPage() {
url: s.url ?? "",
env: envToText(s.env),
insecure: s.insecure ?? false,
timeoutSeconds: s.timeout_seconds ? String(s.timeout_seconds) : "",
});
setTab("config");
setOpen(true);
@@ -158,6 +161,9 @@ export default function MCPPage() {
args: [] as string[],
env: parseEnv(form.env), // in remote mode env holds request headers
insecure: form.insecure,
...(Number.isFinite(Number(form.timeoutSeconds)) && Number(form.timeoutSeconds) > 0
? { timeout_seconds: Number(form.timeoutSeconds) }
: {}),
}
: {
transport: "stdio" as const,
@@ -306,6 +312,15 @@ export default function MCPPage() {
/>
Skip TLS certificate verification (self-signed certs)
</label>
<Label htmlFor="m-timeout">Request timeout (seconds, default 120)</Label>
<Input
id="m-timeout"
type="number"
min={5}
placeholder="120"
value={form.timeoutSeconds}
onChange={(e) => setF({ timeoutSeconds: e.target.value })}
/>
</div>
)}
<div className="grid gap-2">
+1
View File
@@ -1178,6 +1178,7 @@ export interface MCPServer {
url?: string;
enabled: boolean;
insecure?: boolean; // http: skip TLS cert verification (self-signed servers)
timeout_seconds?: number; // http/sse: per-request timeout in seconds (default 120)
tools?: string[]; // mcp_tools_cache (names only, for the count)
}