Class: Vehicles::McpServer

Inherits:
Object
  • Object
show all
Defined in:
lib/vehicles/mcp_server.rb

Overview

A tiny, read-only MCP (Model Context Protocol) server over the bundled dataset — so agents and LLM apps can ground themselves in real vehicle data instead of hallucinating nameplates. Runs on stdio, speaks JSON-RPC 2.0, needs no network, no API key, no database: vehicles-mcp and done.

{ "mcpServers": { "vehicles": { "command": "vehicles-mcp" } } }

Design notes:

* stdlib-only, like the rest of the gem (JSON over $stdin/$stdout).
* Read-only by construction — there is no tool that mutates anything.
* Answers come from the same snapshot the gem serves your app, so an
agent and your UI can never disagree about what exists.
* Protocol: newline-delimited JSON-RPC per the MCP stdio transport.
We answer `initialize`, `ping`, `tools/list`, `tools/call`, and
ignore notifications — the minimum a well-behaved server needs.

Constant Summary collapse

PROTOCOL_VERSION =
"2025-06-18"
TOOLS =

Tool definitions, MCP-shaped. Descriptions are written for the MODEL reading them (what to call, when, and what comes back), not for humans.

[
  {
    name: "search_makes",
    description: "Search vehicle manufacturers (makes) by name. Returns matching makes " \
                 "with their slug and which vehicle kinds they produce (car, motorcycle, " \
                 "moped, van, truck, bus). Call with an empty query to list every make. " \
                 "Forgiving: 'vw', 'merc' and 'škoda' resolve.",
    inputSchema: {
      type: "object",
      properties: {
        query: { type: "string", description: "Make name or fragment (empty = all makes)" },
        kind: { type: "string", enum: %w[car motorcycle moped van truck bus],
                description: "Only makes producing this kind" }
      }
    }
  },
  {
    name: "search_models",
    description: "Search vehicle models by name across all makes (e.g. 'golf', 'corolla', " \
                 "'transit'). Returns ranked matches with make, kind, body type, global " \
                 "popularity decile (1 = top 10%) and the countries where the model is " \
                 "evidenced by official sources.",
    inputSchema: {
      type: "object",
      properties: {
        query: { type: "string", description: "Model name or fragment; 'make model' works too" },
        limit: { type: "integer", description: "Max results (default 20)" }
      },
      required: ["query"]
    }
  },
  {
    name: "get_model",
    description: "Look up ONE exact vehicle model by make + model name and get its full " \
                 "record: canonical names, slugs, kind, body type, popularity decile, and " \
                 "availability countries. Use after search_models, or directly when you " \
                 "already know the pair (e.g. make: 'Toyota', model: 'Hilux').",
    inputSchema: {
      type: "object",
      properties: {
        make: { type: "string", description: "Make name, slug, or alias ('vw' works)" },
        model: { type: "string", description: "Model name or slug" }
      },
      required: %w[make model]
    }
  },
  {
    name: "top_models",
    description: "The most popular vehicle models by registration data, optionally filtered " \
                 "by kind and/or country (ISO alpha-2, e.g. 'nl', 'gb', 'th'). Popularity is " \
                 "measured from official registration/fleet counts across 12+ countries, " \
                 "expressed as deciles (1 = most popular).",
    inputSchema: {
      type: "object",
      properties: {
        kind: { type: "string", enum: %w[car motorcycle moped van truck bus] },
        country: { type: "string", description: "ISO-3166-1 alpha-2 country code" },
        limit: { type: "integer", description: "Max results (default 20)" }
      }
    }
  }
].freeze

Instance Method Summary collapse

Constructor Details

#initialize(input: $stdin, output: $stdout) ⇒ McpServer

Returns a new instance of McpServer.



89
90
91
92
# File 'lib/vehicles/mcp_server.rb', line 89

def initialize(input: $stdin, output: $stdout)
  @input = input
  @output = output
end

Instance Method Details

#runObject

Blocking serve loop: one JSON-RPC message per line until EOF. Malformed lines get a -32700 parse error instead of killing the process — an agent host restarting us mid-message must not take the server down.



97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/vehicles/mcp_server.rb', line 97

def run
  @input.each_line do |line|
    line = line.strip
    next if line.empty?

    begin
      msg = JSON.parse(line)
    rescue JSON::ParserError
      reply(id: nil, error: { code: -32_700, message: "Parse error" })
      next
    end
    handle(msg)
  end
end