Commit 4f382af
authored
Gateway: enforce allowed-tools filtering server-side on tools/list and tools/call (#3333)
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
(`"tool %q is not in the allowed-tools list for this server"`)
- 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
using the pre-computed `allowedToolSets` — they never appear in
`tools/list` responses and are never registered with the SDK server.
### Config usage
The existing `tools` field on each MCP server entry is the allow-list:
```json
{
"mcpServers": {
"github": {
"type": "stdio",
"container": "ghcr.io/github/github-mcp-server:latest",
"tools": ["search_code", "get_file_contents", "list_issues"]
}
}
}
```
When `tools` is empty or absent, all tools remain accessible (no
behavior change for existing configs).
## Testing
A dedicated `internal/server/allowed_tools_integration_test.go` provides
11 integration tests covering the full enforcement path end-to-end:
- **tools/list filtering**: unified server with a single backend,
multiple independent backends, and no-restriction passthrough
- **tools/call enforcement**: allowed tool executes successfully;
blocked tool returns `IsError: true` with the backend verified to *not*
receive the forwarded request; unrestricted server passes all tools
- **Routed mode**: filtered tools/list and blocked call handler not
registered
- **Helpers**: `buildAllowedToolSets` with multiple servers and
empty/nil config; `isToolAllowed` with real config
Additional unit tests in `call_backend_tool_test.go` and
`tool_registry_test.go` cover the `isToolAllowed` helper,
`callBackendTool` rejection, and registration filtering.File tree
5 files changed
+847
-0
lines changed- internal/server
5 files changed
+847
-0
lines changed
0 commit comments