tokentrace 0.13.0 → 0.14.1
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/CHANGELOG.md +17 -0
- package/README.md +14 -0
- package/TOKENTRACE_AGENT.md +11 -0
- package/dist/runtime/agent.mjs +21 -1
- package/dist/runtime/mcp.mjs +286 -0
- package/llms.txt +11 -0
- package/package.json +3 -1
- package/scripts/build-cli-runtime.mjs +1 -0
- package/scripts/mcp.ts +39 -0
- package/scripts/package-inspect.mjs +2 -1
- package/scripts/smoke-cli/commands.mjs +15 -0
- package/scripts/smoke-packed-install.mjs +14 -1
- package/server.json +29 -0
- package/src/cli/commands.js +12 -0
- package/src/cli/help.js +1 -0
- package/src/lib/agent-discovery.ts +21 -1
- package/src/lib/mcp-server.ts +289 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ All notable changes to TokenTrace are documented here.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## [0.14.1] - 2026-05-20
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Added the required npm `mcpName` package metadata so the MCP registry can verify the TokenTrace npm package.
|
|
12
|
+
|
|
13
|
+
## [0.14.0] - 2026-05-20
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `tokentrace mcp` now starts a local stdio MCP server for capabilities, status, Scan Health, evidence, repair queue, reports, and explicit local scans.
|
|
18
|
+
- Added a validated MCP registry manifest for the npm package, with `tokentrace mcp` as the stdio entrypoint and no placeholder secrets.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Agent discovery, package smoke checks, packed-install smoke checks, and package inspection now cover the MCP server surface.
|
|
23
|
+
|
|
7
24
|
## [0.13.0] - 2026-05-20
|
|
8
25
|
|
|
9
26
|
### Changed
|
package/README.md
CHANGED
|
@@ -41,6 +41,7 @@ tokentrace capabilities --json
|
|
|
41
41
|
# Alias for agent discovery manifest
|
|
42
42
|
tokentrace roadmap --json
|
|
43
43
|
# Print release status handoff
|
|
44
|
+
tokentrace mcp # Start the local stdio MCP server
|
|
44
45
|
tokentrace scan # Scan local AI CLI usage logs
|
|
45
46
|
tokentrace doctor --json
|
|
46
47
|
# Inspect scan health and repair recommendations
|
|
@@ -106,6 +107,19 @@ or npm package contents before invoking commands:
|
|
|
106
107
|
- [llms.txt](llms.txt)
|
|
107
108
|
- [docs/agent-discovery.schema.json](docs/agent-discovery.schema.json)
|
|
108
109
|
|
|
110
|
+
MCP-capable clients can start the local stdio server after installing or using
|
|
111
|
+
the npm package:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
tokentrace mcp
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The MCP server exposes the same local-first surfaces as tools: capabilities,
|
|
118
|
+
status, Scan Health, evidence, repair queue, reports, and an explicit scan tool.
|
|
119
|
+
It does not scan files on startup, and its scan tool requires
|
|
120
|
+
`confirmLocalScan=true` before reading local usage files or writing the local
|
|
121
|
+
database.
|
|
122
|
+
|
|
109
123
|
When the local dashboard is already running, agents can fetch the same manifest
|
|
110
124
|
over localhost:
|
|
111
125
|
|
package/TOKENTRACE_AGENT.md
CHANGED
|
@@ -19,6 +19,16 @@ tokentrace capabilities --json
|
|
|
19
19
|
|
|
20
20
|
The manifest follows the schema in `docs/agent-discovery.schema.json`.
|
|
21
21
|
|
|
22
|
+
MCP-capable clients can use the local stdio server:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
tokentrace mcp
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The MCP server does not scan on startup. Its `run_scan` tool requires
|
|
29
|
+
`confirmLocalScan=true` before reading local usage files or writing the local
|
|
30
|
+
database.
|
|
31
|
+
|
|
22
32
|
If the local dashboard is already running, the same manifest is available from:
|
|
23
33
|
|
|
24
34
|
```bash
|
|
@@ -67,6 +77,7 @@ curl http://127.0.0.1:3030/api/roadmap
|
|
|
67
77
|
- Do not call processed tokens current context size. Use `ctx` for live context-window pressure.
|
|
68
78
|
- Treat database paths, source file paths, prompts, and raw transcript settings as local sensitive data.
|
|
69
79
|
- Discovery is read-only: it does not scan files, initialize the dashboard database, start a server, or make a network request.
|
|
80
|
+
- MCP startup is read-only; use `run_scan` only when the human expects a local scan.
|
|
70
81
|
|
|
71
82
|
## Integrations
|
|
72
83
|
|
package/dist/runtime/agent.mjs
CHANGED
|
@@ -125,6 +125,25 @@ var commands = [
|
|
|
125
125
|
["tokentrace", "agent", "--json"]
|
|
126
126
|
]
|
|
127
127
|
},
|
|
128
|
+
{
|
|
129
|
+
id: "mcp",
|
|
130
|
+
title: "Start local MCP server",
|
|
131
|
+
command: ["tokentrace", "mcp"],
|
|
132
|
+
description: "Expose TokenTrace's local-first JSON workflows as a stdio Model Context Protocol server.",
|
|
133
|
+
output: "terminal",
|
|
134
|
+
mutatesLocalState: false,
|
|
135
|
+
startsLongRunningProcess: true,
|
|
136
|
+
requiresNetwork: false,
|
|
137
|
+
safeForAutomation: false,
|
|
138
|
+
useWhen: "An MCP-capable agent or client wants structured access to TokenTrace status, doctor, evidence, repair, report, and explicit scan tools.",
|
|
139
|
+
followUps: [
|
|
140
|
+
["tokentrace", "agent", "--json"]
|
|
141
|
+
],
|
|
142
|
+
notes: [
|
|
143
|
+
"The MCP server itself does not scan files on startup.",
|
|
144
|
+
"The run_scan MCP tool requires confirmLocalScan=true before reading local usage files and writing the local database."
|
|
145
|
+
]
|
|
146
|
+
},
|
|
128
147
|
{
|
|
129
148
|
id: "status",
|
|
130
149
|
title: "Print local live usage status",
|
|
@@ -221,7 +240,8 @@ function buildAgentDiscoveryManifest(options = {}) {
|
|
|
221
240
|
},
|
|
222
241
|
discoveryCommands: [
|
|
223
242
|
["tokentrace", "agent", "--json"],
|
|
224
|
-
["tokentrace", "capabilities", "--json"]
|
|
243
|
+
["tokentrace", "capabilities", "--json"],
|
|
244
|
+
["tokentrace", "mcp"]
|
|
225
245
|
],
|
|
226
246
|
apiEndpoints: [
|
|
227
247
|
{
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { createRequire as __tokentraceCreateRequire } from 'node:module'; const require = __tokentraceCreateRequire(import.meta.url);
|
|
2
|
+
|
|
3
|
+
// scripts/mcp.ts
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
|
|
6
|
+
// src/lib/mcp-server.ts
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
var protocolVersion = "2025-06-18";
|
|
11
|
+
function packageVersion() {
|
|
12
|
+
try {
|
|
13
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(process.cwd(), "package.json"), "utf8"));
|
|
14
|
+
return typeof packageJson.version === "string" ? packageJson.version : "unknown";
|
|
15
|
+
} catch {
|
|
16
|
+
return "unknown";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function toolInputSchema(properties = {}, required = []) {
|
|
20
|
+
return {
|
|
21
|
+
type: "object",
|
|
22
|
+
additionalProperties: false,
|
|
23
|
+
properties,
|
|
24
|
+
required
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
var mcpTools = [
|
|
28
|
+
{
|
|
29
|
+
name: "get_capabilities",
|
|
30
|
+
title: "Get TokenTrace capabilities",
|
|
31
|
+
description: "Return the read-only TokenTrace agent discovery manifest. Does not initialize local app data.",
|
|
32
|
+
inputSchema: toolInputSchema()
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "get_status",
|
|
36
|
+
title: "Get local usage status",
|
|
37
|
+
description: "Return the current local TokenTrace usage status snapshot as JSON.",
|
|
38
|
+
inputSchema: toolInputSchema()
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "run_doctor",
|
|
42
|
+
title: "Run Scan Health doctor",
|
|
43
|
+
description: "Inspect scan freshness, parser trust, source coverage, package trust, and repair recommendations.",
|
|
44
|
+
inputSchema: toolInputSchema()
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "get_evidence",
|
|
48
|
+
title: "Get metric evidence trail",
|
|
49
|
+
description: "Return evidence behind a TokenTrace metric without raw prompt or message bodies.",
|
|
50
|
+
inputSchema: toolInputSchema({
|
|
51
|
+
metric: {
|
|
52
|
+
type: "string",
|
|
53
|
+
enum: [
|
|
54
|
+
"processed-tokens",
|
|
55
|
+
"non-cache-tokens",
|
|
56
|
+
"cached-tokens",
|
|
57
|
+
"estimated-cost",
|
|
58
|
+
"sessions",
|
|
59
|
+
"unknown-cost",
|
|
60
|
+
"guardrails",
|
|
61
|
+
"review-queue"
|
|
62
|
+
],
|
|
63
|
+
description: "Evidence metric to inspect. Defaults to processed-tokens."
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "get_repair_queue",
|
|
69
|
+
title: "Get unknown-cost repair queue",
|
|
70
|
+
description: "Return grouped unknown-cost repair causes, next actions, and review state.",
|
|
71
|
+
inputSchema: toolInputSchema()
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "get_report",
|
|
75
|
+
title: "Get local usage report",
|
|
76
|
+
description: "Return a deterministic local report as markdown or JSON.",
|
|
77
|
+
inputSchema: toolInputSchema({
|
|
78
|
+
format: {
|
|
79
|
+
type: "string",
|
|
80
|
+
enum: ["markdown", "json"],
|
|
81
|
+
description: "Report format. Defaults to markdown."
|
|
82
|
+
},
|
|
83
|
+
since: {
|
|
84
|
+
type: "string",
|
|
85
|
+
description: "Optional scope: last-scan, yesterday, or YYYY-MM-DD."
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "run_scan",
|
|
91
|
+
title: "Run local scan",
|
|
92
|
+
description: "Explicitly scan local AI CLI artifacts and update the local TokenTrace database. Requires confirmLocalScan=true.",
|
|
93
|
+
inputSchema: toolInputSchema(
|
|
94
|
+
{
|
|
95
|
+
confirmLocalScan: {
|
|
96
|
+
type: "boolean",
|
|
97
|
+
description: "Must be true to confirm local file reads and local database writes."
|
|
98
|
+
},
|
|
99
|
+
folders: {
|
|
100
|
+
type: "array",
|
|
101
|
+
items: { type: "string" },
|
|
102
|
+
description: "Optional local folders to scan. Defaults to TokenTrace's supported local roots."
|
|
103
|
+
},
|
|
104
|
+
force: {
|
|
105
|
+
type: "boolean",
|
|
106
|
+
description: "Force parser reprocessing for files that would otherwise be deduplicated."
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
["confirmLocalScan"]
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
];
|
|
113
|
+
function jsonRpcResponse(id, result) {
|
|
114
|
+
return {
|
|
115
|
+
jsonrpc: "2.0",
|
|
116
|
+
id: id ?? null,
|
|
117
|
+
result
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function jsonRpcError(id, code, message) {
|
|
121
|
+
return {
|
|
122
|
+
jsonrpc: "2.0",
|
|
123
|
+
id: id ?? null,
|
|
124
|
+
error: { code, message }
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function command(args) {
|
|
128
|
+
const bin = path.join(process.cwd(), "bin", "tokentrace.js");
|
|
129
|
+
const result = spawnSync(process.execPath, [bin, ...args], {
|
|
130
|
+
cwd: process.cwd(),
|
|
131
|
+
env: process.env,
|
|
132
|
+
encoding: "utf8",
|
|
133
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
134
|
+
timeout: 6e4,
|
|
135
|
+
maxBuffer: 4 * 1024 * 1024
|
|
136
|
+
});
|
|
137
|
+
if (result.error) throw result.error;
|
|
138
|
+
if (result.status !== 0) {
|
|
139
|
+
const stderr = result.stderr.trim();
|
|
140
|
+
const stdout = result.stdout.trim();
|
|
141
|
+
throw new Error(stderr || stdout || `tokentrace ${args.join(" ")} exited with code ${result.status}`);
|
|
142
|
+
}
|
|
143
|
+
return result.stdout;
|
|
144
|
+
}
|
|
145
|
+
function commandJson(args) {
|
|
146
|
+
return JSON.parse(command(args));
|
|
147
|
+
}
|
|
148
|
+
function toolResult(value) {
|
|
149
|
+
return {
|
|
150
|
+
content: [
|
|
151
|
+
{
|
|
152
|
+
type: "text",
|
|
153
|
+
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function toolError(error) {
|
|
159
|
+
return {
|
|
160
|
+
isError: true,
|
|
161
|
+
content: [
|
|
162
|
+
{
|
|
163
|
+
type: "text",
|
|
164
|
+
text: error instanceof Error ? error.message : String(error)
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function stringArg(args, key) {
|
|
170
|
+
const value = args[key];
|
|
171
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
172
|
+
}
|
|
173
|
+
function scanArgs(args) {
|
|
174
|
+
if (args.confirmLocalScan !== true) {
|
|
175
|
+
throw new Error("run_scan requires confirmLocalScan=true to acknowledge local file reads and local database writes.");
|
|
176
|
+
}
|
|
177
|
+
const cliArgs = ["scan"];
|
|
178
|
+
if (args.force === true) cliArgs.push("--force");
|
|
179
|
+
if (Array.isArray(args.folders)) {
|
|
180
|
+
for (const folder of args.folders) {
|
|
181
|
+
if (typeof folder !== "string" || !folder.trim()) {
|
|
182
|
+
throw new Error("run_scan folders must be non-empty strings.");
|
|
183
|
+
}
|
|
184
|
+
cliArgs.push(folder);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
cliArgs.push("--json");
|
|
188
|
+
return cliArgs;
|
|
189
|
+
}
|
|
190
|
+
async function callTool(params) {
|
|
191
|
+
const args = params.arguments ?? {};
|
|
192
|
+
try {
|
|
193
|
+
if (params.name === "get_capabilities") {
|
|
194
|
+
return toolResult(commandJson(["agent", "--json"]));
|
|
195
|
+
}
|
|
196
|
+
if (params.name === "get_status") {
|
|
197
|
+
return toolResult(commandJson(["status", "--json"]));
|
|
198
|
+
}
|
|
199
|
+
if (params.name === "run_doctor") {
|
|
200
|
+
return toolResult(commandJson(["doctor", "--json"]));
|
|
201
|
+
}
|
|
202
|
+
if (params.name === "get_evidence") {
|
|
203
|
+
const metric = stringArg(args, "metric");
|
|
204
|
+
return toolResult(commandJson(metric ? ["evidence", "--json", `--metric=${metric}`] : ["evidence", "--json"]));
|
|
205
|
+
}
|
|
206
|
+
if (params.name === "get_repair_queue") {
|
|
207
|
+
return toolResult(commandJson(["repair", "--json"]));
|
|
208
|
+
}
|
|
209
|
+
if (params.name === "get_report") {
|
|
210
|
+
const format = stringArg(args, "format") ?? "markdown";
|
|
211
|
+
const since = stringArg(args, "since");
|
|
212
|
+
const cliArgs = ["report", format === "json" ? "--json" : "--markdown"];
|
|
213
|
+
if (since) cliArgs.push("--since", since);
|
|
214
|
+
const output = command(cliArgs);
|
|
215
|
+
return toolResult(format === "json" ? JSON.parse(output) : output.trimEnd());
|
|
216
|
+
}
|
|
217
|
+
if (params.name === "run_scan") {
|
|
218
|
+
return toolResult(commandJson(scanArgs(args)));
|
|
219
|
+
}
|
|
220
|
+
throw new Error(`Unknown TokenTrace MCP tool: ${params.name ?? "(missing name)"}`);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
return toolError(error);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function asObject(value) {
|
|
226
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
227
|
+
}
|
|
228
|
+
async function handleMcpMessage(message) {
|
|
229
|
+
const id = message.id;
|
|
230
|
+
const method = message.method;
|
|
231
|
+
if (!method) return jsonRpcError(id, -32600, "Invalid JSON-RPC request: missing method.");
|
|
232
|
+
if (method.startsWith("notifications/")) return null;
|
|
233
|
+
if (method === "initialize") {
|
|
234
|
+
return jsonRpcResponse(id, {
|
|
235
|
+
protocolVersion,
|
|
236
|
+
capabilities: { tools: {} },
|
|
237
|
+
serverInfo: {
|
|
238
|
+
name: "tokentrace",
|
|
239
|
+
version: packageVersion()
|
|
240
|
+
},
|
|
241
|
+
instructions: "TokenTrace is local-first. Tools read local TokenTrace data; run_scan requires explicit confirmation before local file reads and database writes."
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
if (method === "ping") {
|
|
245
|
+
return jsonRpcResponse(id, {});
|
|
246
|
+
}
|
|
247
|
+
if (method === "tools/list") {
|
|
248
|
+
return jsonRpcResponse(id, { tools: mcpTools });
|
|
249
|
+
}
|
|
250
|
+
if (method === "tools/call") {
|
|
251
|
+
return jsonRpcResponse(id, await callTool(asObject(message.params)));
|
|
252
|
+
}
|
|
253
|
+
return jsonRpcError(id, -32601, `Method not found: ${method}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// scripts/mcp.ts
|
|
257
|
+
function write(message) {
|
|
258
|
+
process.stdout.write(`${JSON.stringify(message)}
|
|
259
|
+
`);
|
|
260
|
+
}
|
|
261
|
+
async function handleLine(line) {
|
|
262
|
+
const trimmed = line.trim();
|
|
263
|
+
if (!trimmed) return;
|
|
264
|
+
let message;
|
|
265
|
+
try {
|
|
266
|
+
message = JSON.parse(trimmed);
|
|
267
|
+
} catch {
|
|
268
|
+
write(jsonRpcError(null, -32700, "Parse error: expected one JSON-RPC message per line."));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const response = await handleMcpMessage(message);
|
|
272
|
+
if (response) write(response);
|
|
273
|
+
}
|
|
274
|
+
var reader = createInterface({
|
|
275
|
+
input: process.stdin,
|
|
276
|
+
crlfDelay: Infinity
|
|
277
|
+
});
|
|
278
|
+
var queue = Promise.resolve();
|
|
279
|
+
reader.on("line", (line) => {
|
|
280
|
+
queue = queue.then(() => handleLine(line)).catch((error) => {
|
|
281
|
+
write(jsonRpcError(null, -32603, error instanceof Error ? error.message : String(error)));
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
reader.on("close", () => {
|
|
285
|
+
queue.finally(() => process.exit(0));
|
|
286
|
+
});
|
package/llms.txt
CHANGED
|
@@ -18,6 +18,16 @@ tokentrace capabilities --json
|
|
|
18
18
|
|
|
19
19
|
The manifest is read-only and follows `docs/agent-discovery.schema.json`.
|
|
20
20
|
|
|
21
|
+
MCP-capable clients can start TokenTrace over stdio:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
tokentrace mcp
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The MCP server does not scan on startup. Its `run_scan` tool requires
|
|
28
|
+
`confirmLocalScan=true` before reading local usage files or writing the local
|
|
29
|
+
database.
|
|
30
|
+
|
|
21
31
|
When the local dashboard is running, the same manifest is available at
|
|
22
32
|
`/api/agent` and `/api/capabilities`.
|
|
23
33
|
|
|
@@ -32,6 +42,7 @@ Roadmap status is available through `tokentrace roadmap --json` and
|
|
|
32
42
|
- `tokentrace repair --json`: inspect unknown-cost repair work.
|
|
33
43
|
- `tokentrace digest --json`: summarize local usage.
|
|
34
44
|
- `tokentrace roadmap --json`: inspect the current release handoff, action recipes, evidence paths, and release status.
|
|
45
|
+
- `tokentrace mcp`: start the local stdio MCP server.
|
|
35
46
|
- `tokentrace statusline setup claude`: print Claude Code status-line setup.
|
|
36
47
|
- `tokentrace watch --session --compact`: terminal sidecar fallback for Codex.
|
|
37
48
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokentrace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
|
+
"mcpName": "io.github.abhiyoheswaran1/tokentrace",
|
|
4
5
|
"description": "Local-first dashboard for AI CLI token, cost, and session analytics.",
|
|
5
6
|
"author": {
|
|
6
7
|
"name": "Abhi Yoheswaran",
|
|
@@ -27,6 +28,7 @@
|
|
|
27
28
|
"components",
|
|
28
29
|
"docs/agent-discovery.schema.json",
|
|
29
30
|
"docs/assets",
|
|
31
|
+
"server.json",
|
|
30
32
|
"CHANGELOG.md",
|
|
31
33
|
"CONTRIBUTING.md",
|
|
32
34
|
"SECURITY.md",
|
|
@@ -15,6 +15,7 @@ const entryPoints = {
|
|
|
15
15
|
doctor: "scripts/doctor.ts",
|
|
16
16
|
evidence: "scripts/evidence.ts",
|
|
17
17
|
insights: "scripts/insights.ts",
|
|
18
|
+
mcp: "scripts/mcp.ts",
|
|
18
19
|
"pricing-refresh": "scripts/pricing-refresh.ts",
|
|
19
20
|
report: "scripts/report.ts",
|
|
20
21
|
repair: "scripts/repair.ts",
|
package/scripts/mcp.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
import { handleMcpMessage, jsonRpcError } from "@/src/lib/mcp-server";
|
|
3
|
+
|
|
4
|
+
function write(message: unknown) {
|
|
5
|
+
process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async function handleLine(line: string) {
|
|
9
|
+
const trimmed = line.trim();
|
|
10
|
+
if (!trimmed) return;
|
|
11
|
+
|
|
12
|
+
let message: unknown;
|
|
13
|
+
try {
|
|
14
|
+
message = JSON.parse(trimmed);
|
|
15
|
+
} catch {
|
|
16
|
+
write(jsonRpcError(null, -32700, "Parse error: expected one JSON-RPC message per line."));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const response = await handleMcpMessage(message as Parameters<typeof handleMcpMessage>[0]);
|
|
21
|
+
if (response) write(response);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const reader = createInterface({
|
|
25
|
+
input: process.stdin,
|
|
26
|
+
crlfDelay: Infinity
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
let queue = Promise.resolve();
|
|
30
|
+
|
|
31
|
+
reader.on("line", (line) => {
|
|
32
|
+
queue = queue.then(() => handleLine(line)).catch((error) => {
|
|
33
|
+
write(jsonRpcError(null, -32603, error instanceof Error ? error.message : String(error)));
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
reader.on("close", () => {
|
|
38
|
+
queue.finally(() => process.exit(0));
|
|
39
|
+
});
|
|
@@ -68,6 +68,7 @@ const requiredPackageFiles = [
|
|
|
68
68
|
"TOKENTRACE_AGENT.md",
|
|
69
69
|
"llms.txt",
|
|
70
70
|
"docs/agent-discovery.schema.json",
|
|
71
|
+
"server.json",
|
|
71
72
|
expectedBin
|
|
72
73
|
];
|
|
73
74
|
|
|
@@ -136,7 +137,7 @@ console.log("- no npm install lifecycle scripts");
|
|
|
136
137
|
console.log("- Next.js dependency floor is pinned to the patched range");
|
|
137
138
|
console.log("- Drizzle ORM and PostCSS floors are pinned to patched ranges");
|
|
138
139
|
console.log("- generated Next.js build output is excluded from the package");
|
|
139
|
-
console.log("- agent discovery docs, schema, and executable CLI bin are included");
|
|
140
|
+
console.log("- agent discovery docs, MCP registry manifest, schema, and executable CLI bin are included");
|
|
140
141
|
for (const note of notes) {
|
|
141
142
|
console.log(`- ${note}`);
|
|
142
143
|
}
|
|
@@ -24,6 +24,19 @@ export function jsonCommand(context, args) {
|
|
|
24
24
|
return JSON.parse(output);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function mcpSmoke(context) {
|
|
28
|
+
const input = [
|
|
29
|
+
{ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-06-18" } },
|
|
30
|
+
{ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} }
|
|
31
|
+
].map((message) => JSON.stringify(message)).join("\n") + "\n";
|
|
32
|
+
const output = run(context, ["mcp"], { input, timeout: 30_000 });
|
|
33
|
+
const responses = output.split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
34
|
+
const toolNames = responses[1]?.result?.tools?.map((tool) => tool.name) ?? [];
|
|
35
|
+
if (!toolNames.includes("get_capabilities") || !toolNames.includes("run_scan")) {
|
|
36
|
+
throw new Error("MCP tools/list is missing expected TokenTrace tools.");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
27
40
|
export function expectFailure(context, args, expectedStderr) {
|
|
28
41
|
const result = spawnSync(process.execPath, [context.bin, ...args], {
|
|
29
42
|
cwd: context.root,
|
|
@@ -61,6 +74,8 @@ export async function smokeCliDiscovery(context) {
|
|
|
61
74
|
if (roadmap.version !== "0.12.0" || roadmap.release?.releaseAllowed !== true || roadmap.handoff?.schemaVersion !== "tokentrace.roadmap.v2") {
|
|
62
75
|
throw new Error("Roadmap JSON is missing 0.12.0 release-ready handoff status.");
|
|
63
76
|
}
|
|
77
|
+
|
|
78
|
+
mcpSmoke(context);
|
|
64
79
|
} catch (error) {
|
|
65
80
|
throw new Error(`CLI discovery smoke failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
66
81
|
}
|
|
@@ -23,6 +23,7 @@ function run(command, args, options = {}) {
|
|
|
23
23
|
...options.env
|
|
24
24
|
},
|
|
25
25
|
encoding: "utf8",
|
|
26
|
+
input: options.input,
|
|
26
27
|
timeout: options.timeout ?? 120_000
|
|
27
28
|
});
|
|
28
29
|
|
|
@@ -50,6 +51,7 @@ const requiredPackageFiles = [
|
|
|
50
51
|
"TOKENTRACE_AGENT.md",
|
|
51
52
|
"llms.txt",
|
|
52
53
|
"docs/agent-discovery.schema.json",
|
|
54
|
+
"server.json",
|
|
53
55
|
"bin/tokentrace.js"
|
|
54
56
|
];
|
|
55
57
|
|
|
@@ -109,6 +111,17 @@ function assertDiscoverySmoke(bin, cwd, env) {
|
|
|
109
111
|
if (roadmap.version !== "0.12.0" || roadmap.release?.releaseAllowed !== true) {
|
|
110
112
|
throw new Error("Packed tokentrace roadmap --json is missing the 0.12.0 release-ready status.");
|
|
111
113
|
}
|
|
114
|
+
|
|
115
|
+
const mcpInput = [
|
|
116
|
+
{ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-06-18" } },
|
|
117
|
+
{ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} }
|
|
118
|
+
].map((message) => JSON.stringify(message)).join("\n") + "\n";
|
|
119
|
+
const mcpOutput = run(bin, ["mcp"], { cwd, env, input: mcpInput, timeout: 30_000 });
|
|
120
|
+
const mcpResponses = mcpOutput.split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
121
|
+
const mcpTools = mcpResponses[1]?.result?.tools?.map((tool) => tool.name) ?? [];
|
|
122
|
+
if (!mcpTools.includes("get_capabilities") || !mcpTools.includes("run_scan")) {
|
|
123
|
+
throw new Error("Packed tokentrace mcp did not expose expected MCP tools.");
|
|
124
|
+
}
|
|
112
125
|
}
|
|
113
126
|
|
|
114
127
|
function findFreePort(hostname) {
|
|
@@ -223,7 +236,7 @@ try {
|
|
|
223
236
|
}
|
|
224
237
|
|
|
225
238
|
const help = run(bin, ["--help"], { cwd: tempRoot });
|
|
226
|
-
if (!help.includes("tokentrace scan") || !help.includes("tokentrace statusline claude")) {
|
|
239
|
+
if (!help.includes("tokentrace scan") || !help.includes("tokentrace mcp") || !help.includes("tokentrace statusline claude")) {
|
|
227
240
|
throw new Error("Packed tokentrace --help is missing expected commands.");
|
|
228
241
|
}
|
|
229
242
|
|
package/server.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.abhiyoheswaran1/tokentrace",
|
|
4
|
+
"title": "TokenTrace",
|
|
5
|
+
"description": "Local-first AI CLI usage analytics for agents.",
|
|
6
|
+
"websiteUrl": "https://www.abhiyoheswaran.com/apps/tokentrace",
|
|
7
|
+
"repository": {
|
|
8
|
+
"url": "https://github.com/abhiyoheswaran1/tokentrace",
|
|
9
|
+
"source": "github"
|
|
10
|
+
},
|
|
11
|
+
"version": "0.14.1",
|
|
12
|
+
"packages": [
|
|
13
|
+
{
|
|
14
|
+
"registryType": "npm",
|
|
15
|
+
"identifier": "tokentrace",
|
|
16
|
+
"version": "0.14.1",
|
|
17
|
+
"runtimeHint": "npx",
|
|
18
|
+
"packageArguments": [
|
|
19
|
+
{
|
|
20
|
+
"type": "positional",
|
|
21
|
+
"value": "mcp"
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"transport": {
|
|
25
|
+
"type": "stdio"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
package/src/cli/commands.js
CHANGED
|
@@ -21,6 +21,14 @@ async function roadmap(context, args) {
|
|
|
21
21
|
await runNodeScript(context, "roadmap", args, { env: process.env });
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
async function mcp(context, args) {
|
|
25
|
+
if (args.length) {
|
|
26
|
+
console.error("Usage: tokentrace mcp");
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
await runNodeScript(context, "mcp", [], { env: process.env });
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
async function doctor(context, args) {
|
|
25
33
|
await initializeDatabase(context, { quiet: true, refreshPrices: false });
|
|
26
34
|
await runNodeScript(context, "doctor", args);
|
|
@@ -214,6 +222,10 @@ export async function runCliCommand(context, rawArgs = process.argv.slice(2)) {
|
|
|
214
222
|
await roadmap(context, args);
|
|
215
223
|
return;
|
|
216
224
|
}
|
|
225
|
+
if (command === "mcp") {
|
|
226
|
+
await mcp(context, args);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
217
229
|
if (command === "scan") {
|
|
218
230
|
await scan(context, args);
|
|
219
231
|
return;
|
package/src/cli/help.js
CHANGED
|
@@ -10,6 +10,7 @@ Usage:
|
|
|
10
10
|
Alias for agent discovery manifest
|
|
11
11
|
tokentrace roadmap --json
|
|
12
12
|
Print release status handoff
|
|
13
|
+
tokentrace mcp Start the local stdio MCP server
|
|
13
14
|
tokentrace scan Scan local AI CLI usage logs
|
|
14
15
|
tokentrace doctor --json
|
|
15
16
|
Inspect scan health and repair recommendations
|
|
@@ -193,6 +193,25 @@ const commands: AgentDiscoveryCommand[] = [
|
|
|
193
193
|
["tokentrace", "agent", "--json"]
|
|
194
194
|
]
|
|
195
195
|
},
|
|
196
|
+
{
|
|
197
|
+
id: "mcp",
|
|
198
|
+
title: "Start local MCP server",
|
|
199
|
+
command: ["tokentrace", "mcp"],
|
|
200
|
+
description: "Expose TokenTrace's local-first JSON workflows as a stdio Model Context Protocol server.",
|
|
201
|
+
output: "terminal",
|
|
202
|
+
mutatesLocalState: false,
|
|
203
|
+
startsLongRunningProcess: true,
|
|
204
|
+
requiresNetwork: false,
|
|
205
|
+
safeForAutomation: false,
|
|
206
|
+
useWhen: "An MCP-capable agent or client wants structured access to TokenTrace status, doctor, evidence, repair, report, and explicit scan tools.",
|
|
207
|
+
followUps: [
|
|
208
|
+
["tokentrace", "agent", "--json"]
|
|
209
|
+
],
|
|
210
|
+
notes: [
|
|
211
|
+
"The MCP server itself does not scan files on startup.",
|
|
212
|
+
"The run_scan MCP tool requires confirmLocalScan=true before reading local usage files and writing the local database."
|
|
213
|
+
]
|
|
214
|
+
},
|
|
196
215
|
{
|
|
197
216
|
id: "status",
|
|
198
217
|
title: "Print local live usage status",
|
|
@@ -290,7 +309,8 @@ export function buildAgentDiscoveryManifest(options: AgentDiscoveryOptions = {})
|
|
|
290
309
|
},
|
|
291
310
|
discoveryCommands: [
|
|
292
311
|
["tokentrace", "agent", "--json"],
|
|
293
|
-
["tokentrace", "capabilities", "--json"]
|
|
312
|
+
["tokentrace", "capabilities", "--json"],
|
|
313
|
+
["tokentrace", "mcp"]
|
|
294
314
|
],
|
|
295
315
|
apiEndpoints: [
|
|
296
316
|
{
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
type JsonRpcId = string | number | null;
|
|
6
|
+
|
|
7
|
+
type JsonRpcRequest = {
|
|
8
|
+
jsonrpc?: string;
|
|
9
|
+
id?: JsonRpcId;
|
|
10
|
+
method?: string;
|
|
11
|
+
params?: unknown;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type ToolCallParams = {
|
|
15
|
+
name?: string;
|
|
16
|
+
arguments?: Record<string, unknown>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const protocolVersion = "2025-06-18";
|
|
20
|
+
|
|
21
|
+
function packageVersion() {
|
|
22
|
+
try {
|
|
23
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(process.cwd(), "package.json"), "utf8"));
|
|
24
|
+
return typeof packageJson.version === "string" ? packageJson.version : "unknown";
|
|
25
|
+
} catch {
|
|
26
|
+
return "unknown";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function toolInputSchema(properties: Record<string, unknown> = {}, required: string[] = []) {
|
|
31
|
+
return {
|
|
32
|
+
type: "object",
|
|
33
|
+
additionalProperties: false,
|
|
34
|
+
properties,
|
|
35
|
+
required
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const mcpTools = [
|
|
40
|
+
{
|
|
41
|
+
name: "get_capabilities",
|
|
42
|
+
title: "Get TokenTrace capabilities",
|
|
43
|
+
description: "Return the read-only TokenTrace agent discovery manifest. Does not initialize local app data.",
|
|
44
|
+
inputSchema: toolInputSchema()
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "get_status",
|
|
48
|
+
title: "Get local usage status",
|
|
49
|
+
description: "Return the current local TokenTrace usage status snapshot as JSON.",
|
|
50
|
+
inputSchema: toolInputSchema()
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "run_doctor",
|
|
54
|
+
title: "Run Scan Health doctor",
|
|
55
|
+
description: "Inspect scan freshness, parser trust, source coverage, package trust, and repair recommendations.",
|
|
56
|
+
inputSchema: toolInputSchema()
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "get_evidence",
|
|
60
|
+
title: "Get metric evidence trail",
|
|
61
|
+
description: "Return evidence behind a TokenTrace metric without raw prompt or message bodies.",
|
|
62
|
+
inputSchema: toolInputSchema({
|
|
63
|
+
metric: {
|
|
64
|
+
type: "string",
|
|
65
|
+
enum: [
|
|
66
|
+
"processed-tokens",
|
|
67
|
+
"non-cache-tokens",
|
|
68
|
+
"cached-tokens",
|
|
69
|
+
"estimated-cost",
|
|
70
|
+
"sessions",
|
|
71
|
+
"unknown-cost",
|
|
72
|
+
"guardrails",
|
|
73
|
+
"review-queue"
|
|
74
|
+
],
|
|
75
|
+
description: "Evidence metric to inspect. Defaults to processed-tokens."
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "get_repair_queue",
|
|
81
|
+
title: "Get unknown-cost repair queue",
|
|
82
|
+
description: "Return grouped unknown-cost repair causes, next actions, and review state.",
|
|
83
|
+
inputSchema: toolInputSchema()
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "get_report",
|
|
87
|
+
title: "Get local usage report",
|
|
88
|
+
description: "Return a deterministic local report as markdown or JSON.",
|
|
89
|
+
inputSchema: toolInputSchema({
|
|
90
|
+
format: {
|
|
91
|
+
type: "string",
|
|
92
|
+
enum: ["markdown", "json"],
|
|
93
|
+
description: "Report format. Defaults to markdown."
|
|
94
|
+
},
|
|
95
|
+
since: {
|
|
96
|
+
type: "string",
|
|
97
|
+
description: "Optional scope: last-scan, yesterday, or YYYY-MM-DD."
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "run_scan",
|
|
103
|
+
title: "Run local scan",
|
|
104
|
+
description:
|
|
105
|
+
"Explicitly scan local AI CLI artifacts and update the local TokenTrace database. Requires confirmLocalScan=true.",
|
|
106
|
+
inputSchema: toolInputSchema(
|
|
107
|
+
{
|
|
108
|
+
confirmLocalScan: {
|
|
109
|
+
type: "boolean",
|
|
110
|
+
description: "Must be true to confirm local file reads and local database writes."
|
|
111
|
+
},
|
|
112
|
+
folders: {
|
|
113
|
+
type: "array",
|
|
114
|
+
items: { type: "string" },
|
|
115
|
+
description: "Optional local folders to scan. Defaults to TokenTrace's supported local roots."
|
|
116
|
+
},
|
|
117
|
+
force: {
|
|
118
|
+
type: "boolean",
|
|
119
|
+
description: "Force parser reprocessing for files that would otherwise be deduplicated."
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
["confirmLocalScan"]
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
function jsonRpcResponse(id: JsonRpcId | undefined, result: unknown) {
|
|
128
|
+
return {
|
|
129
|
+
jsonrpc: "2.0",
|
|
130
|
+
id: id ?? null,
|
|
131
|
+
result
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function jsonRpcError(id: JsonRpcId | undefined, code: number, message: string) {
|
|
136
|
+
return {
|
|
137
|
+
jsonrpc: "2.0",
|
|
138
|
+
id: id ?? null,
|
|
139
|
+
error: { code, message }
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function command(args: string[]) {
|
|
144
|
+
const bin = path.join(process.cwd(), "bin", "tokentrace.js");
|
|
145
|
+
const result = spawnSync(process.execPath, [bin, ...args], {
|
|
146
|
+
cwd: process.cwd(),
|
|
147
|
+
env: process.env,
|
|
148
|
+
encoding: "utf8",
|
|
149
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
150
|
+
timeout: 60_000,
|
|
151
|
+
maxBuffer: 4 * 1024 * 1024
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
if (result.error) throw result.error;
|
|
155
|
+
if (result.status !== 0) {
|
|
156
|
+
const stderr = result.stderr.trim();
|
|
157
|
+
const stdout = result.stdout.trim();
|
|
158
|
+
throw new Error(stderr || stdout || `tokentrace ${args.join(" ")} exited with code ${result.status}`);
|
|
159
|
+
}
|
|
160
|
+
return result.stdout;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function commandJson(args: string[]) {
|
|
164
|
+
return JSON.parse(command(args));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function toolResult(value: unknown) {
|
|
168
|
+
return {
|
|
169
|
+
content: [
|
|
170
|
+
{
|
|
171
|
+
type: "text",
|
|
172
|
+
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
173
|
+
}
|
|
174
|
+
]
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function toolError(error: unknown) {
|
|
179
|
+
return {
|
|
180
|
+
isError: true,
|
|
181
|
+
content: [
|
|
182
|
+
{
|
|
183
|
+
type: "text",
|
|
184
|
+
text: error instanceof Error ? error.message : String(error)
|
|
185
|
+
}
|
|
186
|
+
]
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function stringArg(args: Record<string, unknown>, key: string) {
|
|
191
|
+
const value = args[key];
|
|
192
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function scanArgs(args: Record<string, unknown>) {
|
|
196
|
+
if (args.confirmLocalScan !== true) {
|
|
197
|
+
throw new Error("run_scan requires confirmLocalScan=true to acknowledge local file reads and local database writes.");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const cliArgs = ["scan"];
|
|
201
|
+
if (args.force === true) cliArgs.push("--force");
|
|
202
|
+
if (Array.isArray(args.folders)) {
|
|
203
|
+
for (const folder of args.folders) {
|
|
204
|
+
if (typeof folder !== "string" || !folder.trim()) {
|
|
205
|
+
throw new Error("run_scan folders must be non-empty strings.");
|
|
206
|
+
}
|
|
207
|
+
cliArgs.push(folder);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
cliArgs.push("--json");
|
|
211
|
+
return cliArgs;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function callTool(params: ToolCallParams) {
|
|
215
|
+
const args = params.arguments ?? {};
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
if (params.name === "get_capabilities") {
|
|
219
|
+
return toolResult(commandJson(["agent", "--json"]));
|
|
220
|
+
}
|
|
221
|
+
if (params.name === "get_status") {
|
|
222
|
+
return toolResult(commandJson(["status", "--json"]));
|
|
223
|
+
}
|
|
224
|
+
if (params.name === "run_doctor") {
|
|
225
|
+
return toolResult(commandJson(["doctor", "--json"]));
|
|
226
|
+
}
|
|
227
|
+
if (params.name === "get_evidence") {
|
|
228
|
+
const metric = stringArg(args, "metric");
|
|
229
|
+
return toolResult(commandJson(metric ? ["evidence", "--json", `--metric=${metric}`] : ["evidence", "--json"]));
|
|
230
|
+
}
|
|
231
|
+
if (params.name === "get_repair_queue") {
|
|
232
|
+
return toolResult(commandJson(["repair", "--json"]));
|
|
233
|
+
}
|
|
234
|
+
if (params.name === "get_report") {
|
|
235
|
+
const format = stringArg(args, "format") ?? "markdown";
|
|
236
|
+
const since = stringArg(args, "since");
|
|
237
|
+
const cliArgs = ["report", format === "json" ? "--json" : "--markdown"];
|
|
238
|
+
if (since) cliArgs.push("--since", since);
|
|
239
|
+
const output = command(cliArgs);
|
|
240
|
+
return toolResult(format === "json" ? JSON.parse(output) : output.trimEnd());
|
|
241
|
+
}
|
|
242
|
+
if (params.name === "run_scan") {
|
|
243
|
+
return toolResult(commandJson(scanArgs(args)));
|
|
244
|
+
}
|
|
245
|
+
throw new Error(`Unknown TokenTrace MCP tool: ${params.name ?? "(missing name)"}`);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
return toolError(error);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function asObject(value: unknown): Record<string, unknown> {
|
|
252
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export async function handleMcpMessage(message: JsonRpcRequest) {
|
|
256
|
+
const id = message.id;
|
|
257
|
+
const method = message.method;
|
|
258
|
+
|
|
259
|
+
if (!method) return jsonRpcError(id, -32600, "Invalid JSON-RPC request: missing method.");
|
|
260
|
+
|
|
261
|
+
if (method.startsWith("notifications/")) return null;
|
|
262
|
+
|
|
263
|
+
if (method === "initialize") {
|
|
264
|
+
return jsonRpcResponse(id, {
|
|
265
|
+
protocolVersion,
|
|
266
|
+
capabilities: { tools: {} },
|
|
267
|
+
serverInfo: {
|
|
268
|
+
name: "tokentrace",
|
|
269
|
+
version: packageVersion()
|
|
270
|
+
},
|
|
271
|
+
instructions:
|
|
272
|
+
"TokenTrace is local-first. Tools read local TokenTrace data; run_scan requires explicit confirmation before local file reads and database writes."
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (method === "ping") {
|
|
277
|
+
return jsonRpcResponse(id, {});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (method === "tools/list") {
|
|
281
|
+
return jsonRpcResponse(id, { tools: mcpTools });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (method === "tools/call") {
|
|
285
|
+
return jsonRpcResponse(id, await callTool(asObject(message.params) as ToolCallParams));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return jsonRpcError(id, -32601, `Method not found: ${method}`);
|
|
289
|
+
}
|