Module: Axn::MCP
- Extended by:
- Configurable, Tools::AdapterRoots
- Defined in:
- lib/axn/mcp.rb,
lib/axn/mcp/tool.rb,
lib/axn/mcp/wrap.rb,
lib/axn/mcp/version.rb,
lib/axn/mcp/invocation.rb,
lib/axn/mcp/serializer.rb,
lib/axn/mcp/annotations.rb
Defined Under Namespace
Modules: Annotations, Invocation, Serializer Classes: Error, SchemaError, Tool
Constant Summary collapse
- VALID_PRESENT_AS =
Exposes any Axn (whether or not it subclasses Axn::MCP::Tool) as an ::MCP::Tool subclass. The wrapped class's own .call/.call! are untouched -- direct callers keep getting a plain Axn::Result. All MCP transport concerns (schema, server_context routing, response mapping) live entirely on the generated subclass, via the same Axn::MCP::Invocation path Axn::MCP::Tool#call uses -- proves the "author once" story from PRO-2844/PRO-2842.
The generated subclass is MCP-transport-only, unlike Axn::MCP::Tool: its .call always returns MCP::Tool::Response (never a raw Axn::Result), and it deliberately has no .call! -- MCP::Server itself only ever calls .call, and a consumer wanting real bang/raise-on-failure semantics can call the original wrapped class's own .call! directly (unwrapped).
%i[structured message].freeze
- VERSION =
"0.2.0"
Class Method Summary collapse
-
.deprecator ⇒ Object
Shared deprecator for this gem's own deprecated API (e.g. the legacy annotation bang-methods -- see lib/axn/mcp/tool.rb).
-
.server_context ⇒ Object
The live MCP server context for the current wrapped-tool call -- an
MCP::ServerContextover a realMCP::Server, or whatever raw value was passed asserver_context:to a direct call -- orniloutside a wrapped#call. -
.tools ⇒ Object
The gem-level convenience for building a ready-to-register MCP tool list (PRO-2923), symmetric with
Axn::RubyLLM.tools: enumerate every Axn that belongs to the :mcp adapter (via axn core's process-global registry) and wrap each one. -
.with_server_context(value) ⇒ Object
Sets Axn::MCP.server_context for the duration of the block.
- .wrap(axn_class, description: nil, name: nil, title: nil, icons: nil, meta: nil, annotations: nil, present_as: nil, mcp_text_content: RENAMED_MCP_TEXT_CONTENT) ⇒ Object
Class Method Details
.deprecator ⇒ Object
Shared deprecator for this gem's own deprecated API (e.g. the legacy annotation
bang-methods -- see lib/axn/mcp/tool.rb). A dedicated ActiveSupport::Deprecation instance,
not the old global ActiveSupport::Deprecation.warn, so a consuming Rails app can register it
(Rails.application.deprecators[:axn_mcp] = Axn::MCP.deprecator) and govern its behavior
(silence in test, raise in CI, etc.) the same way it already does for its own deprecations.
66 67 68 |
# File 'lib/axn/mcp.rb', line 66 def self.deprecator @deprecator ||= ActiveSupport::Deprecation.new("1.0", "axn-mcp") end |
.server_context ⇒ Object
The live MCP server context for the current wrapped-tool call -- an MCP::ServerContext over a
real MCP::Server, or whatever raw value was passed as server_context: to a direct call --
or nil outside a wrapped #call. This is the MCP-specific handle for transport capabilities
(Axn::MCP.server_context.report_progress(...), .cancelled?), which are not ambient data and
don't survive ambient_context's declared-key filtering. Ambient data still belongs in
ambient_context (expects :user_id, on: :ambient_context), which wrap spreads from the same
server_context and which stays adapter-agnostic. A tool reaching for this is knowingly
MCP-coupled -- appropriate, since these operations are MCP-transport-only.
78 79 80 |
# File 'lib/axn/mcp.rb', line 78 def self.server_context ActiveSupport::IsolatedExecutionState[:__axn_mcp_server_context] end |
.tools ⇒ Object
The gem-level convenience for building a ready-to-register MCP tool list (PRO-2923),
symmetric with Axn::RubyLLM.tools: enumerate every Axn that belongs to the :mcp adapter
(via axn core's process-global registry) and wrap each one. Zero-arg by design -- per-tool
customization comes from each class's own configure(:mcp) { ... } (honored inside wrap)
and its tool name:/description, not per-call kwargs -- so a consumer registers tools with
just MCP::Server.new(tools: Axn::MCP.tools, ...) instead of a hand-maintained array.
38 39 40 |
# File 'lib/axn/mcp/wrap.rb', line 38 def tools Axn::Tools.for(:mcp).map { |axn_class| wrap(axn_class) } end |
.with_server_context(value) ⇒ Object
Sets Axn::MCP.server_context for the duration of the block. Uses ActiveSupport's IsolatedExecutionState (thread- or fiber-scoped per the configured isolation_level), matching how axn core scopes its own per-execution state and CurrentAttributes -- so this stays correct under a Fiber scheduler rather than silently leaking across fibers a raw Thread-local would. Restores the previous value on the way out so nested wrapped calls compose.
87 88 89 90 91 92 93 |
# File 'lib/axn/mcp.rb', line 87 def self.with_server_context(value) previous = ActiveSupport::IsolatedExecutionState[:__axn_mcp_server_context] ActiveSupport::IsolatedExecutionState[:__axn_mcp_server_context] = value yield ensure ActiveSupport::IsolatedExecutionState[:__axn_mcp_server_context] = previous end |
.wrap(axn_class, description: nil, name: nil, title: nil, icons: nil, meta: nil, annotations: nil, present_as: nil, mcp_text_content: RENAMED_MCP_TEXT_CONTENT) ⇒ Object
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/axn/mcp/wrap.rb', line 42 def wrap(axn_class, description: nil, name: nil, title: nil, icons: nil, meta: nil, annotations: nil, present_as: nil, mcp_text_content: RENAMED_MCP_TEXT_CONTENT) reject_renamed_mcp_text_content!(mcp_text_content) validate_present_as!(present_as) reject_reserved_input_fields!(axn_class) resolved_name = resolve_wrap_tool_name(axn_class, name) resolved_description = description || axn_class.description # Each metadata kwarg falls back to the wrapped Axn's own `configure(:mcp)` value (via axn # core's shadow-proof resolver), so it's carried through the zero-arg `Axn::MCP.tools` path # too; an explicit `wrap` kwarg still wins. `annotations` also falls back further, to the # `semantic_hints`-derived defaults -- precedence: kwarg > configure(:mcp) > semantic_hints. resolved_title = title || Axn::MCP.resolve_override_for(axn_class, :title) resolved_icons = icons || Axn::MCP.resolve_override_for(axn_class, :icons) = || Axn::MCP.resolve_override_for(axn_class, :meta) configured_annotations = annotations || Axn::MCP.resolve_override_for(axn_class, :annotations) # Surface the resolved revision in `_meta` (never the name -- name is the cross-adapter # identity), so an operator/model can see which version `Axn::MCP.tools` collapsed to. Only # for actually-versioned tools; an unversioned tool (default `tool_version` 1) stays # meta-free. Keyed `tool_version` to mirror the DSL and avoid clobbering a consumer's own # `version` meta key. = { **( || {}), tool_version: axn_class.tool_version } if axn_class.tool_version > 1 Class.new(::MCP::Tool) do tool_name(resolved_name) description(resolved_description) title(resolved_title) if resolved_title icons(resolved_icons) if resolved_icons () if # axn_class.input_schema/.output_schema are axn core's own public reflection entry points -- # wiring to those directly, rather than reaching past them for the lower-level builder the # wrapped Axn's own methods already call, keeps this on the documented surface. input_schema(axn_class.input_schema) output_schema(axn_class.output_schema) unless axn_class.external_field_configs.empty? hint_annotations = Axn::MCP::Annotations.annotations_for(axn_class._semantic_hints) resolved_annotations = configured_annotations || (hint_annotations if hint_annotations.any?) # Skip the setter for an EMPTY hash, not just nil: `annotations: {}` (or a `configure(:mcp)` # empty override) is how a caller suppresses the semantic-hint-derived annotations, and # `MCP::Tool.annotations()` with no kwargs would instead advertise the SDK's own defaults # (destructiveHint: true, openWorldHint: true, ...) -- the opposite of what they asked for. self.annotations(**resolved_annotations) if resolved_annotations.present? define_singleton_method(:call) do |**kwargs| # Resolved fresh on every call, not captured once at wrap-time, so a gem-wide config # change takes effect immediately, even for tools already wrapped before the change. # # Reads the wrapped Axn's own per-action override (e.g. `axn_class.configure(:mcp) { |c| # c.present_as = :message }`) via Axn::MCP.resolve_override_for rather than # axn_class.present_as -- axn_class is a plain Axn that may never have included # Axn::MCP.overrides, so it may have no such method at all; resolve_override_for reads # the override store directly and falls back to the gem-wide config on its own, with no # dependency on axn_class having that accessor. Axn::MCP::Invocation.perform( axn_class, kwargs, text_content: present_as || Axn::MCP.resolve_override_for(axn_class, :present_as), reject_opaque_exposed_values: Axn::MCP.resolve_override_for(axn_class, :reject_opaque_exposed_values) ) end end end |