tl-openmeter-api-mcp-server
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
| Skill | Purpose |
|---|---|
| tl-openmeter-api | REST API reference (knowledge) |
| tl-openmeter-local-dev | Local dev setup: Docker, ngrok, Stripe App, webhooks |
| tl-openmeter-api-mcp-server | This 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
| Variable | Default | Description |
|---|---|---|
OPENMETER_URL |
http://localhost:8888 |
Base URL of OpenMeter API |
OPENMETER_API_KEY |
(none) | Optional Bearer token for auth |
Local dev often runs without OPENMETER_API_KEY.
Available Tools
| Tool | Purpose |
|---|---|
openmeter_list_meters |
GET /api/v1/meters |
openmeter_get_meter |
GET /api/v1/meters/{idOrSlug} |
openmeter_query_usage |
GET /api/v1/meters/{idOrSlug}/query |
openmeter_list_customers |
GET /api/v1/customers (paginated) |
openmeter_get_customer |
GET /api/v1/customers/{idOrKey} |
openmeter_create_customer |
POST /api/v1/customers |
openmeter_list_subscriptions |
List subscriptions (optional customerId) |
openmeter_create_subscription |
POST /api/v1/subscriptions |
openmeter_cancel_subscription |
POST /api/v1/subscriptions/{id}/cancel |
openmeter_list_plans |
GET /api/v1/plans |
openmeter_list_features |
GET /api/v1/features |
openmeter_get_entitlements |
GET /api/v1/customers/{id}/entitlements |
openmeter_ingest_event |
POST /api/v1/events (CloudEvents JSON) |
openmeter_list_apps |
GET /api/v1/apps |
openmeter_list_billing_profiles |
GET /api/v1/billing/profiles |
openmeter_check_status |
Verify 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
- Start OpenMeter (e.g.
docker compose upor your stack) - Ensure the OpenMeter MCP server is configured and enabled in Cursor
- In a chat, ask: "Check the OpenMeter status" or "List OpenMeter meters"
- The agent should call
openmeter_check_statusoropenmeter_list_metersand return results
Errors (e.g. connection refused) are returned as tool results so the agent can report them.
Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
| Tools not available | Server not in mcp.json |
Add config entry, restart Cursor |
| Connection refused | OpenMeter not running | Start OpenMeter (docker compose up) |
| 401 Unauthorized | Missing API key | Set OPENMETER_API_KEY in env |
| Server won't start | Not built | Run 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
- anthropics/skills/mcp-builder — MCP server development guide
First-Party Documentation
- MCP Specification — Protocol specification
- MCP TypeScript SDK — SDK reference
- OpenMeter API — API endpoints
MCP Resources
- MCP Server Examples — Reference implementations
- MCP Inspector — Debugging tool
- MCP Best Practices — Universal guidelines
More from toddlevy/tl-agent-skills
tl-openmeter-api
Works with the OpenMeter REST API for usage metering, billing, and entitlements. Covers CloudEvents ingestion, meters, features, plans, customers, subscriptions, entitlements, notifications, billing profiles, invoices, apps, addons, grants, and the Stripe marketplace. Use when integrating OpenMeter, debugging metering, building catalog sync scripts, or when the user mentions OpenMeter API.
13tl-first-principles
Foundational software design principles traced to their intellectual origins. Covers information hiding, separation of concerns, abstraction, SSOT/DRY, conceptual integrity, and composition. Use when making architectural decisions, evaluating trade-offs, or understanding *why* best practices exist.
13tl-knip
Find and remove unused files, dependencies, and exports in TypeScript/JavaScript projects using Knip. Covers configuration-first workflow, plugin system, barrel file handling, CI integration, monorepo support, and agent-specific cleanup guidance.
12tl-docs-create
Create documentation from scratch for codebases. Covers SSOT-driven generation, writing standards, and templates for README/AGENTS.md/CHANGELOG. Use when creating new docs or documenting an undocumented codebase.
12tl-devlog
Maintain a structured development changelog (DEVLOG.md) capturing architectural decisions, milestones, incidents, and insights. Use when the user says "log this", "devlog", "archive this", or at natural pause points after significant decisions. Trigger on changelog, decision log, work log, or progress tracking.
12tl-docs-audit
Audit existing documentation for gaps, staleness, and sync issues. Generates sync reports with actionable findings. Use when reviewing doc coverage, finding outdated docs, or syncing docs with code.
12