Class: Foobara::McpConnector

Inherits:
CommandConnector show all
Defined in:
foobara-mcp-connector-0.0.5/src/mcp_connector.rb,
foobara-mcp-connector-0.0.5/src/request.rb,
foobara-mcp-connector-0.0.5/src/session.rb,
foobara-mcp-connector-0.0.5/src/stdio_runner.rb,
foobara-mcp-connector-0.0.5/src/commands/noop.rb,
foobara-mcp-connector-0.0.5/src/jsonrpc_request.rb,
foobara-mcp-connector-0.0.5/src/commands/initialize.rb,
foobara-mcp-connector-0.0.5/src/commands/list_commands.rb

Overview

TODO: somehow give a way to specify an association depth when registering commands?

Defined Under Namespace

Modules: Commands Classes: JsonrpcRequest, Request, Session, StdioRunner

Instance Attribute Summary collapse

Attributes inherited from CommandConnector

#authenticator, #capture_unknown_error, #command_registry

Instance Method Summary collapse

Methods inherited from CommandConnector

#all_exposed_commands, #all_exposed_type_names, allowed_rules_to_register, authenticator_registry, #build_command_instance, #build_response, #connect, #connect_delayed, #delayed_connections, #desugarize_connect_args, #determine_command_class, find_builtin_command_class, #find_builtin_command_class, #foobara_manifest, #foobara_manifest_in_current_namespace, #lookup_command, #mutate_response, #normalize_manifest, #patch_up_broken_parents_for_errors_with_missing_command_parents, #process_delayed_connections, register_allowed_rule, register_authenticator, #request_to_command_instance, #request_to_response, #run_command, to_authenticator, #transform_command_class, #type_from_name

Constructor Details

#initialize(capture_unknown_error: true, server_name: default_server_name, server_version: default_server_version, instructions: default_instructions, default_serializers: Foobara::CommandConnectors::Serializers::AtomicSerializer) ⇒ McpConnector

Returns a new instance of McpConnector.



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# File 'foobara-mcp-connector-0.0.5/src/mcp_connector.rb', line 6

def initialize(
  *,
  capture_unknown_error: true,
  server_name: default_server_name,
  server_version: default_server_version,
  instructions: default_instructions,
  default_serializers: Foobara::CommandConnectors::Serializers::AtomicSerializer,
  **,
  &
)
  self.server_name = server_name
  self.server_version = server_version
  self.instructions = instructions

  super(*, capture_unknown_error:, default_serializers:, **, &)
end

Instance Attribute Details

#current_sessionObject

Returns the value of attribute current_session.



4
5
6
# File 'foobara-mcp-connector-0.0.5/src/mcp_connector.rb', line 4

def current_session
  @current_session
end

#instructionsObject

Returns the value of attribute instructions.



4
5
6
# File 'foobara-mcp-connector-0.0.5/src/mcp_connector.rb', line 4

def instructions
  @instructions
end

#server_nameObject

Returns the value of attribute server_name.



4
5
6
# File 'foobara-mcp-connector-0.0.5/src/mcp_connector.rb', line 4

def server_name
  @server_name
end

#server_versionObject

Returns the value of attribute server_version.



4
5
6
# File 'foobara-mcp-connector-0.0.5/src/mcp_connector.rb', line 4

def server_version
  @server_version
end

Instance Method Details

#build_requestObject



95
96
97
98
99
100
101
102
103
# File 'foobara-mcp-connector-0.0.5/src/mcp_connector.rb', line 95

def build_request(*, **, &)
  request = super

  # We add a serializer to the top-level request but not to the children of a batch requests

  request.serializers = [*request.serializers, json_serializer]

  request
end

#default_instructionsObject



31
32
33
# File 'foobara-mcp-connector-0.0.5/src/mcp_connector.rb', line 31

def default_instructions
  "This is a Foobara MCP command connector which exposes Foobara commands to you as tools that you can invoke."
end

#default_server_nameObject



23
24
25
# File 'foobara-mcp-connector-0.0.5/src/mcp_connector.rb', line 23

def default_server_name
  "Foobara MCP Command connector"
end

#default_server_versionObject



27
28
29
# File 'foobara-mcp-connector-0.0.5/src/mcp_connector.rb', line 27

def default_server_version
  Gem.loaded_specs["foobara-mcp-connector"].version.to_s
end

#json_serializerObject



105
106
107
# File 'foobara-mcp-connector-0.0.5/src/mcp_connector.rb', line 105

def json_serializer
  @json_serializer ||= Foobara::CommandConnectors::Serializers::JsonSerializer.new
end

#request_to_command_class(request) ⇒ Object



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'foobara-mcp-connector-0.0.5/src/mcp_connector.rb', line 43

def request_to_command_class(request)
  return if request.error?

  builtin_command_class_name = case request.action
                               when "initialize"
                                 "Initialize"
                               when "notifications/initialized", "notifications/cancelled",
                                    "notifications/progress", "notifications/roots/list_changed"
                                 "Noop"
                               else
                                 return super
                               end

  command_class = find_builtin_command_class(builtin_command_class_name)

  full_command_name = command_class.full_command_name

  transformed_command_from_name(full_command_name) || transform_command_class(command_class)
rescue CommandConnector::NoCommandFoundError => e
  request.error = e
  nil
end

#request_to_command_inputs(request) ⇒ Object



66
67
68
69
70
71
72
73
74
75
76
# File 'foobara-mcp-connector-0.0.5/src/mcp_connector.rb', line 66

def request_to_command_inputs(request)
  return if request.error?

  case request.action
  when "initialize", "notifications/initialized", "notifications/cancelled", "notifications/progress",
    "notifications/roots/list_changed"
    (request.params || {}).merge(request:)
  else
    super
  end
end

#run(*args, **opts) ⇒ Object



35
36
37
# File 'foobara-mcp-connector-0.0.5/src/mcp_connector.rb', line 35

def run(*args, **opts, &)
  super.body
end

#run_request(request) ⇒ Object



83
84
85
86
87
88
89
90
91
92
93
# File 'foobara-mcp-connector-0.0.5/src/mcp_connector.rb', line 83

def run_request(request)
  if request.batch? && !request.error
    request.batch.each do |batch_request|
      super(batch_request)
    end

    build_response(request)
  else
    super
  end
end

#run_stdio_server(io_in: $stdin, io_out: $stdout, io_err: $stderr) ⇒ Object



39
40
41
# File 'foobara-mcp-connector-0.0.5/src/mcp_connector.rb', line 39

def run_stdio_server(io_in: $stdin, io_out: $stdout, io_err: $stderr)
  StdioRunner.new(self).run(io_in: io_in, io_out: io_out, io_err: io_err)
end

#serialize_response_body(response) ⇒ Object

TODO: feels awkward needing to override this for such basic/typical behavior TODO: fix this interface/pattern higher-up



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'foobara-mcp-connector-0.0.5/src/mcp_connector.rb', line 111

def serialize_response_body(response)
  super

  request = response.request

  unless request.notification?
    # TODO: total hack, clean this up somehow...
    if request.tool_call? && request.success?
      result = response.body[:result]

      result = json_serializer.serialize(result)

      response.body[:result] = { content: [{ type: "text", text: result }] }
    end

    if request.serializer
      response.body = request.serializer.process_value!(response.body)
    end
  end
end

#session_created(session) ⇒ Object

TODO: figure out how to support multiple sessions



79
80
81
# File 'foobara-mcp-connector-0.0.5/src/mcp_connector.rb', line 79

def session_created(session)
  self.current_session = session
end

#set_response_body(response) ⇒ Object

TODO: Should this be implemented on response object instead??



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'foobara-mcp-connector-0.0.5/src/mcp_connector.rb', line 133

def set_response_body(response)
  request = response.request

  return if request.notification?

  response.body = if request.valid_batch?
                    request.batch.map do |batched_request|
                      batched_request.response.body
                    end.compact
                  else
                    body = { jsonrpc: "2.0", id: request.request_id }

                    if request.error
                      body.merge(error: { message: request.error.message, code: response.status })
                    else
                      outcome = request.outcome

                      if outcome.success?
                        body.merge(result: outcome.result)
                      else
                        body.merge(error: {
                                     message: outcome.errors_sentence,
                                     data: outcome.errors_hash,
                                     code: response.status
                                   })
                      end
                    end
                  end
end

#set_response_status(response) ⇒ Object



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'foobara-mcp-connector-0.0.5/src/mcp_connector.rb', line 163

def set_response_status(response)
  return if response.request.valid_batch?

  request = response.request

  response.status = if request.error
                      case request.error
                      when Request::InvalidJsonrpcVersionError, Request::InvalidJsonrpcMethodError,
                        Request::InvalidJsonrpcParamsError, Request::EmptyBatchError,
                        Request::InvalidJsonrpcRequestStructureError
                        -32_600
                      when Request::InvalidJsonError
                        -32_700
                      when CommandConnector::NotFoundError
                        -32_601
                      when Request::FoobaraCommandsDoNotAcceptArraysError
                        -32_602
                      else
                        -32_603
                      end
                    elsif request.success?
                      0
                    else
                      errors = request.error_collection.error_array
                      error = errors.first

                      # Going to steal some http codes to be less confusing
                      case error
                      when Foobara::Entity::NotFoundError
                        # TODO: we should not be coupled to Entities here...
                        # :nocov:
                        404
                        # :nocov:
                      when CommandConnector::UnauthenticatedError
                        # :nocov:
                        401
                        # :nocov:
                      when CommandConnector::NotAllowedError
                        # :nocov:
                        403
                        # :nocov:
                      when CommandConnector::UnknownError
                        -32_603
                      when Foobara::DataError
                        -32_602
                      end || -32_600
                    end
end