Cookbook
View as Markdown

Give your own agent Victoria AI tools

A small agent on the Claude API that attaches the Victoria AI MCP server through the MCP connector, kept read-only by its key and an allow-list of tools.

Last updated

  • Claude API
  • Slack

MCP tools used

  • campaigns_listread, campaigns:readList the organization's campaigns.
  • campaign_analysisread, campaigns:readFunnel, replies, channels and the A/B verdict.
  • campaign_queueread, campaigns:readBacklog, daily capacity, runway and warnings.
  • campaign_step_funnelread, campaigns:readHow leads move through each step.
  • campaign_sender_breakdownread, campaigns:readOutbound and replies per sender.
  • sender_accountsread, accounts:readThe connected LinkedIn and email accounts.
  • deals_lookupread, crm:readFind deals by stage, owner or lead.

What a connected assistant can do

The connectors in Claude and ChatGPT are the easy way to talk to Victoria AI. When the conversation should live somewhere else, in a Slack bot, an internal tool or a nightly job that writes a paragraph, you build the agent yourself on the Claude API. The MCP connector makes that short: the API connects to Victoria AI's MCP server on Anthropic's side, so your code sends a question and gets an answer, with the tool calls in between. This recipe is that agent, kept read-only by two independent means.

Before you start

  • An Anthropic API key in ANTHROPIC_API_KEY. The MCP connector is a beta feature of the Messages API; the SDK call below sets the beta header.
  • A Victoria AI API key with campaigns:read, accounts:read and crm:read in VICTORIA_API_KEY, and nothing else. See Create an API key. The server lists only the tools the key's scopes allow, so a read-only key can't reach a write tool whatever the model asks.
  • Python 3.10 or later with anthropic, or Node.js 18 or later with @anthropic-ai/sdk.

How it works

  1. The request names the Victoria AI MCP server in mcp_servers, with the API key as its authorization_token, and adds an mcp_toolset for it to tools. Both are required together.
  2. The toolset's default_config disables every tool, and configs enables the seven this agent may use. That's the second guard: even with a wider key, the model sees only these.
  3. Claude reads the question, calls the tools it needs (the API makes those calls to Victoria AI server-side and feeds the results back), and answers. The response carries mcp_tool_use and mcp_tool_result blocks alongside the text, so the code can show which calls the answer rests on.

The agent

# ask_victoria.py
import os
import sys

import anthropic

VICTORIA_MCP_URL = "https://api.versionseven.ai/mcp"
READ_TOOLS = (
    "campaigns_list",
    "campaign_analysis",
    "campaign_queue",
    "campaign_step_funnel",
    "campaign_sender_breakdown",
    "sender_accounts",
    "deals_lookup",
)
SYSTEM = (
    "You answer questions about the user's outbound campaigns in Victoria AI, using the tools. "
    "Quote the numbers you read and name the campaign they belong to. Everything is per lead unless a metric says messages. "
    "If a tool answers with an error, say what it was. You cannot change anything; say so if asked to."
)

client = anthropic.Anthropic()


def ask(question: str) -> tuple[str, list[str]]:
    """The answer, and one line per tool call it rested on."""
    response = client.beta.messages.create(
        model="claude-opus-5-5",
        max_tokens=4000,
        betas=["mcp-client-2025-11-20"],
        system=SYSTEM,
        messages=[{"role": "user", "content": question}],
        mcp_servers=[
            {
                "type": "url",
                "url": VICTORIA_MCP_URL,
                "name": "victoria",
                "authorization_token": os.environ["VICTORIA_API_KEY"],
            }
        ],
        tools=[
            {
                "type": "mcp_toolset",
                "mcp_server_name": "victoria",
                "default_config": {"enabled": False},
                "configs": {name: {"enabled": True} for name in READ_TOOLS},
            }
        ],
    )
    calls = []
    answer = []
    for block in response.content:
        if block.type == "mcp_tool_use":
            calls.append(f"{block.name}({', '.join(f'{k}={v!r}' for k, v in block.input.items())})")
        elif block.type == "mcp_tool_result" and block.is_error:
            calls.append(f"  ↳ error: {block.content[0].text if block.content else 'unknown'}")
        elif block.type == "text":
            answer.append(block.text)
    if response.stop_reason == "refusal":
        return "The model declined to answer this question.", calls
    return "\n".join(answer).strip(), calls


if __name__ == "__main__":
    question = " ".join(sys.argv[1:]) or "Which campaigns got positive replies in the last 7 days, and how many each?"
    answer, calls = ask(question)
    print(answer)
    if calls:
        print("\nBased on:\n  " + "\n  ".join(calls))
pip install anthropic && python ask_victoria.py "Which campaign is closest to running out of leads?"
npm install @anthropic-ai/sdk && node ask-victoria.mjs "How did Q3 outbound do this week?"

The output is the answer, then the calls it rested on:

Q3 outbound got 3 positive replies from 25 leads first contacted in the last 7 days (12%), …

Based on:
  campaigns_list()
  campaign_analysis(campaign_id="550e8400-…", date_filter="7d")

Putting it in Slack

A Slack slash command is the natural home: a user types /victoria how did Q3 do this week?, Slack posts the text to your endpoint, your endpoint calls ask() and replies. Slack wants an acknowledgement within 3 seconds and the agent takes longer, so answer 200 at once and post the result to the response_url Slack included. Verify Slack's request signature (its signing secret, over the timestamp and body) before trusting the text, the same discipline as the webhook receiver. Slack's own docs cover the slash-command handshake; the agent above is the only Victoria-specific part.

Why read-only, twice

The connector runs the tool calls on Anthropic's side, inside the one API request. Nothing stops between the model deciding to call a tool and the call happening, which is fine for reads and not fine for a write: there is no moment for a person to confirm, which is what Claude and ChatGPT provide for the create_campaign, update_sequence or activate_campaign tools. So this agent is kept to reads in two places that fail independently: the Victoria key's scopes (the server won't list or run a write for a read key) and the toolset allow-list (the model never sees anything but the seven). An agent that should write needs its own confirmation step, in your code, before each write; the simplest form is to run reads through this connector and route any requested write to a person, or to a connected assistant where the host asks.

What to expect

  • Credits: none of the seven tools spends Victoria AI credits; the Anthropic side bills your API usage. The two lead-database tools are not in the allow-list and the key doesn't permit them.
  • Limits: each API key gets 120 tool calls a minute and 2,000 a day on the Victoria side. A question uses two to five.
  • Errors come back as data: a refused tool (over the limit, or outside the key's scopes) is an mcp_tool_result with is_error: true and the code in its text, which the agent reports rather than crashing. Victoria AI being unreachable surfaces the same way.
  • Transport: the connector needs a public HTTP MCP server; Victoria AI's is Streamable HTTP at https://api.versionseven.ai/mcp, which is what mcp_servers takes. A 401 from the server means the key is wrong or revoked.
  • Model: the examples name claude-opus-5-5. Any current model with the connector works; the call shape is the same.

Next steps