Anchored Growth MCP — Getting Started
Anchored Growth is a Model Context Protocol (MCP) server that gives AI sales agents calibrated outcome predictions: Bayesian per-channel posteriors, CUSUM drift state, per-contact decision history, recommendation profiles, and a tamper-evident audit trail. You ingest outbound outcomes through a sibling REST endpoint; the MCP tools return calibrated views your agent can call.
This doc gets you from "I have an API key" to "I just made a calibrated tool call" in 5 minutes.
Status: live in production at
mcp.anchoredgrowth.io. Every URL below points at production. MCP access is currently included with any active subscription — 3 keys per company, 1,000 tool calls per month.
What you get in V1
Seven read-only MCP tools:
| Tool | Purpose | Required args |
|---|---|---|
get_campaign_health |
Composite health score for one campaign | tenant_id, campaign_id |
get_campaign_recommendations |
Pre-baked recommendation profiles (tone, day-of-week, body word count, etc.) | tenant_id |
get_bandit_state |
Thompson-sampler Beta posteriors per (channel, position) arm | tenant_id |
get_drift_state |
CUSUM drift detection — is the model still calibrated? | tenant_id |
get_workflow_decisions |
Per-contact decision history (what we recommended and why) | tenant_id |
get_recipient_intelligence |
Composite per-contact view (decisions joined with position guidance) | tenant_id, contact_id |
get_action_audit_log |
Self-referential audit trail of every prior tool call | tenant_id |
Every tool response is locked to schema_version: "1.0.0". Adding fields is
non-breaking; renames or removals require a major version bump.
Beyond tools: resources and prompts
The server also serves three resources — static reference documents your agent can read in-band, at no quota cost:
| URI | What it is |
|---|---|
ag://reference/alerts |
Every alert code, its severity, what it means, what to do |
ag://reference/freshness |
How current the data is; what as_of vs data_as_of mean |
ag://reference/trust-model |
Which fields tell you whether a number is backed by your own data |
Read ag://reference/alerts when you hit an alert you do not recognise. The
alerts array is how this service discloses small samples, borrowed priors,
placeholder maths and stale data — an unfamiliar code is a reason to look it
up, not a reason to ignore it.
And three prompts, which encode the tool ORDER that produces a defensible answer (the tools have non-obvious interdependencies — drift calibration governs whether the bandit posteriors mean anything at all):
| Prompt | Arguments | Use for |
|---|---|---|
diagnose_campaign |
campaign_id |
"Why is this campaign underperforming?" |
assess_data_trust |
— | "How much of this company's intelligence is real?" |
brief_on_contact |
contact_id |
"Tell me about this prospect" |
Full per-tool schemas: tool-reference.md (generated
from the server's own tools/list, so it cannot drift).
Limits and headers
| Limit | Value |
|---|---|
| Monthly quota | 1,000 tool calls per key |
| Burst | 200 tool calls per 15 minutes per key |
| Active keys | 3 per company |
| Key lifetime | 90 days |
Only tools/call counts against quota and burst. initialize, tools/list,
resources/* and prompts/* are free, so an agent that is over its limits can
still connect and discover — it just cannot call tools.
Successful responses carry your remaining headroom:
RateLimit-Limit: 200
RateLimit-Remaining: 199
RateLimit-Reset: 209
A 429 carries Retry-After. Burst rejections do not consume monthly
quota.
You can omit tenant_id
Every tool accepts tenant_id, but your API key is already bound to exactly
one company, so omit it and the server resolves it for you. Pass it
explicitly only if you are using a multi-tenant bootstrap credential. Passing a
company other than the one your key is bound to returns permission_denied —
and the attempt is recorded in that company's audit log.
Endpoints
| Surface | URL |
|---|---|
| MCP server (streamable-http) | https://mcp.anchoredgrowth.io/mcp |
| Health check (public) | https://mcp.anchoredgrowth.io/healthz |
| Outcome ingestion (REST) | https://c3ltly5r5j.execute-api.us-east-2.amazonaws.com/prod/ingest/events |
mcp.anchoredgrowth.io is the stable hostname; use it rather than the raw
execute-api address, which can change.
Prerequisites
- An Anchored Growth API key (starts with
ag_). See Getting a key. - An MCP-compatible client. Three options are supported below:
- Claude Desktop (via the
mcp-remotebridge) - Python with the official
mcpSDK - curl for protocol-level debugging
Step 1: Confirm the server is reachable
curl https://mcp.anchoredgrowth.io/healthz
Expected response:
{"status":"ok","transport":"streamable-http"}
If you do not get 200 OK, stop here and contact us — your network is blocking
the host.
Step 2: Configure your client
Option A: Claude Desktop
Claude Desktop launches MCP servers over stdio, so a remote streamable-http
server needs the mcp-remote bridge (Node.js, run via npx).
Edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS)
or %APPDATA%\Claude\claude_desktop_config.json (Windows):
{
"mcpServers": {
"anchored-growth": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://mcp.anchoredgrowth.io/mcp",
"--header",
"Authorization:Bearer ag_YOUR_KEY_HERE",
"--transport",
"http-only"
]
}
}
}
Restart Claude Desktop. The seven Anchored Growth tools (get_campaign_health,
get_bandit_state, etc.) will appear in the MCP tool menu.
Option B: Python
pip install mcp
import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
MCP_URL = "https://mcp.anchoredgrowth.io/mcp"
API_KEY = "ag_YOUR_KEY_HERE"
async def main():
headers = {"Authorization": f"Bearer {API_KEY}"}
async with streamablehttp_client(MCP_URL, headers=headers) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
# List the tools the server exposes
tools = await session.list_tools()
print([t.name for t in tools.tools])
# Call one
result = await session.call_tool(
"get_action_audit_log",
{"tenant_id": "YOUR_TENANT_ID", "limit": 5},
)
print(result.content[0].text)
asyncio.run(main())
The tenant_id is whatever AG company ID was associated with your key when it
was issued (we tell you when we send the key).
Option C: curl
Every MCP request is JSON-RPC 2.0. The streamable-http transport replies with
Server-Sent Events, so the Accept header must include both content types.
KEY="ag_YOUR_KEY_HERE"
MCP_URL="https://mcp.anchoredgrowth.io/mcp"
# initialize
curl -X POST $MCP_URL \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{
"protocolVersion":"2025-11-25",
"capabilities":{},
"clientInfo":{"name":"curl","version":"0.0.1"}
}
}'
Expected (the response is SSE-framed; the JSON-RPC payload is on the data: line):
event: message
data: {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{...},"serverInfo":{"name":"ag-mcp","version":"1.28.1"}}}
Step 3: Make your first tool call
get_action_audit_log is the fastest tool to try because it returns
immediately whether or not you've sent us any data — every prior tool call
(including this one) is recorded, and the response is the same shape on a
fresh tenant.
curl -X POST $MCP_URL \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{
"name":"get_action_audit_log",
"arguments":{"tenant_id":"YOUR_TENANT_ID","limit":5}
}
}'
Expected (excerpt — the text field is a JSON-encoded string per the MCP
content-block contract):
{
"schema_version": "1.0.0",
"tenant_id": "YOUR_TENANT_ID",
"scope": "company",
"entry_count": 0,
"entries": [],
"summary": "No audit entries for this tenant.",
"tool_breakdown": {},
"error_rate": 0.0,
"alerts": [
"no_audit_entries_available",
"payloads_not_stored_only_sha256_hashes"
],
"as_of": "2026-05-25T22:08:06.044096Z"
}
Call the tool a second time and entry_count increments — the audit trail is
self-referential by design.
Step 4: Send us your outcome data
The MCP tools become meaningful once they have outcomes to reason over. POST outbound outcomes to the public-API ingest endpoint:
curl -X POST https://c3ltly5r5j.execute-api.us-east-2.amazonaws.com/prod/ingest/events \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{
"events": [{
"channel": "email",
"sequence_step": 0,
"recipient_id": "prospect-001",
"recipient_email": "demo@example.com",
"recipient_position": "VP of Engineering",
"sent_at": "2026-05-20T15:30:00Z",
"outcome_at": "2026-05-21T09:12:00Z",
"outcome": "responded",
"tone": "direct",
"word_count": 87
}]
}'
Expected: HTTP 202 with body {"accepted":1,"rejected":0}.
Required event fields:
| Field | Type | Notes |
|---|---|---|
channel |
string | email or linkedin |
sequence_step |
int | 0-indexed step (0..50). NOT a job title. |
recipient_id |
string | Your opaque prospect identifier |
sent_at |
ISO8601 | When the message was sent |
outcome_at |
ISO8601 | When the outcome was observed |
outcome |
string | One of responded, no_response, bounced, unsubscribed |
Useful optional fields:
| Field | Notes |
|---|---|
event_id |
UUID for client-side idempotency |
recipient_email |
RFC 5321 address; used by the pipeline's reply-detection |
recipient_position |
Job title string (e.g. "VP of Engineering") — feeds position bucketing |
thread_id |
If omitted, derived deterministically from (companyId, recipient, campaign) |
tone |
One of direct, conversational, professional, empathetic, questioning. The string casual is NOT accepted — use conversational. |
word_count, subject, body_excerpt, body, industry |
Content features. body is opt-in and capped at 10000 chars. |
metadata |
Arbitrary JSON, max 2 KB serialized. metadata.campaign_id or metadata.sequence_id contributes to thread derivation. |
Batch + size limits: max 500 events per request. Use {"events":[...]} for
batches, or POST a single event object (no events wrapper) for one-offs.
A CSV variant is also live at POST /ingest/csv (header row + up to 500 data
rows, 5 MB body cap, same field names as columns).
Step 5: Get a calibrated read
After your first batch of outcomes lands, the analytics pipeline will start
populating per-tenant state. get_bandit_state exposes the Bayesian
per-(channel, position) arm posteriors that drive AG's decisions:
curl -X POST $MCP_URL \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc":"2.0","id":3,"method":"tools/call",
"params":{
"name":"get_bandit_state",
"arguments":{"tenant_id":"YOUR_TENANT_ID","top_n":5}
}
}'
On a brand-new tenant you'll see:
{
"schema_version": "1.0.0",
"tenant_id": "YOUR_TENANT_ID",
"scope": "company",
"arm_count": 0,
"arms": [],
"summary": "No bandit state available for this tenant.",
"alerts": [
"no_bandit_state_available",
"recommendations_are_company_scoped_not_per_campaign"
],
"as_of": "..."
}
The intelligence pipeline runs daily; arms populate after the first batch is
ingested and processed. Each populated arm carries alpha, beta,
posterior_mean, posterior_variance, and a data_source flag
(migration = bootstrapped from history, live = streamed).
Troubleshooting
HTTP 401 — missing_token
The Authorization header was absent or empty. Make sure the header is sent
exactly as Authorization: Bearer ag_....
HTTP 401 — invalid_token
The token doesn't match any configured credential. The bootstrap-key path returns this code; the self-serve key path returns the JSON below instead.
HTTP 401 — self_serve_auth_failed
The response body includes a reason field that tells you what's wrong:
INVALID_FORMAT— the token didn't pass the format check. Look for copy-paste truncation or whitespace.TOKEN_NOT_FOUND— the key was never issued or has been deleted.TOKEN_INACTIVE— the key was revoked.TOKEN_EXPIRED/TOKEN_LOCKED— the key was issued with an expiry, or has been temporarily locked after repeated failures.TOKEN_INVALID— bcrypt mismatch. Most likely a typo.
HTTP 429 — QUOTA_EXCEEDED
Monthly tool-call quota exhausted for this key. The response body includes
quotaLimit, currentUsage, and a resetAt ISO8601 timestamp for the start
of the next UTC month. Contact us for a higher limit.
HTTP 429 — TOO_MANY_FAILED_ATTEMPTS
Five bad authentication attempts from the same source IP for the same key
within one hour. The response also returns Retry-After: 3600. Wait an hour
or call from a different IP.
HTTP 406 — Not Acceptable
Your Accept header doesn't include both application/json AND
text/event-stream. Streamable-http always responds in SSE-framed form. Use:
Accept: application/json, text/event-stream
HTTP 200 but the response body looks like an SSE stream
That's expected — streamable-http frames the JSON-RPC payload inside an
SSE data: line. The Python SDK and mcp-remote parse this transparently.
For raw curl debugging, the JSON of interest is the line starting with
data:.
Audit and privacy
Every tool call writes one row to our audit log, blocking the response on the audit write. Each row contains:
- The API key ID (NOT the raw key)
- SHA-256 hashes of the input and output (NOT the values)
- Latency, timestamp, success or error type
- The calling actor type and ID
We do not store message bodies, recipient emails, or other payload data for analytical purposes beyond what's needed to compute responses. Hashed audit rows are the only persistent record of a tool call.
Data retention and deletion
Ingested events are auto-deleted via DynamoDB TTL 24 months after ingestion.
To delete a single event:
curl -X DELETE \
https://c3ltly5r5j.execute-api.us-east-2.amazonaws.com/prod/ingest/events/{event_id} \
-H "Authorization: Bearer $KEY"
To delete every event for a recipient:
curl -X DELETE \
"https://c3ltly5r5j.execute-api.us-east-2.amazonaws.com/prod/ingest/events?recipient_id=prospect-001" \
-H "Authorization: Bearer $KEY"
Both are tenant-scoped — there is no path that deletes events outside your company.
What's V1 vs what's coming
V1 (today, test-preview):
- Seven read-only MCP tools (above)
POST /ingest/eventsJSON ingestion andPOST /ingest/csvCSV ingestionDELETE /ingest/events/{event_id}andDELETE /ingest/events?recipient_id=…- API-key auth (
ag_*self-serve keys validated against the same DynamoDB table the REST endpoints use; per-key monthly quota; per-(IP, key) failure throttle) - 24-month auto-retention via DynamoDB TTL
- Audit log on every tool call, surfaced by
get_action_audit_log
Coming soon:
- Write tools:
record_outcome(alternative to REST ingest),request_decision,start_experiment - Industry-segmented global priors (today: one global prior across all customers — useful for cold-start, less precise than the per-industry version on the roadmap)
- Production endpoint at
mcp.anchoredgrowth.io - Self-serve key issuance in an admin portal (today: keys are issued out-of-band — see below)
Getting a key
Mint your own key in the app: sign in at https://app.anchoredgrowth.io,
open AI Agent Access in the sidebar, and click Generate API key.
You need the manage_billing permission (Company Owner or Organization
Admin). The key is shown ONCE — copy it before closing the dialog. The same
screen shows your tenant_id and ready-made config snippets.
doc. Keys are issued in the format ag_<48 chars>_<8-char checksum> and shown
exactly once.
Schema versioning
Every tool response includes schema_version: "1.0.0". We will increment the
major version for any breaking change. Until 2.0, all responses are
forward-compatible: new fields may appear, but existing fields will not be
renamed or removed.
Reporting issues
Bug reports and product feedback: jchasecrafton@anchoredgrowth.io.
Please include the value of the as_of timestamp from any tool response, the
HTTP status code, and the full error body (with the API key redacted).