Class: Hitch::MCP::Tool

Inherits:
Object
  • Object
show all
Defined in:
app/models/hitch/mcp/tool.rb

Overview

Declarative MCP tool descriptor. Registry admission, listing, and later execution resolve the current named subclass; instances are never needed.

Class Method Summary collapse

Class Method Details

.annotations(value = NOT_SET, **keywords) ⇒ Object



54
55
56
57
58
# File 'app/models/hitch/mcp/tool.rb', line 54

def annotations(value = NOT_SET, **keywords)
  return @annotations if value.equal?(NOT_SET) && keywords.empty?

  @annotations = copy_declaration(combine_value_and_keywords(value, keywords))
end

.authorize!(_context, arguments:) ⇒ Object

Argument-aware host policy is deny-default. Its return value is not an authority signal: allowing means returning without raising.

Raises:



68
69
70
# File 'app/models/hitch/mcp/tool.rb', line 68

def authorize!(_context, arguments:)
  raise Forbidden
end

.available_to?(_context) ⇒ Boolean

Coarse request-local admission. Host applications must opt each tool in explicitly.

Returns:

  • (Boolean)


62
63
64
# File 'app/models/hitch/mcp/tool.rb', line 62

def available_to?(_context)
  false
end

.call(server_context:, **sdk_arguments) ⇒ Object

This SDK-facing boundary is final. Registry validation rejects any subclass that replaces it, so every invocation follows the same context extraction, argument normalization, policy, host, and Result normalization order.



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'app/models/hitch/mcp/tool.rb', line 82

def call(server_context:, **sdk_arguments)
  phase = :context
  invocation = nil
  reporting_tool_name = tool_name
  context = server_context.fetch(:hitch_context)
  phase = :arguments
  arguments = normalize_arguments(sdk_arguments)
  invocation = Internal::Observation.start_invocation(tool_name: reporting_tool_name)
  phase = :authorization
  authorize!(context, arguments:)
  invocation&.argument_policy_allowed!
  phase = :execution
  invocation&.execution_started!
  result = perform(context, arguments:)
  phase = :result
  normalized = Internal::ResultNormalizer.call(
    result:,
    output_schema: output_schema,
    max_bytes: Hitch.configuration.mcp.max_result_bytes
  )
  invocation&.result_normalized!(kind: result.kind)
  normalized
rescue StandardError, SystemStackError => error
  invocation&.failed!(
    phase:,
    expected_denial: phase == :authorization && error.is_a?(Hitch::MCP::Forbidden)
  )
  Internal::ErrorNormalizer.call(
    error:,
    phase:,
    context:,
    tool_name: reporting_tool_name
  )
ensure
  invocation&.finish!
end

.description(value = NOT_SET) ⇒ Object



36
37
38
39
40
# File 'app/models/hitch/mcp/tool.rb', line 36

def description(value = NOT_SET)
  return @description if value.equal?(NOT_SET)

  @description = copy_declaration(value)
end

.inherited(subclass) ⇒ Object



21
22
23
24
25
26
27
28
# File 'app/models/hitch/mcp/tool.rb', line 21

def inherited(subclass)
  super
  subclass.instance_variable_set(:@tool_name, nil)
  subclass.instance_variable_set(:@description, nil)
  subclass.instance_variable_set(:@input_schema, nil)
  subclass.instance_variable_set(:@output_schema, nil)
  subclass.instance_variable_set(:@annotations, nil)
end

.input_schema(value = NOT_SET, **keywords) ⇒ Object



42
43
44
45
46
# File 'app/models/hitch/mcp/tool.rb', line 42

def input_schema(value = NOT_SET, **keywords)
  return @input_schema if value.equal?(NOT_SET) && keywords.empty?

  @input_schema = copy_declaration(combine_value_and_keywords(value, keywords))
end

.output_schema(value = NOT_SET, **keywords) ⇒ Object



48
49
50
51
52
# File 'app/models/hitch/mcp/tool.rb', line 48

def output_schema(value = NOT_SET, **keywords)
  return @output_schema if value.equal?(NOT_SET) && keywords.empty?

  @output_schema = copy_declaration(combine_value_and_keywords(value, keywords))
end

.perform(_context, arguments:) ⇒ Object

Host behavior runs only after availability, static scope, SDK schema, and argument-aware policy have all admitted the invocation.



74
75
76
# File 'app/models/hitch/mcp/tool.rb', line 74

def perform(_context, arguments:)
  raise "MCP tool perform must be implemented"
end

.tool_name(value = NOT_SET) ⇒ Object



30
31
32
33
34
# File 'app/models/hitch/mcp/tool.rb', line 30

def tool_name(value = NOT_SET)
  return @tool_name if value.equal?(NOT_SET)

  @tool_name = copy_declaration(value)
end