You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(config): layered config tool filter — config_denied over user toggles (#468)
* feat(config): add enabled_tools/disabled_tools per-server allowlist/denylist
Adds two mutually exclusive fields to ServerConfig that let operators
declare tool visibility statically in mcp_config.json rather than
having to call the API or CLI after every fresh install.
enabled_tools: ["list_issues", "get_issue"] // allowlist — only these visible
disabled_tools: ["delete_repo", "force_push"] // denylist — hide these, allow rest
Config validation rejects a server that has both fields set.
On every applyDifferentialToolUpdate (server connect / tool refresh),
applyConfigToolFilter walks the in-memory config, computes the desired
enabled/disabled state for each discovered tool, and calls
setToolEnabledNoEmit to persist it in BBolt. All existing enforcement
paths (isToolCallable, retrieve_tools pre-ranking, call_tool_*) pick
up the change automatically with no further modifications.
Five unit tests cover: allowlist disables unlisted tools, allowlist
re-enables a tool moved back into the list, denylist disables listed
tools, no-op when neither field is set, and end-to-end integration
through applyDifferentialToolUpdate.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(config): add IsToolAllowedByConfig helper on ServerConfig
* feat(runtime): replace BBolt-writing applyConfigToolFilter with IsToolConfigDenied
Replace the BBolt-writing applyConfigToolFilter function (which overwrote
user preferences on every reconnect, emitted spurious audit events, and had
no provenance) with an evaluation-time IsToolConfigDenied method that:
- Reads config at call time, never writes to BBolt
- Preserves user-set tool preferences and audit trail
- Enables separation of concerns: config vs user intent
Key changes:
- Delete applyConfigToolFilter from tool_quarantine.go (63 lines removed)
- Add IsToolConfigDenied(serverName, toolName string) bool to Runtime
- Remove applyConfigToolFilter call from lifecycle.go
- Rewrite tool_config_filter_test.go: 5 new tests for IsToolConfigDenied
* AllowList: tools not listed are denied
* DenyList: listed tools are denied
* NoFilter: all tools allowed when config has no filter
* UnknownServer: returns false for missing servers
* UserDisabledPreserved: BBolt state is independent from config layer
All 198 runtime tests pass. No behavior change to actual tool visibility—
the config layer is now just evaluated at call time instead of persisted.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(runtime): skip config-denied tools in SetAllToolsEnabled bulk toggle
* feat(server): enforce config tool filter in isToolCallable
Add IsToolConfigDenied delegation on *Server and insert a config-layer
check in isToolCallable so tools denied by enabled_tools/disabled_tools
in the server config are hard-off at MCP call time, evaluated at
runtime without touching BBolt storage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(api): expose config_denied on tool response; reject enabling config-denied tools
- Add ConfigDenied bool field to contracts.Tool (json: config_denied,omitempty)
- Enrich config_denied in handleGetServerTools via IsToolConfigDenied interface
- Return HTTP 409 in handleSetToolEnabled when req.Enabled is true for a config-denied tool
- Remove debug fmt.Printf lines from enrichment loop; use logger.Debug instead
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ui): show locked badge and disable toggle for config-denied tools
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(types): add disabled/config_denied fields to Tool interface
Extended the Tool interface to include runtime fields that the backend
sends and Vue components use: server_name, schema, usage, last_used,
approval_status, disabled, and config_denied. This allows proper typing
of these fields in the frontend instead of using unsafe casts.
Simplified type assertions in isToolConfigDenied and isToolEnabled
functions to use the properly-typed Tool interface directly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(storage): persist EnabledTools/DisabledTools fields through BBolt round-trip
* chore(oas): regenerate OpenAPI spec with config_denied tool field
* chore: trigger GitHub merge-check recalculation
* fix(ux): status-aware TOOL_BLOCKED + lock-badge polish (review #468#1-#4)
#1 (bug): config-denied tools returned the generic 'Tool is disabled
and not callable' over MCP — identical to a user-toggled tool, but the
remediation differs (edit mcp_config.json vs a UI toggle that 409s).
blockedToolMessage() now branches on IsToolConfigDenied and tells the
agent it is operator policy, not user-overridable. Split into a pure
blockedToolMessageFor(bool) with a dedicated unit test.
#2: config-lock badges badge-error (red/alarming) -> badge-neutral + 🔒;
a policy lock is not an error.
#3: 'Enable All' toast now reports 'N tools remain locked by config'
(client-side from config_denied; no API contract change).
#4: unified copy — stray 'config locked' label -> '🔒 locked by config'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
EnabledTools []string`json:"enabled_tools,omitempty" mapstructure:"enabled_tools"`// Allowlist: only these tools are exposed; mutually exclusive with disabled_tools
244
+
DisabledTools []string`json:"disabled_tools,omitempty" mapstructure:"disabled_tools"`// Denylist: these tools are hidden; mutually exclusive with enabled_tools
243
245
}
244
246
245
247
// OAuthConfig represents OAuth configuration for a server
0 commit comments