tare-mcp 0.2.1 → 0.3.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/README.md CHANGED
@@ -4,16 +4,30 @@
4
4
  [![npm](https://img.shields.io/npm/v/tare-mcp?label=npm)](https://www.npmjs.com/package/tare-mcp)
5
5
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
6
 
7
- See what your MCP tools weigh before your agent does anything.
7
+ Measure the MCP tool surface your agent is about to send to a model.
8
8
 
9
9
  ```bash
10
- npx tare-mcp
10
+ npm install tare-mcp
11
11
  ```
12
12
 
13
- MCP made tools easy to connect.
14
- It did not make them cheap to carry.
13
+ ```ts
14
+ import { measureTools } from "tare-mcp";
15
+
16
+ const tools = await mcpClient.listTools();
17
+ const report = await measureTools(tools, { budget: 40_000 });
15
18
 
16
- `tare-mcp` inspects your MCP setup and shows, in one local run:
19
+ console.log(
20
+ `MCP tool surface: ${report.summary.tools} tools, ~${report.summary.estimatedTokens.claude} Claude tokens`
21
+ );
22
+
23
+ if (report.metadata.budgetExceeded) {
24
+ throw new Error("MCP tool surface exceeds budget");
25
+ }
26
+ ```
27
+
28
+ MCP made tools easy to connect. It did not make them cheap to carry.
29
+
30
+ `tare-mcp` shows, from inside your agent or from the CLI:
17
31
 
18
32
  - how many tools your agent sees
19
33
  - how much context those tools consume, estimated for Claude and OpenAI cl100k
@@ -21,36 +35,20 @@ It did not make them cheap to carry.
21
35
  - which tools overlap and compete for model attention
22
36
  - whether your setup exceeds a context budget
23
37
 
24
- Use it once to see the current cost:
38
+ Use the CLI when you want to inspect config-discovered MCP servers locally:
25
39
 
26
40
  ```bash
27
41
  npx tare-mcp
28
42
  ```
29
43
 
30
- Use it in CI to catch MCP bloat before merge:
31
-
32
- ```bash
33
- # one time
34
- mkdir -p .tare
35
- npx tare-mcp --json > .tare/baseline.json
36
- git add .tare/baseline.json
37
-
38
- # on every PR
39
- npx tare-mcp --json > tare-report.json
40
- npx tare-mcp diff \
41
- --base .tare/baseline.json \
42
- --head tare-report.json \
43
- --max-token-increase 5000 \
44
- --max-tool-increase 20
45
- ```
46
-
47
44
  Think of it as `du -sh node_modules`, but for agent tool context.
48
45
 
49
46
  ## Table of Contents
50
47
 
51
48
  - [Why This Matters](#why-this-matters)
52
49
  - [Why Token Count Is Not the Whole Problem](#why-token-count-is-not-the-whole-problem)
53
- - [Quickstart](#quickstart)
50
+ - [Using tare-mcp in your agent](#using-tare-mcp-in-your-agent)
51
+ - [CLI Quickstart](#cli-quickstart)
54
52
  - [Hosted MCP Quickstart](#hosted-mcp-quickstart)
55
53
  - [Scenario Examples](#scenario-examples)
56
54
  - [Example Output](#example-output)
@@ -88,7 +86,80 @@ If three servers all expose tools that look like "search", the model has to choo
88
86
  - what your tools weigh
89
87
  - where your tools overlap
90
88
 
91
- ## Quickstart
89
+ ## Using tare-mcp in your agent
90
+
91
+ Production agents often do not have a stable `.mcp.json` file to inspect. They connect to MCP servers, call `tools/list`, and pass those tool definitions to the model on each request.
92
+
93
+ Use `measureTools()` when you already have the tool definitions in memory:
94
+
95
+ ```ts
96
+ import { measureTools } from "tare-mcp";
97
+
98
+ const tools = await mcpClient.listTools();
99
+ const report = await measureTools(tools);
100
+
101
+ console.log(
102
+ `MCP tool surface: ${report.summary.tools} tools, ~${report.summary.estimatedTokens.claude} Claude tokens`
103
+ );
104
+ ```
105
+
106
+ For multiple MCP servers, add `server` per tool so overlap warnings and per-server totals remain useful:
107
+
108
+ ```ts
109
+ import { measureTools } from "tare-mcp";
110
+
111
+ const last9Tools = await last9Client.listTools();
112
+ const githubTools = await githubClient.listTools();
113
+
114
+ const report = await measureTools([
115
+ ...last9Tools.map((tool) => ({ ...tool, server: "last9" })),
116
+ ...githubTools.map((tool) => ({ ...tool, server: "github" }))
117
+ ]);
118
+ ```
119
+
120
+ For unattributed tools, pass a fallback server name:
121
+
122
+ ```ts
123
+ const report = await measureTools(tools, {
124
+ serverName: "agent"
125
+ });
126
+ ```
127
+
128
+ Budget checks are metadata, not exceptions. That keeps the library easy to use in request paths, logs, and CI:
129
+
130
+ ```ts
131
+ const report = await measureTools(tools, { budget: 40_000 });
132
+
133
+ if (report.metadata.budgetExceeded) {
134
+ throw new Error(
135
+ `MCP tool surface exceeds budget: ~${report.summary.estimatedTokens.claude} Claude tokens`
136
+ );
137
+ }
138
+ ```
139
+
140
+ Structured logging example:
141
+
142
+ ```ts
143
+ logger.info("mcp.tool_surface", {
144
+ tools: report.summary.tools,
145
+ servers: report.summary.servers,
146
+ tokens_claude: report.summary.estimatedTokens.claude,
147
+ tokens_openai_cl100k: report.summary.estimatedTokens.openaiCl100k,
148
+ overlap_clusters: report.overlapClusters.length,
149
+ budget_exceeded: report.metadata.budgetExceeded ?? false
150
+ });
151
+ ```
152
+
153
+ The programmatic API is local-first. It does not read config files, spawn MCP servers, or call cloud tokenization APIs by default. API-backed Claude token counting is opt-in:
154
+
155
+ ```ts
156
+ const report = await measureTools(tools, {
157
+ claudeTokenizerMode: "api",
158
+ anthropicApiKey: process.env.ANTHROPIC_API_KEY
159
+ });
160
+ ```
161
+
162
+ ## CLI Quickstart
92
163
 
93
164
  Run it without installing:
94
165
 
@@ -314,10 +385,11 @@ Recommendations:
314
385
 
315
386
  ## Supported transports
316
387
 
317
- v0.2 supports live inspection for:
388
+ v0.3 supports:
318
389
 
319
390
  - stdio MCP servers
320
391
  - Streamable HTTP MCP servers
392
+ - programmatic tool definitions through `measureTools()`
321
393
 
322
394
  SSE may be supported best-effort later.
323
395
 
@@ -357,12 +429,12 @@ That mode requires `ANTHROPIC_API_KEY` and uses Anthropic's `POST /v1/messages/c
357
429
 
358
430
  Environment variables that control tokenization:
359
431
 
360
- | Variable | Values | Default | Description |
361
- |---|---|---|---|
362
- | `ANTHROPIC_API_KEY` | string | — | Required for `--claude-tokenizer api` |
363
- | `TARE_CLAUDE_TOKENIZER` | `local`, `api` | `local` | Override `--claude-tokenizer` via env |
364
- | `TARE_ANTHROPIC_MODEL` | model ID | `claude-sonnet-4-6` | Model used for API-backed token counting |
365
- | `TARE_DISABLE_ANTHROPIC_TOKEN_API` | `1` | unset | Disable API-backed counting even when requested |
432
+ | Variable | Values | Default | Description |
433
+ | ---------------------------------- | -------------- | ------------------- | ----------------------------------------------- |
434
+ | `ANTHROPIC_API_KEY` | string | — | Required for `--claude-tokenizer api` |
435
+ | `TARE_CLAUDE_TOKENIZER` | `local`, `api` | `local` | Override `--claude-tokenizer` via env |
436
+ | `TARE_ANTHROPIC_MODEL` | model ID | `claude-sonnet-4-6` | Model used for API-backed token counting |
437
+ | `TARE_DISABLE_ANTHROPIC_TOKEN_API` | `1` | unset | Disable API-backed counting even when requested |
366
438
 
367
439
  ## Security model
368
440
 
@@ -512,11 +584,11 @@ When a growth is intentional, regenerate `.tare/baseline.json` on the accepted s
512
584
 
513
585
  Exit codes:
514
586
 
515
- | Code | Meaning |
516
- |---:|---|
517
- | `0` | Diff completed and thresholds passed, or no thresholds were set. |
518
- | `1` | A configured regression threshold was exceeded. |
519
- | `2` | Invalid usage or invalid input, including missing files, invalid JSON, or missing report fields. |
587
+ | Code | Meaning |
588
+ | ---: | ------------------------------------------------------------------------------------------------ |
589
+ | `0` | Diff completed and thresholds passed, or no thresholds were set. |
590
+ | `1` | A configured regression threshold was exceeded. |
591
+ | `2` | Invalid usage or invalid input, including missing files, invalid JSON, or missing report fields. |
520
592
 
521
593
  Estimates are still estimates. The value of the baseline workflow is consistency: the same tool estimates are compared over time, so accidental MCP bloat becomes visible during review.
522
594
 
@@ -579,6 +651,7 @@ npx tare-mcp --no-exec --json
579
651
  ## Publishing to npm
580
652
 
581
653
  This repository includes [`.github/workflows/publish-npm.yml`](.github/workflows/publish-npm.yml).
654
+ Maintainers should use the [release checklist](https://github.com/nishantmodak/tare-mcp/blob/main/docs/releasing.md).
582
655
 
583
656
  To publish from GitHub Actions:
584
657
 
@@ -656,15 +729,23 @@ Options:
656
729
 
657
730
  ## Roadmap
658
731
 
732
+ v0.3:
733
+
734
+ - [x] Programmatic API for running agents through `measureTools()`
735
+ - [x] Programmatic JSON reports compatible with `tare-mcp diff`
736
+
659
737
  v0.2:
738
+
660
739
  - [x] PR diff/regression mode for JSON reports
661
740
  - [x] Threshold flags for token, tool, server, and overlap growth
662
741
 
663
742
  Next:
743
+
664
744
  - [ ] Per-tool schema breakdown
665
745
  - [ ] Context budget config file (`tare.config.json`)
666
746
 
667
747
  Later:
748
+
668
749
  - [ ] Better SSE fallback
669
750
  - [ ] Improved Claude local token estimator
670
751
  - [ ] Opt-in API-backed token counting improvements
@@ -673,7 +754,7 @@ Later:
673
754
  - [ ] MCP profile generator
674
755
  - [ ] `tare-mcp --fix` to generate lean MCP profiles
675
756
 
676
- Dashboards, profile generation, and auto-fix are intentionally not part of v0.2.
757
+ Dashboards, profile generation, and auto-fix are intentionally not part of v0.3.
677
758
 
678
759
  ## License
679
760
 
package/dist/cli.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { Command } from 'commander';
2
+
3
+ declare function createProgram(): Command;
4
+
5
+ export { createProgram };
package/dist/cli.js CHANGED
@@ -5,7 +5,7 @@ import { Command } from "commander";
5
5
  import { z as z4 } from "zod";
6
6
 
7
7
  // src/version.ts
8
- var VERSION = "0.2.1";
8
+ var VERSION = "0.3.0";
9
9
 
10
10
  // src/utils/stableJson.ts
11
11
  function stableValue(value) {
@@ -387,7 +387,7 @@ async function analyzeServers(inspectedServers, tokenEstimator, options) {
387
387
  estimatedTokens: analyzedTool.estimatedTokens,
388
388
  hasInputSchema: analyzedTool.hasInputSchema
389
389
  });
390
- if (server.inspectionMode === "live") {
390
+ if (server.inspectionMode === "live" || server.inspectionMode === "programmatic") {
391
391
  liveToolsForOverlap.push(analyzedTool);
392
392
  }
393
393
  }
@@ -444,7 +444,9 @@ async function analyzeServers(inspectedServers, tokenEstimator, options) {
444
444
  openaiCl100k: windowUsage(totalOpenAi, 2e5)
445
445
  }
446
446
  },
447
- insufficientServers: analyzedServers.filter((server) => server.inspectionMode !== "live").length
447
+ insufficientServers: analyzedServers.filter(
448
+ (server) => server.inspectionMode === "static-insufficient" || server.inspectionMode === "fallback-static-insufficient"
449
+ ).length
448
450
  },
449
451
  servers: analyzedServers,
450
452
  overlapClusters,
@@ -452,7 +454,7 @@ async function analyzeServers(inspectedServers, tokenEstimator, options) {
452
454
  warnings,
453
455
  metadata: {
454
456
  staticOnly: options.staticOnly,
455
- inspectionMode: options.staticOnly ? "static-only" : "live default"
457
+ inspectionMode: options.inspectionMode ?? (options.staticOnly ? "static-only" : "live default")
456
458
  }
457
459
  };
458
460
  report.recommendations = buildRecommendations(report);
@@ -996,13 +998,18 @@ var TareReportSchema = z3.object({
996
998
  z3.object({
997
999
  name: z3.string(),
998
1000
  sourceConfigPath: z3.string(),
999
- transport: z3.enum(["stdio", "streamable-http", "sse", "unknown"]),
1001
+ transport: z3.enum(["stdio", "streamable-http", "sse", "programmatic", "unknown"]),
1000
1002
  command: z3.string().optional(),
1001
1003
  args: z3.array(z3.string()).optional(),
1002
1004
  urlHost: z3.string().optional(),
1003
1005
  toolCount: z3.number(),
1004
1006
  estimatedTokens: TokenTotalsSchema,
1005
- inspectionMode: z3.enum(["live", "static-insufficient", "fallback-static-insufficient"]),
1007
+ inspectionMode: z3.enum([
1008
+ "live",
1009
+ "programmatic",
1010
+ "static-insufficient",
1011
+ "fallback-static-insufficient"
1012
+ ]),
1006
1013
  confidence: z3.enum(["high", "medium", "low"]),
1007
1014
  warnings: z3.array(z3.string()),
1008
1015
  tools: z3.array(
@@ -1025,7 +1032,10 @@ var TareReportSchema = z3.object({
1025
1032
  warnings: z3.array(z3.string()),
1026
1033
  metadata: z3.object({
1027
1034
  staticOnly: z3.boolean(),
1028
- inspectionMode: z3.enum(["live default", "static-only"])
1035
+ inspectionMode: z3.enum(["live default", "static-only", "programmatic"]),
1036
+ budgetExceeded: z3.boolean().optional(),
1037
+ budgetTokens: z3.number().optional(),
1038
+ budgetTokenizer: z3.literal("claude").optional()
1029
1039
  }).passthrough()
1030
1040
  }).passthrough();
1031
1041
  var ReportLoadError = class extends Error {
@@ -1908,7 +1918,9 @@ function renderHumanReport(report) {
1908
1918
  }
1909
1919
  }
1910
1920
  }
1911
- const insufficientServers = report.servers.filter((server) => server.inspectionMode !== "live");
1921
+ const insufficientServers = report.servers.filter(
1922
+ (server) => server.inspectionMode === "static-insufficient" || server.inspectionMode === "fallback-static-insufficient"
1923
+ );
1912
1924
  if (insufficientServers.length > 0) {
1913
1925
  lines.push("");
1914
1926
  lines.push("Insufficient data:");
@@ -0,0 +1,425 @@
1
+ import { z } from 'zod';
2
+
3
+ declare const VERSION = "0.3.0";
4
+
5
+ type TransportKind = "stdio" | "streamable-http" | "http" | "sse" | "unknown";
6
+ type ReportTransportKind = "stdio" | "streamable-http" | "sse" | "programmatic" | "unknown";
7
+ type InspectionMode = "live" | "programmatic" | "static-insufficient" | "fallback-static-insufficient";
8
+ type Confidence = "high" | "medium" | "low";
9
+ type NormalizedServer = {
10
+ name: string;
11
+ command?: string;
12
+ args?: string[];
13
+ env?: Record<string, string>;
14
+ url?: string;
15
+ headers?: Record<string, string>;
16
+ disabled?: boolean;
17
+ sourceConfigPath: string;
18
+ transport?: TransportKind;
19
+ };
20
+ type McpToolDefinition = {
21
+ name: string;
22
+ description?: string;
23
+ inputSchema?: unknown;
24
+ annotations?: unknown;
25
+ outputSchema?: unknown;
26
+ metadata?: unknown;
27
+ };
28
+ type InspectedServer = {
29
+ name: string;
30
+ sourceConfigPath: string;
31
+ transport: ReportTransportKind;
32
+ command?: string;
33
+ args?: string[];
34
+ urlHost?: string;
35
+ toolDefinitions: McpToolDefinition[];
36
+ inspectionMode: InspectionMode;
37
+ confidence: Confidence;
38
+ warnings: string[];
39
+ };
40
+ type InspectorOptions = {
41
+ timeoutMs: number;
42
+ fetch?: typeof globalThis.fetch;
43
+ };
44
+ type ToolContextPayload = {
45
+ server: string;
46
+ transport: ReportTransportKind;
47
+ tool: {
48
+ name: string;
49
+ description?: string;
50
+ inputSchema?: unknown;
51
+ annotations?: unknown;
52
+ outputSchema?: unknown;
53
+ metadata?: unknown;
54
+ };
55
+ };
56
+
57
+ declare function getDefaultConfigCandidates(cwd?: string, home?: string): string[];
58
+ type DiscoverConfigResult = {
59
+ paths: string[];
60
+ warnings: string[];
61
+ };
62
+ declare function discoverConfigs(cwd?: string, home?: string): Promise<DiscoverConfigResult>;
63
+
64
+ type ParsedConfig = {
65
+ path: string;
66
+ servers: NormalizedServer[];
67
+ warnings: string[];
68
+ };
69
+ declare function parseConfigText(text: string, sourceConfigPath: string): ParsedConfig;
70
+ declare function parseConfigFile(filePath: string): Promise<ParsedConfig>;
71
+
72
+ type NormalizeResult = {
73
+ server?: NormalizedServer;
74
+ warnings: string[];
75
+ };
76
+ declare function normalizeServer(name: string, rawConfig: unknown, sourceConfigPath: string): NormalizeResult;
77
+
78
+ declare function createStaticInspection(server: NormalizedServer, mode?: InspectionMode, warnings?: string[]): InspectedServer;
79
+
80
+ declare function buildServerEnv(server: NormalizedServer): Record<string, string>;
81
+ declare function inspectStdioServer(server: NormalizedServer, options: InspectorOptions): Promise<InspectedServer>;
82
+
83
+ declare function inspectStreamableHttpServer(server: NormalizedServer, options: InspectorOptions): Promise<InspectedServer>;
84
+
85
+ type TokenEstimate = {
86
+ tokenizer: "claude-estimate" | "claude-api" | "openai-cl100k" | "fallback-char-ratio";
87
+ tokens: number;
88
+ confidence: "high" | "medium" | "low";
89
+ warning?: string;
90
+ };
91
+ interface TokenCounter {
92
+ count(text: string): Promise<TokenEstimate>;
93
+ }
94
+ type DualTokenEstimate = {
95
+ claude: TokenEstimate;
96
+ openaiCl100k: TokenEstimate;
97
+ };
98
+ type ClaudeTokenizerMode = "local" | "api";
99
+
100
+ type CountTokensOptions = {
101
+ claudeTokenizerMode: ClaudeTokenizerMode;
102
+ anthropicApiKey?: string;
103
+ anthropicModel?: string;
104
+ anthropicDisabled?: boolean;
105
+ timeoutMs?: number;
106
+ onWarning?: (warning: string) => void;
107
+ };
108
+ declare class TokenEstimator {
109
+ private readonly options;
110
+ private readonly cache;
111
+ private readonly openAiCounter;
112
+ private readonly anthropicQueue;
113
+ private anthropicApiUnavailable;
114
+ private emittedMissingKeyWarning;
115
+ private emittedDisabledWarning;
116
+ private emittedApiFailureWarning;
117
+ constructor(options: CountTokensOptions);
118
+ count(text: string): Promise<DualTokenEstimate>;
119
+ private countClaude;
120
+ private countClaudeWithApi;
121
+ }
122
+
123
+ type AnalyzedTool = {
124
+ server: string;
125
+ name: string;
126
+ description?: string;
127
+ inputSchema?: unknown;
128
+ estimatedTokens: {
129
+ claude: number;
130
+ openaiCl100k: number;
131
+ };
132
+ hasInputSchema: boolean;
133
+ };
134
+ type OverlapCluster = {
135
+ label: string;
136
+ score: number;
137
+ reason: string;
138
+ signals: Array<"tfidf" | "intent-heuristic">;
139
+ tools: Array<{
140
+ server: string;
141
+ name: string;
142
+ description?: string;
143
+ estimatedTokens?: {
144
+ claude?: number;
145
+ openaiCl100k?: number;
146
+ };
147
+ }>;
148
+ recommendation: string;
149
+ };
150
+ type TareReport = {
151
+ version: string;
152
+ generatedAt: string;
153
+ summary: {
154
+ configFiles: number;
155
+ servers: number;
156
+ tools: number;
157
+ estimatedTokens: {
158
+ claude: number;
159
+ openaiCl100k: number;
160
+ };
161
+ contextWindows: {
162
+ "64000": {
163
+ claude: number;
164
+ openaiCl100k: number;
165
+ };
166
+ "128000": {
167
+ claude: number;
168
+ openaiCl100k: number;
169
+ };
170
+ "200000": {
171
+ claude: number;
172
+ openaiCl100k: number;
173
+ };
174
+ };
175
+ insufficientServers: number;
176
+ };
177
+ servers: Array<{
178
+ name: string;
179
+ sourceConfigPath: string;
180
+ transport: ReportTransportKind;
181
+ command?: string;
182
+ args?: string[];
183
+ urlHost?: string;
184
+ toolCount: number;
185
+ estimatedTokens: {
186
+ claude: number;
187
+ openaiCl100k: number;
188
+ };
189
+ inspectionMode: InspectionMode;
190
+ confidence: Confidence;
191
+ warnings: string[];
192
+ tools: Array<{
193
+ name: string;
194
+ description?: string;
195
+ estimatedTokens: {
196
+ claude: number;
197
+ openaiCl100k: number;
198
+ };
199
+ hasInputSchema: boolean;
200
+ }>;
201
+ }>;
202
+ overlapClusters: OverlapCluster[];
203
+ recommendations: Array<{
204
+ type: string;
205
+ message: string;
206
+ }>;
207
+ warnings: string[];
208
+ metadata: {
209
+ staticOnly: boolean;
210
+ inspectionMode: "live default" | "static-only" | "programmatic";
211
+ budgetExceeded?: boolean;
212
+ budgetTokens?: number;
213
+ budgetTokenizer?: "claude";
214
+ };
215
+ };
216
+
217
+ type AnalyzeOptions = {
218
+ configFiles: number;
219
+ staticOnly: boolean;
220
+ warnings?: string[];
221
+ inspectionMode?: "live default" | "static-only" | "programmatic";
222
+ };
223
+ declare function analyzeServers(inspectedServers: InspectedServer[], tokenEstimator: TokenEstimator, options: AnalyzeOptions): Promise<TareReport>;
224
+
225
+ type McpToolInput = {
226
+ name: string;
227
+ description?: string;
228
+ inputSchema?: unknown;
229
+ annotations?: unknown;
230
+ outputSchema?: unknown;
231
+ metadata?: unknown;
232
+ };
233
+ type AttributedMcpToolInput = McpToolInput & {
234
+ server: string;
235
+ };
236
+ type MeasureToolsOptions = {
237
+ serverName?: string;
238
+ budget?: number;
239
+ claudeTokenizerMode?: ClaudeTokenizerMode;
240
+ anthropicApiKey?: string;
241
+ anthropicModel?: string;
242
+ timeoutMs?: number;
243
+ };
244
+
245
+ declare function measureTools(tools: readonly (McpToolInput | AttributedMcpToolInput)[], options?: MeasureToolsOptions): Promise<TareReport>;
246
+
247
+ declare class OverlapDetector {
248
+ private readonly threshold;
249
+ constructor(threshold?: number);
250
+ detect(tools: AnalyzedTool[]): OverlapCluster[];
251
+ }
252
+
253
+ declare function buildRecommendations(report: Pick<TareReport, "summary" | "overlapClusters" | "servers">): TareReport["recommendations"];
254
+
255
+ declare class OpenAICl100kCounter implements TokenCounter {
256
+ count(text: string): Promise<TokenEstimate>;
257
+ }
258
+
259
+ declare class LocalClaudeEstimator implements TokenCounter {
260
+ private readonly openAiCount?;
261
+ constructor(openAiCount?: (() => Promise<TokenEstimate>) | undefined);
262
+ count(text: string): Promise<TokenEstimate>;
263
+ }
264
+
265
+ type BudgetTokenizer = "claude" | "openai";
266
+ declare function renderHumanReport(report: TareReport): string;
267
+ declare function renderBudgetFailure(report: TareReport, budget: number, tokenizer: BudgetTokenizer): string;
268
+
269
+ declare function renderJsonReport(report: TareReport): string;
270
+
271
+ declare const TareReportSchema: z.ZodType<TareReport>;
272
+ type LoadedTareReport = {
273
+ path: string;
274
+ report: TareReport;
275
+ };
276
+ declare class ReportLoadError extends Error {
277
+ readonly path: string;
278
+ constructor(filePath: string, message: string);
279
+ }
280
+ declare function loadReport(filePath: string): Promise<LoadedTareReport>;
281
+
282
+ type DiffTokenizer = "claude" | "openai";
283
+ type DiffTokenTotals = {
284
+ claude: number;
285
+ openaiCl100k: number;
286
+ };
287
+ type NumericDelta = {
288
+ base: number;
289
+ head: number;
290
+ delta: number;
291
+ };
292
+ type TokenDelta = {
293
+ base: DiffTokenTotals;
294
+ head: DiffTokenTotals;
295
+ delta: DiffTokenTotals;
296
+ };
297
+ type ValueDelta<T> = {
298
+ base: T;
299
+ head: T;
300
+ changed: boolean;
301
+ };
302
+ type DiffServer = {
303
+ name: string;
304
+ sourceConfigPath: string;
305
+ transport: ReportTransportKind;
306
+ command?: string;
307
+ args?: string[];
308
+ urlHost?: string;
309
+ toolCount: number;
310
+ estimatedTokens: DiffTokenTotals;
311
+ inspectionMode: InspectionMode;
312
+ confidence: Confidence;
313
+ };
314
+ type DiffTool = {
315
+ server: string;
316
+ name: string;
317
+ description?: string;
318
+ estimatedTokens: DiffTokenTotals;
319
+ hasInputSchema: boolean;
320
+ };
321
+ type DiffServerChange = {
322
+ name: string;
323
+ toolCount: NumericDelta;
324
+ estimatedTokens: TokenDelta;
325
+ transport: ValueDelta<ReportTransportKind>;
326
+ sourceConfigPath: ValueDelta<string>;
327
+ command: ValueDelta<string | null>;
328
+ args: ValueDelta<string[] | null>;
329
+ urlHost: ValueDelta<string | null>;
330
+ inspectionMode: ValueDelta<InspectionMode>;
331
+ confidence: ValueDelta<Confidence>;
332
+ };
333
+ type DiffToolChange = {
334
+ server: string;
335
+ name: string;
336
+ estimatedTokens: TokenDelta;
337
+ descriptionChanged: boolean;
338
+ inputSchemaPresenceChanged: boolean;
339
+ };
340
+ type DiffOverlapCluster = {
341
+ id: string;
342
+ label: string;
343
+ score: number;
344
+ tools: Array<{
345
+ server: string;
346
+ name: string;
347
+ }>;
348
+ recommendation: string;
349
+ };
350
+ type ThresholdResult = {
351
+ flag: string;
352
+ tokenizer?: DiffTokenizer;
353
+ allowed: number;
354
+ actual: number;
355
+ exceeded: boolean;
356
+ };
357
+ type TareDiffReport = {
358
+ version: string;
359
+ generatedAt: string;
360
+ base: {
361
+ path: string;
362
+ reportVersion: string;
363
+ generatedAt: string;
364
+ };
365
+ head: {
366
+ path: string;
367
+ reportVersion: string;
368
+ generatedAt: string;
369
+ };
370
+ summary: {
371
+ servers: NumericDelta;
372
+ tools: NumericDelta;
373
+ estimatedTokens: TokenDelta;
374
+ overlapClusters: NumericDelta;
375
+ };
376
+ servers: {
377
+ added: DiffServer[];
378
+ removed: DiffServer[];
379
+ changed: DiffServerChange[];
380
+ };
381
+ tools: {
382
+ added: DiffTool[];
383
+ removed: DiffTool[];
384
+ changed: DiffToolChange[];
385
+ };
386
+ overlapClusters: {
387
+ added: DiffOverlapCluster[];
388
+ removed: DiffOverlapCluster[];
389
+ };
390
+ thresholds: ThresholdResult[];
391
+ recommendations: Array<{
392
+ type: string;
393
+ message: string;
394
+ }>;
395
+ warnings: string[];
396
+ };
397
+ type DiffThresholdOptions = {
398
+ maxTokenIncrease?: number;
399
+ maxToolIncrease?: number;
400
+ maxServerIncrease?: number;
401
+ maxOverlapIncrease?: number;
402
+ tokenizer: DiffTokenizer;
403
+ };
404
+
405
+ type DiffReportsOptions = {
406
+ basePath: string;
407
+ headPath: string;
408
+ generatedAt?: string;
409
+ };
410
+ type ReportOverlapCluster = TareReport["overlapClusters"][number];
411
+ declare function diffReports(baseReport: TareReport, headReport: TareReport, options: DiffReportsOptions): TareDiffReport;
412
+ declare function overlapClusterIdentity(cluster: ReportOverlapCluster): string;
413
+
414
+ declare function evaluateDiffThresholds(report: TareDiffReport, options: DiffThresholdOptions): ThresholdResult[];
415
+ declare function hasThresholdFailure(report: Pick<TareDiffReport, "thresholds">): boolean;
416
+
417
+ type DiffHumanReporterOptions = {
418
+ tokenizer: DiffTokenizer;
419
+ };
420
+ declare function renderDiffHumanReport(report: TareDiffReport, options: DiffHumanReporterOptions): string;
421
+ declare function renderDiffThresholdFailure(report: TareDiffReport, options: DiffHumanReporterOptions): string;
422
+
423
+ declare function renderDiffJsonReport(report: TareDiffReport): string;
424
+
425
+ export { type AnalyzedTool, type AttributedMcpToolInput, type ClaudeTokenizerMode, type Confidence, type DiffHumanReporterOptions, type DiffOverlapCluster, type DiffReportsOptions, type DiffServer, type DiffServerChange, type DiffThresholdOptions, type DiffTokenTotals, type DiffTokenizer, type DiffTool, type DiffToolChange, type DualTokenEstimate, type InspectedServer, type InspectionMode, LocalClaudeEstimator, type McpToolDefinition, type McpToolInput, type MeasureToolsOptions, type NormalizedServer, type NumericDelta, OpenAICl100kCounter, type OverlapCluster, OverlapDetector, ReportLoadError, type ReportTransportKind, type TareDiffReport, type TareReport, TareReportSchema, type ThresholdResult, type TokenCounter, type TokenDelta, type TokenEstimate, TokenEstimator, type ToolContextPayload, type TransportKind, VERSION, type ValueDelta, analyzeServers, buildRecommendations, buildServerEnv, createStaticInspection, diffReports, discoverConfigs, evaluateDiffThresholds, getDefaultConfigCandidates, hasThresholdFailure, inspectStdioServer, inspectStreamableHttpServer, loadReport, measureTools, normalizeServer, overlapClusterIdentity, parseConfigFile, parseConfigText, renderBudgetFailure, renderDiffHumanReport, renderDiffJsonReport, renderDiffThresholdFailure, renderHumanReport, renderJsonReport };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/version.ts
4
- var VERSION = "0.2.1";
4
+ var VERSION = "0.3.0";
5
5
 
6
6
  // src/discovery/discoverConfigs.ts
7
7
  import os2 from "os";
@@ -1017,7 +1017,7 @@ async function analyzeServers(inspectedServers, tokenEstimator, options) {
1017
1017
  estimatedTokens: analyzedTool.estimatedTokens,
1018
1018
  hasInputSchema: analyzedTool.hasInputSchema
1019
1019
  });
1020
- if (server.inspectionMode === "live") {
1020
+ if (server.inspectionMode === "live" || server.inspectionMode === "programmatic") {
1021
1021
  liveToolsForOverlap.push(analyzedTool);
1022
1022
  }
1023
1023
  }
@@ -1074,7 +1074,9 @@ async function analyzeServers(inspectedServers, tokenEstimator, options) {
1074
1074
  openaiCl100k: windowUsage(totalOpenAi, 2e5)
1075
1075
  }
1076
1076
  },
1077
- insufficientServers: analyzedServers.filter((server) => server.inspectionMode !== "live").length
1077
+ insufficientServers: analyzedServers.filter(
1078
+ (server) => server.inspectionMode === "static-insufficient" || server.inspectionMode === "fallback-static-insufficient"
1079
+ ).length
1078
1080
  },
1079
1081
  servers: analyzedServers,
1080
1082
  overlapClusters,
@@ -1082,7 +1084,7 @@ async function analyzeServers(inspectedServers, tokenEstimator, options) {
1082
1084
  warnings,
1083
1085
  metadata: {
1084
1086
  staticOnly: options.staticOnly,
1085
- inspectionMode: options.staticOnly ? "static-only" : "live default"
1087
+ inspectionMode: options.inspectionMode ?? (options.staticOnly ? "static-only" : "live default")
1086
1088
  }
1087
1089
  };
1088
1090
  report.recommendations = buildRecommendations(report);
@@ -1288,6 +1290,121 @@ var TokenEstimator = class {
1288
1290
  }
1289
1291
  };
1290
1292
 
1293
+ // src/api/measureTools.ts
1294
+ var DEFAULT_SERVER_NAME = "agent";
1295
+ var PROGRAMMATIC_SOURCE = "programmatic";
1296
+ async function measureTools(tools, options = {}) {
1297
+ validateTools(tools);
1298
+ const normalizedOptions = validateOptions(options);
1299
+ const tokenWarnings = [];
1300
+ const inspectedServers = inspectedServersFromTools(tools, normalizedOptions.serverName);
1301
+ const report = await analyzeServers(
1302
+ inspectedServers,
1303
+ new TokenEstimator({
1304
+ claudeTokenizerMode: normalizedOptions.claudeTokenizerMode ?? "local",
1305
+ anthropicApiKey: normalizedOptions.anthropicApiKey,
1306
+ anthropicModel: normalizedOptions.anthropicModel,
1307
+ timeoutMs: normalizedOptions.timeoutMs,
1308
+ onWarning: (warning) => tokenWarnings.push(warning)
1309
+ }),
1310
+ {
1311
+ configFiles: 0,
1312
+ staticOnly: false,
1313
+ inspectionMode: "programmatic"
1314
+ }
1315
+ );
1316
+ report.warnings.push(...tokenWarnings);
1317
+ if (normalizedOptions.budget !== void 0) {
1318
+ report.metadata.budgetTokens = normalizedOptions.budget;
1319
+ report.metadata.budgetTokenizer = "claude";
1320
+ report.metadata.budgetExceeded = report.summary.estimatedTokens.claude > normalizedOptions.budget;
1321
+ }
1322
+ return report;
1323
+ }
1324
+ function inspectedServersFromTools(tools, fallbackServerName) {
1325
+ const groups = /* @__PURE__ */ new Map();
1326
+ const defaultServerName = normalizeServerName(fallbackServerName) ?? DEFAULT_SERVER_NAME;
1327
+ for (const tool of tools) {
1328
+ const serverName = serverNameForTool(tool, defaultServerName);
1329
+ const group = groups.get(serverName) ?? [];
1330
+ group.push(toToolDefinition(tool));
1331
+ groups.set(serverName, group);
1332
+ }
1333
+ return [...groups.entries()].map(([name, toolDefinitions]) => ({
1334
+ name,
1335
+ sourceConfigPath: PROGRAMMATIC_SOURCE,
1336
+ transport: "programmatic",
1337
+ toolDefinitions,
1338
+ inspectionMode: "programmatic",
1339
+ confidence: "high",
1340
+ warnings: []
1341
+ }));
1342
+ }
1343
+ function serverNameForTool(tool, fallbackServerName) {
1344
+ return isAttributedTool(tool) ? normalizeServerName(tool.server) ?? fallbackServerName : fallbackServerName;
1345
+ }
1346
+ function toToolDefinition(tool) {
1347
+ return {
1348
+ name: tool.name,
1349
+ description: tool.description,
1350
+ inputSchema: tool.inputSchema,
1351
+ annotations: tool.annotations,
1352
+ outputSchema: tool.outputSchema,
1353
+ metadata: tool.metadata
1354
+ };
1355
+ }
1356
+ function isAttributedTool(tool) {
1357
+ return "server" in tool && typeof tool.server === "string" && tool.server.trim().length > 0;
1358
+ }
1359
+ function normalizeServerName(serverName) {
1360
+ const trimmed = serverName?.trim();
1361
+ return trimmed && trimmed.length > 0 ? trimmed : void 0;
1362
+ }
1363
+ function validateTools(tools) {
1364
+ if (!Array.isArray(tools)) {
1365
+ throw new TypeError("measureTools expected tools to be an array.");
1366
+ }
1367
+ for (const [index, tool] of tools.entries()) {
1368
+ if (!tool || typeof tool !== "object") {
1369
+ throw new TypeError(`measureTools expected tools[${index}] to be an object.`);
1370
+ }
1371
+ if (typeof tool.name !== "string" || tool.name.trim().length === 0) {
1372
+ throw new TypeError(`measureTools expected tools[${index}].name to be a non-empty string.`);
1373
+ }
1374
+ if ("server" in tool && tool.server !== void 0 && (typeof tool.server !== "string" || tool.server.trim().length === 0)) {
1375
+ throw new TypeError(
1376
+ `measureTools expected tools[${index}].server to be a non-empty string when provided.`
1377
+ );
1378
+ }
1379
+ }
1380
+ }
1381
+ function validateOptions(options) {
1382
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
1383
+ throw new TypeError("measureTools expected options to be an object when provided.");
1384
+ }
1385
+ if (options.serverName !== void 0 && normalizeServerName(options.serverName) === void 0) {
1386
+ throw new TypeError("measureTools expected options.serverName to be a non-empty string.");
1387
+ }
1388
+ if (options.budget !== void 0 && (!Number.isFinite(options.budget) || options.budget < 0)) {
1389
+ throw new TypeError("measureTools expected options.budget to be a non-negative number.");
1390
+ }
1391
+ if (options.timeoutMs !== void 0 && (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0)) {
1392
+ throw new TypeError("measureTools expected options.timeoutMs to be a positive number.");
1393
+ }
1394
+ if (options.claudeTokenizerMode !== void 0 && options.claudeTokenizerMode !== "local" && options.claudeTokenizerMode !== "api") {
1395
+ throw new TypeError(
1396
+ 'measureTools expected options.claudeTokenizerMode to be "local" or "api".'
1397
+ );
1398
+ }
1399
+ if (options.anthropicApiKey !== void 0 && typeof options.anthropicApiKey !== "string") {
1400
+ throw new TypeError("measureTools expected options.anthropicApiKey to be a string.");
1401
+ }
1402
+ if (options.anthropicModel !== void 0 && typeof options.anthropicModel !== "string") {
1403
+ throw new TypeError("measureTools expected options.anthropicModel to be a string.");
1404
+ }
1405
+ return options;
1406
+ }
1407
+
1291
1408
  // src/reporters/humanReporter.ts
1292
1409
  import pc from "picocolors";
1293
1410
  function formatNumber(value) {
@@ -1381,7 +1498,9 @@ function renderHumanReport(report) {
1381
1498
  }
1382
1499
  }
1383
1500
  }
1384
- const insufficientServers = report.servers.filter((server) => server.inspectionMode !== "live");
1501
+ const insufficientServers = report.servers.filter(
1502
+ (server) => server.inspectionMode === "static-insufficient" || server.inspectionMode === "fallback-static-insufficient"
1503
+ );
1385
1504
  if (insufficientServers.length > 0) {
1386
1505
  lines.push("");
1387
1506
  lines.push("Insufficient data:");
@@ -1483,13 +1602,18 @@ var TareReportSchema = z3.object({
1483
1602
  z3.object({
1484
1603
  name: z3.string(),
1485
1604
  sourceConfigPath: z3.string(),
1486
- transport: z3.enum(["stdio", "streamable-http", "sse", "unknown"]),
1605
+ transport: z3.enum(["stdio", "streamable-http", "sse", "programmatic", "unknown"]),
1487
1606
  command: z3.string().optional(),
1488
1607
  args: z3.array(z3.string()).optional(),
1489
1608
  urlHost: z3.string().optional(),
1490
1609
  toolCount: z3.number(),
1491
1610
  estimatedTokens: TokenTotalsSchema,
1492
- inspectionMode: z3.enum(["live", "static-insufficient", "fallback-static-insufficient"]),
1611
+ inspectionMode: z3.enum([
1612
+ "live",
1613
+ "programmatic",
1614
+ "static-insufficient",
1615
+ "fallback-static-insufficient"
1616
+ ]),
1493
1617
  confidence: z3.enum(["high", "medium", "low"]),
1494
1618
  warnings: z3.array(z3.string()),
1495
1619
  tools: z3.array(
@@ -1512,7 +1636,10 @@ var TareReportSchema = z3.object({
1512
1636
  warnings: z3.array(z3.string()),
1513
1637
  metadata: z3.object({
1514
1638
  staticOnly: z3.boolean(),
1515
- inspectionMode: z3.enum(["live default", "static-only"])
1639
+ inspectionMode: z3.enum(["live default", "static-only", "programmatic"]),
1640
+ budgetExceeded: z3.boolean().optional(),
1641
+ budgetTokens: z3.number().optional(),
1642
+ budgetTokenizer: z3.literal("claude").optional()
1516
1643
  }).passthrough()
1517
1644
  }).passthrough();
1518
1645
  var ReportLoadError = class extends Error {
@@ -2186,6 +2313,7 @@ export {
2186
2313
  inspectStdioServer,
2187
2314
  inspectStreamableHttpServer,
2188
2315
  loadReport,
2316
+ measureTools,
2189
2317
  normalizeServer,
2190
2318
  overlapClusterIdentity,
2191
2319
  parseConfigFile,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tare-mcp",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Local-first CLI for analyzing MCP context weight and tool ambiguity.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -8,8 +8,12 @@
8
8
  "bin": {
9
9
  "tare-mcp": "./dist/cli.js"
10
10
  },
11
+ "types": "./dist/index.d.ts",
11
12
  "exports": {
12
- ".": "./dist/index.js"
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ }
13
17
  },
14
18
  "main": "./dist/index.js",
15
19
  "repository": {