Module: McpLogs::Docs
- Defined in:
- app/models/mcp_logs/docs.rb
Overview
Renders a server's tool contract as reference documentation for a person, as opposed to the server itself, which serves it to an agent.
Reads MCP::Tool.to_h rather than the tool classes' own DSL, because to_h is the exact payload tools/list returns — so the page cannot drift from what a connected client actually receives.
Defined Under Namespace
Constant Summary collapse
- UNGROUPED =
"Ungrouped".freeze
Class Method Summary collapse
-
.params(schema) ⇒ Object
Property keys arrive as Symbols but the
requiredarray holds Strings, so membership has to be tested on strings. -
.sections(server, sections = McpLogs.tool_sections) ⇒ Object
sectionsis { "Title" => ["tool_name", ...] }, in the order the host wants them rendered. - .tool(klass) ⇒ Object
-
.tools_by_name(server) ⇒ Object
MCP::Server#tools is already { name => tool class }; going through it rather than the class list means the page shows exactly what the server serves.
-
.type_label(spec) ⇒ Object
Folds enum and array item type into the type label rather than the view, so a schema's accepted values are visible on the page instead of only in the tool's source file.
Class Method Details
.params(schema) ⇒ Object
Property keys arrive as Symbols but the required array holds Strings, so
membership has to be tested on strings. Comparing the Symbol key directly
against required returns false for every parameter, silently rendering the
whole contract as optional.
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
# File 'app/models/mcp_logs/docs.rb', line 56 def params(schema) required = Array(schema[:required] || schema["required"]).map(&:to_s) properties = schema[:properties] || schema["properties"] || {} properties.map do |name, spec| spec = spec.symbolize_keys if spec.respond_to?(:symbolize_keys) Param.new( name: name.to_s, type: type_label(spec), required: required.include?(name.to_s), description: spec[:description].to_s ) end end |
.sections(server, sections = McpLogs.tool_sections) ⇒ Object
sections is { "Title" => ["tool_name", ...] }, in the order the host wants
them rendered. A tool missing from every section still renders, under
UNGROUPED, so an omission is visible rather than silent.
20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
# File 'app/models/mcp_logs/docs.rb', line 20 def sections(server, sections = McpLogs.tool_sections) return [] if server.nil? by_name = tools_by_name(server) grouped = sections.map do |title, names| Section.new(title: title, tools: names.filter_map { |name| by_name[name.to_s] }.map { |klass| tool(klass) }) end leftover = by_name.except(*sections.values.flatten.map(&:to_s)) grouped << Section.new(title: UNGROUPED, tools: leftover.values.map { |klass| tool(klass) }) if leftover.any? grouped.reject { |section| section.tools.empty? } end |
.tool(klass) ⇒ Object
41 42 43 44 45 46 47 48 49 50 |
# File 'app/models/mcp_logs/docs.rb', line 41 def tool(klass) contract = klass.to_h Tool.new( name: contract[:name], title: contract[:title], description: contract[:description].to_s.strip, params: params(contract[:inputSchema] || {}) ) end |
.tools_by_name(server) ⇒ Object
MCP::Server#tools is already { name => tool class }; going through it rather than the class list means the page shows exactly what the server serves.
37 38 39 |
# File 'app/models/mcp_logs/docs.rb', line 37 def tools_by_name(server) server.tools.transform_keys(&:to_s) end |
.type_label(spec) ⇒ Object
Folds enum and array item type into the type label rather than the view, so a schema's accepted values are visible on the page instead of only in the tool's source file.
75 76 77 78 79 |
# File 'app/models/mcp_logs/docs.rb', line 75 def type_label(spec) base = spec[:type].to_s base = "#{base} of #{spec.dig(:items, :type)}" if base == "array" && spec.dig(:items, :type) spec[:enum] ? "#{base} (#{spec[:enum].join(" | ")})" : base end |