mcp_toolkit 0.3.0 → 0.4.0
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 +4 -4
- data/CHANGELOG.md +201 -2
- data/README.md +315 -34
- data/config/routes.rb +19 -4
- data/lib/mcp_toolkit/auth/authority.rb +2 -2
- data/lib/mcp_toolkit/authority/composite_tool_provider.rb +32 -0
- data/lib/mcp_toolkit/authority/context.rb +45 -0
- data/lib/mcp_toolkit/authority/controller_methods.rb +408 -0
- data/lib/mcp_toolkit/authority/registry_tool_provider.rb +70 -0
- data/lib/mcp_toolkit/authority/token.rb +124 -0
- data/lib/mcp_toolkit/authority/tools/base.rb +122 -0
- data/lib/mcp_toolkit/authority/tools/get.rb +58 -0
- data/lib/mcp_toolkit/authority/tools/list.rb +84 -0
- data/lib/mcp_toolkit/authority/tools/resource_schema.rb +44 -0
- data/lib/mcp_toolkit/authority/tools/resources.rb +33 -0
- data/lib/mcp_toolkit/authority.rb +24 -0
- data/lib/mcp_toolkit/configuration.rb +205 -2
- data/lib/mcp_toolkit/dispatcher.rb +234 -0
- data/lib/mcp_toolkit/engine.rb +22 -7
- data/lib/mcp_toolkit/engine_controllers.rb +116 -0
- data/lib/mcp_toolkit/gateway/aggregator.rb +122 -0
- data/lib/mcp_toolkit/gateway/client.rb +305 -0
- data/lib/mcp_toolkit/gateway/proxy.rb +70 -0
- data/lib/mcp_toolkit/gateway/unknown_upstream.rb +8 -0
- data/lib/mcp_toolkit/gateway/upstream_call_error.rb +23 -0
- data/lib/mcp_toolkit/gateway/upstream_registry.rb +79 -0
- data/lib/mcp_toolkit/list_executor.rb +17 -0
- data/lib/mcp_toolkit/protocol.rb +106 -0
- data/lib/mcp_toolkit/rate_limiter.rb +73 -0
- data/lib/mcp_toolkit/registry.rb +30 -2
- data/lib/mcp_toolkit/resource.rb +125 -7
- data/lib/mcp_toolkit/resource_schema.rb +14 -1
- data/lib/mcp_toolkit/serializer/base.rb +2 -2
- data/lib/mcp_toolkit/session.rb +17 -9
- data/lib/mcp_toolkit/tools/authority_base.rb +127 -0
- data/lib/mcp_toolkit/transport/controller_methods.rb +2 -2
- data/lib/mcp_toolkit/usage_metering/recorder.rb +95 -0
- data/lib/mcp_toolkit/version.rb +1 -1
- data/lib/mcp_toolkit.rb +16 -3
- metadata +38 -2
- data/app/controllers/mcp_toolkit/server_controller.rb +0 -19
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e99039b1f2109b3da40c5d271338fbefc565bce7fb472b36947caf322d0ab3f8
|
|
4
|
+
data.tar.gz: db79d656f89ce7566e5437956a8f0feac0ba9f4a906896078db3cee4879b512b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '0804f99b21ac1cf54aa5d379957b1a3bec5af4a2aa3630bb2c0210d3b594cb98cf56dc3af4cc3693e1d872b2855ade1f1f2dc97a03760f56d16af85994f56547'
|
|
7
|
+
data.tar.gz: 07626c80512de4d475cd1cd4b7fbe31a210718dbc8a5d2b81a97009900f8cc1df91b79cae903bfc1b0769a049ab224232cccadfd2b86f61c50ce83e5e08766f5
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,201 @@
|
|
|
1
|
+
## [0.4.0] - 2026-07-06
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- **Authority dispatch path** — a hand-rolled JSON-RPC front-end for a first-party
|
|
6
|
+
server that authenticates tokens locally and serves its OWN tools (and, as a
|
|
7
|
+
gateway, aggregates + proxies upstreams) WITHOUT the official `mcp` SDK in the
|
|
8
|
+
request path. It coexists with the SDK-backed satellite path
|
|
9
|
+
(`McpToolkit::Server.build`) by design — the gem now carries two dispatch
|
|
10
|
+
front-ends, each for its role. The satellite path is unchanged.
|
|
11
|
+
- `McpToolkit::Protocol` — JSON-RPC constants + envelope helpers
|
|
12
|
+
(`SUPPORTED_VERSIONS`, `LATEST_VERSION`, `JSONRPC_VERSION`, `ErrorCodes`,
|
|
13
|
+
`Error` + subclasses with `#code`/`#data`/`#to_h`, `success_response` /
|
|
14
|
+
`error_response`). The byte contract of a first-party endpoint's error
|
|
15
|
+
envelope + version negotiation.
|
|
16
|
+
- `McpToolkit::Dispatcher` — `new(context:, config:)` + `#handle_request(request)`.
|
|
17
|
+
Dispatches `initialize` / `initialized` / `tools/list` / `tools/call` / `ping`
|
|
18
|
+
and the custom `notifications/<app>/tools/list_changed` cache-bust. `tools/list`
|
|
19
|
+
merges the host's own tool definitions with the gateway's namespaced upstream
|
|
20
|
+
tools; `tools/call` routes a namespaced name to `Gateway::Proxy` (translating
|
|
21
|
+
`UnknownUpstream` → method-not-found and relaying an upstream JSON-RPC error
|
|
22
|
+
verbatim) or a host tool (scope-gated) otherwise. Server identity + negotiated
|
|
23
|
+
versions come from config; NO SDK touchpoint.
|
|
24
|
+
- `McpToolkit::Authority::ControllerMethods` — the authority transport as an
|
|
25
|
+
includable concern. Every billing/tenancy step is an overridable hook
|
|
26
|
+
(`mcp_authenticate!`, `mcp_rate_limit!`, `mcp_track_usage`, `mcp_flush_usage`,
|
|
27
|
+
`mcp_resolve_account`, `mcp_session_data`, `mcp_dispatch`, `mcp_health_payload`,
|
|
28
|
+
`mcp_config`), each defaulting to a config callable so a PURE host needs no
|
|
29
|
+
subclass. The per-request loop RE-RESOLVES the account for every JSON-RPC call
|
|
30
|
+
— including each element of a batch — so a mixed-account batch still meters one
|
|
31
|
+
usage event per call against the right account (the batch is never delegated to
|
|
32
|
+
a bulk handler that couldn't re-resolve per element).
|
|
33
|
+
- `McpToolkit::Authority::ServerController` — a base controller (concern wired in,
|
|
34
|
+
lazily-parented) a host subclasses when its hooks touch app models (the
|
|
35
|
+
recommended path).
|
|
36
|
+
- `McpToolkit::Authority::Context` — the per-request context threaded into the
|
|
37
|
+
dispatcher + tools: `account`, `principal`, `bearer_token`, and a derived
|
|
38
|
+
`superuser?` (duck-typed off the principal).
|
|
39
|
+
- `McpToolkit::Tools::AuthorityBase` — an optional base for a host's own tools
|
|
40
|
+
(class DSL `tool_name` / `description` / `input_schema` /
|
|
41
|
+
`required_permissions_scope` / `definition`; `.call(context:, **arguments)`
|
|
42
|
+
entry; context accessors; `ensure_resource_accessible!`; ArgumentError →
|
|
43
|
+
InvalidParams / StandardError → InternalError mapping). Host tools plug in
|
|
44
|
+
through the api-agnostic `config.tool_provider` seam — the gem never references
|
|
45
|
+
a host's API layer, serializers, or resource catalog.
|
|
46
|
+
- **`config.tool_provider`** — the api-agnostic tool seam. Duck-typed:
|
|
47
|
+
`provider.tool_definitions(context) -> [{ name:, description:, inputSchema: }]`
|
|
48
|
+
(context lets the host hide superuser-only tools) and `provider.find(name) -> a
|
|
49
|
+
tool object` (responding to `#required_permissions_scope` + `#call(context:,
|
|
50
|
+
**arguments)`). The dispatcher enforces the per-tool scope gate CENTRALLY.
|
|
51
|
+
- **Registry-backed authority tools** — the authority-path counterpart to the
|
|
52
|
+
satellite's SDK tools, so a first-party server can serve the SAME four generic
|
|
53
|
+
read tools (`resources` / `resource_schema` / `get` / `list`) over
|
|
54
|
+
`config.registry` through the hand-rolled dispatcher, reusing the existing
|
|
55
|
+
`ListExecutor` / `GetExecutor` / `ResourceSchema` / `Serialization` /
|
|
56
|
+
`FieldSelection` / `Filtering` UNCHANGED.
|
|
57
|
+
- `McpToolkit::Authority::RegistryToolProvider.new(config:)` — a `tool_provider`
|
|
58
|
+
serving the four generic tools; `find(name)` returns a tool instance, and each
|
|
59
|
+
tool declares NO static scope (the per-resource scope is enforced dynamically
|
|
60
|
+
at call time). The satellite SDK tool path (`McpToolkit::Tools::*`,
|
|
61
|
+
`McpToolkit::Server`) is untouched — this is added alongside it.
|
|
62
|
+
- `McpToolkit::Authority::Tools::{Resources,ResourceSchema,Get,List}` — the four
|
|
63
|
+
thin tools. Each resolves the `resource` argument against the registry
|
|
64
|
+
(InvalidParams for unknown), gates a `superusers_only?` resource against
|
|
65
|
+
`context.superuser?` (REFUSE in get/list/resource_schema, HIDE in resources),
|
|
66
|
+
gates the resource's `required_scope_for` against the principal, and (get/list)
|
|
67
|
+
requires a resolved `context.account`. Returns a raw Hash for the dispatcher to
|
|
68
|
+
wrap — distinct by design from the satellite tools' `MCP::Tool::Response`.
|
|
69
|
+
- `McpToolkit::Authority::CompositeToolProvider.new(*providers)` — composes
|
|
70
|
+
several providers (e.g. the RegistryToolProvider + a host's bespoke tools)
|
|
71
|
+
behind one `config.tool_provider`: `tool_definitions` concatenates in order,
|
|
72
|
+
`find` returns the first match.
|
|
73
|
+
- `config.generic_tool_name_prefix` (default `""`) — namespaces the four generic
|
|
74
|
+
Registry-backed tools. When set (e.g. `"foo_"`) the provider advertises and
|
|
75
|
+
resolves them as `foo_resources` / `foo_resource_schema` / `foo_get` /
|
|
76
|
+
`foo_list`, letting a host keep stable, namespaced tool names for existing
|
|
77
|
+
clients; the empty default keeps the bare base names.
|
|
78
|
+
- **`McpToolkit::Resource` generic seams** (all api-agnostic) — `superusers_only!`
|
|
79
|
+
/ `superusers_only?` (authority tools honor it), `note(text)` + reader (surfaced
|
|
80
|
+
by `resource_schema`), and `filter(name, type:, description:, &applier)` +
|
|
81
|
+
`custom_filters` — a resource-specific filter whose block narrows the scoped
|
|
82
|
+
relation from a TOP-LEVEL request param, so a host can express a relational
|
|
83
|
+
filter the generic equality/operator allowlist can't. `ListExecutor` applies the
|
|
84
|
+
matching custom filters BEFORE the allowlist filters (its only change).
|
|
85
|
+
- **`McpToolkit::ResourceSchema` enrichment** — each attribute now advertises the
|
|
86
|
+
filter `operators` it accepts (derived from `Filtering::OPERATORS_BY_TYPE`), and
|
|
87
|
+
the resource `note` is passed through, so a client can discover exactly which
|
|
88
|
+
`{ op:, value: }` conditions `list` will accept.
|
|
89
|
+
- **Server-vs-gateway identity split** — `config.gateway_client_name` /
|
|
90
|
+
`gateway_client_version` (each defaulting to `server_name` / `server_version`).
|
|
91
|
+
`Gateway::Client`'s handshake `clientInfo` now reads the GATEWAY identity, so an
|
|
92
|
+
authority can advertise its own `server_name` to its callers while keeping its
|
|
93
|
+
upstream handshake byte-identical.
|
|
94
|
+
- **`config.supported_protocol_versions`** (default
|
|
95
|
+
`McpToolkit::Protocol::SUPPORTED_VERSIONS`) — the version set the authority
|
|
96
|
+
dispatcher negotiates.
|
|
97
|
+
- **`config.rate_limiter` / `usage_recorder` / `usage_flusher`** — the authority
|
|
98
|
+
transport's billing hooks as config callables (all default `nil` / no-op).
|
|
99
|
+
- **Built-in rate limiting** — `McpToolkit::RateLimiter`, a fixed-window
|
|
100
|
+
per-principal counter backed by `config.cache_store`, plus
|
|
101
|
+
`config.rate_limit_max_requests` (Integer, default `nil` = OFF) and
|
|
102
|
+
`config.rate_limit_window` (seconds, default `3600`). When a cap is set, the
|
|
103
|
+
authority transport's `mcp_rate_limit!` counts each request, sets the
|
|
104
|
+
`X-RateLimit-Limit` / `X-RateLimit-Remaining` / `X-RateLimit-Reset` headers on
|
|
105
|
+
every capped response, and over the limit renders a JSON-RPC error (code
|
|
106
|
+
`-32029`) at HTTP `429` with a `Retry-After` header. Two new overridable hooks —
|
|
107
|
+
`mcp_rate_limit_max_requests` (default `config.rate_limit_max_requests`) and
|
|
108
|
+
`mcp_rate_limit_key` (default `mcp_principal.id`) — let a host keep the cap in
|
|
109
|
+
its own constant/model or bucket the counter differently. `config.rate_limiter`
|
|
110
|
+
remains as an escape hatch that fully replaces the built-in when set. A pure
|
|
111
|
+
host that sets no cap is unaffected.
|
|
112
|
+
- **`config.superuser_resolver`** — an optional `->(principal) -> Boolean` making
|
|
113
|
+
superuser a first-class, OPTIONAL gem concept. `Authority::Context#superuser?`
|
|
114
|
+
calls it when set, else falls back to duck-typing `principal.superuser?` (false
|
|
115
|
+
when the principal doesn't respond to it). Together with the existing
|
|
116
|
+
`superusers_only!` resource flag and the authority tools' gating, this
|
|
117
|
+
formalizes superuser gating; the default (no resolver, no superuser-aware
|
|
118
|
+
principal) is "no superusers".
|
|
119
|
+
- **Lazy `parent_controller` (Constraint B)** — the engine's `ServerController` /
|
|
120
|
+
`TokensController` and the authority `ServerController` are no longer eager-
|
|
121
|
+
loadable files; they are built from the CURRENT config by
|
|
122
|
+
`McpToolkit.build_engine_controllers!`, triggered lazily via `const_missing` and
|
|
123
|
+
reset on each reload by the engine's `config.to_prepare`. The parent is therefore
|
|
124
|
+
read only at build time — after the host's initializers/to_prepare — so a host's
|
|
125
|
+
whole MCP initializer can live in `to_prepare`. `TokensController#introspect`
|
|
126
|
+
behavior is preserved exactly.
|
|
127
|
+
|
|
128
|
+
### Fixed
|
|
129
|
+
|
|
130
|
+
- **Authority boundary returns JSON-RPC errors for bad input, not a 500** — a
|
|
131
|
+
malformed JSON body now maps to a JSON-RPC parse error (`-32700`) via a
|
|
132
|
+
`respond_to?`-guarded `rescue_from` (fires even from the session before_action),
|
|
133
|
+
and a non-object request or batch element maps to `invalid_request` instead of
|
|
134
|
+
raising a `NoMethodError` in the per-call loop.
|
|
135
|
+
- **`initialize` advertises `instructions`** — the authority dispatcher now
|
|
136
|
+
includes `config.server_instructions` in the `initialize` result when set
|
|
137
|
+
(omitted when nil), matching the SDK-backed satellite server and the documented
|
|
138
|
+
contract.
|
|
139
|
+
- **Gateway tool-list cache is contract-enforced, not assumption-based** — the
|
|
140
|
+
per-upstream list cache is keyed by upstream only, which is safe only when every
|
|
141
|
+
upstream's `tools/list` is caller-independent. That is now an explicit
|
|
142
|
+
registration contract: an upstream that filters its list by caller privilege
|
|
143
|
+
registers `public_tool_list: false` and is pulled live per request (never
|
|
144
|
+
cached), so a privileged caller's list can't leak to an unprivileged one.
|
|
145
|
+
|
|
146
|
+
### Removed
|
|
147
|
+
|
|
148
|
+
- The engine's `app/controllers/mcp_toolkit/{server_controller,tokens_controller}.rb`
|
|
149
|
+
files, replaced by the lazy builder above (their routes + behavior are unchanged).
|
|
150
|
+
|
|
151
|
+
- **Gateway / upstream layer** (`McpToolkit::Gateway::*`) — the generic,
|
|
152
|
+
SDK-independent machinery a central app uses to aggregate *other* MCP servers
|
|
153
|
+
and proxy calls to them, previously an app-only concern. All app-specific values
|
|
154
|
+
(upstream URLs, account-selector meta key, logger, timeouts) are injected via
|
|
155
|
+
`McpToolkit::Configuration`; nothing in the layer names a deployment.
|
|
156
|
+
- `McpToolkit::Gateway::UpstreamRegistry` — a PER-CONFIG registry of upstream
|
|
157
|
+
servers (`Upstream = Data.define(:key, :url, :public_tool_list)` with
|
|
158
|
+
`#name_for`), exposed as `config.upstreams` and reset with a fresh config (test
|
|
159
|
+
isolation for free). API: `#register(key:, url:, public_tool_list: true)` (blank
|
|
160
|
+
url ignored), `#reset!`, `#all`, `#find`, `#split_tool_name`. Config sugar:
|
|
161
|
+
`config.register_upstream(key:, url:, public_tool_list: true)`.
|
|
162
|
+
- `McpToolkit::Gateway::Client` — a minimal Streamable-HTTP MCP client
|
|
163
|
+
(`#tools_list`, `#tools_call`) with single-shot session-loss recovery (HTTP
|
|
164
|
+
404 / JSON-RPC `-32001`), SSE `data:` unwrapping, and content negotiation. Its
|
|
165
|
+
`Client::Error` (< `McpToolkit::Error`) carries `jsonrpc_error` / `http_status`
|
|
166
|
+
and references NO transport/protocol-error class — the consumer maps it. The
|
|
167
|
+
handshake `clientInfo` and protocol version come from config
|
|
168
|
+
(`DEFAULT_PROTOCOL_VERSION` falls back to the wrapped `mcp` SDK's latest).
|
|
169
|
+
- `McpToolkit::Gateway::Aggregator` — namespaces + caches (`config.cache_store`,
|
|
170
|
+
`config.upstream_list_ttl`) each upstream's tool list, pulled CONCURRENTLY via
|
|
171
|
+
concurrent-ruby (wrapped in `Rails.application.executor` when a booted Rails app
|
|
172
|
+
is present, plain futures otherwise). Only a non-empty pull is cached; a stale
|
|
173
|
+
empty is a miss (poisoned-cache self-heal); a failing upstream degrades (omit +
|
|
174
|
+
log) rather than breaking the list.
|
|
175
|
+
- `McpToolkit::Gateway::Proxy` — proxies a namespaced call, forwarding the
|
|
176
|
+
resolved `account_id` as `_meta[config.account_meta_key]`. An unknown key raises
|
|
177
|
+
`McpToolkit::Gateway::UnknownUpstream`; an upstream failure raises
|
|
178
|
+
`McpToolkit::Gateway::UpstreamCallError` (carrying `jsonrpc_error` /
|
|
179
|
+
`http_status`). Neither is mapped to a protocol-error class here.
|
|
180
|
+
- **Authority introspection endpoint** — `McpToolkit::TokensController#introspect`,
|
|
181
|
+
drawn by the engine at `POST /mcp/tokens/introspect`, so a central app answers
|
|
182
|
+
introspection with no controller of its own. Its parent class is configurable via
|
|
183
|
+
`parent_controller` (like `ServerController`). The route is drawn ONLY when
|
|
184
|
+
`auth_role` is `:authority`: introspection is the provider side of the protocol,
|
|
185
|
+
so a satellite (the default role) that mounts the engine gets no such route
|
|
186
|
+
rather than one it should never answer. The controller also fails safe as defence
|
|
187
|
+
in depth — with no `token_authenticator` it answers `{ valid: false }`.
|
|
188
|
+
- **`McpToolkit::Session#data`** — an opaque payload attachable at
|
|
189
|
+
`create!(data:)` and round-tripped through `find`, so an authority can bind a
|
|
190
|
+
session to a token id (letting a revoked token kill an in-flight session). The
|
|
191
|
+
gem does not interpret it; legacy rows default to `{}`.
|
|
192
|
+
- `McpToolkit::Configuration` gains `upstreams` (a `Gateway::UpstreamRegistry`),
|
|
193
|
+
`register_upstream`, `upstream_timeout` (default `10`), `upstream_list_ttl`
|
|
194
|
+
(default `900`), and `logger` (default `nil`; all gateway/session call sites
|
|
195
|
+
guard with `logger&.`).
|
|
196
|
+
- `concurrent-ruby` is now a direct dependency (already transitive via
|
|
197
|
+
activesupport) — the aggregator's parallel upstream pulls require it.
|
|
198
|
+
|
|
1
199
|
## [0.3.0] - 2026-07-03
|
|
2
200
|
|
|
3
201
|
### Added
|
|
@@ -119,5 +317,6 @@ hand-rolled controller wiring.
|
|
|
119
317
|
|
|
120
318
|
### Notes
|
|
121
319
|
|
|
122
|
-
- The gateway / upstream-aggregation layer
|
|
123
|
-
|
|
320
|
+
- The gateway / upstream-aggregation layer was intentionally out of scope for this
|
|
321
|
+
initial extraction. It was later extracted into the gem in 0.4.0 (see above), as
|
|
322
|
+
`McpToolkit::Gateway::*` — fully config-injected and app-agnostic.
|