Module: OdataDuty::McpServerBuilder
- Extended by:
- McpServerBuilder
- Included in:
- McpServerBuilder
- Defined in:
- lib/odata_duty/mcp_server_builder.rb
Constant Summary collapse
- QUERY_OPTION_SPELLINGS =
Inverse of McpInputSchemas::QUERY_OPTION_ALIASES: translates a tool call's
odata_*arguments back to their$-prefixed OData spelling before they reach Executor. McpInputSchemas::QUERY_OPTION_ALIASES.invert.freeze
Instance Method Summary collapse
- #build(schema) ⇒ Object
-
#define_tool(server, schema, action, url_for:, **tool_args) ⇒ Object
On the .mutant.yml ignore list:
server_context[:context]has only equivalent mutants ([]vsfetch/dig); the key is always present, so no public-API test distinguishes them. -
#keyed_url_for(endpoint) ⇒ Object
Builds the
<url>('<key>')locator from the tool arguments. -
#query_options_for(action, args) ⇒ Object
The
odata_*aliases only stand in for OData query options on read (:execute) tools —:create/:update/:deletetools' arguments are property values, so a property literally named e.g. - #register_collection_tools(server, schema, endpoint) ⇒ Object
- #register_count_tool(server, schema, endpoint) ⇒ Object
- #register_create_tool(server, schema, endpoint) ⇒ Object
- #register_delete_tool(server, schema, endpoint) ⇒ Object
- #register_endpoint_tools(server, schema, endpoint) ⇒ Object
- #register_get_tool(server, schema, endpoint) ⇒ Object
- #register_key_tool(server, schema, endpoint, action) ⇒ Object
- #register_list_tool(server, schema, endpoint) ⇒ Object
- #register_update_tool(server, schema, endpoint) ⇒ Object
-
#run_tool(action, url:, schema:, context:, query_options:) ⇒ Object
On the .mutant.yml ignore list:
e.messagehas only an equivalent mutant (e), since an OdataDuty::Error renders identically to its message in the text content block. - #tool_description(verb_text, endpoint) ⇒ Object
Instance Method Details
#build(schema) ⇒ Object
14 15 16 17 18 19 20 21 22 23 24 25 26 |
# File 'lib/odata_duty/mcp_server_builder.rb', line 14 def build(schema) server = MCP::Server.new( name: schema.title, version: schema.version, # Relies on the `mcp` gem's `initialize` response builder `.compact`-ing away a nil # `instructions:`, so a schema without a description omits the key rather than sending # `"instructions": null` — worth re-checking on `mcp` gem upgrades. instructions: schema.description, capabilities: { tools: {} } ) schema.endpoints.each { |endpoint| register_endpoint_tools(server, schema, endpoint) } server end |
#define_tool(server, schema, action, url_for:, **tool_args) ⇒ Object
On the .mutant.yml ignore list: server_context[:context] has only equivalent mutants
([] vs fetch/dig); the key is always present, so no public-API test distinguishes them.
109 110 111 112 113 114 115 116 117 |
# File 'lib/odata_duty/mcp_server_builder.rb', line 109 def define_tool(server, schema, action, url_for:, **tool_args) McpIdentifierValidator.validate_tool_name!(tool_args[:name]) server.define_tool(**tool_args) do |server_context:, **args| McpServerBuilder.run_tool(action, url: url_for.call(args), schema: schema, context: server_context[:context], query_options: McpServerBuilder.(action, args)) end end |
#keyed_url_for(endpoint) ⇒ Object
Builds the <url>('<key>') locator from the tool arguments. The dynamic args[key] lookup
has no mutation-testable equivalent (the key is always present, enforced by the SDK's
required-argument check), so this is the one MCP subject left on the .mutant.yml ignore list.
102 103 104 105 |
# File 'lib/odata_duty/mcp_server_builder.rb', line 102 def keyed_url_for(endpoint) key = endpoint.entity_type.property_refs.first.name ->(args) { "#{endpoint.url}('#{args[key]}')" } end |
#query_options_for(action, args) ⇒ Object
The odata_* aliases only stand in for OData query options on read (:execute) tools —
:create/:update/:delete tools' arguments are property values, so a property literally
named e.g. odata_select must reach Executor unchanged, not get aliased to $select.
122 123 124 125 126 |
# File 'lib/odata_duty/mcp_server_builder.rb', line 122 def (action, args) return args.transform_keys(&:to_s) unless action == :execute args.to_h { |key, value| [QUERY_OPTION_SPELLINGS.fetch(key.to_s, key.to_s), value] } end |
#register_collection_tools(server, schema, endpoint) ⇒ Object
36 37 38 39 |
# File 'lib/odata_duty/mcp_server_builder.rb', line 36 def register_collection_tools(server, schema, endpoint) register_list_tool(server, schema, endpoint) register_count_tool(server, schema, endpoint) if endpoint.supports_count? end |
#register_count_tool(server, schema, endpoint) ⇒ Object
57 58 59 60 61 62 63 64 |
# File 'lib/odata_duty/mcp_server_builder.rb', line 57 def register_count_tool(server, schema, endpoint) input_schema = McpInputSchemas.count_input_schema(supports_search: endpoint.supports_search?) description = tool_description(OperationVerbs.count(endpoint.name), endpoint) tool_args = { name: "count_#{endpoint.name}", description: description, input_schema: input_schema } define_tool(server, schema, :execute, url_for: ->(_args) { "#{endpoint.url}/$count" }, **tool_args) end |
#register_create_tool(server, schema, endpoint) ⇒ Object
66 67 68 69 70 71 72 73 |
# File 'lib/odata_duty/mcp_server_builder.rb', line 66 def register_create_tool(server, schema, endpoint) tool_name = "create_#{endpoint.name}" input_schema = McpInputSchemas.create_input_schema(endpoint.entity_type) description = tool_description(OperationVerbs.create(endpoint.name), endpoint) tool_args = { name: tool_name, description: description, input_schema: input_schema } McpIdentifierValidator.validate_properties!(endpoint, tool_name, input_schema) define_tool(server, schema, :create, url_for: ->(_args) { endpoint.url }, **tool_args) end |
#register_delete_tool(server, schema, endpoint) ⇒ Object
45 46 47 |
# File 'lib/odata_duty/mcp_server_builder.rb', line 45 def register_delete_tool(server, schema, endpoint) register_key_tool(server, schema, endpoint, :delete) end |
#register_endpoint_tools(server, schema, endpoint) ⇒ Object
28 29 30 31 32 33 34 |
# File 'lib/odata_duty/mcp_server_builder.rb', line 28 def register_endpoint_tools(server, schema, endpoint) register_collection_tools(server, schema, endpoint) if endpoint.supports_collection? register_get_tool(server, schema, endpoint) if endpoint.supports_individual? register_create_tool(server, schema, endpoint) if endpoint.supports_create? register_update_tool(server, schema, endpoint) if endpoint.supports_update? register_delete_tool(server, schema, endpoint) if endpoint.supports_delete? end |
#register_get_tool(server, schema, endpoint) ⇒ Object
75 76 77 78 79 80 81 82 |
# File 'lib/odata_duty/mcp_server_builder.rb', line 75 def register_get_tool(server, schema, endpoint) tool_name = "get_#{endpoint.name}" input_schema = McpInputSchemas.get_input_schema(endpoint.entity_type, tool_name: tool_name) description = tool_description(OperationVerbs.get(endpoint.name), endpoint) tool_args = { name: tool_name, description: description, input_schema: input_schema } McpIdentifierValidator.validate_properties!(endpoint, tool_name, input_schema) define_tool(server, schema, :execute, url_for: keyed_url_for(endpoint), **tool_args) end |
#register_key_tool(server, schema, endpoint, action) ⇒ Object
84 85 86 87 88 89 90 91 |
# File 'lib/odata_duty/mcp_server_builder.rb', line 84 def register_key_tool(server, schema, endpoint, action) tool_name = "#{action}_#{endpoint.name}" input_schema = McpInputSchemas.public_send("#{action}_input_schema", endpoint.entity_type) description = tool_description(OperationVerbs.public_send(action, endpoint.name), endpoint) tool_args = { name: tool_name, description: description, input_schema: input_schema } McpIdentifierValidator.validate_properties!(endpoint, tool_name, input_schema) define_tool(server, schema, action, url_for: keyed_url_for(endpoint), **tool_args) end |
#register_list_tool(server, schema, endpoint) ⇒ Object
49 50 51 52 53 54 55 |
# File 'lib/odata_duty/mcp_server_builder.rb', line 49 def register_list_tool(server, schema, endpoint) input_schema = McpInputSchemas.list_input_schema(supports_search: endpoint.supports_search?) description = tool_description(OperationVerbs.list(endpoint.name), endpoint) tool_args = { name: "list_#{endpoint.name}", description: description, input_schema: input_schema } define_tool(server, schema, :execute, url_for: ->(_args) { endpoint.url }, **tool_args) end |
#register_update_tool(server, schema, endpoint) ⇒ Object
41 42 43 |
# File 'lib/odata_duty/mcp_server_builder.rb', line 41 def register_update_tool(server, schema, endpoint) register_key_tool(server, schema, endpoint, :update) end |
#run_tool(action, url:, schema:, context:, query_options:) ⇒ Object
On the .mutant.yml ignore list: e.message has only an equivalent mutant (e), since an
OdataDuty::Error renders identically to its message in the text content block.
130 131 132 133 134 135 136 |
# File 'lib/odata_duty/mcp_server_builder.rb', line 130 def run_tool(action, url:, schema:, context:, query_options:) result = Executor.public_send(action, url: url, context: context, query_options: , schema: schema) MCP::Tool::Response.new([{ type: 'text', text: result.to_s }]) rescue OdataDuty::Error => e MCP::Tool::Response.new([{ type: 'text', text: e. }], error: true) end |
#tool_description(verb_text, endpoint) ⇒ Object
93 94 95 96 97 |
# File 'lib/odata_duty/mcp_server_builder.rb', line 93 def tool_description(verb_text, endpoint) return verb_text unless endpoint.description "#{verb_text}. #{endpoint.description}" end |