on main help sign in
feat(tools): create an allow list for MCP tools (#2800)
959bf7eBruno Krugel
internal/agent/tools/mcp/tools.go+24−11
@@ -142,7 +142,10 @@ func getTools(ctx context.Context, session *ClientSession) ([]*Tool, error) {
142}
142}
143
143
144func updateTools(cfg *config.ConfigStore, name string, tools []*Tool) int {
144func updateTools(cfg *config.ConfigStore, name string, tools []*Tool) int {
145− tools = filterDisabledTools(cfg, name, tools)
145+ mcpCfg, ok := cfg.Config().MCP[name]
146+ if ok {
147+ tools = filterTools(mcpCfg, tools)
148+ }
146 if len(tools) == 0 {
149 if len(tools) == 0 {
147 allTools.Del(name)
150 allTools.Del(name)
148 return 0
151 return 0
@@ -151,20 +154,30 @@ func updateTools(cfg *config.ConfigStore, name string, tools []*Tool) int {
151 return len(tools)
154 return len(tools)
152}
155}
153
156
154−// filterDisabledTools removes tools that are disabled via config.
157+// filterTools filters tools based on enabled_tools (allow list) and
155−func filterDisabledTools(cfg *config.ConfigStore, mcpName string, tools []*Tool) []*Tool {
158+// disabled_tools (deny list) from the MCP config.
156− mcpCfg, ok := cfg.Config().MCP[mcpName]
159+func filterTools(mcpCfg config.MCPConfig, tools []*Tool) []*Tool {
157− if !ok || len(mcpCfg.DisabledTools) == 0 {
160+ if len(mcpCfg.EnabledTools) > 0 {
158− return tools
161+ filtered := make([]*Tool, 0, len(mcpCfg.EnabledTools))
162+ for _, tool := range tools {
163+ if slices.Contains(mcpCfg.EnabledTools, tool.Name) {
164+ filtered = append(filtered, tool)
165+ }
166+ }
167+ tools = filtered
159 }
168 }
160
169
170+ if len(mcpCfg.DisabledTools) > 0 {
161− filtered := make([]*Tool, 0, len(tools))
171+ filtered := make([]*Tool, 0, len(tools))
162− for _, tool := range tools {
172+ for _, tool := range tools {
163− if !slices.Contains(mcpCfg.DisabledTools, tool.Name) {
173+ if !slices.Contains(mcpCfg.DisabledTools, tool.Name) {
164− filtered = append(filtered, tool)
174+ filtered = append(filtered, tool)
175+ }
165 }
176 }
177+ tools = filtered
166 }
178 }
179+
167− return filtered
180+ return tools
168}
181}
169
182
170// ensureRawBytes normalizes MCP media data into raw binary bytes.
183// ensureRawBytes normalizes MCP media data into raw binary bytes.
internal/agent/tools/mcp/tools_test.go+48
@@ -5,6 +5,7 @@ import (
5 "encoding/base64"
5 "encoding/base64"
6 "testing"
6 "testing"
7
7
8+ "github.com/charmbracelet/crush/internal/config"
8 "github.com/stretchr/testify/require"
9 "github.com/stretchr/testify/require"
9)
10)
10
11
@@ -67,3 +68,50 @@ func TestEnsureRawBytes(t *testing.T) {
67 })
68 })
68 }
69 }
69}
70}
71+
72+func TestFilterTools(t *testing.T) {
73+ t.Parallel()
74+
75+ tools := []*Tool{
76+ {Name: "tool_a"},
77+ {Name: "tool_b"},
78+ {Name: "tool_c"},
79+ }
80+
81+ t.Run("no filters returns all tools", func(t *testing.T) {
82+ t.Parallel()
83+ result := filterTools(config.MCPConfig{}, tools)
84+ require.Len(t, result, 3)
85+ })
86+
87+ t.Run("disabled tools filters deny list", func(t *testing.T) {
88+ t.Parallel()
89+ result := filterTools(config.MCPConfig{DisabledTools: []string{"tool_a"}}, tools)
90+ require.Len(t, result, 2)
91+ require.Equal(t, "tool_b", result[0].Name)
92+ require.Equal(t, "tool_c", result[1].Name)
93+ })
94+
95+ t.Run("enabled tools acts as allow list", func(t *testing.T) {
96+ t.Parallel()
97+ result := filterTools(config.MCPConfig{EnabledTools: []string{"tool_b"}}, tools)
98+ require.Len(t, result, 1)
99+ require.Equal(t, "tool_b", result[0].Name)
100+ })
101+
102+ t.Run("enabled and disabled both apply", func(t *testing.T) {
103+ t.Parallel()
104+ result := filterTools(config.MCPConfig{
105+ EnabledTools: []string{"tool_a", "tool_b"},
106+ DisabledTools: []string{"tool_b"},
107+ }, tools)
108+ require.Len(t, result, 1)
109+ require.Equal(t, "tool_a", result[0].Name)
110+ })
111+
112+ t.Run("enabled with non-existent tool returns empty", func(t *testing.T) {
113+ t.Parallel()
114+ result := filterTools(config.MCPConfig{EnabledTools: []string{"non_existent"}}, tools)
115+ require.Len(t, result, 0)
116+ })
117+}
internal/config/config.go+1
@@ -188,6 +188,7 @@ type MCPConfig struct {
188 URL string `json:"url,omitempty" jsonschema:"description=URL for HTTP or SSE MCP servers,format=uri,example=http://localhost:3000/mcp"`
188 URL string `json:"url,omitempty" jsonschema:"description=URL for HTTP or SSE MCP servers,format=uri,example=http://localhost:3000/mcp"`
189 Disabled bool `json:"disabled,omitempty" jsonschema:"description=Whether this MCP server is disabled,default=false"`
189 Disabled bool `json:"disabled,omitempty" jsonschema:"description=Whether this MCP server is disabled,default=false"`
190 DisabledTools []string `json:"disabled_tools,omitempty" jsonschema:"description=List of tools from this MCP server to disable,example=get-library-doc"`
190 DisabledTools []string `json:"disabled_tools,omitempty" jsonschema:"description=List of tools from this MCP server to disable,example=get-library-doc"`
191+ EnabledTools []string `json:"enabled_tools,omitempty" jsonschema:"description=Allow list of tools from this MCP server,example=get-library-doc"`
191 Timeout int `json:"timeout,omitempty" jsonschema:"description=Timeout in seconds for MCP server connections,default=15,example=30,example=60,example=120"`
192 Timeout int `json:"timeout,omitempty" jsonschema:"description=Timeout in seconds for MCP server connections,default=15,example=30,example=60,example=120"`
192
193
193 // Headers are HTTP headers for HTTP/SSE MCP servers. Values run
194 // Headers are HTTP headers for HTTP/SSE MCP servers. Values run
schema.json+10
@@ -264,6 +264,16 @@
264 "type": "array",
264 "type": "array",
265 "description": "List of tools from this MCP server to disable"
265 "description": "List of tools from this MCP server to disable"
266 },
266 },
267+ "enabled_tools": {
268+ "items": {
269+ "type": "string",
270+ "examples": [
271+ "get-library-doc"
272+ ]
273+ },
274+ "type": "array",
275+ "description": "Allow list of tools from this MCP server"
276+ },
267 "timeout": {
277 "timeout": {
268 "type": "integer",
278 "type": "integer",
269 "description": "Timeout in seconds for MCP server connections",
279 "description": "Timeout in seconds for MCP server connections",