Guide · LLM Observability & Evaluation

MCP Server Helicone — proxy setup, custom properties, cache headers, per-tool cost attribution

Three Helicone behaviours catch MCP server authors off-guard: Helicone is a proxy, not an SDK import — you route requests by changing baseURL to https://oai.helicone.ai/v1 and sending Helicone-Auth: Bearer <helicone_key> as a header; the OpenAI Authorization header continues to carry your OpenAI API key as the passthrough credential — swapping the two keys causes Helicone to log correctly but forward an invalid key to OpenAI, producing a silent 401; custom properties for per-tool cost attribution require Helicone-Property-* request headers, not body fields — set Helicone-Property-ToolName: search_web and Helicone-Property-SessionId: <id> in defaultHeaders at client construction or per-request via the SDK's headers option; and Helicone cache hits are detectable only via response headers — read Helicone-Cache-Hit: true via .withResponse() because a cache hit returns a stale completion with no exception, and time-sensitive tools must check this before treating the output as current.

TL;DR

Create your OpenAI client with baseURL: 'https://oai.helicone.ai/v1', apiKey: process.env.OPENAI_API_KEY, and defaultHeaders: { 'Helicone-Auth': 'Bearer ' + process.env.HELICONE_API_KEY }. Add per-tool context with 'Helicone-Property-ToolName': toolName in the same defaultHeaders map. For per-request properties (session ID, user ID) pass them in the second argument to SDK methods: client.chat.completions.create(params, { headers: { 'Helicone-Property-SessionId': sessionId } }). Never put the Helicone key in the apiKey slot — it will reach OpenAI as your bearer token and fail with 401.

Proxy client setup for OpenAI and Anthropic

Helicone works by intercepting HTTPS requests and logging them before forwarding. For the OpenAI SDK, swap the base URL. For Anthropic, use Helicone's Anthropic gateway URL. Both require a Helicone-Auth header; the original provider key stays in apiKey or x-api-key.

import OpenAI from 'openai';
import Anthropic from '@anthropic-ai/sdk';
import { z } from 'zod';

// Helicone-proxied OpenAI client — one instance at module scope
const openai = new OpenAI({
  apiKey:  process.env.OPENAI_API_KEY,   // OpenAI key — forwarded to OpenAI by Helicone
  baseURL: 'https://oai.helicone.ai/v1', // Helicone OpenAI gateway

  // Helicone-Auth goes in defaultHeaders, NOT in apiKey
  defaultHeaders: {
    'Helicone-Auth':                `Bearer ${process.env.HELICONE_API_KEY}`,
    'Helicone-Property-Service':    'alivemcp',
    'Helicone-Property-Environment': process.env.NODE_ENV ?? 'development',
  },
});

// Helicone-proxied Anthropic client
const anthropic = new Anthropic({
  apiKey:  process.env.ANTHROPIC_API_KEY,
  baseURL: 'https://anthropic.helicone.ai',  // Helicone Anthropic gateway

  defaultHeaders: {
    'Helicone-Auth':                `Bearer ${process.env.HELICONE_API_KEY}`,
    'Helicone-Property-Service':    'alivemcp',
    'Helicone-Property-Environment': process.env.NODE_ENV ?? 'development',
  },
});

// MCP tool using proxied OpenAI — all calls logged to Helicone automatically
server.tool(
  'generate_summary',
  {
    content:    z.string().min(1).max(10_000),
    session_id: z.string().optional(),
  },
  async ({ content, session_id }) => {
    // Per-request properties — include session context without rebuilding the client
    const perRequestHeaders: Record<string, string> = {
      'Helicone-Property-ToolName': 'generate_summary',
    };
    if (session_id) {
      perRequestHeaders['Helicone-Property-SessionId'] = session_id;
    }

    const completion = await openai.chat.completions.create(
      {
        model:      'gpt-4o-mini',
        messages:   [{ role: 'user', content: `Summarise in 3 bullet points: ${content}` }],
        max_tokens: 256,
      },
      { headers: perRequestHeaders }  // second arg to .create() — per-request headers
    );

    const summary = completion.choices[0]?.message?.content ?? '';
    return { content: [{ type: 'text', text: summary }] };
  }
);

Helicone custom properties (Helicone-Property-*) appear as filter dimensions in the Helicone dashboard. Setting Helicone-Property-ToolName on every request lets you break down cost, latency, and error rate by tool name without any additional instrumentation code. Properties are case-sensitive after the prefix — Helicone-Property-toolname and Helicone-Property-ToolName create two separate dimensions.

Cache control and freshness detection

Helicone's cache feature stores responses and returns them for identical prompts within the TTL window, controlled by the Helicone-Cache-Enabled: true request header. When a cache hit occurs, Helicone returns the stored response immediately with Helicone-Cache-Hit: true in the response headers. The OpenAI SDK discards response headers by default — use .withResponse() to access them. For time-sensitive tools (live data queries, actions with side effects), check the cache hit header and either disable caching or reject stale responses.

// Tool where caching is appropriate — lookup is expensive, output doesn't expire quickly
server.tool(
  'lookup_mcp_server_info',
  { server_slug: z.string().min(1).max(100) },
  async ({ server_slug }) => {
    // Enable Helicone cache — responses cached for 1 hour
    const { data: completion, response } = await openai.chat.completions
      .create(
        {
          model:    'gpt-4o-mini',
          messages: [{ role: 'user', content: `Describe the MCP server: ${server_slug}` }],
          max_tokens: 256,
        },
        {
          headers: {
            'Helicone-Property-ToolName': 'lookup_mcp_server_info',
            'Helicone-Cache-Enabled':     'true',
            'Helicone-Cache-Ttl':         '3600',  // 1 hour in seconds
          },
        }
      )
      .withResponse();  // returns { data: completion, response: Response }

    const cacheHit   = response.headers.get('Helicone-Cache-Hit') === 'true';
    const cacheId    = response.headers.get('Helicone-Cache-Id') ?? null;
    const heliconeId = response.headers.get('Helicone-Id') ?? null;

    const info = completion.choices[0]?.message?.content ?? '';

    return {
      content: [{
        type: 'text',
        text: JSON.stringify({
          info,
          from_cache:  cacheHit,
          cache_id:    cacheId,
          helicone_id: heliconeId,
        }),
      }],
    };
  }
);

// Tool where caching must be disabled — side effects or live data
server.tool(
  'send_alert',
  { message: z.string().min(1).max(500), severity: z.enum(['info', 'warning', 'critical']) },
  async ({ message, severity }) => {
    // Helicone-Cache-Enabled header omitted = caching disabled (default)
    const { data: completion, response } = await openai.chat.completions
      .create(
        {
          model:    'gpt-4o-mini',
          messages: [{ role: 'user', content: `Format this ${severity} alert for Slack: ${message}` }],
          max_tokens: 128,
        },
        {
          headers: {
            'Helicone-Property-ToolName': 'send_alert',
            'Helicone-Property-Severity':  severity,
            // Cache explicitly disabled — Helicone default, but be explicit for clarity
            'Helicone-Cache-Enabled': 'false',
          },
        }
      )
      .withResponse();

    // Verify cache was NOT used — defensive check for action tools
    if (response.headers.get('Helicone-Cache-Hit') === 'true') {
      throw new Error('Alert formatter returned cached response — real-time formatting required');
    }

    const formatted = completion.choices[0]?.message?.content ?? message;
    return { content: [{ type: 'text', text: formatted }] };
  }
);

Streaming with Helicone

Helicone proxies streaming responses transparently. The Helicone-Id response header is returned on the first chunk's HTTP response, before the stream body completes. Streaming responses are logged to Helicone asynchronously after the stream closes — they appear in the dashboard within a few seconds of stream completion.

// Streaming tool — Helicone logs accumulated response after stream closes
server.tool(
  'stream_analysis',
  { text: z.string().min(1).max(5000), session_id: z.string().optional() },
  async ({ text, session_id }) => {
    const headers: Record<string, string> = {
      'Helicone-Property-ToolName': 'stream_analysis',
    };
    if (session_id) headers['Helicone-Property-SessionId'] = session_id;

    // Helicone passes streaming chunks through without buffering
    const stream = await openai.chat.completions.create(
      {
        model:    'gpt-4o-mini',
        messages: [{ role: 'user', content: `Analyze and explain: ${text}` }],
        max_tokens: 1024,
        stream:   true,
        stream_options: { include_usage: true },  // usage on final chunk
      },
      { headers }
    );

    let fullText = '';
    let promptTokens = 0;
    let completionTokens = 0;

    for await (const chunk of stream) {
      const delta = chunk.choices[0]?.delta?.content ?? '';
      fullText += delta;

      if (chunk.usage) {
        promptTokens     = chunk.usage.prompt_tokens;
        completionTokens = chunk.usage.completion_tokens;
      }
    }

    // Helicone logs: promptTokens, completionTokens, and full accumulated text
    // after stream closes — no extra instrumentation needed for cost tracking

    return {
      content: [{
        type: 'text',
        text: JSON.stringify({
          analysis:          fullText,
          prompt_tokens:     promptTokens,
          completion_tokens: completionTokens,
        }),
      }],
    };
  }
);

Rate limit headers and budget monitoring

Helicone forwards provider rate limit headers and adds its own Helicone-RateLimit-* headers for Helicone-enforced rate limits on your account. Read these to implement proactive backpressure in MCP tools rather than waiting for a 429 error to be thrown.

// Health resource — expose Helicone rate limit headroom to the agent
server.resource('helicone_quota', 'helicone://quota', async () => {
  try {
    const { data: _completion, response } = await openai.chat.completions
      .create(
        {
          model:      'gpt-4o-mini',
          messages:   [{ role: 'user', content: 'ping' }],
          max_tokens: 1,
        },
        { headers: { 'Helicone-Property-ToolName': 'quota_probe' } }
      )
      .withResponse();

    // OpenAI rate limit headers (forwarded by Helicone)
    const remainingRequests = response.headers.get('x-ratelimit-remaining-requests');
    const remainingTokens   = response.headers.get('x-ratelimit-remaining-tokens');
    const resetRequests     = response.headers.get('x-ratelimit-reset-requests');
    const resetTokens       = response.headers.get('x-ratelimit-reset-tokens');

    // Helicone request cost (this probe call)
    const heliconeId = response.headers.get('Helicone-Id');

    return {
      contents: [{
        uri: 'helicone://quota',
        text: JSON.stringify({
          status:              'ok',
          remaining_requests:  remainingRequests ? parseInt(remainingRequests) : null,
          remaining_tokens:    remainingTokens   ? parseInt(remainingTokens)   : null,
          reset_requests_in:   resetRequests,
          reset_tokens_in:     resetTokens,
          helicone_request_id: heliconeId,
          checked_at:          new Date().toISOString(),
        }),
      }],
    };
  } catch (err: any) {
    return {
      contents: [{
        uri:  'helicone://quota',
        text: JSON.stringify({ status: 'error', message: err.message }),
      }],
    };
  }
});

Helicone's dashboard can send webhook alerts when cost per hour exceeds a threshold — configure these in the Helicone UI under Alerts. For programmatic budget enforcement, read per-request cost from Helicone-Id and look it up via the Helicone API, or implement token-count-based cost estimation locally using the usage field from the response and a model pricing table.