Class: Utcp::Providers::McpProvider

Inherits:
BaseProvider show all
Defined in:
lib/utcp/providers/mcp_provider.rb

Overview

Minimal HTTP-based MCP provider. Works in two modes:

- Manual discovery: GET {url}{discovery_path} returns a UTCP manual (tools array).
- Execution: POST {url}{call_path} with {"tool": name, "arguments": {...}}.

Streaming:

- If the server replies with 'text/event-stream', we'll parse SSE 'data:' lines and yield them.
- Otherwise, if a block is given, chunks from the HTTP body are yielded as they arrive.

Instance Attribute Summary

Attributes inherited from BaseProvider

#auth, #name, #type

Instance Method Summary collapse

Constructor Details

#initialize(name:, url:, headers: {}, auth: nil, manual: false, discovery_path: "/manual", call_path: "/call") ⇒ McpProvider

Returns a new instance of McpProvider.



22
23
24
25
26
27
28
29
# File 'lib/utcp/providers/mcp_provider.rb', line 22

def initialize(name:, url:, headers: {}, auth: nil, manual: false, discovery_path: "/manual", call_path: "/call")
  super(name: name, provider_type: manual ? "mcp_manual" : "mcp", auth: auth)
  @url = Utils::Subst.apply(url)
  @headers = Utils::Subst.apply(headers || {})
  @manual = manual
  @discovery_path = discovery_path
  @call_path = call_path
end

Instance Method Details

#call_tool(tool, arguments = {}, &block) ⇒ Object

Expects tool.provider to include MCP endpoint info: { “provider_type”: “mcp”, “url”: “host:port/mcp”,

"call_path": "/call", "headers": { ... } }


53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/utcp/providers/mcp_provider.rb', line 53

def call_tool(tool, arguments = {}, &block)
  p = tool.provider || {}
  base = Utils::Subst.apply(p["url"] || @url)
  call_path = p["call_path"] || @call_path
  uri = URI(join_path(base, call_path))

  body = { "tool" => tool.name, "arguments" => Utils::Subst.apply(arguments || {}) }
  req = Net::HTTP::Post.new(uri)
  headers = default_headers.merge({ "Content-Type" => "application/json" }).merge(Utils::Subst.apply(p["headers"] || {}))
  apply_auth!(uri, headers)
  headers.each { |k, v| req[k] = v }
  req.body = JSON.dump(body)

  http = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https")
  begin
    if block_given?
      http.request(req) do |res|
        ctype = (res["Content-Type"] || "").downcase
        if ctype.include?("text/event-stream")
          buffer = +""
          res.read_body do |chunk|
            buffer << chunk
            while (line = buffer.slice!(/.*\n/))
              line = line.strip
              next if line.empty? || line.start_with?(":")
              if line.start_with?("data:")
                data = line.sub(/^data:\s?/, "")
                yield data
              end
            end
          end
        else
          res.read_body do |chunk|
            yield chunk
          end
        end
      end
      nil
    else
      res = http.request(req)
      begin
        JSON.parse(res.body)
      rescue
        res.body
      end
    end
  ensure
    http.finish if http.active?
  end
end

#discover_tools!Object

Raises:



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/utcp/providers/mcp_provider.rb', line 31

def discover_tools!
  raise ProviderError, "Not a manual provider" unless @manual
  uri = URI(join_path(@url, @discovery_path))
  req = Net::HTTP::Get.new(uri)
  headers = default_headers
  apply_auth!(uri, headers)
  headers.each { |k, v| req[k] = v }

  http = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https")
  begin
    res = http.request(req)
    raise ProviderError, "MCP discovery failed: #{res.code}" unless res.is_a?(Net::HTTPSuccess)
    manual = JSON.parse(res.body)
    to_tools(manual)
  ensure
    http.finish if http.active?
  end
end