tokentrace 0.13.0 → 0.14.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/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@ All notable changes to TokenTrace are documented here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## [0.14.0] - 2026-05-20
8
+
9
+ ### Added
10
+
11
+ - `tokentrace mcp` now starts a local stdio MCP server for capabilities, status, Scan Health, evidence, repair queue, reports, and explicit local scans.
12
+ - Added a validated MCP registry manifest for the npm package, with `tokentrace mcp` as the stdio entrypoint and no placeholder secrets.
13
+
14
+ ### Changed
15
+
16
+ - Agent discovery, package smoke checks, packed-install smoke checks, and package inspection now cover the MCP server surface.
17
+
7
18
  ## [0.13.0] - 2026-05-20
8
19
 
9
20
  ### 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
 
@@ -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
 
@@ -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,6 @@
1
1
  {
2
2
  "name": "tokentrace",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Local-first dashboard for AI CLI token, cost, and session analytics.",
5
5
  "author": {
6
6
  "name": "Abhi Yoheswaran",
@@ -27,6 +27,7 @@
27
27
  "components",
28
28
  "docs/agent-discovery.schema.json",
29
29
  "docs/assets",
30
+ "server.json",
30
31
  "CHANGELOG.md",
31
32
  "CONTRIBUTING.md",
32
33
  "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.0",
12
+ "packages": [
13
+ {
14
+ "registryType": "npm",
15
+ "identifier": "tokentrace",
16
+ "version": "0.14.0",
17
+ "runtimeHint": "npx",
18
+ "packageArguments": [
19
+ {
20
+ "type": "positional",
21
+ "value": "mcp"
22
+ }
23
+ ],
24
+ "transport": {
25
+ "type": "stdio"
26
+ }
27
+ }
28
+ ]
29
+ }
@@ -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
+ }