mcp-cost-tracker-router 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.
- package/.env.example +27 -0
- package/CHANGELOG.md +68 -0
- package/LICENSE +21 -0
- package/README.md +239 -0
- package/dist/alerting.d.ts +5 -0
- package/dist/alerting.js +62 -0
- package/dist/audit-log.d.ts +16 -0
- package/dist/audit-log.js +32 -0
- package/dist/auth.d.ts +2 -0
- package/dist/auth.js +56 -0
- package/dist/chargeback.d.ts +17 -0
- package/dist/chargeback.js +103 -0
- package/dist/db.d.ts +64 -0
- package/dist/db.js +159 -0
- package/dist/http-server.d.ts +2 -0
- package/dist/http-server.js +33 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +63 -0
- package/dist/pricing.d.ts +7 -0
- package/dist/pricing.js +35 -0
- package/dist/project-allocator.d.ts +14 -0
- package/dist/project-allocator.js +70 -0
- package/dist/rate-limiter.d.ts +2 -0
- package/dist/rate-limiter.js +24 -0
- package/dist/routing-enforcer.d.ts +15 -0
- package/dist/routing-enforcer.js +43 -0
- package/dist/server.d.ts +16 -0
- package/dist/server.js +754 -0
- package/dist/tokenizer.d.ts +2 -0
- package/dist/tokenizer.js +29 -0
- package/dist/tools/budget.d.ts +8 -0
- package/dist/tools/budget.js +21 -0
- package/dist/tools/estimation.d.ts +26 -0
- package/dist/tools/estimation.js +27 -0
- package/dist/tools/history.d.ts +10 -0
- package/dist/tools/history.js +37 -0
- package/dist/tools/html_report.d.ts +3 -0
- package/dist/tools/html_report.js +255 -0
- package/dist/tools/routing.d.ts +23 -0
- package/dist/tools/routing.js +165 -0
- package/dist/tools/session.d.ts +19 -0
- package/dist/tools/session.js +84 -0
- package/package.json +67 -0
package/.env.example
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# ─── mcp-cost-tracker-router environment variables ───────────────────────────
|
|
2
|
+
#
|
|
3
|
+
# Copy this file to .env and fill in values for your environment.
|
|
4
|
+
# None of these variables are required; the server runs without them.
|
|
5
|
+
# They are only needed when running in HTTP mode (--http-port).
|
|
6
|
+
|
|
7
|
+
# ─── Authentication (HTTP mode only) ─────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
# Static API key for X-API-Key header authentication.
|
|
10
|
+
# When set, every HTTP request must include the header:
|
|
11
|
+
# X-API-Key: <value>
|
|
12
|
+
# Leave unset to disable API key authentication.
|
|
13
|
+
MCP_API_KEY=your-api-key-here
|
|
14
|
+
|
|
15
|
+
# HMAC-SHA256 secret used to verify HS256 JWT Bearer tokens.
|
|
16
|
+
# When set, every HTTP request must include the header:
|
|
17
|
+
# Authorization: Bearer <jwt>
|
|
18
|
+
# Leave unset to disable JWT authentication.
|
|
19
|
+
MCP_JWT_SECRET=your-jwt-secret-here
|
|
20
|
+
|
|
21
|
+
# ─── Alerting ─────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
# Slack incoming webhook URL for budget alert notifications.
|
|
24
|
+
# When set, a Slack message is posted whenever a session reaches 80% or 100%
|
|
25
|
+
# of its configured budget threshold.
|
|
26
|
+
# Leave unset to disable Slack notifications.
|
|
27
|
+
MCP_SLACK_WEBHOOK=your-slack-webhook-url-here
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to MCP Cost Tracker & Router will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [1.0.0] - 2026-03-23
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `gemini-2.0-flash` added to the built-in pricing table (`input: $0.0001`, `output: $0.0004` per 1K tokens).
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Token counting migrated from `tiktoken-lite` to `js-tiktoken` (`cl100k_base` encoding) for offline, dependency-free token estimates. Falls back to `char/4` approximation on encoding failure.
|
|
19
|
+
- `@modelcontextprotocol/sdk` upgraded from `^1.0.0` to `^1.12.0`.
|
|
20
|
+
- `@types/node` upgraded from `^20.x` to `^24.12.0` (Node 24 LTS).
|
|
21
|
+
- `eslint` upgraded from `^9.x` to `^10.0.3`; `eslint-config-prettier` from `^9.x` to `^10.1.8`.
|
|
22
|
+
- `yargs` upgraded from `^17.x` to `^18.0.0`.
|
|
23
|
+
- `@types/express` moved from `dependencies` to `devDependencies` — it is a type-only package with no runtime value.
|
|
24
|
+
- Added `author`, `license`, `repository`, `homepage`, and `engines` (`>=20.19.0`) fields to `package.json`.
|
|
25
|
+
- Added `prepublishOnly: npm run build` to ensure a fresh build before every `npm publish`.
|
|
26
|
+
- Added `.env.example` documenting `MCP_API_KEY`, `MCP_JWT_SECRET`, and `MCP_SLACK_WEBHOOK`.
|
|
27
|
+
- Notifications in `src/server.ts` wrapped in `tryNotify` helper to swallow unsupported-transport errors gracefully.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- Removed unused `base64urlDecode` function from `src/auth.ts`.
|
|
32
|
+
- Converted `require()` import in `tests/alerting.test.ts` to ESM `import` to comply with `no-require-imports` lint rule.
|
|
33
|
+
- Replaced untyped `Function` type with explicit call signatures in `tests/server-full.test.ts`.
|
|
34
|
+
|
|
35
|
+
### Security
|
|
36
|
+
|
|
37
|
+
- Resolved **GHSA-67mh-4wv8-2f99** (`esbuild` ≤ 0.24.2 dev-server cross-origin exposure) by upgrading `vitest` and `@vitest/coverage-v8` to `^4.1.0`. Affects local development only; not a production runtime concern.
|
|
38
|
+
|
|
39
|
+
## [0.2.0] - 2026-03-12
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- **Alerting** (`src/alerting.ts`): configurable budget alerts delivered to Slack webhooks.
|
|
44
|
+
- **Audit log** (`src/audit-log.ts`): append-only JSONL audit trail of every cost-tracking tool call.
|
|
45
|
+
- **JWT / API-key auth middleware** (`src/auth.ts`): HTTP transport protected via `MCP_API_KEY` or `MCP_JWT_SECRET`. stdio is unaffected.
|
|
46
|
+
- **Per-client rate limiter** (`src/rate-limiter.ts`): sliding-window request throttle on the HTTP transport.
|
|
47
|
+
- **Chargeback allocation** (`src/chargeback.ts`): distribute session cost across configured projects by token share.
|
|
48
|
+
- **Project allocator** (`src/project-allocator.ts`): multi-project cost reporting and allocation tracking.
|
|
49
|
+
- **Routing enforcer** (`src/routing-enforcer.ts`): YAML-based routing rules to block, warn on, or allow model selections.
|
|
50
|
+
- **New tools**: `allocate_session_cost`, `get_project_report`, `enforce_routing`.
|
|
51
|
+
- **`npm run inspect` script**: launches MCP Inspector for interactive pre-publish verification.
|
|
52
|
+
- MCP Inspector verification instructions added to README.
|
|
53
|
+
- `js-yaml` moved to `dependencies` (used at runtime by the routing enforcer).
|
|
54
|
+
- Tests for alerting, audit log, auth, chargeback, project allocator, rate limiter, and routing enforcer.
|
|
55
|
+
|
|
56
|
+
## [0.1.0] - 2026-03-12
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
|
|
60
|
+
- Initial public release of `mcp-cost-tracker-router`.
|
|
61
|
+
- Per-tool-call and per-session token metering.
|
|
62
|
+
- Persistent spend history stored locally in SQLite.
|
|
63
|
+
- Model routing suggestions based on configurable cost thresholds.
|
|
64
|
+
- Built-in pricing table covering major model providers.
|
|
65
|
+
- Session budget alerts with configurable warning and hard-stop thresholds.
|
|
66
|
+
- Streamable HTTP transport via `--http-port` flag (default: disabled, uses stdio).
|
|
67
|
+
- GitHub Actions CI workflow running build, test, and lint on push/PR to `main`.
|
|
68
|
+
- Vitest test suite with coverage via `@vitest/coverage-v8`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MCP Cost Tracker & Router contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# MCP Cost Tracker & Router
|
|
2
|
+
|
|
3
|
+
npm `mcp-cost-tracker-router` package
|
|
4
|
+
|
|
5
|
+
Local-first cost awareness for MCP agent workflows. Token counts are calculated offline using js-tiktoken — no proxy, no API round-trip, no spend data leaving your machine. When costs climb, routing suggestions point you to cheaper models before the invoice arrives.
|
|
6
|
+
|
|
7
|
+
[Tool reference](#tools) | [Configuration](#configuration) | [Contributing](#contributing) | [Troubleshooting](#troubleshooting)
|
|
8
|
+
|
|
9
|
+
## Key features
|
|
10
|
+
|
|
11
|
+
- **Per-tool cost breakdown**: See exactly which tool calls are consuming the most tokens and budget.
|
|
12
|
+
- **Budget alerts**: Set a session spend threshold and get warned at 80% and 100% before you exceed it.
|
|
13
|
+
- **Offline token counting**: Uses js-tiktoken for accurate counts — no API calls required.
|
|
14
|
+
- **Model routing suggestions**: Recommends cheaper models for the current task type (advisory, never enforced without opt-in).
|
|
15
|
+
- **Multi-provider pricing**: Tracks costs across Claude, OpenAI, and Gemini models from a single configurable pricing table.
|
|
16
|
+
- **Spend history**: Query daily, weekly, and monthly totals by model or tool.
|
|
17
|
+
- **Project cost allocation**: Tag sessions to named projects and generate chargeback reports.
|
|
18
|
+
- **HTML spend reports**: Export a single-file, self-contained HTML report with charts and budget status.
|
|
19
|
+
- **Audit log**: Append-only log of every budget enforcement decision.
|
|
20
|
+
|
|
21
|
+
## Why this over proxy-based cost trackers?
|
|
22
|
+
|
|
23
|
+
Most cost-tracking tools work by routing all your API traffic through their server and measuring tokens server-side. That means your prompts and responses transit a third-party service, and you're dependent on their uptime.
|
|
24
|
+
|
|
25
|
+
| | mcp-cost-tracker-router | Proxy-based trackers (Helicone, LLMonitor, etc.) |
|
|
26
|
+
| ----------------- | ------------------------------------------- | ------------------------------------------------ |
|
|
27
|
+
| Token counting | Offline via js-tiktoken — no network call | Counted server-side after traffic is proxied |
|
|
28
|
+
| Data residency | Local SQLite only | Prompts + responses pass through vendor servers |
|
|
29
|
+
| Model routing | Built-in `suggest_model_routing` tool | Rarely included; usually a separate paid tier |
|
|
30
|
+
| Multi-provider | Claude, OpenAI, Gemini in one pricing table | Often single-provider or requires separate setup |
|
|
31
|
+
| Uptime dependency | None — fully offline | Breaks if proxy is down |
|
|
32
|
+
|
|
33
|
+
If your prompts contain sensitive information or you can't route traffic through a third party, this is the right tool. If you need a managed dashboard with team sharing, a proxy-based service may suit you better.
|
|
34
|
+
|
|
35
|
+
## Disclaimers
|
|
36
|
+
|
|
37
|
+
`mcp-cost-tracker-router` stores tool call metadata (token counts, model names, timestamps) locally in SQLite. It does not store prompt or response content. Cost calculations are estimates based on a local pricing table and may not exactly match your provider's invoice.
|
|
38
|
+
|
|
39
|
+
## Requirements
|
|
40
|
+
|
|
41
|
+
- Node.js v20.19 or newer.
|
|
42
|
+
- npm.
|
|
43
|
+
|
|
44
|
+
## Getting started
|
|
45
|
+
|
|
46
|
+
Add the following config to your MCP client:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"mcpServers": {
|
|
51
|
+
"cost-tracker": {
|
|
52
|
+
"command": "npx",
|
|
53
|
+
"args": ["-y", "mcp-cost-tracker-router@latest"]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
To set a session budget alert:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"mcpServers": {
|
|
64
|
+
"cost-tracker": {
|
|
65
|
+
"command": "npx",
|
|
66
|
+
"args": ["-y", "mcp-cost-tracker-router@latest", "--budget-alert=5.00"]
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### MCP Client configuration
|
|
73
|
+
|
|
74
|
+
Amp · Claude Code · Cline · Cursor · VS Code · Windsurf · Zed
|
|
75
|
+
|
|
76
|
+
## Your first prompt
|
|
77
|
+
|
|
78
|
+
Enter the following in your MCP client to verify everything is working:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
How much has this session cost so far?
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Your client should return a token and USD cost summary for the current session.
|
|
85
|
+
|
|
86
|
+
## Tools
|
|
87
|
+
|
|
88
|
+
### Session (4 tools)
|
|
89
|
+
|
|
90
|
+
- `get_session_cost` — Returns token totals and USD cost estimates for the current session. Read-only.
|
|
91
|
+
- `get_tool_costs` — Returns per-tool cost breakdown for the session, sorted by cost descending. Read-only.
|
|
92
|
+
- `reset_session` — Start a new cost-tracking session. Previous session data is retained in history.
|
|
93
|
+
- `record_usage` — Record token usage for a tool call. Takes `tool_name`, `model` (optional), `input_tokens`, and `output_tokens`. Emits a budget warning notification if 80% of threshold is reached.
|
|
94
|
+
|
|
95
|
+
### Budgets & routing (3 tools)
|
|
96
|
+
|
|
97
|
+
- `set_budget_alert` — Set a budget threshold in USD (`threshold_usd`). Warns at 80% and 100% of the threshold. Use with `--enforce-budget` to block calls beyond the limit.
|
|
98
|
+
- `suggest_model_routing` — Heuristic model recommendation by task type. Takes `task_description` and optional `constraints.max_cost_usd`. Returns recommended model with reasoning and estimated cost.
|
|
99
|
+
- `check_routing_policy` — Check whether a model is allowed for a given task type under the routing policy. Takes `task_type` and `model`.
|
|
100
|
+
|
|
101
|
+
### History & reports (4 tools)
|
|
102
|
+
|
|
103
|
+
- `get_spend_history` — Query historical spend aggregated by `period` (`day`/`week`/`month`). Returns breakdown by model and tool. Read-only.
|
|
104
|
+
- `estimate_workflow_cost` — Pre-run cost estimation for a multi-step workflow. Takes a `steps` array with `tool_name`, `estimated_input_tokens`, `estimated_output_tokens`, and optional `model`. Read-only.
|
|
105
|
+
- `export_spend_report` — Generate a single-file HTML spend report with session breakdown, historical spend, model cost comparison, and budget status. Read-only.
|
|
106
|
+
- `export_budget_audit` — Export the audit log of budget enforcement decisions. Accepts optional `from_date`, `to_date`, and `format` (`json`/`csv`). Read-only.
|
|
107
|
+
|
|
108
|
+
### Project allocation (4 tools)
|
|
109
|
+
|
|
110
|
+
- `set_project` — Create or update a project with an optional `budget_usd`. Takes `project_name`.
|
|
111
|
+
- `tag_session` — Tag the current session with a `project_name` for cost allocation.
|
|
112
|
+
- `get_project_costs` — Get cost report for a project. Takes `project_name` and optional `since` (ISO date). Read-only.
|
|
113
|
+
- `export_chargeback` — Generate a chargeback report for internal billing. Takes `from_date`, `to_date`, optional `group_by` (`project`/`session`), and optional `format` (`json`/`csv`). Read-only.
|
|
114
|
+
|
|
115
|
+
## Configuration
|
|
116
|
+
|
|
117
|
+
### `--budget-alert`
|
|
118
|
+
|
|
119
|
+
Session spend threshold in USD. A warning is returned when session costs reach 80% and again at 100% of this threshold.
|
|
120
|
+
|
|
121
|
+
Type: `number`
|
|
122
|
+
|
|
123
|
+
### `--db` / `--db-path`
|
|
124
|
+
|
|
125
|
+
Path to the SQLite database file used to store cost history.
|
|
126
|
+
|
|
127
|
+
Type: `string`
|
|
128
|
+
Default: `~/.mcp/costs.db`
|
|
129
|
+
|
|
130
|
+
### `--pricing-table`
|
|
131
|
+
|
|
132
|
+
Path to a JSON file containing custom model pricing ($/1K tokens). Merged with the built-in table; missing models fall back to defaults.
|
|
133
|
+
|
|
134
|
+
Type: `string`
|
|
135
|
+
|
|
136
|
+
### `--default-model`
|
|
137
|
+
|
|
138
|
+
Model name to attribute costs to when no model can be inferred from context.
|
|
139
|
+
|
|
140
|
+
Type: `string`
|
|
141
|
+
Default: `claude-sonnet-4-6`
|
|
142
|
+
|
|
143
|
+
### `--enforce-budget`
|
|
144
|
+
|
|
145
|
+
Block tool calls that would cause the session to exceed the budget alert threshold. Requires `--budget-alert` to be set.
|
|
146
|
+
|
|
147
|
+
Type: `boolean`
|
|
148
|
+
Default: `false`
|
|
149
|
+
|
|
150
|
+
### `--http-port`
|
|
151
|
+
|
|
152
|
+
Start in HTTP mode using Streamable HTTP transport instead of stdio. Useful for sharing a single cost-tracking instance across a team.
|
|
153
|
+
|
|
154
|
+
Type: `number`
|
|
155
|
+
Default: disabled (uses stdio)
|
|
156
|
+
|
|
157
|
+
Pass flags via the `args` property in your JSON config:
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"mcpServers": {
|
|
162
|
+
"cost-tracker": {
|
|
163
|
+
"command": "npx",
|
|
164
|
+
"args": [
|
|
165
|
+
"-y",
|
|
166
|
+
"mcp-cost-tracker-router@latest",
|
|
167
|
+
"--budget-alert=2.00",
|
|
168
|
+
"--enforce-budget"
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Supported models and pricing
|
|
176
|
+
|
|
177
|
+
Built-in pricing table (USD per 1K tokens):
|
|
178
|
+
|
|
179
|
+
| Model | Input | Output |
|
|
180
|
+
| ----------------- | --------- | --------- |
|
|
181
|
+
| claude-opus-4-6 | $0.0150 | $0.0750 |
|
|
182
|
+
| claude-sonnet-4-6 | $0.0030 | $0.0150 |
|
|
183
|
+
| claude-haiku-4-5 | $0.0008 | $0.0040 |
|
|
184
|
+
| gpt-4o | $0.0025 | $0.0100 |
|
|
185
|
+
| gpt-4o-mini | $0.000150 | $0.000600 |
|
|
186
|
+
| gemini-1.5-pro | $0.001250 | $0.005000 |
|
|
187
|
+
| gemini-1.5-flash | $0.000075 | $0.000300 |
|
|
188
|
+
| gemini-2.0-flash | $0.000100 | $0.000400 |
|
|
189
|
+
|
|
190
|
+
Override individual model prices with `--pricing-table`. All costs are estimates.
|
|
191
|
+
|
|
192
|
+
## Verification
|
|
193
|
+
|
|
194
|
+
Before publishing a new version, verify the server with MCP Inspector to confirm all tools are exposed correctly and the protocol handshake succeeds.
|
|
195
|
+
|
|
196
|
+
**Interactive UI** (opens browser):
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
npm run build && npm run inspect
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**CLI mode** (scripted / CI-friendly):
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
# List all tools
|
|
206
|
+
npx @modelcontextprotocol/inspector --cli node dist/index.js --method tools/list
|
|
207
|
+
|
|
208
|
+
# List resources and prompts
|
|
209
|
+
npx @modelcontextprotocol/inspector --cli node dist/index.js --method resources/list
|
|
210
|
+
npx @modelcontextprotocol/inspector --cli node dist/index.js --method prompts/list
|
|
211
|
+
|
|
212
|
+
# Call a read-only tool
|
|
213
|
+
npx @modelcontextprotocol/inspector --cli node dist/index.js \
|
|
214
|
+
--method tools/call --tool-name get_session_cost
|
|
215
|
+
|
|
216
|
+
# Call record_usage with arguments
|
|
217
|
+
npx @modelcontextprotocol/inspector --cli node dist/index.js \
|
|
218
|
+
--method tools/call --tool-name record_usage \
|
|
219
|
+
--tool-arg tool_name=my_tool --tool-arg input_tokens=500 --tool-arg output_tokens=200
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Run before publishing to catch regressions in tool registration and runtime startup.
|
|
223
|
+
|
|
224
|
+
## Contributing
|
|
225
|
+
|
|
226
|
+
Update `src/pricing.ts` when new models are released. All cost calculation changes must include unit tests with known token counts and expected USD values. Routing suggestions live in `src/tools/routing.ts`.
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
npm install && npm test
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## MCP Registry & Marketplace
|
|
233
|
+
|
|
234
|
+
This plugin is available on:
|
|
235
|
+
|
|
236
|
+
- [MCP Registry](https://registry.modelcontextprotocol.io)
|
|
237
|
+
- [MCP Marketplace](https://marketplace.modelcontextprotocol.io)
|
|
238
|
+
|
|
239
|
+
Search for `mcp-cost-tracker-router`.
|
package/dist/alerting.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { request as httpsRequest } from "https";
|
|
2
|
+
function postSlackMessage(webhookUrl, text) {
|
|
3
|
+
return new Promise((resolve, reject) => {
|
|
4
|
+
const body = JSON.stringify({
|
|
5
|
+
blocks: [
|
|
6
|
+
{
|
|
7
|
+
type: "section",
|
|
8
|
+
text: { type: "mrkdwn", text },
|
|
9
|
+
},
|
|
10
|
+
],
|
|
11
|
+
});
|
|
12
|
+
const url = new URL(webhookUrl);
|
|
13
|
+
const options = {
|
|
14
|
+
hostname: url.hostname,
|
|
15
|
+
path: url.pathname + url.search,
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: {
|
|
18
|
+
"Content-Type": "application/json",
|
|
19
|
+
"Content-Length": Buffer.byteLength(body),
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
const req = httpsRequest(options, (res) => {
|
|
23
|
+
res.resume();
|
|
24
|
+
res.on("end", () => resolve());
|
|
25
|
+
});
|
|
26
|
+
req.on("error", reject);
|
|
27
|
+
req.write(body);
|
|
28
|
+
req.end();
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export async function checkBudgetAlerts(db, channels) {
|
|
32
|
+
if (!channels.slackWebhook)
|
|
33
|
+
return;
|
|
34
|
+
// Find sessions with budget thresholds set
|
|
35
|
+
const rows = db
|
|
36
|
+
.prepare(`
|
|
37
|
+
SELECT
|
|
38
|
+
s.id,
|
|
39
|
+
s.budget_threshold_usd,
|
|
40
|
+
COALESCE(SUM(tu.cost_usd), 0) AS total_cost_usd
|
|
41
|
+
FROM sessions s
|
|
42
|
+
LEFT JOIN tool_usage tu ON tu.session_id = s.id
|
|
43
|
+
WHERE s.budget_threshold_usd IS NOT NULL
|
|
44
|
+
GROUP BY s.id, s.budget_threshold_usd
|
|
45
|
+
`)
|
|
46
|
+
.all();
|
|
47
|
+
const alerts = [];
|
|
48
|
+
for (const row of rows) {
|
|
49
|
+
if (row.budget_threshold_usd === null)
|
|
50
|
+
continue;
|
|
51
|
+
const pct = (row.total_cost_usd / row.budget_threshold_usd) * 100;
|
|
52
|
+
if (pct >= 80 && pct < 100) {
|
|
53
|
+
alerts.push(`:warning: *Budget Alert* — Session \`${row.id}\` is at *${pct.toFixed(1)}%* of its $${row.budget_threshold_usd.toFixed(4)} budget ($${row.total_cost_usd.toFixed(6)} spent).`);
|
|
54
|
+
}
|
|
55
|
+
else if (pct >= 100) {
|
|
56
|
+
alerts.push(`:red_circle: *Budget Exceeded* — Session \`${row.id}\` has exceeded its $${row.budget_threshold_usd.toFixed(4)} budget ($${row.total_cost_usd.toFixed(6)} spent, ${pct.toFixed(1)}%).`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
for (const alertText of alerts) {
|
|
60
|
+
await postSlackMessage(channels.slackWebhook, alertText);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface AuditEntry {
|
|
2
|
+
timestamp: string;
|
|
3
|
+
session_id: string;
|
|
4
|
+
tool_name: string;
|
|
5
|
+
tokens: number;
|
|
6
|
+
cost_usd: number;
|
|
7
|
+
budget_usd: number | null;
|
|
8
|
+
decision: "allowed" | "blocked";
|
|
9
|
+
reason: string;
|
|
10
|
+
}
|
|
11
|
+
export declare class AuditLog {
|
|
12
|
+
private readonly filePath;
|
|
13
|
+
constructor(filePath?: string);
|
|
14
|
+
record(entry: AuditEntry): void;
|
|
15
|
+
export(from?: string, to?: string): AuditEntry[];
|
|
16
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { appendFileSync, readFileSync, mkdirSync, existsSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { dirname } from "path";
|
|
4
|
+
export class AuditLog {
|
|
5
|
+
filePath;
|
|
6
|
+
constructor(filePath) {
|
|
7
|
+
this.filePath = filePath ?? `${homedir()}/.mcp/cost-tracker-audit.jsonl`;
|
|
8
|
+
const dir = dirname(this.filePath);
|
|
9
|
+
if (!existsSync(dir)) {
|
|
10
|
+
mkdirSync(dir, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
record(entry) {
|
|
14
|
+
const line = JSON.stringify(entry) + "\n";
|
|
15
|
+
appendFileSync(this.filePath, line, "utf-8");
|
|
16
|
+
}
|
|
17
|
+
export(from, to) {
|
|
18
|
+
if (!existsSync(this.filePath)) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
const raw = readFileSync(this.filePath, "utf-8");
|
|
22
|
+
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
23
|
+
const entries = lines.map((l) => JSON.parse(l));
|
|
24
|
+
return entries.filter((e) => {
|
|
25
|
+
if (from && e.timestamp < from)
|
|
26
|
+
return false;
|
|
27
|
+
if (to && e.timestamp > to)
|
|
28
|
+
return false;
|
|
29
|
+
return true;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
package/dist/auth.d.ts
ADDED
package/dist/auth.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createHmac } from "crypto";
|
|
2
|
+
function verifyJwt(token, secret) {
|
|
3
|
+
const parts = token.split(".");
|
|
4
|
+
if (parts.length !== 3)
|
|
5
|
+
return false;
|
|
6
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
7
|
+
const signingInput = `${headerB64}.${payloadB64}`;
|
|
8
|
+
const expectedSig = createHmac("sha256", secret)
|
|
9
|
+
.update(signingInput)
|
|
10
|
+
.digest("base64url");
|
|
11
|
+
// Constant-time comparison
|
|
12
|
+
const expectedBuf = Buffer.from(expectedSig);
|
|
13
|
+
const actualBuf = Buffer.from(signatureB64 ?? "");
|
|
14
|
+
if (expectedBuf.length !== actualBuf.length)
|
|
15
|
+
return false;
|
|
16
|
+
let diff = 0;
|
|
17
|
+
for (let i = 0; i < expectedBuf.length; i++) {
|
|
18
|
+
diff |= (expectedBuf[i] ?? 0) ^ (actualBuf[i] ?? 0);
|
|
19
|
+
}
|
|
20
|
+
return diff === 0;
|
|
21
|
+
}
|
|
22
|
+
export function createAuthMiddleware() {
|
|
23
|
+
return (req, res, next) => {
|
|
24
|
+
const apiKeyEnv = process.env["MCP_API_KEY"];
|
|
25
|
+
const jwtSecretEnv = process.env["MCP_JWT_SECRET"];
|
|
26
|
+
// If neither env var is set, pass through
|
|
27
|
+
if (!apiKeyEnv && !jwtSecretEnv) {
|
|
28
|
+
next();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
// Validate X-API-Key if env var is set
|
|
32
|
+
if (apiKeyEnv) {
|
|
33
|
+
const providedKey = req.headers["x-api-key"];
|
|
34
|
+
if (!providedKey || providedKey !== apiKeyEnv) {
|
|
35
|
+
res
|
|
36
|
+
.status(401)
|
|
37
|
+
.json({ error: "Unauthorized: invalid or missing API key" });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Validate Authorization: Bearer JWT if env var is set
|
|
42
|
+
if (jwtSecretEnv) {
|
|
43
|
+
const authHeader = req.headers["authorization"];
|
|
44
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
45
|
+
res.status(401).json({ error: "Unauthorized: missing Bearer token" });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const token = authHeader.slice("Bearer ".length).trim();
|
|
49
|
+
if (!verifyJwt(token, jwtSecretEnv)) {
|
|
50
|
+
res.status(401).json({ error: "Unauthorized: invalid JWT signature" });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
next();
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
export interface ToolBreakdown {
|
|
3
|
+
tool: string;
|
|
4
|
+
cost: number;
|
|
5
|
+
}
|
|
6
|
+
export interface ChargebackGroup {
|
|
7
|
+
name: string;
|
|
8
|
+
cost_usd: number;
|
|
9
|
+
sessions: number;
|
|
10
|
+
tool_breakdown: ToolBreakdown[];
|
|
11
|
+
}
|
|
12
|
+
export interface ChargebackReport {
|
|
13
|
+
period: string;
|
|
14
|
+
groups: ChargebackGroup[];
|
|
15
|
+
}
|
|
16
|
+
export declare function generateChargebackReport(db: Database.Database, from: string, to: string, groupBy: "project" | "session"): ChargebackReport;
|
|
17
|
+
export declare function chargebackToCSV(report: ChargebackReport): string;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export function generateChargebackReport(db, from, to, groupBy) {
|
|
2
|
+
const fromTs = new Date(from).getTime();
|
|
3
|
+
const toTs = new Date(to).getTime();
|
|
4
|
+
const period = `${from} to ${to}`;
|
|
5
|
+
if (groupBy === "project") {
|
|
6
|
+
const projectNames = db
|
|
7
|
+
.prepare(`
|
|
8
|
+
SELECT DISTINCT sp.project_name
|
|
9
|
+
FROM session_projects sp
|
|
10
|
+
INNER JOIN tool_usage tu ON tu.session_id = sp.session_id
|
|
11
|
+
WHERE tu.recorded_at >= ? AND tu.recorded_at <= ?
|
|
12
|
+
`)
|
|
13
|
+
.all(fromTs, toTs);
|
|
14
|
+
const groups = projectNames.map(({ project_name }) => {
|
|
15
|
+
const totRow = db
|
|
16
|
+
.prepare(`
|
|
17
|
+
SELECT
|
|
18
|
+
COALESCE(SUM(tu.cost_usd), 0) AS cost_usd,
|
|
19
|
+
COUNT(DISTINCT tu.session_id) AS sessions
|
|
20
|
+
FROM tool_usage tu
|
|
21
|
+
INNER JOIN session_projects sp ON sp.session_id = tu.session_id
|
|
22
|
+
WHERE sp.project_name = ?
|
|
23
|
+
AND tu.recorded_at >= ? AND tu.recorded_at <= ?
|
|
24
|
+
`)
|
|
25
|
+
.get(project_name, fromTs, toTs);
|
|
26
|
+
const toolRows = db
|
|
27
|
+
.prepare(`
|
|
28
|
+
SELECT
|
|
29
|
+
tu.tool_name AS tool,
|
|
30
|
+
SUM(tu.cost_usd) AS cost
|
|
31
|
+
FROM tool_usage tu
|
|
32
|
+
INNER JOIN session_projects sp ON sp.session_id = tu.session_id
|
|
33
|
+
WHERE sp.project_name = ?
|
|
34
|
+
AND tu.recorded_at >= ? AND tu.recorded_at <= ?
|
|
35
|
+
GROUP BY tu.tool_name
|
|
36
|
+
ORDER BY cost DESC
|
|
37
|
+
`)
|
|
38
|
+
.all(project_name, fromTs, toTs);
|
|
39
|
+
return {
|
|
40
|
+
name: project_name,
|
|
41
|
+
cost_usd: totRow.cost_usd,
|
|
42
|
+
sessions: totRow.sessions,
|
|
43
|
+
tool_breakdown: toolRows,
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
return { period, groups };
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
// groupBy === "session"
|
|
50
|
+
const sessionRows = db
|
|
51
|
+
.prepare(`
|
|
52
|
+
SELECT DISTINCT session_id
|
|
53
|
+
FROM tool_usage
|
|
54
|
+
WHERE recorded_at >= ? AND recorded_at <= ?
|
|
55
|
+
`)
|
|
56
|
+
.all(fromTs, toTs);
|
|
57
|
+
const groups = sessionRows.map(({ session_id }) => {
|
|
58
|
+
const totRow = db
|
|
59
|
+
.prepare(`
|
|
60
|
+
SELECT
|
|
61
|
+
COALESCE(SUM(cost_usd), 0) AS cost_usd,
|
|
62
|
+
COUNT(DISTINCT session_id) AS sessions
|
|
63
|
+
FROM tool_usage
|
|
64
|
+
WHERE session_id = ?
|
|
65
|
+
AND recorded_at >= ? AND recorded_at <= ?
|
|
66
|
+
`)
|
|
67
|
+
.get(session_id, fromTs, toTs);
|
|
68
|
+
const toolRows = db
|
|
69
|
+
.prepare(`
|
|
70
|
+
SELECT
|
|
71
|
+
tool_name AS tool,
|
|
72
|
+
SUM(cost_usd) AS cost
|
|
73
|
+
FROM tool_usage
|
|
74
|
+
WHERE session_id = ?
|
|
75
|
+
AND recorded_at >= ? AND recorded_at <= ?
|
|
76
|
+
GROUP BY tool_name
|
|
77
|
+
ORDER BY cost DESC
|
|
78
|
+
`)
|
|
79
|
+
.all(session_id, fromTs, toTs);
|
|
80
|
+
return {
|
|
81
|
+
name: session_id,
|
|
82
|
+
cost_usd: totRow.cost_usd,
|
|
83
|
+
sessions: totRow.sessions,
|
|
84
|
+
tool_breakdown: toolRows,
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
return { period, groups };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export function chargebackToCSV(report) {
|
|
91
|
+
const lines = ["group_name,cost_usd,sessions,tool,tool_cost"];
|
|
92
|
+
for (const group of report.groups) {
|
|
93
|
+
if (group.tool_breakdown.length === 0) {
|
|
94
|
+
lines.push(`"${group.name}",${group.cost_usd},${group.sessions},,`);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
for (const tb of group.tool_breakdown) {
|
|
98
|
+
lines.push(`"${group.name}",${group.cost_usd},${group.sessions},"${tb.tool}",${tb.cost}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return lines.join("\n");
|
|
103
|
+
}
|