feat: rea reverse-engineering skill + per-server MCP HTTP request timeout

Add a rea skill (skill-gated MCP): agents get binary and application package
reverse engineering through a rea MCP server, with tools kept deferred so the
172-tool catalog never bloats the prompt until the skill loads.

Remote MCP servers now take timeout_seconds (default 120): analysis servers
with minutes-long tool calls were cut off mid-call by the fixed 120s client
timeout. Applies to the Streamable HTTP transport; wired through discovery,
the ScopeSentry sync client, the API and the MCP dialog.

Closes #8
This commit is contained in:
BaaRai
2026-10-11 19:02:23 +02:00
parent b17fa7fe3a
commit 24c8ea13c0
9 changed files with 167 additions and 21 deletions
+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()
}