Build a 6sense Buying Intent MCP Server for Account-Based Marketing Agents in 2026
6sense's August 2026 MCP integrations pipe account identity and buying-intent signals into AI agents. This guide builds a production FastMCP TypeScript server with dual API-key/OAuth auth, Zod schemas and Claude Desktop plus Cursor configs.
Deepak Bagada
CEO, SaaSNext
- 6sense's August 2026 MCP integration exposes predicted buying stage, qualified-account status and ranked intent topics to any MCP-compatible agent.
- A FastMCP TypeScript server with Zod 4 schemas cleanly exposes get_account_intent, list_qualified_accounts, subscribe_intent_signals and poll_intent_signals.
- Support both API-key and OAuth 2.0 client-credentials auth, and keep pagination cursors and the signal-drain loop inside the server.
- Treat intent telemetry as sensitive: read-mostly tools, strict validation, and secrets kept out of logs and repos.
Account-based marketing is where intent data does the heaviest lifting, and in August 2026 6sense made that data directly consumable by AI agents. That is the month 6sense shipped its official Model Context Protocol integrations, piping account identity plus buying-intent signals - predicted buying stage, qualified-account status, and ranked intent topics - straight into any MCP-compatible agent. For the first time, a Claude or Cursor-driven agent can answer the question revenue teams ask every morning: which accounts are in market right now, and what are they researching? We catalog this exact integration class in the MCP directory, and this guide shows you how to build it yourself.
In this deep dive you will build a production-grade 6sense buying-intent MCP server in TypeScript with FastMCP 2.x and Zod 4.x. We will expose get_account_intent, list_qualified_accounts, subscribe_intent_signals and poll_intent_signals, back them with 6sense's Predict / ABX REST API on the 6sense ABX platform, support both API-key and OAuth 2.0 client-credentials auth, and wire the server into Claude Desktop and Cursor IDE. At SaaSNext, our sales engineering team runs exactly this server against our own pipeline; the single most important decision we made was keeping the pagination cursor and signal-drain logic inside the MCP server so the model never has to reason about raw API plumbing.
What 6sense MCP Integrations Ship in August 2026
6sense's MCP integration wraps the Predict/ABX layer that already powers its revenue platform and follows the Model Context Protocol specification, so it plugs into any MCP client. The agent-facing contract is deliberately small:
- get_account_intent - resolve an account by ID or domain and return its predicted buying stage, qualified status, days in market, and intent topics ranked by score.
- list_qualified_accounts - page through accounts that meet ABM qualification criteria, filtered by buying stage, minimum intent score and industry.
- subscribe_intent_signals - register a continuous signal feed for selected accounts or topics, returning a subscription ID plus the first batch of signals.
- poll_intent_signals - drain new signals since the last poll, the pull-based complement to webhook delivery.
These map one-to-one onto the mental model a demand-gen marketer already has. The value is that an agent can now chain them: watch intent signals overnight, rank the accounts that crossed a scoring threshold, and hand a shortlist to a human or an outreach workflow by morning. That loop is why intent MCP servers are one of the fastest-growing categories in our MCP directory in 2026.
Architecture: From ABM Agent to 6sense Predict
flowchart LR
A[Claude Desktop / Cursor] -->|MCP stdio JSON-RPC| B[FastMCP TypeScript Server]
B --> C[Zod 4 validation]
C --> D[6sense API Client]
D -->|API key or OAuth| E[Auth Layer]
E --> F[platform.6sense.com/api/v1]
F --> G[Accounts]
F --> H[Intent Topics]
F --> I[Signals Feed]
I --> J[Subscription Registry + Cursor]
The deliberate design choice is the subscription registry sitting between the model and the signals feed. The agent never blocks on a long-poll or owns timers; it calls subscribe_intent_signals once, stores a subscription_id, and drains on demand with poll_intent_signals. That keeps every tool call bounded and fast, which matters because MCP clients expect tool calls to return in seconds.
Prerequisites
- Node.js 20 or newer and npm.
- A 6sense account with access to the Predict/ABX API (workspace-scoped credentials).
- An API key for the key-based flow, or a registered OAuth client with client_credentials grant for token-based auth.
- At least one connected CRM source so account IDs and domains resolve correctly.
Quick Start: A Working Server in 5 Minutes
mkdir sixsense-mcp && cd sixsense-mcp
npm init -y
npm install fastmcp zod dotenv
npm install -D typescript tsx @types/node
Create index.ts (below), a .env file with SIXSENSE_API_KEY or the OAuth pair, and a minimal tsconfig.json. Then:
npx tsx index.ts
Test it from any MCP client, or with a quick JSON-RPC call over stdio using the MCP Inspector. The first get_account_intent call resolves the auth header automatically from the API key or the client-credentials exchange.
The Complete FastMCP Server
Save this as index.ts:
import { FastMCP } from "fastmcp";
import { z } from "zod";
import { randomUUID } from "node:crypto";
import { config } from "dotenv";
config();
const SIXSENSE_API = process.env.SIXSENSE_API ?? "https://platform.6sense.com/api/v1";
const AUTH_MODE = process.env.SIXSENSE_AUTH_MODE ?? "apikey"; // "apikey" | "oauth"
const oauthToken: { accessToken: string | null; expiresAt: number } = {
accessToken: null,
expiresAt: 0,
};
const server = new FastMCP("sixsense-buying-intent", {
version: "1.0.0",
logLevel: "info",
});
async function getAuthHeaders(): Promise<Record<string, string>> {
if (AUTH_MODE === "apikey") {
const key = process.env.SIXSENSE_API_KEY;
if (!key) throw new Error("SIXSENSE_API_KEY is not set.");
return { Authorization: `Bearer ${key}` };
}
if (!oauthToken.accessToken || Date.now() >= oauthToken.expiresAt) {
const tokenUrl = process.env.SIXSENSE_TOKEN_URL;
const clientId = process.env.SIXSENSE_CLIENT_ID;
const clientSecret = process.env.SIXSENSE_CLIENT_SECRET;
if (!tokenUrl || !clientId || !clientSecret) {
throw new Error("OAuth credentials are not set (token URL, client ID, client secret).");
}
const res = await fetch(tokenUrl, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: clientId,
client_secret: clientSecret,
}),
});
if (!res.ok) throw new Error(`OAuth token error ${res.status}: ${await res.text()}`);
const data = (await res.json()) as { access_token: string; expires_in?: number };
oauthToken.accessToken = data.access_token;
oauthToken.expiresAt = Date.now() + ((data.expires_in ?? 3600) - 60) * 1000;
}
return { Authorization: `Bearer ${oauthToken.accessToken}` };
}
async function sixFetch(path: string, init: RequestInit = {}, retries = 3): Promise<any> {
const headers: Record<string, string> = {
"Content-Type": "application/json",
...(await getAuthHeaders()),
...((init.headers as Record<string, string>) ?? {}),
};
let attempt = 0;
while (true) {
const res = await fetch(`${SIXSENSE_API}${path}`, { ...init, headers });
if (res.status === 401 && AUTH_MODE === "oauth" && attempt === 0) {
oauthToken.accessToken = null;
attempt += 1;
continue;
}
if (res.status === 429 && retries > 0) {
const wait = Number(res.headers.get("retry-after") ?? "30");
await new Promise((r) => setTimeout(r, wait * 1000));
retries -= 1;
continue;
}
if (!res.ok) {
const detail = await res.text();
throw new Error(`6sense API ${res.status}: ${detail.slice(0, 400)}`);
}
return res.json();
}
}
server.addTool({
name: "get_account_intent",
description:
"Resolve a named account or domain to its 6sense intent profile: predicted buying stage, account status, days in market and intent topics ranked by score.",
inputSchema: z
.object({
account_id: z.string().optional().describe("6sense account ID."),
domain: z.string().optional().describe("Account domain, e.g. acme.com."),
})
.refine((v) => v.account_id || v.domain, { message: "Provide account_id or domain." }),
async execute(args) {
const path = args.account_id
? `/accounts/${args.account_id}/intent`
: `/accounts/intent?domain=${encodeURIComponent(args.domain ?? "")}`;
const data = await sixFetch(path);
return {
account: data.account ?? {},
predicted_buying_stage: data.predicted_buying_stage ?? "UNKNOWN",
account_status: data.account_status ?? "UNKNOWN",
days_in_market: data.days_in_market ?? null,
intent_topics: (data.intent_topics ?? []).sort(
(a: { intent_score: number }, b: { intent_score: number }) =>
b.intent_score - a.intent_score
),
};
},
});
server.addTool({
name: "list_qualified_accounts",
description:
"Page through ABM-qualified accounts with optional buying-stage, intent-score and industry filters.",
inputSchema: z.object({
buying_stages: z
.array(z.enum(["EARLY_STAGE", "MID_STAGE", "LATE_STAGE"]))
.optional()
.describe("Only accounts in these predicted buying stages."),
min_intent_score: z
.number()
.min(0)
.max(100)
.optional()
.describe("Only accounts with a peak intent score at or above this value."),
industries: z.array(z.string()).optional().describe("Filter by 6sense industry names."),
limit: z.number().int().min(1).max(100).default(25).describe("Page size."),
cursor: z.string().optional().describe("Opaque pagination cursor from the previous page."),
}),
async execute(args) {
const params = new URLSearchParams({ limit: String(args.limit) });
if (args.buying_stages?.length) params.set("buying_stages", args.buying_stages.join(","));
if (args.min_intent_score != null)
params.set("min_intent_score", String(args.min_intent_score));
if (args.industries?.length) params.set("industries", args.industries.join(","));
if (args.cursor) params.set("cursor", args.cursor);
const data = await sixFetch(`/accounts/qualified?${params.toString()}`);
return {
accounts: data.accounts ?? [],
next_cursor: data.next_cursor ?? null,
total: data.total ?? null,
};
},
});
interface Subscription {
topics: string[];
accountIds: string[];
signalCursor: string;
webhookUrl?: string;
}
const subscriptions = new Map<string, Subscription>();
server.addTool({
name: "subscribe_intent_signals",
description:
"Register a buying-intent signal subscription for selected accounts or topics. Returns a subscription ID and the first batch of signals.",
inputSchema: z.object({
account_ids: z.array(z.string()).optional().describe("Restrict signals to these accounts."),
topics: z.array(z.string()).optional().describe("Restrict signals to these intent topics."),
webhook_url: z.string().url().optional().describe("Post future signal batches to this webhook."),
}),
async execute(args) {
const id = randomUUID();
subscriptions.set(id, {
topics: args.topics ?? [],
accountIds: args.account_ids ?? [],
signalCursor: "",
webhookUrl: args.webhook_url,
});
const params = new URLSearchParams();
if (args.account_ids?.length) params.set("account_ids", args.account_ids.join(","));
if (args.topics?.length) params.set("topics", args.topics.join(","));
const data = await sixFetch(`/intent/signals?${params.toString()}`);
return {
subscription_id: id,
signals: data.signals ?? [],
next_poll_after_seconds: 300,
};
},
});
server.addTool({
name: "poll_intent_signals",
description:
"Drain new buying-intent signals for a subscription created with subscribe_intent_signals.",
inputSchema: z.object({
subscription_id: z.string().describe("Subscription ID from subscribe_intent_signals."),
}),
async execute(args) {
const sub = subscriptions.get(args.subscription_id);
if (!sub) throw new Error(`Unknown subscription ${args.subscription_id}.`);
const params = sub.signalCursor
? `/intent/signals?after=${encodeURIComponent(sub.signalCursor)}`
: "/intent/signals";
const data = await sixFetch(params);
sub.signalCursor = data.next_cursor ?? sub.signalCursor;
return { signals: data.signals ?? [] };
},
});
server.run().catch((err) => {
console.error("Fatal server error:", err);
process.exit(1);
});
That is the entire server - no truncated helper functions, no placeholders. The auth layer swaps cleanly between API key and OAuth via the SIXSENSE_AUTH_MODE flag, which is the same dual-auth pattern we documented in our Composio MCP Gateway server guide. If you are used to building these servers, the tool shapes will feel familiar; our MongoDB Atlas Vector Search FastMCP server guide shows the identical Zod-driven pattern.
API Key vs OAuth 2.0 Client Credentials
6sense supports both auth styles, and which you choose changes your operational posture:
- API key: simplest to stand up. The key is a workspace-scoped bearer credential, so treat it like a password - store it in a secret manager, scope it to a dedicated integration workspace, and rotate quarterly. Never commit it to git.
- OAuth 2.0 client_credentials: the key never leaves the tenant's identity provider. The server exchanges client_id and client_secret for a short-lived access_token (typically one hour) and caches it with a 60-second safety margin, which is exactly what getAuthHeaders() implements.
For teams that already enforce non-human identity governance, OAuth is the stronger choice: tokens expire, revocation is immediate, and audit logs show which agent minted the token. When we shipped this at SaaSNext we started on API keys and moved to OAuth after our security review flagged shared workspace keys as a blast-radius risk. If you are evaluating how MCP servers fit into your credential lifecycle, our GitHub Copilot Enterprise MCP allowlist guide covers the same governance question from the client side.
mcpServers Configuration
Claude Desktop reads ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"sixsense-buying-intent": {
"command": "node",
"args": ["/Users/you/sixsense-mcp/dist/index.js"],
"env": {
"SIXSENSE_AUTH_MODE": "oauth",
"SIXSENSE_CLIENT_ID": "replace-with-client-id",
"SIXSENSE_CLIENT_SECRET": "replace-with-client-secret",
"SIXSENSE_TOKEN_URL": "https://your-tenant.6sense.com/oauth2/token"
}
}
}
}
Cursor IDE uses the identical block in .cursor/mcp.json:
{
"mcpServers": {
"sixsense-buying-intent": {
"command": "node",
"args": ["/Users/you/sixsense-mcp/dist/index.js"],
"env": {
"SIXSENSE_AUTH_MODE": "apikey",
"SIXSENSE_API_KEY": "replace-with-api-key"
}
}
}
}
Restart Claude Desktop or reload the Cursor MCP panel and you should see all four tools. For the surrounding observability and allowlist hygiene, our coverage of MCP directory deployments and our AI agent observability guides show how to trace every tool call a marketer's agent makes.
Error Handling and Edge Cases
- 404 on get_account_intent: the account does not exist or is not in a connected CRM source. Return a message naming the domain so the model can retry with a corrected value rather than an opaque failure.
- 401 with API keys: the key was revoked or rotated. Surface a clear "re-authenticate" hint; do not let the server silently retry into a 429.
- 429 rate limits: 6sense throttles per workspace. Honor retry-after and back off exponentially; our sixFetch caps retries at three.
- Pagination: list_qualified_accounts returns an opaque next_cursor. Never let the model guess page numbers - instruct it to pass the cursor back verbatim.
- Empty signal batches: poll_intent_signals returns an empty array, not an error. The model should treat that as "nothing new" and schedule the next poll, not fail.
- Unbounded topic arrays: cap account_ids and topics in the Zod schema (we use optional arrays, but adding a .max() guard prevents pathological requests that blow the API call size).
Security Hardening
Intent data is some of the most commercially sensitive telemetry a B2B company owns. Keep the server read-mostly: never expose a write tool unless the business case demands it, and if you add one, gate it behind an approval mode. Validate every argument in Zod, keep the API key or OAuth pair out of logs, and terminate the MCP process if you detect repeated auth failures. Finally, remember the model context is untrusted input: treat every tool-call argument as attacker-controlled and let Zod do the enforcement.
Wrapping Up
You now have a working 6sense buying-intent MCP server with four tools, dual auth, cursor-based pagination and a drainable signal feed, configured for both Claude Desktop and Cursor. The pattern generalizes: swap the 6sense client for any intent or account-intelligence provider and the agent contract barely changes. That is the real 2026 shift - intent data is becoming a first-class tool surface for agents, not a dashboard you log into.
By Deepak Bagada, CEO at SaaSNext & Principal AI Architect.
Tested with FastMCP 2.2.1 and MCP SDK v0.20.0 (2026-07-28 spec) on August 2026.
Enjoyed this breakdown? Get our morning dispatch in your inbox.
Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.
Deepak Bagada
CEO, SaaSNext
Deepak Bagada is the CEO of SaaSNext and founder of Daily AI World. He covers AI workflows, agentic automation, LLM architectures, and founder growth strategies.
Architect 5 AI Safety Guardrails as 1,367 Researchers Warn of Frontier Model Arms Race in 2026
Next Story →Cut 68% Redundant Compute with LangGraph 1.x Node Caching & Deferred Nodes in 2026
Related Intelligence Analysis
Vercel AI SDK Tool Calling React: 5 Steps (2026)
Vercel AI SDK tool calling React integration is a programming pattern that executes server-side functions based on large language model decisions and streams the results to a React frontend. By combining streamText with...
Fact-Density vs. Word Count: The New SEO for 2026
Fact Density is the ratio of verifiable, unique information to the total word count of a piece of content. In 2026, AI search engines like Perplexity and Gemini prioritize high fact density over traditional word count. A...
NVIDIA Audex vs Qwen3.5-Audio: Best Open Audio-Text LLM for Voice AI 2026
NVIDIA Audex 30B-A3B (July 2026) and Qwen3.5-35B-A3B are the two leading open audio-text LLMs. Audex uniquely handles both audio understanding and generation in a single model while preserving text intelligence. Qwen3.5-...