mcp-cost-tracker 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -0
  3. package/dist/dashboard/generator.d.ts +54 -0
  4. package/dist/dashboard/generator.d.ts.map +1 -0
  5. package/dist/dashboard/generator.js +577 -0
  6. package/dist/dashboard/generator.js.map +1 -0
  7. package/dist/index.d.ts +12 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +60 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/pricing/models.d.ts +48 -0
  12. package/dist/pricing/models.d.ts.map +1 -0
  13. package/dist/pricing/models.js +207 -0
  14. package/dist/pricing/models.js.map +1 -0
  15. package/dist/storage/database.d.ts +129 -0
  16. package/dist/storage/database.d.ts.map +1 -0
  17. package/dist/storage/database.js +374 -0
  18. package/dist/storage/database.js.map +1 -0
  19. package/dist/tools/index.d.ts +4 -0
  20. package/dist/tools/index.d.ts.map +1 -0
  21. package/dist/tools/index.js +660 -0
  22. package/dist/tools/index.js.map +1 -0
  23. package/dist/tools/prompts.d.ts +3 -0
  24. package/dist/tools/prompts.d.ts.map +1 -0
  25. package/dist/tools/prompts.js +111 -0
  26. package/dist/tools/prompts.js.map +1 -0
  27. package/dist/tools/resources.d.ts +4 -0
  28. package/dist/tools/resources.d.ts.map +1 -0
  29. package/dist/tools/resources.js +138 -0
  30. package/dist/tools/resources.js.map +1 -0
  31. package/dist/utils/helpers.d.ts +29 -0
  32. package/dist/utils/helpers.d.ts.map +1 -0
  33. package/dist/utils/helpers.js +81 -0
  34. package/dist/utils/helpers.js.map +1 -0
  35. package/package.json +52 -0
  36. package/src/dashboard/generator.ts +628 -0
  37. package/src/index.ts +73 -0
  38. package/src/pricing/models.ts +246 -0
  39. package/src/storage/database.ts +525 -0
  40. package/src/tools/index.ts +780 -0
  41. package/src/tools/prompts.ts +124 -0
  42. package/src/tools/resources.ts +171 -0
  43. package/src/utils/helpers.ts +71 -0
  44. package/tsconfig.json +20 -0
@@ -0,0 +1,525 @@
1
+ import Database from 'better-sqlite3';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import fs from 'fs';
5
+
6
+ export interface UsageRecord {
7
+ id?: number;
8
+ timestamp: string;
9
+ provider: string;
10
+ model: string;
11
+ input_tokens: number;
12
+ output_tokens: number;
13
+ cached_input_tokens: number;
14
+ total_tokens: number;
15
+ input_cost: number;
16
+ output_cost: number;
17
+ cached_input_cost: number;
18
+ total_cost: number;
19
+ session_id: string;
20
+ project: string;
21
+ description: string;
22
+ metadata: string;
23
+ }
24
+
25
+ export interface BudgetRecord {
26
+ id?: number;
27
+ name: string;
28
+ limit_amount: number;
29
+ period: 'daily' | 'weekly' | 'monthly' | 'total';
30
+ provider_filter: string;
31
+ model_filter: string;
32
+ project_filter: string;
33
+ created_at: string;
34
+ }
35
+
36
+ export interface CustomPricingRecord {
37
+ id?: number;
38
+ provider: string;
39
+ model: string;
40
+ input_price_per_mtok: number;
41
+ output_price_per_mtok: number;
42
+ cached_input_price_per_mtok: number;
43
+ created_at: string;
44
+ }
45
+
46
+ export class CostDatabase {
47
+ private db: Database.Database;
48
+
49
+ constructor(dbPath?: string) {
50
+ const defaultDir = path.join(os.homedir(), '.mcp-cost-tracker');
51
+ if (!fs.existsSync(defaultDir)) {
52
+ fs.mkdirSync(defaultDir, { recursive: true });
53
+ }
54
+ const finalPath = dbPath || path.join(defaultDir, 'costs.db');
55
+ this.db = new Database(finalPath);
56
+ this.db.pragma('journal_mode = WAL');
57
+ this.db.pragma('foreign_keys = ON');
58
+ this.initialize();
59
+ }
60
+
61
+ private initialize(): void {
62
+ this.db.exec(`
63
+ CREATE TABLE IF NOT EXISTS usage_records (
64
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
65
+ timestamp TEXT NOT NULL DEFAULT (datetime('now')),
66
+ provider TEXT NOT NULL,
67
+ model TEXT NOT NULL,
68
+ input_tokens INTEGER NOT NULL DEFAULT 0,
69
+ output_tokens INTEGER NOT NULL DEFAULT 0,
70
+ cached_input_tokens INTEGER NOT NULL DEFAULT 0,
71
+ total_tokens INTEGER NOT NULL DEFAULT 0,
72
+ input_cost REAL NOT NULL DEFAULT 0,
73
+ output_cost REAL NOT NULL DEFAULT 0,
74
+ cached_input_cost REAL NOT NULL DEFAULT 0,
75
+ total_cost REAL NOT NULL DEFAULT 0,
76
+ session_id TEXT DEFAULT '',
77
+ project TEXT DEFAULT 'default',
78
+ description TEXT DEFAULT '',
79
+ metadata TEXT DEFAULT '{}'
80
+ );
81
+
82
+ CREATE TABLE IF NOT EXISTS budgets (
83
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
84
+ name TEXT NOT NULL UNIQUE,
85
+ limit_amount REAL NOT NULL,
86
+ period TEXT NOT NULL DEFAULT 'monthly',
87
+ provider_filter TEXT DEFAULT '',
88
+ model_filter TEXT DEFAULT '',
89
+ project_filter TEXT DEFAULT '',
90
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
91
+ );
92
+
93
+ CREATE TABLE IF NOT EXISTS custom_pricing (
94
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
95
+ provider TEXT NOT NULL,
96
+ model TEXT NOT NULL,
97
+ input_price_per_mtok REAL NOT NULL,
98
+ output_price_per_mtok REAL NOT NULL,
99
+ cached_input_price_per_mtok REAL DEFAULT 0,
100
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
101
+ UNIQUE(provider, model)
102
+ );
103
+
104
+ CREATE INDEX IF NOT EXISTS idx_usage_timestamp ON usage_records(timestamp);
105
+ CREATE INDEX IF NOT EXISTS idx_usage_provider ON usage_records(provider);
106
+ CREATE INDEX IF NOT EXISTS idx_usage_model ON usage_records(model);
107
+ CREATE INDEX IF NOT EXISTS idx_usage_project ON usage_records(project);
108
+ CREATE INDEX IF NOT EXISTS idx_usage_session ON usage_records(session_id);
109
+ `);
110
+ }
111
+
112
+ // ============================================================
113
+ // Usage Records
114
+ // ============================================================
115
+
116
+ recordUsage(record: Omit<UsageRecord, 'id'>): UsageRecord {
117
+ const stmt = this.db.prepare(`
118
+ INSERT INTO usage_records (
119
+ timestamp, provider, model, input_tokens, output_tokens,
120
+ cached_input_tokens, total_tokens, input_cost, output_cost,
121
+ cached_input_cost, total_cost, session_id, project, description, metadata
122
+ ) VALUES (
123
+ @timestamp, @provider, @model, @input_tokens, @output_tokens,
124
+ @cached_input_tokens, @total_tokens, @input_cost, @output_cost,
125
+ @cached_input_cost, @total_cost, @session_id, @project, @description, @metadata
126
+ )
127
+ `);
128
+ const result = stmt.run(record);
129
+ return { ...record, id: result.lastInsertRowid as number };
130
+ }
131
+
132
+ getUsageRecords(options: {
133
+ startDate?: string;
134
+ endDate?: string;
135
+ provider?: string;
136
+ model?: string;
137
+ project?: string;
138
+ sessionId?: string;
139
+ limit?: number;
140
+ offset?: number;
141
+ } = {}): UsageRecord[] {
142
+ let query = 'SELECT * FROM usage_records WHERE 1=1';
143
+ const params: Record<string, unknown> = {};
144
+
145
+ if (options.startDate) {
146
+ query += ' AND timestamp >= @startDate';
147
+ params.startDate = options.startDate;
148
+ }
149
+ if (options.endDate) {
150
+ query += ' AND timestamp <= @endDate';
151
+ params.endDate = options.endDate;
152
+ }
153
+ if (options.provider) {
154
+ query += ' AND provider = @provider';
155
+ params.provider = options.provider;
156
+ }
157
+ if (options.model) {
158
+ query += ' AND model = @model';
159
+ params.model = options.model;
160
+ }
161
+ if (options.project) {
162
+ query += ' AND project = @project';
163
+ params.project = options.project;
164
+ }
165
+ if (options.sessionId) {
166
+ query += ' AND session_id = @sessionId';
167
+ params.sessionId = options.sessionId;
168
+ }
169
+
170
+ query += ' ORDER BY timestamp DESC';
171
+
172
+ if (options.limit) {
173
+ query += ' LIMIT @limit';
174
+ params.limit = options.limit;
175
+ }
176
+ if (options.offset) {
177
+ query += ' OFFSET @offset';
178
+ params.offset = options.offset;
179
+ }
180
+
181
+ return this.db.prepare(query).all(params) as UsageRecord[];
182
+ }
183
+
184
+ // ============================================================
185
+ // Aggregation Queries
186
+ // ============================================================
187
+
188
+ getSummary(options: {
189
+ startDate?: string;
190
+ endDate?: string;
191
+ provider?: string;
192
+ project?: string;
193
+ } = {}): {
194
+ total_cost: number;
195
+ total_input_tokens: number;
196
+ total_output_tokens: number;
197
+ total_cached_tokens: number;
198
+ total_requests: number;
199
+ avg_cost_per_request: number;
200
+ } {
201
+ let query = `
202
+ SELECT
203
+ COALESCE(SUM(total_cost), 0) as total_cost,
204
+ COALESCE(SUM(input_tokens), 0) as total_input_tokens,
205
+ COALESCE(SUM(output_tokens), 0) as total_output_tokens,
206
+ COALESCE(SUM(cached_input_tokens), 0) as total_cached_tokens,
207
+ COUNT(*) as total_requests,
208
+ COALESCE(AVG(total_cost), 0) as avg_cost_per_request
209
+ FROM usage_records WHERE 1=1
210
+ `;
211
+ const params: Record<string, unknown> = {};
212
+
213
+ if (options.startDate) {
214
+ query += ' AND timestamp >= @startDate';
215
+ params.startDate = options.startDate;
216
+ }
217
+ if (options.endDate) {
218
+ query += ' AND timestamp <= @endDate';
219
+ params.endDate = options.endDate;
220
+ }
221
+ if (options.provider) {
222
+ query += ' AND provider = @provider';
223
+ params.provider = options.provider;
224
+ }
225
+ if (options.project) {
226
+ query += ' AND project = @project';
227
+ params.project = options.project;
228
+ }
229
+
230
+ return this.db.prepare(query).get(params) as any;
231
+ }
232
+
233
+ getCostByModel(options: {
234
+ startDate?: string;
235
+ endDate?: string;
236
+ project?: string;
237
+ } = {}): Array<{
238
+ provider: string;
239
+ model: string;
240
+ total_cost: number;
241
+ total_input_tokens: number;
242
+ total_output_tokens: number;
243
+ request_count: number;
244
+ }> {
245
+ let query = `
246
+ SELECT
247
+ provider,
248
+ model,
249
+ SUM(total_cost) as total_cost,
250
+ SUM(input_tokens) as total_input_tokens,
251
+ SUM(output_tokens) as total_output_tokens,
252
+ COUNT(*) as request_count
253
+ FROM usage_records WHERE 1=1
254
+ `;
255
+ const params: Record<string, unknown> = {};
256
+
257
+ if (options.startDate) {
258
+ query += ' AND timestamp >= @startDate';
259
+ params.startDate = options.startDate;
260
+ }
261
+ if (options.endDate) {
262
+ query += ' AND timestamp <= @endDate';
263
+ params.endDate = options.endDate;
264
+ }
265
+ if (options.project) {
266
+ query += ' AND project = @project';
267
+ params.project = options.project;
268
+ }
269
+
270
+ query += ' GROUP BY provider, model ORDER BY total_cost DESC';
271
+
272
+ return this.db.prepare(query).all(params) as any[];
273
+ }
274
+
275
+ getCostByProvider(options: {
276
+ startDate?: string;
277
+ endDate?: string;
278
+ } = {}): Array<{
279
+ provider: string;
280
+ total_cost: number;
281
+ total_input_tokens: number;
282
+ total_output_tokens: number;
283
+ request_count: number;
284
+ }> {
285
+ let query = `
286
+ SELECT
287
+ provider,
288
+ SUM(total_cost) as total_cost,
289
+ SUM(input_tokens) as total_input_tokens,
290
+ SUM(output_tokens) as total_output_tokens,
291
+ COUNT(*) as request_count
292
+ FROM usage_records WHERE 1=1
293
+ `;
294
+ const params: Record<string, unknown> = {};
295
+
296
+ if (options.startDate) {
297
+ query += ' AND timestamp >= @startDate';
298
+ params.startDate = options.startDate;
299
+ }
300
+ if (options.endDate) {
301
+ query += ' AND timestamp <= @endDate';
302
+ params.endDate = options.endDate;
303
+ }
304
+
305
+ query += ' GROUP BY provider ORDER BY total_cost DESC';
306
+
307
+ return this.db.prepare(query).all(params) as any[];
308
+ }
309
+
310
+ getCostByDay(options: {
311
+ startDate?: string;
312
+ endDate?: string;
313
+ provider?: string;
314
+ project?: string;
315
+ days?: number;
316
+ } = {}): Array<{
317
+ date: string;
318
+ total_cost: number;
319
+ total_tokens: number;
320
+ request_count: number;
321
+ }> {
322
+ const days = options.days || 30;
323
+ let query = `
324
+ SELECT
325
+ DATE(timestamp) as date,
326
+ SUM(total_cost) as total_cost,
327
+ SUM(total_tokens) as total_tokens,
328
+ COUNT(*) as request_count
329
+ FROM usage_records
330
+ WHERE timestamp >= datetime('now', '-${days} days')
331
+ `;
332
+ const params: Record<string, unknown> = {};
333
+
334
+ if (options.startDate) {
335
+ query = query.replace(
336
+ `timestamp >= datetime('now', '-${days} days')`,
337
+ 'timestamp >= @startDate'
338
+ );
339
+ params.startDate = options.startDate;
340
+ }
341
+ if (options.endDate) {
342
+ query += ' AND timestamp <= @endDate';
343
+ params.endDate = options.endDate;
344
+ }
345
+ if (options.provider) {
346
+ query += ' AND provider = @provider';
347
+ params.provider = options.provider;
348
+ }
349
+ if (options.project) {
350
+ query += ' AND project = @project';
351
+ params.project = options.project;
352
+ }
353
+
354
+ query += ' GROUP BY DATE(timestamp) ORDER BY date ASC';
355
+
356
+ return this.db.prepare(query).all(params) as any[];
357
+ }
358
+
359
+ getCostByProject(): Array<{
360
+ project: string;
361
+ total_cost: number;
362
+ total_tokens: number;
363
+ request_count: number;
364
+ }> {
365
+ return this.db.prepare(`
366
+ SELECT
367
+ project,
368
+ SUM(total_cost) as total_cost,
369
+ SUM(total_tokens) as total_tokens,
370
+ COUNT(*) as request_count
371
+ FROM usage_records
372
+ GROUP BY project
373
+ ORDER BY total_cost DESC
374
+ `).all() as any[];
375
+ }
376
+
377
+ getTopExpensiveRequests(limit: number = 10): UsageRecord[] {
378
+ return this.db.prepare(`
379
+ SELECT * FROM usage_records
380
+ ORDER BY total_cost DESC
381
+ LIMIT ?
382
+ `).all(limit) as UsageRecord[];
383
+ }
384
+
385
+ // ============================================================
386
+ // Budget Management
387
+ // ============================================================
388
+
389
+ setBudget(budget: Omit<BudgetRecord, 'id' | 'created_at'>): BudgetRecord {
390
+ const stmt = this.db.prepare(`
391
+ INSERT OR REPLACE INTO budgets (name, limit_amount, period, provider_filter, model_filter, project_filter)
392
+ VALUES (@name, @limit_amount, @period, @provider_filter, @model_filter, @project_filter)
393
+ `);
394
+ stmt.run(budget);
395
+ return this.db.prepare('SELECT * FROM budgets WHERE name = ?').get(budget.name) as BudgetRecord;
396
+ }
397
+
398
+ getBudgets(): BudgetRecord[] {
399
+ return this.db.prepare('SELECT * FROM budgets ORDER BY name').all() as BudgetRecord[];
400
+ }
401
+
402
+ deleteBudget(name: string): boolean {
403
+ const result = this.db.prepare('DELETE FROM budgets WHERE name = ?').run(name);
404
+ return result.changes > 0;
405
+ }
406
+
407
+ checkBudget(budgetName: string): {
408
+ budget: BudgetRecord;
409
+ spent: number;
410
+ remaining: number;
411
+ percentage: number;
412
+ exceeded: boolean;
413
+ } | null {
414
+ const budget = this.db.prepare('SELECT * FROM budgets WHERE name = ?').get(budgetName) as BudgetRecord | undefined;
415
+ if (!budget) return null;
416
+
417
+ let dateFilter = '';
418
+ switch (budget.period) {
419
+ case 'daily':
420
+ dateFilter = "AND timestamp >= datetime('now', 'start of day')";
421
+ break;
422
+ case 'weekly':
423
+ dateFilter = "AND timestamp >= datetime('now', '-7 days')";
424
+ break;
425
+ case 'monthly':
426
+ dateFilter = "AND timestamp >= datetime('now', 'start of month')";
427
+ break;
428
+ case 'total':
429
+ dateFilter = '';
430
+ break;
431
+ }
432
+
433
+ let providerFilter = '';
434
+ if (budget.provider_filter) {
435
+ providerFilter = ` AND provider = '${budget.provider_filter}'`;
436
+ }
437
+
438
+ let modelFilter = '';
439
+ if (budget.model_filter) {
440
+ modelFilter = ` AND model = '${budget.model_filter}'`;
441
+ }
442
+
443
+ let projectFilter = '';
444
+ if (budget.project_filter) {
445
+ projectFilter = ` AND project = '${budget.project_filter}'`;
446
+ }
447
+
448
+ const result = this.db.prepare(`
449
+ SELECT COALESCE(SUM(total_cost), 0) as spent
450
+ FROM usage_records
451
+ WHERE 1=1 ${dateFilter} ${providerFilter} ${modelFilter} ${projectFilter}
452
+ `).get() as { spent: number };
453
+
454
+ const spent = result.spent;
455
+ const remaining = Math.max(0, budget.limit_amount - spent);
456
+ const percentage = budget.limit_amount > 0 ? (spent / budget.limit_amount) * 100 : 0;
457
+
458
+ return {
459
+ budget,
460
+ spent,
461
+ remaining,
462
+ percentage,
463
+ exceeded: spent >= budget.limit_amount,
464
+ };
465
+ }
466
+
467
+ // ============================================================
468
+ // Custom Pricing
469
+ // ============================================================
470
+
471
+ setCustomPricing(record: Omit<CustomPricingRecord, 'id' | 'created_at'>): CustomPricingRecord {
472
+ const stmt = this.db.prepare(`
473
+ INSERT OR REPLACE INTO custom_pricing (provider, model, input_price_per_mtok, output_price_per_mtok, cached_input_price_per_mtok)
474
+ VALUES (@provider, @model, @input_price_per_mtok, @output_price_per_mtok, @cached_input_price_per_mtok)
475
+ `);
476
+ stmt.run(record);
477
+ return this.db.prepare('SELECT * FROM custom_pricing WHERE provider = ? AND model = ?')
478
+ .get(record.provider, record.model) as CustomPricingRecord;
479
+ }
480
+
481
+ getCustomPricing(): CustomPricingRecord[] {
482
+ return this.db.prepare('SELECT * FROM custom_pricing ORDER BY provider, model').all() as CustomPricingRecord[];
483
+ }
484
+
485
+ deleteCustomPricing(provider: string, model: string): boolean {
486
+ const result = this.db.prepare('DELETE FROM custom_pricing WHERE provider = ? AND model = ?').run(provider, model);
487
+ return result.changes > 0;
488
+ }
489
+
490
+ // ============================================================
491
+ // Utility
492
+ // ============================================================
493
+
494
+ clearAllData(): void {
495
+ this.db.exec('DELETE FROM usage_records');
496
+ }
497
+
498
+ getStats(): {
499
+ total_records: number;
500
+ first_record: string | null;
501
+ last_record: string | null;
502
+ db_size_bytes: number;
503
+ } {
504
+ const count = this.db.prepare('SELECT COUNT(*) as count FROM usage_records').get() as { count: number };
505
+ const first = this.db.prepare('SELECT MIN(timestamp) as ts FROM usage_records').get() as { ts: string | null };
506
+ const last = this.db.prepare('SELECT MAX(timestamp) as ts FROM usage_records').get() as { ts: string | null };
507
+
508
+ const dbPath = this.db.name;
509
+ let dbSize = 0;
510
+ try {
511
+ dbSize = fs.statSync(dbPath).size;
512
+ } catch { /* ignore */ }
513
+
514
+ return {
515
+ total_records: count.count,
516
+ first_record: first.ts,
517
+ last_record: last.ts,
518
+ db_size_bytes: dbSize,
519
+ };
520
+ }
521
+
522
+ close(): void {
523
+ this.db.close();
524
+ }
525
+ }