Guide · Product Analytics

MCP Tools for Segment — identify/track/group calls, Destination delivery, Protocols, and health monitoring

Segment (Twilio Segment) is a customer data platform that routes event streams to dozens of downstream Destinations (Amplitude, Mixpanel, Salesforce, Braze, warehouses, etc.). MCP tools that integrate with Segment send events once and rely on Segment to fan them out. The critical architectural subtlety: Segment's Node.js SDK (@segment/analytics-node) confirms that your event reached Segment's intake API — it cannot confirm that any downstream Destination received or processed the event. A Destination can fail silently for hours while the SDK reports successful delivery. The second major pattern: Segment Protocols can operate in three enforcement modes — blocking (reject non-conforming events), omit-properties (accept but strip invalid properties), or allow (log violations only). An MCP tool sending events with inconsistent schemas may have its data silently stripped in omit-properties mode without any error in the HTTP response.

TL;DR

Init: const analytics = new Analytics({writeKey: 'xxx'}). Calls: analytics.track({userId, event, properties}), analytics.identify({userId, traits}), analytics.group({userId, groupId, traits}). Batch endpoint: https://api.segment.io/v1/batch (SDK uses this). EU data residency: set host: 'https://events.eu1.segmentapis.com'. Shutdown: await analytics.closeAndFlush() — REQUIRED before process exit. Delivery confirmation: the SDK callback fires after intake API accepts the event — not after Destinations process it. Schema enforcement: check your Source's Protocols settings — blocking mode returns 400 for non-conforming events; omit-properties silently strips invalid fields.

Segment architecture for MCP tool authors

Segment's data flow has two stages. First, your MCP tool calls the SDK, which sends events to Segment's Source (identified by your write key). Segment receives the event, validates it against any Protocols schema, and accepts or rejects it. Second, Segment's internal pipeline routes accepted events to your configured Destinations — external tools and warehouses. This second stage is entirely asynchronous and opaque to your MCP tool. If Amplitude is a Destination and Amplitude's ingestion endpoint is down, Segment queues the events internally and retries — but your SDK call already completed with "success."

Write keys are Source-specific. Each Source (e.g., "MCP Server Production", "MCP Server Staging") has its own write key. The write key is write-only — it can send data into Segment but cannot read, query, or manage Segment resources. The Segment Public API uses a different authentication model (workspace token from Settings → API Keys) for management operations.

import { Analytics } from '@segment/analytics-node';

// Module-level singleton
let analytics = null;

function getAnalytics() {
  if (analytics !== null) return analytics;

  analytics = new Analytics({
    writeKey: process.env.SEGMENT_WRITE_KEY,
    // EU data residency — must match your Source's data residency setting
    // host: 'https://events.eu1.segmentapis.com',

    // Batch settings
    flushAt: 15,           // send when 15 events are queued
    flushInterval: 10_000, // or every 10 seconds, whichever comes first
    maxEventsInBatch: 15,  // same as flushAt for @segment/analytics-node v1.x

    // Retry on failure
    maxRetries: 3,
  });

  return analytics;
}

// CRITICAL: must be called before process exit
// closeAndFlush() drains the queue, sends remaining events, and closes HTTP connections
// Without it, events buffered since the last auto-flush are lost
async function shutdown() {
  if (!analytics) return;
  await analytics.closeAndFlush({ timeout: 10_000 });
  analytics = null;
}

process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);

The five Segment method types

Segment defines five canonical event types that map to different analytics patterns. MCP tools should use the appropriate type rather than routing everything through track().

function getClient() { return getAnalytics(); }

// TRACK: behavioral event — what the user did
function trackToolCall(userId, toolName, inputTokens, outputTokens, success, context = {}) {
  getClient().track({
    userId,               // identified user; use anonymousId if pre-login
    // anonymousId: 'anon-session-id',  // required if no userId
    event: 'MCP Tool Called',
    properties: {
      tool_name: toolName,
      input_tokens: inputTokens,
      output_tokens: outputTokens,
      success,
      // Segment passes all properties to Destinations unchanged
      // Protocols schema enforcement applies here — match your tracking plan
    },
    context: {
      ...context,
      // app: { name: 'AliveMCP', version: '2.0.0' },
      // ip: '0.0.0.0',  set to '0.0.0.0' to disable IP collection
    },
    timestamp: new Date(),  // override event time (ISO 8601 string or Date)
    // messageId: 'custom-uuid',  // set for deduplication (Segment uses this as idempotency key)
  });
}

// IDENTIFY: who the user is — sets traits on the user profile
function identifyUser(userId, email, plan, role, createdAt) {
  getClient().identify({
    userId,
    traits: {
      email,
      plan,
      role,
      createdAt,
      // Segment forwards traits to CRM Destinations (Salesforce, HubSpot)
      // as contact/lead properties — match their field names in trait keys
    },
  });
}

// GROUP: link user to an account/org/team
function associateGroup(userId, groupId, groupName, plan, memberCount) {
  getClient().group({
    userId,
    groupId,
    traits: {
      name: groupName,
      plan,
      member_count: memberCount,
    },
  });
}

// PAGE: page or screen view (useful for MCP web dashboard events)
function trackPageView(userId, pageName, url, referrer) {
  getClient().page({
    userId,
    name: pageName,
    properties: { url, referrer },
  });
}

// ALIAS: link anonymous ID to identified user ID (for identity merge)
// Call once at login to merge pre-login anonymous events to the user profile
function aliasUser(anonymousId, userId) {
  getClient().alias({
    previousId: anonymousId,
    userId,
  });
}

Protocols and schema enforcement

Segment Protocols lets you define a Tracking Plan — a schema that specifies which events are valid, what properties each event may carry, and what types those properties must have. The enforcement mode determines what happens when a non-conforming event arrives.

Enforcement modes and their health monitoring implications:

// Wrapper that makes Protocols enforcement mode explicit in your MCP tool
async function trackWithProtocolsAwareness(userId, event, properties) {
  const client = getClient();

  return new Promise((resolve, reject) => {
    client.track({
      userId,
      event,
      properties,
    }, (err) => {
      if (err) {
        // In blocking mode: err includes the 400 response body with violation details
        // {"type":"track","message":"Event 'MCP Tool Called' not found in Tracking Plan"}
        // In omit-properties mode: err is null even if properties were stripped
        reject(new Error(`Segment track failed: ${err.message}`));
      } else {
        resolve();
      }
    });
    // Note: callback fires after INTAKE API confirms — not after Destination delivery
  });
}

// Query Segment's Violations API to detect schema drift (requires workspace token)
async function checkProtocolsViolations(workspaceToken, sourceId) {
  const res = await fetch(
    `https://api.segmentapis.com/catalog/sources/${sourceId}/violations`,
    {
      headers: { 'Authorization': `Bearer ${workspaceToken}` },
      signal: AbortSignal.timeout(10_000),
    }
  );
  if (!res.ok) throw new Error(`Violations API → ${res.status}`);
  return res.json();
  // Returns recent violations: event name, property, violation type, count
  // Use this to detect when a code change started sending non-conforming events
}

Health monitoring: intake vs Destination delivery

Segment's health model has two tiers. The intake API is what your MCP tool directly measures — it accepts events and is highly available. The Destination delivery pipeline is what matters for business outcomes — if Amplitude is receiving events, if Salesforce leads are being created, if the data warehouse is being loaded. Only the intake is directly probeable; Destination health requires monitoring the Destinations themselves or checking Segment's Delivery Overview in the dashboard.

async function probeSegment(writeKey, host = 'https://api.segment.io') {
  const results = await Promise.allSettled([

    // 1. Intake API health — does Segment accept events?
    (async () => {
      const body = {
        batch: [{
          type: 'track',
          userId: '__alivemcp_probe__',
          event: '__health_probe__',
          properties: { synthetic: true },
          timestamp: new Date().toISOString(),
          messageId: `probe-${Date.now()}`,
        }],
        writeKey,
        sentAt: new Date().toISOString(),
      };
      const encoded = Buffer.from(`${writeKey}:`).toString('base64');
      const res = await fetch(`${host}/v1/batch`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Basic ${encoded}`,
        },
        body: JSON.stringify(body),
        signal: AbortSignal.timeout(5_000),
      });
      if (!res.ok) throw new Error(`Segment batch → ${res.status}`);
      // Segment returns 200 with empty body {} on success
      return { kind: 'intake', accepted: true };
    })(),

    // 2. Source connectivity check via single-event endpoint
    (async () => {
      const encoded = Buffer.from(`${writeKey}:`).toString('base64');
      const res = await fetch(`${host}/v1/track`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Basic ${encoded}`,
        },
        body: JSON.stringify({
          userId: '__alivemcp_probe__',
          event: '__health_probe_single__',
          properties: { synthetic: true },
          timestamp: new Date().toISOString(),
          messageId: `probe-single-${Date.now()}`,
        }),
        signal: AbortSignal.timeout(5_000),
      });
      if (!res.ok) throw new Error(`Segment /v1/track → ${res.status}`);
      return { kind: 'track_endpoint', accepted: true };
    })(),
  ]);

  return {
    healthy: results.every(r => r.status === 'fulfilled'),
    // Note: healthy=true here means INTAKE accepted events.
    // Destination delivery health must be monitored separately at each Destination.
    components: results.map(r =>
      r.status === 'fulfilled'
        ? { ...r.value, ok: true }
        : { ok: false, error: r.reason?.message }
    ),
  };
}

Set AliveMCP alerts: critical when the intake API returns 4xx (write key revoked, source deleted, or Protocols blocking enforcement triggered); warning when intake latency exceeds 500ms (Segment's intake API is normally sub-100ms — high latency indicates internal queue pressure); info for monitoring key Destinations independently (e.g., probe your Amplitude or Mixpanel Destination directly to verify end-to-end delivery).

Common integration pitfalls
Forgetting closeAndFlush() on container shutdown
The @segment/analytics-node SDK batches events in memory. Containerized MCP servers that receive SIGTERM and exit without calling await analytics.closeAndFlush() lose all events queued since the last auto-flush. Always register a SIGTERM handler and await closeAndFlush() — it accepts a timeout parameter ({timeout: 10_000}) to avoid hanging indefinitely.
Treating SDK success as end-to-end delivery
The SDK callback and promise resolve after Segment's intake API confirms receipt, not after any Destination processes the event. If Salesforce is your CRM Destination and it's experiencing an outage, your analytics.identify() call succeeds while zero Salesforce leads are created. Monitor Destination health in Segment's Delivery Overview or probe each Destination independently.
Using the same write key for multiple environments
Each environment (production, staging, development) should have its own Segment Source with its own write key. Using a production write key in staging pollutes production analytics with test events. Even with filtering rules, misrouted events corrupt funnel conversion rates and A/B test groups. Create separate Sources in Segment and load the write key from environment variables.
omit-properties mode creating invisible data loss
If Protocols is in omit-properties mode and your code ships a new property that wasn't added to the Tracking Plan first, every event carries that property in your code but the property never reaches any Destination. The SDK reports success. This is invisible until you notice missing columns in your data warehouse or missing properties in Amplitude. Always update the Tracking Plan before shipping new event properties.
Sending anonymousId without userId on identified events
Segment requires either userId or anonymousId on every event. Server-side MCP tool calls for authenticated sessions should always use userId. If you send anonymousId for events where the user is known, downstream Destinations create a second, duplicate user profile that is never merged with the identified profile.

Related guides