Class: Marketing::AdlabsMcpClient

Inherits:
Object
  • Object
show all
Defined in:
app/services/marketing/adlabs_mcp_client.rb

Overview

Thin client for the AdLabs MCP server (Amazon Ads / PPC management).

AdLabs exposes a stateful, conversational MCP surface at
https://mcp.adlabs.app, authenticated with a custom +X-ADLABS-MCP-KEY+
header. Unlike a one-shot REST API, its tools thread a +chat_session_id+ and
pass server-side +mcp://data/...+ references between calls, so a single
client instance (one MCP connection) must back every tool call within one
assistant conversation turn for those references to resolve.

Wraps the official MCP Ruby SDK's HTTP transport — the same plumbing as
UpstreamProxy. The API key is read from Rails credentials
(+adlabs_mcp.api_key+) via Heatwave::Configuration, the same secret the
standalone +.mcp.json+ server (Claude Code / Cursor / Zed) reads from
+ADLABS_MCP_KEY+.

Examples:

List the catalog and call a tool

client = Marketing::AdlabsMcpClient.new
client.list_tools.map(&:name)            # => ["start_chat_session", "get_entity_data", ...]
client.call_tool("start_chat_session")   # => "{\"chat_session_id\":\"...\"}"

See Also:

Defined Under Namespace

Classes: AuthenticationError, Error

Constant Summary collapse

URL =

AdLabs MCP endpoint (streamable HTTP).

'https://mcp.adlabs.app'
AUTH_HEADER =

Custom auth header carrying the API key.

'X-ADLABS-MCP-KEY'
OPEN_TIMEOUT =

Connect timeout (seconds) for the underlying HTTP transport.

8
READ_TIMEOUT =

Read timeout (seconds) — queries over large ad accounts can be slow.

60
CATALOG_CACHE_KEY =

Rails.cache key for the (conversation-independent) tool catalog.

'adlabs:mcp:catalog:v1'
CATALOG_TTL =

How long to cache the catalog. Tool definitions are static between
AdLabs deploys; a deploy of this app also clears the in-memory cache.

1.hour

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(api_key: nil) ⇒ AdlabsMcpClient

Returns a new instance of AdlabsMcpClient.

Parameters:

  • api_key (String, nil) (defaults to: nil)

    overrides the credential lookup (tests)

Raises:



77
78
79
80
# File 'app/services/marketing/adlabs_mcp_client.rb', line 77

def initialize(api_key: nil)
  @api_key = api_key.presence || Heatwave::Configuration.fetch(:adlabs_mcp, :api_key)
  raise AuthenticationError, 'AdLabs MCP API key not configured (adlabs_mcp.api_key)' if @api_key.blank?
end

Class Method Details

.catalogArray<Hash>

The AdLabs tool catalog as plain, cacheable hashes
(+{ 'name', 'description', 'input_schema' }+). Conversation-independent
and shared across every build, so the per-turn client doesn't pay a
+tools/list+ round-trip just to enumerate tools. Returns +[]+ (uncached)
on any failure so a transient outage doesn't poison the cache.

Returns:

  • (Array<Hash>)


57
58
59
60
61
62
63
64
65
66
# File 'app/services/marketing/adlabs_mcp_client.rb', line 57

def catalog
  Rails.cache.fetch(CATALOG_CACHE_KEY, expires_in: CATALOG_TTL) do
    new.list_tools.map do |t|
      { 'name' => t.name, 'description' => t.description.to_s, 'input_schema' => t.input_schema }
    end
  end
rescue StandardError => e
  Rails.logger.warn("[AdlabsMcpClient] catalog fetch failed: #{e.message}")
  []
end

.reset_catalog!void

This method returns an undefined value.

Drop the cached catalog (tests, or after an AdLabs tool change).



71
72
73
# File 'app/services/marketing/adlabs_mcp_client.rb', line 71

def reset_catalog!
  Rails.cache.delete(CATALOG_CACHE_KEY)
end

Instance Method Details

#call_tool(tool_name, arguments = {}) ⇒ String

Call an AdLabs tool by its upstream name and return its textual result.
Never raises — upstream/transport failures come back as a JSON error
payload the model can read and react to.

Parameters:

  • tool_name (String)

    the upstream tool name (e.g. "get_entity_data")

  • arguments (Hash) (defaults to: {})

    tool arguments (symbol or string keys)

Returns:

  • (String)

    joined text content, or a JSON +{ "error" => ... }+ payload



96
97
98
99
100
101
102
103
# File 'app/services/marketing/adlabs_mcp_client.rb', line 96

def call_tool(tool_name, arguments = {})
  response = client.call_tool(name: tool_name, arguments: arguments.to_h)
  extract_text(response)
rescue MCP::Client::RequestHandlerError => e
  { error: "AdLabs error: #{e.message}" }.to_json
rescue StandardError => e
  { error: "AdLabs client error: #{e.message}" }.to_json
end

#list_toolsArray<MCP::Client::Tool>

List the tools the AdLabs server exposes. Memoized per instance; the
first call performs the MCP +initialize+ handshake.

Returns:

  • (Array<MCP::Client::Tool>)


85
86
87
# File 'app/services/marketing/adlabs_mcp_client.rb', line 85

def list_tools
  @list_tools ||= client.tools
end