Serve Signals data to your agent with the MCP server
Your agent can fetch Signals data using the Model Context Protocol (MCP), the standard way an agent discovers and calls tools. Once you add the server to your agent's tool list, the agent can list what your deployment has published and read whichever parts the task warrants: calculated attribute values and the raw recent activity in an agentic context.
The server is part of your Signals deployment, so there's nothing to install or host. It serves the same data as the SDKs and API, and every tool is read-only.
This page covers serving Signals data to an agent inside your own application at runtime. To manage Signals configuration conversationally from an AI assistant such as Claude Code or Cursor, use the Snowplow MCP server instead.
When to use the MCP server
The MCP server suits an agent that discovers and selects at runtime what context it needs. It lists the attribute groups and agentic contexts your deployment offers, reads each one's description, and fetches the ones that fit the task, so you don't write retrieval code for each of them. Use the SDKs instead when your application already knows what it needs, and you prefer to handle fetching and injecting context values in code.
Two properties are worth planning around:
- Discovery: you add the server URL once, and the agent discovers each attribute group and agentic context you publish from then on, without any change to your application
- Descriptions at call time: the descriptions you write reach the model as it decides what to call, so what you put on an attribute or a context is what steers the agent toward or away from reading it
Connect to the server
To authenticate you need your Snowplow API key and key ID, passed as the X-API-Key-Id and X-API-Key headers. The server rejects connections that don't send both headers. See connection credentials for where to find each value.
The API key grants access to your organization's Signals data. Connect to the server from server-side code and keep the credentials in your environment secrets. Never ship them to a browser or a mobile app.
- Vercel AI SDK
- Google ADK
- OpenAI Agents SDK
- LangChain
- MCP client config
MCP support lives in the @ai-sdk/mcp package, which is separate from ai and doesn't pull it in, so install both alongside your model provider:
npm install ai @ai-sdk/mcp @ai-sdk/openai
Create a client for the server, then pass its tools to your model call:
import { createMCPClient } from '@ai-sdk/mcp';
import { openai } from '@ai-sdk/openai';
import { generateText } from 'ai';
const mcp = await createMCPClient({
transport: {
type: 'http',
url: `${process.env.SIGNALS_DEPLOYED_URL}/mcp`,
headers: {
'X-API-Key-Id': process.env.CONSOLE_API_KEY_ID!,
'X-API-Key': process.env.CONSOLE_API_KEY!,
},
},
});
// Read this from your app's session and pass it in
const domain_sessionid = '8c9104e3-c300-4b20-82f2-93b7fa0b8feb';
const { text } = await generateText({
model: openai('gpt-5'),
prompt: `The current Signals session identifier is: ${domain_sessionid}.`,
tools: await mcp.tools(),
});
ADK reaches MCP servers through @modelcontextprotocol/sdk, which it declares as an optional peer dependency, so install both:
npm install @google/adk @modelcontextprotocol/sdk
MCPToolset discovers the server's tools, and you pass the toolset itself to the agent:
import { LlmAgent, MCPToolset } from '@google/adk';
const signals = new MCPToolset({
type: 'StreamableHTTPConnectionParams',
url: `${process.env.SIGNALS_DEPLOYED_URL}/mcp`,
transportOptions: {
requestInit: {
headers: {
'X-API-Key-Id': process.env.CONSOLE_API_KEY_ID!,
'X-API-Key': process.env.CONSOLE_API_KEY!,
},
},
},
});
export const rootAgent = new LlmAgent({
name: 'signals_agent',
model: 'gemini-flash-latest',
tools: [signals],
});
Pass the credentials under transportOptions.requestInit.headers. The connection also takes a top-level header field, but it's deprecated and ignored whenever transportOptions is set.
Install the SDK:
pip install openai-agents
Pass the server to an agent as one of its mcp_servers, and the agent picks up every tool the server publishes:
from agents import Agent
from agents.mcp import MCPServerStreamableHttp
signals = MCPServerStreamableHttp(
name="Snowplow Signals",
params={
"url": f"{SIGNALS_DEPLOYED_URL}/mcp",
"headers": {
"X-API-Key-Id": CONSOLE_API_KEY_ID,
"X-API-Key": CONSOLE_API_KEY,
},
},
)
agent = Agent(name="Assistant", mcp_servers=[signals])
Install the MCP adapters:
pip install langchain-mcp-adapters
MultiServerMCPClient converts the server's tools into LangChain tools, which you then pass to an agent:
import asyncio
from langchain_mcp_adapters.client import MultiServerMCPClient
client = MultiServerMCPClient(
{
"snowplow_signals": {
"transport": "streamable_http",
"url": f"{SIGNALS_DEPLOYED_URL}/mcp",
"headers": {
"X-API-Key-Id": CONSOLE_API_KEY_ID,
"X-API-Key": CONSOLE_API_KEY,
},
}
}
)
tools = asyncio.run(client.get_tools())
Most MCP clients take a remote server as a URL with headers. Add the server to your client's configuration file:
{
"mcpServers": {
"snowplow-signals": {
"type": "http",
"url": "https://{{123abc}}.signals.snowplowanalytics.com/mcp",
"headers": {
"X-API-Key-Id": "<CONSOLE_API_KEY_ID>",
"X-API-Key": "<CONSOLE_API_KEY>"
}
}
}
}
Check your client's documentation for the exact key names: some use transport rather than type, and clients that only support local servers need a bridge such as mcp-remote.
Available tools
The server exposes four read-only tools in two families. Attributes are the values Signals keeps calculated; agentic contexts are the raw recent events in order. Each family has one discovery tool that returns definitions only, and one that reads a user's data.
| Tool | What it returns |
|---|---|
list_attribute_groups | Each published attribute group, with its version, the attribute key it's read by, and the attributes it holds |
get_attributes | Calculated attribute values, for one or more attribute groups in a single call |
list_agentic_contexts | The name and description of each published agentic context |
get_agentic_context | One user's recent activity for a named agentic context, as a narrative summary (default) or as JSON |
A grounded answer takes at least two calls: the agent lists what your deployment publishes, then reads whichever part fits the task. The server lists only published groups and contexts. The server returns any prompt you configure on an agentic context alongside the activity, so what you write into a context definition reaches the model with the data. If the agent gets a name or an attribute key wrong, the response tells it what to use instead, so it can retry in the same turn.
Attribute keys
You have to tell your agent which attribute keys it can use. Most keys identify a user, but a custom key can identify a product, a campaign, or a region.
Inject the values server-side into your system prompt, labelled with each attribute key's exact name:
Snowplow Signals identifiers for this user, labelled by attribute key name:
domain_sessionid: {{SESSION_ID}}
domain_userid: {{USER_ID}}
The label is the contract, so prefer the registry's names verbatim. Supplying more than you need costs nothing, because identifiers no group uses are ignored.
Snowplow sessions roll over after around 30 minutes of inactivity, so send domain_sessionid to your backend with every message rather than once at the start.
Inspect the server
The MCP Inspector shows the tools and their descriptions as your agent receives them, and lets you call them by hand. Use it to confirm your credentials work and to check what a tool returns, before you wire the server into an agent.
Launch the Inspector:
npx @modelcontextprotocol/inspector
In the Inspector UI:
- Set the transport to Streamable HTTP
- Set the URL to your Signals API URL followed by
/mcp - Under Authentication, add both headers using the Header Name and Value inputs:
X-API-Key-IdandX-API-Key - Click Connect
The server is fully behind authentication, so without both headers it rejects the connection during initialization. Once connected, open the Tools tab to list the tools, read their descriptions and annotations, and run one with your own arguments.