Skip to content
tl-openmeter-api-mcp-server logo

Tl Openmeter API MCP Server

tl-openmeter-api-mcp-server

MCP server providing tools to interact with a local OpenMeter instance. Enables AI assistants to list meters, manage customers, create subscriptions, query usage, and ingest events. Use when working with OpenMeter locally and you need to call the API directly from the agent.

toddlevy/tl-agent-skills0installs0stars

SKILL.md

Full skill instructions

<!-- Copyright (c) 2026 Todd Levy. Licensed under MIT. SPDX-License-Identifier: MIT -->

tl-openmeter-api-mcp-server

MCP server for your local OpenMeter instance: tools and resources so AI assistants (Cursor, Claude, etc.) can list meters, manage customers and subscriptions, query usage, and read the API quick reference.

Pair this with the tl-openmeter-api skill for knowledge; use this server to run operations against your OpenMeter.

Suite

SkillPurpose
tl-openmeter-apiREST API reference (knowledge)
tl-openmeter-local-devLocal dev setup: Docker, ngrok, Stripe App, webhooks
tl-openmeter-api-mcp-serverThis skill: MCP server for calling OpenMeter from Cursor

When to Use

  • "List my OpenMeter meters"
  • "Create a customer in OpenMeter"
  • "Query usage for this meter"
  • "Ingest a test event"
  • "Check if OpenMeter is running"
  • When you need the agent to actually call OpenMeter API (not just provide documentation)

Prerequisites

  • Node.js 18+
  • A running OpenMeter instance (e.g. Docker, default http://localhost:8888)

Installation

Step 1: Build the Server

From the skill directory:

npm install
npm run build

Step 2: Register in Cursor MCP Config

Add to ~/​.cursor/​mcp.json:

{
  "mcpServers": {
    "OpenMeter": {
      "command": "node",
      "args": ["C:\\Users\\Todd\\.cursor\\skills\\@tl-agent-skills\\skills\\tl-openmeter-api-mcp-server\\dist\\index.js"],
      "env": {
        "OPENMETER_URL": "http://localhost:8888",
        "OPENMETER_API_KEY": ""
      }
    }
  }
}

Adjust the path to match your installation location.

Step 3: Restart Cursor

Cursor needs to restart to pick up MCP config changes.

Environment Variables

VariableDefaultDescription
OPENMETER_URLhttp://localhost:8888Base URL of OpenMeter API
OPENMETER_API_KEY(none)Optional Bearer token for auth

Local dev often runs without OPENMETER_API_KEY.

Available Tools

ToolPurpose
openmeter_list_metersGET /​api/​v1/​meters
openmeter_get_meterGET /​api/​v1/​meters/​{idOrSlug}
openmeter_query_usageGET /​api/​v1/​meters/​{idOrSlug}/​query
openmeter_list_customersGET /​api/​v1/​customers (paginated)
openmeter_get_customerGET /​api/​v1/​customers/​{idOrKey}
openmeter_create_customerPOST /​api/​v1/​customers
openmeter_list_subscriptionsList subscriptions (optional customerId)
openmeter_create_subscriptionPOST /​api/​v1/​subscriptions
openmeter_cancel_subscriptionPOST /​api/​v1/​subscriptions/​{id}/​cancel
openmeter_list_plansGET /​api/​v1/​plans
openmeter_list_featuresGET /​api/​v1/​features
openmeter_get_entitlementsGET /​api/​v1/​customers/​{id}/​entitlements
openmeter_ingest_eventPOST /​api/​v1/​events (CloudEvents JSON)
openmeter_list_appsGET /​api/​v1/​apps
openmeter_list_billing_profilesGET /​api/​v1/​billing/​profiles
openmeter_check_statusVerify connection to OpenMeter

Resources

  • OpenMeter quick reference — Markdown from the tl-openmeter-api skill
  • OpenMeter spec note — Plain text with spec URL and configured base URL

Verification

  1. Start OpenMeter (e.g. docker compose up or your stack)
  2. Ensure the OpenMeter MCP server is configured and enabled in Cursor
  3. In a chat, ask: "Check the OpenMeter status" or "List OpenMeter meters"
  4. The agent should call openmeter_check_status or openmeter_list_meters and return results

Errors (e.g. connection refused) are returned as tool results so the agent can report them.

Troubleshooting

ProblemCauseFix
Tools not availableServer not in mcp.jsonAdd config entry, restart Cursor
Connection refusedOpenMeter not runningStart OpenMeter (docker compose up)
401 UnauthorizedMissing API keySet OPENMETER_API_KEY in env
Server won't startNot builtRun npm run build in skill directory

MCP Design Principles

From anthropics/​skills/​mcp-builder:

Tool Naming

Use consistent, action-oriented prefixes:

openmeter_list_meters     ✓ (list action)
openmeter_get_customer    ✓ (get action)
openmeter_create_subscription ✓ (create action)
meters                    ✗ (no action prefix)

API Coverage vs Workflow Tools

Balance comprehensive API endpoint coverage with specialized workflow tools:

  • Comprehensive coverage gives agents flexibility to compose operations
  • Workflow tools (e.g., openmeter_provision_customer) combine multiple calls for common tasks

When uncertain, prioritize comprehensive API coverage.

Actionable Error Messages

Error messages should guide agents toward solutions:

// Bad
throw new Error('Request failed');

// Good
throw new Error(
  `OpenMeter customer not found: ${customerId}. ` +
  `Use openmeter_list_customers to find valid customer IDs.`
);

Pagination

All list operations support cursor-based pagination:

interface PaginationParams {
  page?: number;
  pageSize?: number;  // default: 100, max: 1000
}

interface PaginatedResponse<T> {
  items: T[];
  page: number;
  pageSize: number;
  totalCount: number;
}

Tool Implementation

server.registerTool({
  name: 'openmeter_list_customers',
  description: 'List customers with pagination. Returns page info and total count.',
  inputSchema: z.object({
    page: z.number().optional().describe('Page number (1-indexed)'),
    pageSize: z.number().max(1000).optional().describe('Items per page, max 1000'),
  }),
  async handler({ page = 1, pageSize = 100 }) {
    const response = await fetch(
      `${baseUrl}/​api/​v1/​customers?page=${page}&pageSize=${pageSize}`
    );
    return {
      content: [{ type: 'text', text: JSON.stringify(await response.json()) }],
    };
  },
});

Batch Operations

Batch Event Ingestion

server.registerTool({
  name: 'openmeter_batch_ingest',
  description: 'Ingest multiple CloudEvents in a single request',
  inputSchema: z.object({
    events: z.array(z.object({
      type: z.string(),
      subject: z.string(),
      data: z.record(z.unknown()),
    })),
  }),
  async handler({ events }) {
    const cloudEvents = events.map(e => ({
      specversion: '1.0',
      id: crypto.randomUUID(),
      source: 'mcp-server',
      type: e.type,
      subject: e.subject,
      time: new Date().toISOString(),
      data: e.data,
    }));
    
    const response = await fetch(`${baseUrl}/​api/​v1/​events`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/​cloudevents-batch+json' },
      body: JSON.stringify(cloudEvents),
    });
    
    return { content: [{ type: 'text', text: `Ingested ${events.length} events` }] };
  },
});

Tool Annotations

Mark tools with hints for agent behavior:

server.registerTool({
  name: 'openmeter_create_customer',
  annotations: {
    readOnlyHint: false,      // Modifies state
    destructiveHint: false,   // Does not delete data
    idempotentHint: false,    // Creates new resource each time
    openWorldHint: true,      // External service interaction
  },
  // ...
});

server.registerTool({
  name: 'openmeter_list_meters',
  annotations: {
    readOnlyHint: true,       // Only reads data
    destructiveHint: false,
    idempotentHint: true,     // Same result for same input
    openWorldHint: true,
  },
  // ...
});

Testing

MCP Inspector

Test tools interactively:

npx @modelcontextprotocol/​inspector

Mock Server for Tests

import { createMockMcpServer } from './​test-utils';

const mockServer = createMockMcpServer({
  tools: {
    openmeter_list_meters: async () => ({
      content: [{ type: 'text', text: JSON.stringify([
        { id: 'test-meter', slug: 'api-calls', aggregation: 'COUNT' }
      ]) }],
    }),
  },
});

Integration Test Pattern

describe('OpenMeter MCP Server', () => {
  it('lists meters from running OpenMeter', async () => {
    const result = await server.callTool('openmeter_list_meters', {});
    expect(result.content[0].type).toBe('text');
    const meters = JSON.parse(result.content[0].text);
    expect(Array.isArray(meters)).toBe(true);
  });

  it('returns actionable error for invalid customer', async () => {
    const result = await server.callTool('openmeter_get_customer', { 
      idOrKey: 'nonexistent' 
    });
    expect(result.content[0].text).toContain('openmeter_list_customers');
  });
});

References

Quilted Skills

First-Party Documentation

MCP Resources