axn-mcp 0.1.0 → 0.2.2

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: 33bb333838d7dfe4375c7d66f95b0058c66330eb309271e752f8587d77de4ac7
4
- data.tar.gz: 9bc64595d2939e420e1fa55c7cfe91b19799d56a73ca0c710dc6ced07edd643f
3
+ metadata.gz: cca06b5f0351e071ef3d4e001e0d70059961dd00614fb25de420bb968427cf4f
4
+ data.tar.gz: 91d148e897c0bf146a6cc73e4fbd1dbd4f31090abe0cf7e67fc3a6ae2b27db82
5
5
  SHA512:
6
- metadata.gz: 4fe4d4184f017f35677742055e7ab2f83c261ada3bf8d6f3da946d15f23d2c3b0dd0b9078d49aad17eddc6389a9d67c1f9b9e84d1d2e4e82023ff13d44b2622f
7
- data.tar.gz: 816a6455d2f2cef47db441aa41af1d85bca24a8f6f3666a88fe5275930fdb8ad0567f3cfe5b3a602402bc1c406d9a1cbb64ec49c2c9da49c5fc3e80af76bbb9a
6
+ metadata.gz: 4a33316fb4ff566273b2ecb95a6d7a174a24e0157d218ae6173fe38ab74806ec717684a3a3ce9673c7ff49db3def9596041ed897733ba0de4805313bd6c7b8ce
7
+ data.tar.gz: b6febb6893e8bef0d83e1168b6c8657a1f84ace28c06d2b8aa58e560651b60f089d226455a8bd33416b6d18f583bcdbfe13d525959cf0ed38445e7dc9d0da746
data/CHANGELOG.md CHANGED
@@ -1,5 +1,214 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.2
4
+
5
+ ### Changed
6
+
7
+ - **BREAKING (behavior): every MCP tool call now runs through axn core's `Axn::Tools::Invoker`
8
+ (PRO-2943/PRO-3332) instead of a bare `axn_class.call`, turning on three behaviors that were
9
+ previously off for every wrapped Axn — no opt-out.** This is the same profile `axn-ruby_llm` and
10
+ `axn-openapi` already use for a model/LLM-facing tool.
11
+ - **Input coercion is now always on.** A wire-typed argument (a JSON string `"25"` for an
12
+ `expects :limit, type: Integer`) is coerced to the declared type before the Axn runs — previously
13
+ only if the class or `Axn.config` opted into `coerce_input_types` itself. A field's own `coerce:`
14
+ still wins. If a tool's `expects` has no `type:` at all, this is a no-op for that field; declaring
15
+ `type:` on every tool input (which `input_schema` already needs to build the JSON Schema) is what
16
+ benefits from it. See axn core's [Tool Invoker](https://teamshares.github.io/axn/reference/tool-invoker) docs.
17
+ - **An inbound-contract violation (a bad or missing argument) now settles as a correctable,
18
+ user-facing tool error instead of a generic failure reported through `on_exception`.** Previously
19
+ a model sending a malformed argument reported as a dev-facing bug (paging `on_exception`, generic
20
+ error text) exactly like an internal error would. Now `result.error` names the specific violation
21
+ (e.g. which field), the call is **not** reported to `on_exception`, and the client gets an
22
+ actionable message instead of a wall.
23
+ - **A top-level tool argument the Axn never declared with `expects` is now rejected as invalid input,
24
+ instead of being silently dropped.** Previously an MCP client could pass an extra/misspelled
25
+ argument and it would just vanish (ordinary Ruby `**kwargs` behavior); now the call fails with a
26
+ message naming the undeclared key.
27
+ - Also stamps every wrapped call's tree with the `invoked_via: "mcp"` dimension (visible in axn's
28
+ own call-completion log line and any tracing/dashboard code keyed off it), so MCP-driven traffic
29
+ is now distinguishable from a direct `.call`. This part is additive/observability-only.
30
+ - **Unaffected:** `ambient_context` resolution/guarding (still adapter-injected, still stripped from
31
+ model-supplied args before this ever ran), the transport-failure guard and its diagnostic hint,
32
+ `present_as`, `reject_opaque_exposed_values`, and every other config/serialization behavior in
33
+ this release. Only the dispatch of `axn_class.call` itself changed.
34
+
35
+ - **Raised the `axn` dependency floor to `>= 0.1.0-alpha.6`** (from `>= 0.1.0-alpha.5`) — required for
36
+ both `Axn::Tools::AdapterSerialization` and `Axn::Tools::Invoker`'s `adapter:` kwarg, above.
37
+
38
+ ### Internal
39
+
40
+ - **[INTERNAL] Routed the adapter config/serialization plumbing through axn core's shared
41
+ `Axn::Tools::AdapterSerialization` (PRO-2996).** `Axn::MCP` now `extend`s the mixin alongside
42
+ `Axn::Tools::AdapterRoots`: `reject_opaque_exposed_values` is declared via
43
+ `declare_reject_opaque_exposed_values! default: false` (was a hand-written `setting`), `tool_roots`
44
+ via `tool_roots_default %w[agent_tools]` (was a re-declared `setting` hand-copying core's
45
+ validation lambda), exposed-value rendering goes through `Axn::MCP.serialize_exposed(result)`, and
46
+ the transport-mapping guard through `Axn::MCP.guard_tool_response`. **No behavior change** —
47
+ identical defaults, identical per-tool override precedence (`configure(:mcp)` beats the gem-wide
48
+ config), identical error response, `on_exception` report and dev re-raise, and an identical
49
+ operator hint line. Internally, `reject_opaque_exposed_values` is now resolved off the result's own
50
+ action class inside `serialize_exposed` rather than threaded through the call, so
51
+ `Axn::MCP::Serializer.result_to_mcp_response` and `Axn::MCP::Invocation.perform` no longer take a
52
+ `reject_opaque_exposed_values:` kwarg. Both are internal entry points (a consumer calls
53
+ `Axn::MCP.wrap`/`.tools`), so this is not a user-facing signature change.
54
+
55
+ ### Fixed
56
+
57
+ - **The transport-failure guard now logs an operator hint when `reject_opaque_exposed_values` may be the
58
+ cause.** The tool-facing error stays generic (`"The tool could not produce a valid response"`), but the
59
+ logged line now names the offending tool and both places the setting could be set (`configure(:mcp)` /
60
+ `Axn::MCP.config.reject_opaque_exposed_values`) whenever the resolved value is `true` — matching
61
+ `axn-openapi`'s dispatcher hint. Previously an operator had to guess which knob caused a rejection since
62
+ the setting is per-tool overridable.
63
+
64
+ ## 0.2.0
65
+
66
+ Re-architected around **author-once**: write a plain Axn and expose it as an `::MCP::Tool` with
67
+ `Axn::MCP.wrap` (one tool) or `Axn::MCP.tools` (every registered `:mcp` tool). The `Axn::MCP::Tool`
68
+ subclass base is retired, and schema generation + value serialization now come from `axn` core
69
+ reflection rather than gem code.
70
+
71
+ ### Added
72
+
73
+ - `Axn::MCP.wrap(axn_class, description: nil, name: nil, title: nil, icons: nil, meta: nil, annotations: nil, present_as: nil)` —
74
+ expose any plain Axn (`include Axn`) as an `::MCP::Tool` subclass whose `.call` always returns an
75
+ `MCP::Tool::Response`. The original Axn is untouched (direct `.call` still returns `Axn::Result`).
76
+ `description:` defaults to the Axn's own `.description`; `name:` to axn core's canonical `tool_name`
77
+ (honors a `tool name:` override + `tool_name_stripped_prefixes`); annotations derive from the Axn's
78
+ `semantic_hints` unless an explicit `annotations:` is passed. Raises if a truly anonymous Axn (no
79
+ class name, no `axn_name`) is wrapped without `name:`.
80
+ - `Axn::MCP.tools` — zero-arg convenience returning every Axn registered as a `:mcp` tool (via
81
+ `tool :mcp`, a `configure(:mcp)` bag, or residency under a configured `tool_roots` directory), each already
82
+ wrapped and deterministically ordered by `tool_name`. `MCP::Server.new(tools: Axn::MCP.tools)`.
83
+ Symmetric with `Axn::RubyLLM.tools`.
84
+ - Registers `:mcp` as a tool adapter with axn core's process-global registry, passing `Axn::MCP`
85
+ itself as the config source (`Axn::Tools.register_adapter(:mcp, self)`) so the registry reads
86
+ `Axn::MCP.config.tool_roots` for directory-based tool membership. `Axn::MCP` `extend`s
87
+ `Axn::Tools::AdapterRoots` and ships a default `tool_roots` of `["agent_tools"]` — an Axn under
88
+ `app/agent_tools/` is exposed as an MCP tool with no `tool :mcp` declaration, and (since
89
+ `axn-ruby_llm` defaults to the same dir) over ruby_llm too. Roots are broad-path-validated
90
+ (`app`/`.`/`actions`/`..` rejected). Membership is `(directory grant ∪ tool declaration) − except`.
91
+ - **Breaking (core, via the axn bump this release requires):** `tool :mcp` now **adds** the adapter
92
+ to the directory grant instead of replacing it (declare all adapters/`name:`/`except:`/per-adapter
93
+ bags in one `tool` call), and the global `Axn.config.tool_paths` is removed in favor of per-adapter
94
+ `<adapter>.config.tool_roots` (for MCP: `Axn::MCP.config.tool_roots`). Per-adapter tool config can
95
+ also be declared inline via `tool mcp: { … }` (sugar over `configure(:mcp)`; axn PRO-2942).
96
+ - `present_as` (`:structured` | `:message`) — picks whether a tool's response text block is the
97
+ serialized `exposes` or the Axn's success/error message. Settable gem-wide
98
+ (`Axn::MCP.config.present_as`), per-class (`configure(:mcp) { |c| c.present_as = … }`), or on
99
+ `Axn::MCP.wrap(present_as:)` (most-local wins). `:structured` is the default; `structuredContent`
100
+ always carries the exposed data regardless.
101
+ - `reject_opaque_exposed_values` (boolean, default `false`) — when an `exposes` value has no JSON
102
+ rendering its author declared (it would ship as `"#<User:0x…>"`, or an ActiveSupport
103
+ instance-variable dump under Rails), `true` fails the call (error response + `on_exception` report)
104
+ instead of shipping that blob. Settable gem-wide (`Axn::MCP.config.reject_opaque_exposed_values`)
105
+ or per-class (`configure(:mcp)`), per-class wins. Output-side only (not inbound `coerce:`), and
106
+ narrow — values with no *honest* JSON form (cycles, non-finite Floats, non-UTF-8 bytes, colliding
107
+ Hash keys) fail regardless (axn #206 / PRO-2988).
108
+ - **`Axn::MCP.wrap(...).call` never raises** — it extends axn's non-bang contract to the transport
109
+ boundary. A transport-layer exception raised *after* the Axn settled (exposed-value serialization,
110
+ response building) is reported via axn's global `on_exception` hook and returned as a generic error
111
+ `MCP::Tool::Response`, rather than escaping to a direct/custom caller. In axn's dev mode
112
+ (`best_effort_raises_in_dev`) it re-raises instead, so real bugs aren't masked.
113
+ - `open_world` / `closed_world` registered as MCP-only `semantic_hints`. A class's `semantic_hints`
114
+ (`:read_only` / `:idempotent` / `:destructive` / `:open_world` / `:closed_world`) drive its MCP
115
+ annotations by default; an explicit `annotations:` on `wrap` always wins.
116
+ - Per-tool MCP metadata — `title` / `icons` / `meta` / `annotations` — settable either as
117
+ `Axn::MCP.wrap` kwargs or declaratively via `configure(:mcp) { |c| c.title = … }`. The
118
+ `configure(:mcp)` form survives the zero-arg `Axn::MCP.tools` path (which calls `wrap` with no
119
+ kwargs); an explicit `wrap` kwarg wins over it. For `annotations`, precedence is `wrap` kwarg →
120
+ `configure(:mcp)` → `semantic_hints`-derived.
121
+ - **Tool versioning (via the axn bump this release requires; axn PRO-2955).** Tool identity is
122
+ `(tool_name, tool_version)`, so `Axn::MCP.tools` exposes only the latest version when several Axns
123
+ share a `tool_name` (`Axn::Tools.for(:mcp)` collapses to the highest `tool_version`). For a
124
+ versioned tool (`tool_version > 1`), `wrap` surfaces the resolved revision as `tool_version` in the
125
+ tool's `_meta` — never in its `name` (the cross-adapter identity). Unversioned tools are unchanged.
126
+ - `Axn::MCP::Error` base error class, marked with axn core's `Axn::Error` boundary (axn PRO-2997), so
127
+ a consumer's `rescue Axn::Error` catches this gem's errors alongside core's and the other adapter
128
+ gems'. `Axn::MCP::SchemaError` now subclasses it (still a `StandardError`).
129
+
130
+ ### Changed
131
+
132
+ - **Schemas and serialization come from `axn` core** — schemas from your `expects`/`exposes`
133
+ declarations and exposed-value rendering via the `Axn::Extensions::Serialization` facade —
134
+ replacing the gem's own builder. Consumer-visible reflection
135
+ differences: nullable/optional fields reflect as a `type` array (`["string", "null"]`) rather than
136
+ a bare type; boolean fields gain `enum: [true]`/`[false]`; a `model:` field's generated `_id` no
137
+ longer asserts `type: "integer"` (a primary key's type isn't statically knowable) but forbids
138
+ `null` when required; every `exposes` field is listed in output `required` (optionality shows up as
139
+ a nullable `type`); a `Numeric` field's output type is omitted (it admits `Complex`); a
140
+ `shape:` block on an `Array` field with no `of:` is no longer reflected in the *output* schema
141
+ (combine `shape:` with `of:` for typed output items — input schema is unaffected); and a required
142
+ `String`/`Array` field reflects axn's non-blank presence validation as `minLength: 1`/`minItems: 1`
143
+ (an optional field is nullable instead, with no minimum).
144
+ - **`server_context` is spread into the Axn's `ambient_context`.** `Axn::MCP.wrap` passes the
145
+ injected `server_context` *as* the Axn's `ambient_context` (not nested under a `server_context`
146
+ key), so a tool declares the data it needs directly and generically — `expects :user_id, on:
147
+ :ambient_context` — and stays adapter-agnostic: the *same* class resolves `user_id` from the MCP
148
+ server context, from `Current` on a direct call, or from `Axn::RubyLLM.wrap`, with no MCP-specific
149
+ intermediate. axn extracts each declared field via `#[]`/`#dig`, so it works for a `Hash` or an
150
+ `MCP::ServerContext` object. `on: :ambient_context` fields are excluded from `inputSchema`, and the
151
+ explicit ambient context replaces any process-wide `Current`-derived default (no server-side
152
+ leakage; `nil` on a direct call with none provided).
153
+ - **Added `Axn::MCP.server_context`** — the MCP-specific handle for transport *capabilities*
154
+ (`Axn::MCP.server_context.report_progress(...)`, `.cancelled?`), the live `MCP::ServerContext`
155
+ object that doesn't survive `ambient_context`'s declared-key filtering. `nil` outside a wrapped
156
+ tool call. Scoped via `ActiveSupport::IsolatedExecutionState` (thread/fiber per the configured
157
+ isolation level), matching how axn scopes its own per-execution state. Data belongs in
158
+ `ambient_context`; only transport capabilities need this.
159
+ - **MCP error responses carry the Axn's own `result.error`** (axn's `"Something went wrong"` for a
160
+ bare `fail!` / validation error / unhandled exception, or the explicit `fail!` reason). Declare a
161
+ per-tool base `error "…"` for a friendlier generic message.
162
+ - Config is declared with axn core's `Axn::Configurable` DSL under `config_namespace :mcp`, so a base
163
+ Axn composes cleanly with other adapters' per-class config (`configure(:mcp)` /
164
+ `configure(:other_adapter)`) on the same class.
165
+
166
+ ### Removed (breaking)
167
+
168
+ - **`Axn::MCP::Tool` subclass base** — author a plain Axn + `Axn::MCP.wrap` instead. Its dual-mode
169
+ `.call` (returning `Axn::Result` *or* `MCP::Tool::Response` depending on a `server_context:` kwarg)
170
+ overrode `input_schema` to a non-Hash `MCP::Tool::InputSchema`, which broke `Axn::RubyLLM.wrap` on
171
+ the same class. Kept as a raising tombstone with a migration message (deletion at 1.0 — see
172
+ `DEPRECATIONS.md`).
173
+ - **`Axn::MCP::Tool.define`** — build a one-off inline tool with
174
+ `Axn::MCP.wrap(Axn::Factory.build(…) { … }, name: "…")`. Also a raising tombstone.
175
+ - **Annotation bang-methods** (`read_only!` / `destructive!` / `idempotent!` / `open_world!` /
176
+ `closed_world!`) — declare `semantic_hints` on the plain Axn instead.
177
+
178
+ ### Fixed
179
+
180
+ - Conditionally-required fields (`expects :token, if: :use_token`) reflect as an `allOf` conditional
181
+ clause instead of an unconditional `required` entry, so `MCP::Server`'s pre-flight
182
+ `missing_required_arguments?` check no longer rejects a valid call that omits the gated field.
183
+
184
+ ### Documentation
185
+
186
+ - README re-oriented to author-once (`wrap` / `.tools`, inline `Factory.build` recipe, mixing with
187
+ native `MCP::Tool` tools). New "Divergences from the raw MCP SDK" section documents the two
188
+ intentionally-unmapped `MCP::Tool::Response` output affordances (non-text content, response-level
189
+ `_meta`). Documents that schema reflection is best-effort and biased stricter-than-runtime (naming
190
+ the one looser-than-runtime case — a field with an invalid literal default), that the gem is scoped
191
+ to MCP Tools only, and `MCP::ServerContext`'s richer session capabilities (`report_progress`,
192
+ `cancelled?`).
193
+
194
+ ### Packaging
195
+
196
+ - The packaged gem now ships only its runtime surface — `lib/`, `README.md`, `CHANGELOG.md`, and
197
+ `LICENSE`. `spec.files` moved from a denylist to an allowlist (mirroring `axn` core), so
198
+ dev-only artifacts (`AGENTS.md`/`CLAUDE.md`, `DEPRECATIONS.md`, `Rakefile`) that 0.1.0 leaked into
199
+ the package are no longer shipped.
200
+
201
+ ### Dependency
202
+
203
+ - Requires `axn >= 0.1.0-alpha.5, < 0.2.0` from RubyGems. This release needs axn core's reflection,
204
+ tool-registry, serialization-facade, and namespaced-config primitives, first available in the
205
+ `alpha.5` prerelease. (During development this gem tracked axn `main` from git; now that `alpha.5`
206
+ is published it depends on the released gem.)
207
+ - Set the `mcp` requirement to `>= 0.5.0, < 2.0`. Floor is `0.5.0` — the first SDK with the `icons`
208
+ setter and full JSON-Schema tool schemas (so conditional `allOf` survives); `0.4.x` lacks both.
209
+ Upper bound tracks the SDK's semver: `1.0` declared its API stable (breaking only in a future
210
+ major), so the whole `1.x` line is supported. Verified against `mcp` 0.5.0 and 1.1.0.
211
+
3
212
  ## 0.1.0
4
213
 
5
214
  - Initial release