Skip to content

Commit 2cb5103

Browse files
authored
Gateway: enforce allowed-tools filtering server-side on tools/list and tools/call (#3334)
Agents with raw HTTP access to the gateway could bypass client-side `--allowed-tools` filters by directly sending `tools/call` JSON-RPC requests for tools they shouldn't be able to call. The existing `tools` field in `StdinServerConfig`/`ServerConfig` was parsed but never enforced at runtime. ## Changes ### Pre-computed allowed-tools sets (`unified.go`) - Added `allowedToolSets map[string]map[string]bool` to `UnifiedServer`, built once at init via `buildAllowedToolSets(cfg)` for O(1) per-call lookup - Added `isToolAllowed(serverID, toolName)` — returns `true` when no list is configured (unrestricted) ### Enforcement in `callBackendTool` (`unified.go`) Before any DIFC/guard work, rejects calls for tools not in the allowed set: - Returns `IsError: true` `CallToolResult` with a descriptive message - Sets OTEL span HTTP status to 403 - Logs at WARN with `logger.LogWarn("client", ...)` including the server ID ### tools/list defense-in-depth (`tool_registry.go`) During backend tool registration, non-allowed tools are filtered out — they never appear in `tools/list` responses and are never registered with the SDK server. ### Lint fix - Removed unused `sendUnifiedMCPRequest` and `parseSSEBody` helper functions from `allowed_tools_integration_test.go` (golangci-lint `unused` violations)
2 parents 1bc9de3 + 1873a07 commit 2cb5103

File tree

1 file changed

+0
-49
lines changed

1 file changed

+0
-49
lines changed

internal/server/allowed_tools_integration_test.go

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,10 @@ package server
1010
// - No restriction when Tools list is absent
1111

1212
import (
13-
"bytes"
1413
"context"
1514
"encoding/json"
16-
"io"
1715
"net/http"
1816
"net/http/httptest"
19-
"strings"
2017
"testing"
2118
"time"
2219

@@ -80,52 +77,6 @@ func newMockMCPBackendWithTools(t *testing.T, serverID string, toolNames []strin
8077
}))
8178
}
8279

83-
// sendUnifiedMCPRequest sends a JSON-RPC request to the unified /mcp endpoint
84-
// and returns the parsed JSON response. It follows the SSE envelope if present.
85-
func sendUnifiedMCPRequest(t *testing.T, serverURL string, payload map[string]interface{}) map[string]interface{} {
86-
t.Helper()
87-
data, err := json.Marshal(payload)
88-
require.NoError(t, err)
89-
90-
req, err := http.NewRequest("POST", serverURL+"/mcp", bytes.NewBuffer(data))
91-
require.NoError(t, err)
92-
req.Header.Set("Content-Type", "application/json")
93-
req.Header.Set("Accept", "application/json, text/event-stream")
94-
req.Header.Set("Authorization", "test-token")
95-
96-
client := &http.Client{Timeout: 5 * time.Second}
97-
resp, err := client.Do(req)
98-
require.NoError(t, err)
99-
defer resp.Body.Close()
100-
101-
body, err := io.ReadAll(resp.Body)
102-
require.NoError(t, err)
103-
104-
bodyStr := string(body)
105-
ct := resp.Header.Get("Content-Type")
106-
if strings.Contains(ct, "text/event-stream") {
107-
return parseSSEBody(t, bodyStr)
108-
}
109-
var result map[string]interface{}
110-
require.NoError(t, json.Unmarshal(body, &result), "failed to parse response: %s", bodyStr)
111-
return result
112-
}
113-
114-
// parseSSEBody extracts the first JSON payload from an SSE-encoded response body.
115-
func parseSSEBody(t *testing.T, body string) map[string]interface{} {
116-
t.Helper()
117-
for _, line := range strings.Split(body, "\n") {
118-
if strings.HasPrefix(line, "data: ") {
119-
var result map[string]interface{}
120-
if err := json.Unmarshal([]byte(strings.TrimPrefix(line, "data: ")), &result); err == nil {
121-
return result
122-
}
123-
}
124-
}
125-
t.Fatalf("no JSON data line found in SSE body: %s", body)
126-
return nil
127-
}
128-
12980
// ----- tools/list filtering integration tests -----------------------------
13081

13182
// TestAllowedTools_ToolsListFiltered_UnifiedServer verifies that tools NOT in the

0 commit comments

Comments
 (0)