Class: Textus::Surfaces::MCP::Server

Inherits:
Object
  • Object
show all
Defined in:
lib/textus/surfaces/mcp/server.rb

Overview

MCP stdio server backed by the official mcp gem. The SDK owns protocol negotiation, tool dispatch, and JSON-RPC framing. This class owns the textus Session lifecycle (built lazily on first tool call) and delegates execution to Catalog.

Instance Method Summary collapse

Constructor Details

#initialize(store:, role: Textus::Role::DEFAULT, stdin: $stdin, stdout: $stdout) ⇒ Server

Returns a new instance of Server.



13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/textus/surfaces/mcp/server.rb', line 13

def initialize(store:, role: Textus::Role::DEFAULT, stdin: $stdin, stdout: $stdout)
  @store  = store
  @role   = role
  @stdin  = stdin
  @stdout = stdout
  # Session built eagerly so the contract_etag is captured at server start.
  # Changes to manifest/hooks/schemas after this point are detected as drift.
  @session = Textus::Session.new(
    role: @role,
    cursor: @store.audit_log.latest_seq,
    propose_lane: @store.manifest.policy.propose_lane_for(@role),
    contract_etag: contract_etag_now,
  )

  @sdk = ::MCP::Server.new(
    name: "textus",
    version: Textus::VERSION,
    tools: Catalog.build_tools(self),
    resources: build_resources,
    server_context: { mcp_server: self },
  )
  @sdk.resources_read_handler { |params, server_context:| handle_resource_read(params[:uri].to_s, server_context) }
end

Instance Method Details

#dispatch(verb_name, args, _server_context) ⇒ Object

Called from every MCP::Tool handler block in Catalog. The SDK parses JSON with symbolize_names: true — all nested keys are symbols. Deep-stringify so Catalog.call receives the string-key format it expects.



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/textus/surfaces/mcp/server.rb', line 54

def dispatch(verb_name, args, _server_context)
  str_args = deep_stringify_keys(args)
  @session.check_etag!(contract_etag_now) unless Catalog.read_verbs.include?(verb_name.to_s)
  result = Catalog.call(verb_name.to_s, session: @session, store: @store, args: str_args)
  update_session_for(verb_name.to_s)
  ::MCP::Tool::Response.new([{ type: "text", text: JSON.dump(result) }])
rescue Textus::ContractDrift => e
  raise_handler_error(e.message, Textus::ContractDrift::JSONRPC_CODE)
rescue CursorExpired => e
  raise_handler_error(e.message, CursorExpired::JSONRPC_CODE)
rescue Textus::Surfaces::MCP::ToolError => e
  raise_handler_error(e.message, ToolError::JSONRPC_CODE)
rescue StandardError => e
  raise_handler_error("internal: #{e.class}: #{e.message}", -32_603)
end

#runObject

Runs the stdio line loop; delegates each JSON line to the SDK.



38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/textus/surfaces/mcp/server.rb', line 38

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

    response = @sdk.handle_json(line)
    next unless response

    @stdout.puts(response)
    @stdout.flush
  end
end