axn-mcp 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 33bb333838d7dfe4375c7d66f95b0058c66330eb309271e752f8587d77de4ac7
4
- data.tar.gz: 9bc64595d2939e420e1fa55c7cfe91b19799d56a73ca0c710dc6ced07edd643f
3
+ metadata.gz: ec6cf38e884f59bab8f659b2b845d23dfc2880189944281874c904629b7bb95e
4
+ data.tar.gz: 6c0763ba60b3f7626d7685b2d6d66e80e1c5092341213ea7b26713fc87b5429e
5
5
  SHA512:
6
- metadata.gz: 4fe4d4184f017f35677742055e7ab2f83c261ada3bf8d6f3da946d15f23d2c3b0dd0b9078d49aad17eddc6389a9d67c1f9b9e84d1d2e4e82023ff13d44b2622f
7
- data.tar.gz: 816a6455d2f2cef47db441aa41af1d85bca24a8f6f3666a88fe5275930fdb8ad0567f3cfe5b3a602402bc1c406d9a1cbb64ec49c2c9da49c5fc3e80af76bbb9a
6
+ metadata.gz: 353cad80a6cb312fab01146ed245e938cd210ab68c1b511ef75c40c6e15501ba12b0c6fcf12c1b1d4878246eb2c5c0f50aac83584745191477a80d936b168210
7
+ data.tar.gz: b0fb52cf792300f749d54fdd57766d0f1c32c0d17086d44bd1edb1e8af5cc9debca8e670c492b847c9f43baa0aa216319a98e6e7fe1e644a53300c9f2fd29c6a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,153 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0
4
+
5
+ Re-architected around **author-once**: write a plain Axn and expose it as an `::MCP::Tool` with
6
+ `Axn::MCP.wrap` (one tool) or `Axn::MCP.tools` (every registered `:mcp` tool). The `Axn::MCP::Tool`
7
+ subclass base is retired, and schema generation + value serialization now come from `axn` core
8
+ reflection rather than gem code.
9
+
10
+ ### Added
11
+
12
+ - `Axn::MCP.wrap(axn_class, description: nil, name: nil, title: nil, icons: nil, meta: nil, annotations: nil, present_as: nil)` —
13
+ expose any plain Axn (`include Axn`) as an `::MCP::Tool` subclass whose `.call` always returns an
14
+ `MCP::Tool::Response`. The original Axn is untouched (direct `.call` still returns `Axn::Result`).
15
+ `description:` defaults to the Axn's own `.description`; `name:` to axn core's canonical `tool_name`
16
+ (honors a `tool name:` override + `tool_name_stripped_prefixes`); annotations derive from the Axn's
17
+ `semantic_hints` unless an explicit `annotations:` is passed. Raises if a truly anonymous Axn (no
18
+ class name, no `axn_name`) is wrapped without `name:`.
19
+ - `Axn::MCP.tools` — zero-arg convenience returning every Axn registered as a `:mcp` tool (via
20
+ `tool :mcp`, a `configure(:mcp)` bag, or residency under a configured `tool_roots` directory), each already
21
+ wrapped and deterministically ordered by `tool_name`. `MCP::Server.new(tools: Axn::MCP.tools)`.
22
+ Symmetric with `Axn::RubyLLM.tools`.
23
+ - Registers `:mcp` as a tool adapter with axn core's process-global registry, passing `Axn::MCP`
24
+ itself as the config source (`Axn::Tools.register_adapter(:mcp, self)`) so the registry reads
25
+ `Axn::MCP.config.tool_roots` for directory-based tool membership. `Axn::MCP` `extend`s
26
+ `Axn::Tools::AdapterRoots` and ships a default `tool_roots` of `["agent_tools"]` — an Axn under
27
+ `app/agent_tools/` is exposed as an MCP tool with no `tool :mcp` declaration, and (since
28
+ `axn-ruby_llm` defaults to the same dir) over ruby_llm too. Roots are broad-path-validated
29
+ (`app`/`.`/`actions`/`..` rejected). Membership is `(directory grant ∪ tool declaration) − except`.
30
+ - **Breaking (core, via the axn bump this release requires):** `tool :mcp` now **adds** the adapter
31
+ to the directory grant instead of replacing it (declare all adapters/`name:`/`except:`/per-adapter
32
+ bags in one `tool` call), and the global `Axn.config.tool_paths` is removed in favor of per-adapter
33
+ `<adapter>.config.tool_roots` (for MCP: `Axn::MCP.config.tool_roots`). Per-adapter tool config can
34
+ also be declared inline via `tool mcp: { … }` (sugar over `configure(:mcp)`; axn PRO-2942).
35
+ - `present_as` (`:structured` | `:message`) — picks whether a tool's response text block is the
36
+ serialized `exposes` or the Axn's success/error message. Settable gem-wide
37
+ (`Axn::MCP.config.present_as`), per-class (`configure(:mcp) { |c| c.present_as = … }`), or on
38
+ `Axn::MCP.wrap(present_as:)` (most-local wins). `:structured` is the default; `structuredContent`
39
+ always carries the exposed data regardless.
40
+ - `reject_opaque_exposed_values` (boolean, default `false`) — when an `exposes` value has no JSON
41
+ rendering its author declared (it would ship as `"#<User:0x…>"`, or an ActiveSupport
42
+ instance-variable dump under Rails), `true` fails the call (error response + `on_exception` report)
43
+ instead of shipping that blob. Settable gem-wide (`Axn::MCP.config.reject_opaque_exposed_values`)
44
+ or per-class (`configure(:mcp)`), per-class wins. Output-side only (not inbound `coerce:`), and
45
+ narrow — values with no *honest* JSON form (cycles, non-finite Floats, non-UTF-8 bytes, colliding
46
+ Hash keys) fail regardless (axn #206 / PRO-2988).
47
+ - **`Axn::MCP.wrap(...).call` never raises** — it extends axn's non-bang contract to the transport
48
+ boundary. A transport-layer exception raised *after* the Axn settled (exposed-value serialization,
49
+ response building) is reported via axn's global `on_exception` hook and returned as a generic error
50
+ `MCP::Tool::Response`, rather than escaping to a direct/custom caller. In axn's dev mode
51
+ (`best_effort_raises_in_dev`) it re-raises instead, so real bugs aren't masked.
52
+ - `open_world` / `closed_world` registered as MCP-only `semantic_hints`. A class's `semantic_hints`
53
+ (`:read_only` / `:idempotent` / `:destructive` / `:open_world` / `:closed_world`) drive its MCP
54
+ annotations by default; an explicit `annotations:` on `wrap` always wins.
55
+ - Per-tool MCP metadata — `title` / `icons` / `meta` / `annotations` — settable either as
56
+ `Axn::MCP.wrap` kwargs or declaratively via `configure(:mcp) { |c| c.title = … }`. The
57
+ `configure(:mcp)` form survives the zero-arg `Axn::MCP.tools` path (which calls `wrap` with no
58
+ kwargs); an explicit `wrap` kwarg wins over it. For `annotations`, precedence is `wrap` kwarg →
59
+ `configure(:mcp)` → `semantic_hints`-derived.
60
+ - **Tool versioning (via the axn bump this release requires; axn PRO-2955).** Tool identity is
61
+ `(tool_name, tool_version)`, so `Axn::MCP.tools` exposes only the latest version when several Axns
62
+ share a `tool_name` (`Axn::Tools.for(:mcp)` collapses to the highest `tool_version`). For a
63
+ versioned tool (`tool_version > 1`), `wrap` surfaces the resolved revision as `tool_version` in the
64
+ tool's `_meta` — never in its `name` (the cross-adapter identity). Unversioned tools are unchanged.
65
+ - `Axn::MCP::Error` base error class, marked with axn core's `Axn::Error` boundary (axn PRO-2997), so
66
+ a consumer's `rescue Axn::Error` catches this gem's errors alongside core's and the other adapter
67
+ gems'. `Axn::MCP::SchemaError` now subclasses it (still a `StandardError`).
68
+
69
+ ### Changed
70
+
71
+ - **Schemas and serialization come from `axn` core** — schemas from your `expects`/`exposes`
72
+ declarations and exposed-value rendering via the `Axn::Extensions::Serialization` facade —
73
+ replacing the gem's own builder. Consumer-visible reflection
74
+ differences: nullable/optional fields reflect as a `type` array (`["string", "null"]`) rather than
75
+ a bare type; boolean fields gain `enum: [true]`/`[false]`; a `model:` field's generated `_id` no
76
+ longer asserts `type: "integer"` (a primary key's type isn't statically knowable) but forbids
77
+ `null` when required; every `exposes` field is listed in output `required` (optionality shows up as
78
+ a nullable `type`); a `Numeric` field's output type is omitted (it admits `Complex`); a
79
+ `shape:` block on an `Array` field with no `of:` is no longer reflected in the *output* schema
80
+ (combine `shape:` with `of:` for typed output items — input schema is unaffected); and a required
81
+ `String`/`Array` field reflects axn's non-blank presence validation as `minLength: 1`/`minItems: 1`
82
+ (an optional field is nullable instead, with no minimum).
83
+ - **`server_context` is spread into the Axn's `ambient_context`.** `Axn::MCP.wrap` passes the
84
+ injected `server_context` *as* the Axn's `ambient_context` (not nested under a `server_context`
85
+ key), so a tool declares the data it needs directly and generically — `expects :user_id, on:
86
+ :ambient_context` — and stays adapter-agnostic: the *same* class resolves `user_id` from the MCP
87
+ server context, from `Current` on a direct call, or from `Axn::RubyLLM.wrap`, with no MCP-specific
88
+ intermediate. axn extracts each declared field via `#[]`/`#dig`, so it works for a `Hash` or an
89
+ `MCP::ServerContext` object. `on: :ambient_context` fields are excluded from `inputSchema`, and the
90
+ explicit ambient context replaces any process-wide `Current`-derived default (no server-side
91
+ leakage; `nil` on a direct call with none provided).
92
+ - **Added `Axn::MCP.server_context`** — the MCP-specific handle for transport *capabilities*
93
+ (`Axn::MCP.server_context.report_progress(...)`, `.cancelled?`), the live `MCP::ServerContext`
94
+ object that doesn't survive `ambient_context`'s declared-key filtering. `nil` outside a wrapped
95
+ tool call. Scoped via `ActiveSupport::IsolatedExecutionState` (thread/fiber per the configured
96
+ isolation level), matching how axn scopes its own per-execution state. Data belongs in
97
+ `ambient_context`; only transport capabilities need this.
98
+ - **MCP error responses carry the Axn's own `result.error`** (axn's `"Something went wrong"` for a
99
+ bare `fail!` / validation error / unhandled exception, or the explicit `fail!` reason). Declare a
100
+ per-tool base `error "…"` for a friendlier generic message.
101
+ - Config is declared with axn core's `Axn::Configurable` DSL under `config_namespace :mcp`, so a base
102
+ Axn composes cleanly with other adapters' per-class config (`configure(:mcp)` /
103
+ `configure(:other_adapter)`) on the same class.
104
+
105
+ ### Removed (breaking)
106
+
107
+ - **`Axn::MCP::Tool` subclass base** — author a plain Axn + `Axn::MCP.wrap` instead. Its dual-mode
108
+ `.call` (returning `Axn::Result` *or* `MCP::Tool::Response` depending on a `server_context:` kwarg)
109
+ overrode `input_schema` to a non-Hash `MCP::Tool::InputSchema`, which broke `Axn::RubyLLM.wrap` on
110
+ the same class. Kept as a raising tombstone with a migration message (deletion at 1.0 — see
111
+ `DEPRECATIONS.md`).
112
+ - **`Axn::MCP::Tool.define`** — build a one-off inline tool with
113
+ `Axn::MCP.wrap(Axn::Factory.build(…) { … }, name: "…")`. Also a raising tombstone.
114
+ - **Annotation bang-methods** (`read_only!` / `destructive!` / `idempotent!` / `open_world!` /
115
+ `closed_world!`) — declare `semantic_hints` on the plain Axn instead.
116
+
117
+ ### Fixed
118
+
119
+ - Conditionally-required fields (`expects :token, if: :use_token`) reflect as an `allOf` conditional
120
+ clause instead of an unconditional `required` entry, so `MCP::Server`'s pre-flight
121
+ `missing_required_arguments?` check no longer rejects a valid call that omits the gated field.
122
+
123
+ ### Documentation
124
+
125
+ - README re-oriented to author-once (`wrap` / `.tools`, inline `Factory.build` recipe, mixing with
126
+ native `MCP::Tool` tools). New "Divergences from the raw MCP SDK" section documents the two
127
+ intentionally-unmapped `MCP::Tool::Response` output affordances (non-text content, response-level
128
+ `_meta`). Documents that schema reflection is best-effort and biased stricter-than-runtime (naming
129
+ the one looser-than-runtime case — a field with an invalid literal default), that the gem is scoped
130
+ to MCP Tools only, and `MCP::ServerContext`'s richer session capabilities (`report_progress`,
131
+ `cancelled?`).
132
+
133
+ ### Packaging
134
+
135
+ - The packaged gem now ships only its runtime surface — `lib/`, `README.md`, `CHANGELOG.md`, and
136
+ `LICENSE`. `spec.files` moved from a denylist to an allowlist (mirroring `axn` core), so
137
+ dev-only artifacts (`AGENTS.md`/`CLAUDE.md`, `DEPRECATIONS.md`, `Rakefile`) that 0.1.0 leaked into
138
+ the package are no longer shipped.
139
+
140
+ ### Dependency
141
+
142
+ - Requires `axn >= 0.1.0-alpha.5, < 0.2.0` from RubyGems. This release needs axn core's reflection,
143
+ tool-registry, serialization-facade, and namespaced-config primitives, first available in the
144
+ `alpha.5` prerelease. (During development this gem tracked axn `main` from git; now that `alpha.5`
145
+ is published it depends on the released gem.)
146
+ - Set the `mcp` requirement to `>= 0.5.0, < 2.0`. Floor is `0.5.0` — the first SDK with the `icons`
147
+ setter and full JSON-Schema tool schemas (so conditional `allOf` survives); `0.4.x` lacks both.
148
+ Upper bound tracks the SDK's semver: `1.0` declared its API stable (breaking only in a future
149
+ major), so the whole `1.x` line is supported. Verified against `mcp` 0.5.0 and 1.1.0.
150
+
3
151
  ## 0.1.0
4
152
 
5
153
  - Initial release