4-package monorepo: - @tokenvault/core: Fastify 5.x proxy server, 7-stage pipeline, 3 provider adapters (Anthropic, OpenAI, Ollama), PostgreSQL ticket system, cost calculator with real provider pricing - @tokenvault/mcp: MCP server (stdio) with tv_ticket, tv_cost, tv_health tools for IDE integration - @tokenvault/client: TypeScript SDK with createTokenVaultClient() - @tokenvault/dashboard: Single-file HTML dashboard with MAGATAMA CI style (indigo #6366f1), bilingual DE+EN, 4 tabs OpenAI-compatible proxy at /v1/chat/completions — drop-in replacement. Every LLM request becomes a trackable ticket (TV-00001).
86 lines
3.8 KiB
TypeScript
86 lines
3.8 KiB
TypeScript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
import { z } from 'zod';
|
|
|
|
const CORE_URL = process.env['TOKENVAULT_URL'] ?? 'http://localhost:3300';
|
|
|
|
async function fetchCore<T>(path: string): Promise<T> {
|
|
const res = await fetch(`${CORE_URL}${path}`);
|
|
if (!res.ok) throw new Error(`TokenVault API error: ${res.status}`);
|
|
return res.json() as Promise<T>;
|
|
}
|
|
|
|
const server = new McpServer({
|
|
name: 'tokenvault',
|
|
version: '0.1.0',
|
|
});
|
|
|
|
// ─── tv_ticket: View and search tickets ──────────────────────────────────────
|
|
server.tool(
|
|
'tv_ticket',
|
|
'View, search, and manage TokenVault tickets. Every LLM request = 1 ticket.',
|
|
{
|
|
action: z.enum(['list', 'get', 'stats']).describe('Action to perform'),
|
|
id: z.string().optional().describe('Ticket ID (for get action)'),
|
|
provider: z.string().optional().describe('Filter by provider'),
|
|
project: z.string().optional().describe('Filter by project'),
|
|
period: z.string().optional().describe('Stats period: today, week, month, all'),
|
|
limit: z.number().optional().describe('Max results (default 20)'),
|
|
},
|
|
async ({ action, id, provider, project, period, limit }) => {
|
|
if (action === 'get' && id) {
|
|
const ticket = await fetchCore(`/v1/tickets/${id}`);
|
|
return { content: [{ type: 'text' as const, text: JSON.stringify(ticket, null, 2) }] };
|
|
}
|
|
|
|
if (action === 'stats') {
|
|
const stats = await fetchCore(`/v1/tickets/stats?period=${period ?? 'today'}`);
|
|
return { content: [{ type: 'text' as const, text: JSON.stringify(stats, null, 2) }] };
|
|
}
|
|
|
|
const params = new URLSearchParams();
|
|
if (provider) params.set('provider', provider);
|
|
if (project) params.set('project', project);
|
|
params.set('limit', String(limit ?? 20));
|
|
const data = await fetchCore(`/v1/tickets?${params}`);
|
|
return { content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }] };
|
|
},
|
|
);
|
|
|
|
// ─── tv_cost: Cost dashboard ─────────────────────────────────────────────────
|
|
server.tool(
|
|
'tv_cost',
|
|
'Show cost dashboard: spend, savings, forecasts. Tracks every token across all LLM providers.',
|
|
{
|
|
period: z.enum(['today', 'week', 'month', 'all']).optional().describe('Time period'),
|
|
group_by: z.enum(['provider', 'model', 'project', 'team']).optional().describe('Group breakdown by'),
|
|
},
|
|
async ({ period, group_by }) => {
|
|
const [summary, breakdown] = await Promise.all([
|
|
fetchCore(`/v1/cost?period=${period ?? 'month'}`),
|
|
group_by ? fetchCore(`/v1/cost/breakdown?group_by=${group_by}`) : Promise.resolve(null),
|
|
]);
|
|
|
|
const parts = [`# Cost Summary (${period ?? 'month'})\n${JSON.stringify(summary, null, 2)}`];
|
|
if (breakdown) {
|
|
parts.push(`\n# Breakdown by ${group_by}\n${JSON.stringify(breakdown, null, 2)}`);
|
|
}
|
|
return { content: [{ type: 'text' as const, text: parts.join('\n') }] };
|
|
},
|
|
);
|
|
|
|
// ─── tv_health: Service health ───────────────────────────────────────────────
|
|
server.tool(
|
|
'tv_health',
|
|
'Check TokenVault service health and configured providers.',
|
|
{},
|
|
async () => {
|
|
const health = await fetchCore('/health');
|
|
return { content: [{ type: 'text' as const, text: JSON.stringify(health, null, 2) }] };
|
|
},
|
|
);
|
|
|
|
// ─── Start ───────────────────────────────────────────────────────────────────
|
|
const transport = new StdioServerTransport();
|
|
await server.connect(transport);
|