stateweave Docs
Guides

MCP servers

Slack, Gmail, Salesforce and Shopify serve the same MCP servers as the real vendors, at the run's address and with the persona's token. An agent that works through MCP tools needs only a config file.

Apps with an MCP server

AppServerTokenExample tools
SlackSlack's MCP serveruser:slack_send_message, slack_add_reaction, slack_create_conversation
GmailGmail's MCP serveruser:search_threads, get_thread, create_draft
SalesforceHosted MCP server sobject-allmcp: soqlQuery, find, getObjectSchema
ShopifyShopify's MCP serversetup:create-collection, add-to-collection, bulk-update-product-status

Google Calendar, Google Sheets, Google Drive and Zendesk have no MCP server in StateWeave: an agent reaches them through their APIs (see Apps and credentials). In a run, their mcp field is null and they are left out of the MCP config.

The MCP config

Every run comes with its MCP servers in the format Claude Code and most MCP clients read. The server names are the app names, and each server is Streamable HTTP with the token in its headers:

mcp.json
{
  "mcpServers": {
    "slack": {
      "type": "http",
      "url": "https://t-7d1kq4.app.stateweave.net/mcp",
      "headers": {"Authorization": "Bearer xoxp-…"}
    },
    "gmail": {
      "type": "http",
      "url": "https://t-m3x9ce.app.stateweave.net/mcp/v1",
      "headers": {"Authorization": "Bearer ya29.…"}
    }
  }
}

Three ways to get it:

  • sw run writes it to a file and passes the path in STATEWEAVE_MCP_CONFIG.
  • run.mcp_servers() in Python returns the inner {app: {...}} object.
  • sw mcp-config RUN prints it for a run that is still going.

Claude Code

Run Claude Code headless with the run's servers as its only MCP servers:

Shell
sw run t-0e4243 --agent claude-code -- sh -c 'claude -p "$STATEWEAVE_PROMPT" \
  --mcp-config "$STATEWEAVE_MCP_CONFIG" --strict-mcp-config \
  --allowedTools "mcp__slack__*"'
  • --strict-mcp-config keeps your own MCP servers out of the run.
  • In print mode (-p) Claude Code refuses a tool nobody approved, so --allowedTools must name the servers of every app the test uses, as mcp__<app>__*.

This script works out the apps from the config by itself and, for an answer test, asks for the answer in the test's format and hands Claude Code's reply in. Save it as claude_code.sh:

claude_code.sh
#!/bin/sh
# An agent for `sw run`: Claude Code, headless, with the run's apps as its only MCP servers and every tool of
# theirs allowed (print mode denies a tool call nobody approved). For an answer task, Claude Code's last message is
# handed in as the answer.
#
#   sw run t-3fa2c1 --agent claude-code -- sh claude_code.sh
#
# Flags as documented in Claude Code's CLI reference, headless and permissions pages (code.claude.com/docs).
set -ef  # -f: the mcp__<app>__* patterns stay patterns
servers=$(python3 -c 'import json, os; print(" ".join(f"mcp__{a}__*" for a in json.load(open(os.environ["STATEWEAVE_MCP_CONFIG"]))["mcpServers"]))')
prompt="$STATEWEAVE_PROMPT"
if [ "$STATEWEAVE_KIND" = "answer" ]; then
  prompt="$prompt

When you are done, reply with only this JSON and nothing else: $STATEWEAVE_ANSWER_FORMAT"
fi
answer=$(claude -p "$prompt" --mcp-config "$STATEWEAVE_MCP_CONFIG" --strict-mcp-config --allowedTools $servers)
if [ "$STATEWEAVE_KIND" = "answer" ]; then
  printf '%s' "$answer" > "$STATEWEAVE_ANSWER_FILE"
fi
Shell
sw run t-bde537 --agent claude-code -- sh claude_code.sh

The Claude Agent SDK's mcp_servers option takes the same objects as run.mcp_servers() returns.

Any MCP client

With the official MCP Python SDK (version 2), connect over Streamable HTTP with the run's headers:

Python
import asyncio

import httpx2
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client


async def list_tools(server):
    async with httpx2.AsyncClient(headers=server["headers"]) as http:
        async with streamable_http_client(server["url"], http_client=http) as streams:
            async with ClientSession(streams[0], streams[1]) as session:
                await session.initialize()
                return [tool.name for tool in (await session.list_tools()).tools]


for name, server in run.mcp_servers().items():
    print(name, asyncio.run(list_tools(server)))

Calls through MCP count. A tool call reaches the same app as an API call does. It is recorded on the run and graded the same way.