toolchest 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9c78a7f55b37d501c0e2d5c4e164048ea4d176f2eb7126c232953a1c4c52e1da
4
- data.tar.gz: 403d4c3b8ee52953630e52f0d4d31f1a31e9d406511fdfdd7c4f47a67f083bf6
3
+ metadata.gz: 545413528704477fdfc39328f3380e82b1f80c9be8096bc36fe201cddf90ce02
4
+ data.tar.gz: 4beea37ac5456c21b9b4ec41711643f5c1b2155669e2370badb2e03ad45c0b49
5
5
  SHA512:
6
- metadata.gz: c4c8ca2520fc43e69398fffa2f25a08db7ec0566c7f9453f5fb5d442744e4c42be5ac53ffcb037ad5181a4bac8395970f0ea9b1c925fd6cfe6ab044f1e097b72
7
- data.tar.gz: b0fd64277dd46191d91681b7c3cbaf14b70b03ac80324fe9d1a01e6b2455486e2d6c6c3c2dbd214430be2867d7f5967538f6ad4a2d76ed855664c44d14c0da65
6
+ metadata.gz: 60ef42ecc96cecf2c0a9b9ea26cf786dc72ced48fafa1840cc56f53141657f9791fc2ec2446dc328f6463aaca9cc37564f9365e8a7c851b30256cdb8fb9e3669
7
+ data.tar.gz: a8ea33024b69f8154a78d7a4664afdbddc57e01b87c4daa5f01b64dc5a8d1191b44004d9fedd060a57a013bc87cc14f8c462be683d08d82d78708d16210d98e1
data/LLMS.txt CHANGED
@@ -213,7 +213,12 @@ end
213
213
 
214
214
  Types: `:string`, `:integer`, `:number`, `:boolean`, `:object`, `[:object]`, `[:string]`, etc.
215
215
 
216
- Options on `tool`: `name: "custom_tool_name"`, `access: :read` or `access: :write` (for scope filtering + annotations), `annotations: { openWorldHint: true }` (override hints).
216
+ Options on `tool`:
217
+
218
+ - `name: "custom_tool_name"` — override generated tool name
219
+ - `access: :read` or `access: :write` — scope filtering + MCP annotations
220
+ - `scope: "admin"` or `scope: ["admin", "superuser"]` — per-tool scope override (see below)
221
+ - `annotations: { openWorldHint: true }` — override MCP hints
217
222
 
218
223
  `access: :read` → `readOnlyHint: true, destructiveHint: false`. `access: :write` → `readOnlyHint: false, destructiveHint: true`.
219
224
 
@@ -345,6 +350,22 @@ Scopes filter `tools/list` — clients only see tools their token allows. Fails
345
350
 
346
351
  Disable: `config.filter_tools_by_scope = false`
347
352
 
353
+ ### Per-tool scope override
354
+
355
+ When a tool doesn't fit its toolbox's scope boundary, set `scope:` to bypass the convention:
356
+
357
+ ```ruby
358
+ tool "Move ticket", scope: "admin" do # only "admin" scope grants access
359
+ param :status, :string, "New status"
360
+ end
361
+
362
+ tool "Escalate", scope: ["admin", "superuser"] do # either scope works (OR)
363
+ param :id, :string, "ID"
364
+ end
365
+ ```
366
+
367
+ `scope:` replaces convention matching for that tool. `tickets:write` won't grant access to a tool with `scope: "admin"`. Enforced on both `tools/list` and `tools/call`.
368
+
348
369
  ### Optional scopes (checkboxes on consent)
349
370
 
350
371
  ```ruby
data/README.md CHANGED
@@ -421,6 +421,32 @@ end
421
421
 
422
422
  Turn it off: `config.filter_tools_by_scope = false`
423
423
 
424
+ ### Per-tool scope override
425
+
426
+ The convention derives scopes from the toolbox name — `OrdersToolbox` tools require `orders:*`. When a tool doesn't fit its toolbox's scope boundary, override it:
427
+
428
+ ```ruby
429
+ class TicketsToolbox < ApplicationToolbox
430
+ tool "List tickets" do
431
+ end
432
+ def list = # ... requires tickets:read (convention)
433
+
434
+ # Only tokens with the "admin" scope can see or call this tool
435
+ tool "Move ticket", scope: "admin" do
436
+ param :status, :string, "New status"
437
+ end
438
+ def move = # ...
439
+
440
+ # Either "admin" OR "superuser" grants access
441
+ tool "Escalate ticket", scope: ["admin", "superuser"] do
442
+ param :id, :string, "Ticket ID"
443
+ end
444
+ def escalate = # ...
445
+ end
446
+ ```
447
+
448
+ `scope:` replaces the convention entirely for that tool — `tickets:write` won't grant access to a tool with `scope: "admin"`. The token must include the literal scope string. Enforced on both `tools/list` (visibility) and `tools/call` (execution).
449
+
424
450
  ### Optional scopes (checkboxes)
425
451
 
426
452
  By default, the consent screen is all-or-nothing — approve all requested scopes or deny. Enable `optional_scopes` and users get checkboxes:
@@ -219,6 +219,10 @@ module Toolchest
219
219
  def tool_allowed_by_scopes?(tool_definition, scopes)
220
220
  return true if scopes.empty?
221
221
 
222
+ if tool_definition.scope
223
+ return tool_definition.scope.any? { |s| scopes.include?(s) }
224
+ end
225
+
222
226
  prefix = tool_definition.toolbox_class.controller_name.split("/").last
223
227
  tool_access = tool_definition.access_level ||
224
228
  (READ_ACTIONS.include?(tool_definition.method_name) ? :read : :write)
@@ -1,14 +1,15 @@
1
1
  module Toolchest
2
2
  class ToolDefinition
3
- attr_reader :method_name, :description, :params, :toolbox_class, :custom_name, :access_level, :annotations
3
+ attr_reader :method_name, :description, :params, :toolbox_class, :custom_name, :access_level, :scope, :annotations
4
4
 
5
- def initialize(method_name:, description:, params:, toolbox_class:, custom_name: nil, access_level: nil, annotations: nil)
5
+ def initialize(method_name:, description:, params:, toolbox_class:, custom_name: nil, access_level: nil, scope: nil, annotations: nil)
6
6
  @method_name = method_name.to_sym
7
7
  @description = description
8
8
  @params = params
9
9
  @toolbox_class = toolbox_class
10
10
  @custom_name = custom_name
11
11
  @access_level = access_level
12
+ @scope = scope ? Array(scope) : nil
12
13
  @annotations = annotations
13
14
  end
14
15
 
@@ -46,10 +46,10 @@ module Toolchest
46
46
  .flat_map { |a| a.send(:own_prompts) }
47
47
  end
48
48
 
49
- def tool(description, name: nil, access: nil, annotations: nil, &block)
49
+ def tool(description, name: nil, access: nil, scope: nil, annotations: nil, &block)
50
50
  builder = ToolBuilder.new
51
51
  builder.instance_eval(&block) if block
52
- @_pending_tool = { description: description, custom_name: name, access_level: access, annotations: annotations, builder: builder }
52
+ @_pending_tool = { description:, custom_name: name, access_level: access, scope:, annotations:, builder: }
53
53
  end
54
54
 
55
55
  def default_param(name, type, description = "", **options)
@@ -110,6 +110,7 @@ module Toolchest
110
110
  toolbox_class: self,
111
111
  custom_name: pending[:custom_name],
112
112
  access_level: pending[:access_level],
113
+ scope: pending[:scope],
113
114
  annotations: pending[:annotations]
114
115
  )
115
116
 
@@ -1,3 +1,3 @@
1
1
  module Toolchest
2
- VERSION = "0.3.2"
2
+ VERSION = "0.3.3"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: toolchest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nora