Skip to main content
Workflows Library MCP Directory Realtime AI News Sponsor Tier Subscribe
Front Page / AI Tools / Deep Dive

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

Deepak Bagada

CEO, SaaSNext

Aug 11, 2026 Published
|
Aug 11, 2026 Updated
|
14 Minutes Reading Time
Core Takeaways for Founders & Builders
  • 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.

Executive Briefing

Enjoyed this breakdown? Get our morning dispatch in your inbox.

Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.

Frequently Asked Questions
Yes. 6sense shipped MCP integrations in August 2026 that pipe account identity and buying-intent signals - predicted buying stage, qualified account status and intent topics - directly into MCP-compatible agents through its Predict/ABX API.
Both work. An API key is fastest to stand up but is a shared workspace credential with a larger blast radius. OAuth 2.0 client-credentials gives short-lived tokens, immediate revocation and better auditability, which is why production deployments usually prefer it.
Use subscribe_intent_signals to register a subscription for accounts or topics, then drain with poll_intent_signals. The server returns batches of signals on demand so tool calls stay bounded and fast, avoiding long-lived blocking connections.
Yes. Both clients support the same mcpServers JSON block. Claude Desktop reads claude_desktop_config.json while Cursor reads .cursor/mcp.json, so you reuse identical command, args and env entries.
Deepak Bagada
Author Profile

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.

Related Intelligence Analysis

Briefing AI Tools

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...

Deepak Bagada Deepak Bagada
12m read
Breaking AI Tools

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...

Deepak Bagada Deepak Bagada
4m read
Audio Briefing
Accessibility Preferences
High Contrast Mode
Accessible Reading Font

Keyboard Shortcuts

Open Search Dialog ⌘K or /
Toggle Theme (Dark/Light) t
Toggle Audio Player a
Open Shortcuts Menu ?
Close Active Dialog Esc