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 +4 -4
- data/CHANGELOG.md +148 -0
- data/README.md +422 -99
- data/lib/axn/mcp/annotations.rb +30 -0
- data/lib/axn/mcp/invocation.rb +70 -0
- data/lib/axn/mcp/serializer.rb +13 -31
- data/lib/axn/mcp/tool.rb +52 -139
- data/lib/axn/mcp/version.rb +1 -1
- data/lib/axn/mcp/wrap.rb +168 -0
- data/lib/axn/mcp.rb +97 -8
- metadata +10 -11
- data/Rakefile +0 -13
- data/lib/axn/mcp/config.rb +0 -27
- data/lib/axn/mcp/field_declarations.rb +0 -21
- data/lib/axn/mcp/schema_builder.rb +0 -224
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ec6cf38e884f59bab8f659b2b845d23dfc2880189944281874c904629b7bb95e
|
|
4
|
+
data.tar.gz: 6c0763ba60b3f7626d7685b2d6d66e80e1c5092341213ea7b26713fc87b5429e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|