tare-mcp 0.2.0 → 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
@@ -1,42 +1,54 @@
1
1
  # tare-mcp
2
2
 
3
- [![Build](https://github.com/nishantmodak/tare-mcp/actions/workflows/publish-npm.yml/badge.svg)](https://github.com/nishantmodak/tare-mcp/actions/workflows/publish-npm.yml)
4
- [![Version](https://img.shields.io/github/package-json/v/nishantmodak/tare-mcp?label=version)](package.json)
3
+ [![Build](https://github.com/nishantmodak/tare-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/nishantmodak/tare-mcp/actions/workflows/ci.yml)
5
4
  [![npm](https://img.shields.io/npm/v/tare-mcp?label=npm)](https://www.npmjs.com/package/tare-mcp)
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
- pnpm install
11
- pnpm build
12
- node dist/cli.js
10
+ npm install tare-mcp
13
11
  ```
14
12
 
15
- MCP made tools easy to connect.
16
- It did not make them cheap to carry.
13
+ ```ts
14
+ import { measureTools } from "tare-mcp";
17
15
 
18
- `tare-mcp` inspects your MCP setup and shows:
16
+ const tools = await mcpClient.listTools();
17
+ const report = await measureTools(tools, { budget: 40_000 });
18
+
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:
19
31
 
20
32
  - how many tools your agent sees
21
- - how much context those tools consume
33
+ - how much context those tools consume, estimated for Claude and OpenAI cl100k
22
34
  - which servers dominate the budget
23
35
  - which tools overlap and compete for model attention
24
36
  - whether your setup exceeds a context budget
25
37
 
26
- Think of it as:
38
+ Use the CLI when you want to inspect config-discovered MCP servers locally:
27
39
 
28
40
  ```bash
29
- du -sh node_modules
41
+ npx tare-mcp
30
42
  ```
31
43
 
32
- but for agent tool context.
44
+ Think of it as `du -sh node_modules`, but for agent tool context.
33
45
 
34
46
  ## Table of Contents
35
47
 
36
48
  - [Why This Matters](#why-this-matters)
37
49
  - [Why Token Count Is Not the Whole Problem](#why-token-count-is-not-the-whole-problem)
38
- - [Current Status](#current-status)
39
- - [Quickstart](#quickstart)
50
+ - [Using tare-mcp in your agent](#using-tare-mcp-in-your-agent)
51
+ - [CLI Quickstart](#cli-quickstart)
40
52
  - [Hosted MCP Quickstart](#hosted-mcp-quickstart)
41
53
  - [Scenario Examples](#scenario-examples)
42
54
  - [Example Output](#example-output)
@@ -74,25 +86,82 @@ If three servers all expose tools that look like "search", the model has to choo
74
86
  - what your tools weigh
75
87
  - where your tools overlap
76
88
 
77
- ## Current status
89
+ ## Using tare-mcp in your agent
78
90
 
79
- Current repository version: `0.2.0`.
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.
80
92
 
81
- The CLI is implemented as `tare-mcp`, and the package is configured for npm as `tare-mcp`. Until the first npm release is published, run it from this repository with `node dist/cli.js` after building. Once published, the same commands work through `npx tare-mcp` or an npm install.
93
+ Use `measureTools()` when you already have the tool definitions in memory:
82
94
 
83
- Most examples below use `npx tare-mcp` because that is the intended published interface. Before the first npm release, replace `npx tare-mcp` with `node /path/to/tare-mcp/dist/cli.js`.
95
+ ```ts
96
+ import { measureTools } from "tare-mcp";
84
97
 
85
- ## Quickstart
98
+ const tools = await mcpClient.listTools();
99
+ const report = await measureTools(tools);
86
100
 
87
- Run from source:
101
+ console.log(
102
+ `MCP tool surface: ${report.summary.tools} tools, ~${report.summary.estimatedTokens.claude} Claude tokens`
103
+ );
104
+ ```
88
105
 
89
- ```bash
90
- pnpm install
91
- pnpm build
92
- node dist/cli.js
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
+ });
93
151
  ```
94
152
 
95
- After the npm package is published:
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
163
+
164
+ Run it without installing:
96
165
 
97
166
  ```bash
98
167
  npx tare-mcp
@@ -120,26 +189,20 @@ Context window usage:
120
189
 
121
190
  If the output is empty or shows "Config files found: 0", see [Config discovery](#config-discovery).
122
191
 
123
- Install it in a project after the npm release:
192
+ Install it in a project:
124
193
 
125
194
  ```bash
126
195
  npm install --save-dev tare-mcp
127
196
  npx tare-mcp
128
197
  ```
129
198
 
130
- Install it globally after the npm release:
199
+ Install it globally:
131
200
 
132
201
  ```bash
133
202
  npm install --global tare-mcp
134
203
  tare-mcp
135
204
  ```
136
205
 
137
- For local development, keep using the source command:
138
-
139
- ```bash
140
- pnpm dev
141
- ```
142
-
143
206
  Static-only mode parses config without starting servers or calling hosted endpoints:
144
207
 
145
208
  ```bash
@@ -159,6 +222,14 @@ Emit JSON for CI or other tools:
159
222
  npx tare-mcp --json
160
223
  ```
161
224
 
225
+ For local development from this repository:
226
+
227
+ ```bash
228
+ pnpm install
229
+ pnpm build
230
+ pnpm dev
231
+ ```
232
+
162
233
  ## Hosted MCP Quickstart
163
234
 
164
235
  Use this when you want to inspect a real hosted MCP endpoint.
@@ -166,7 +237,19 @@ Use this when you want to inspect a real hosted MCP endpoint.
166
237
  ```bash
167
238
  mkdir -p /tmp/tare-mcp-hosted
168
239
  cd /tmp/tare-mcp-hosted
169
- cp /path/to/tare-mcp/examples/scenarios/hosted-streamable-http.mcp.json .mcp.json
240
+ cat > .mcp.json <<'JSON'
241
+ {
242
+ "mcpServers": {
243
+ "last9": {
244
+ "type": "http",
245
+ "url": "https://mcp.last9.io/mcp",
246
+ "headers": {
247
+ "Authorization": "Bearer ${LAST9_MCP_TOKEN}"
248
+ }
249
+ }
250
+ }
251
+ }
252
+ JSON
170
253
  export LAST9_MCP_TOKEN="..."
171
254
  npx tare-mcp --timeout 10000
172
255
  ```
@@ -302,10 +385,11 @@ Recommendations:
302
385
 
303
386
  ## Supported transports
304
387
 
305
- v0.2 supports live inspection for:
388
+ v0.3 supports:
306
389
 
307
390
  - stdio MCP servers
308
391
  - Streamable HTTP MCP servers
392
+ - programmatic tool definitions through `measureTools()`
309
393
 
310
394
  SSE may be supported best-effort later.
311
395
 
@@ -345,12 +429,12 @@ That mode requires `ANTHROPIC_API_KEY` and uses Anthropic's `POST /v1/messages/c
345
429
 
346
430
  Environment variables that control tokenization:
347
431
 
348
- | Variable | Values | Default | Description |
349
- |---|---|---|---|
350
- | `ANTHROPIC_API_KEY` | string | — | Required for `--claude-tokenizer api` |
351
- | `TARE_CLAUDE_TOKENIZER` | `local`, `api` | `local` | Override `--claude-tokenizer` via env |
352
- | `TARE_ANTHROPIC_MODEL` | model ID | `claude-sonnet-4-6` | Model used for API-backed token counting |
353
- | `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 |
354
438
 
355
439
  ## Security model
356
440
 
@@ -500,11 +584,11 @@ When a growth is intentional, regenerate `.tare/baseline.json` on the accepted s
500
584
 
501
585
  Exit codes:
502
586
 
503
- | Code | Meaning |
504
- |---:|---|
505
- | `0` | Diff completed and thresholds passed, or no thresholds were set. |
506
- | `1` | A configured regression threshold was exceeded. |
507
- | `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. |
508
592
 
509
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.
510
594
 
@@ -567,6 +651,7 @@ npx tare-mcp --no-exec --json
567
651
  ## Publishing to npm
568
652
 
569
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).
570
655
 
571
656
  To publish from GitHub Actions:
572
657
 
@@ -587,7 +672,7 @@ npm publish --access public --provenance
587
672
 
588
673
  The npm package is named `tare-mcp` because the unscoped `tare` package name is already occupied on npm.
589
674
 
590
- After the first npm release, users can install it with:
675
+ Users can install it with:
591
676
 
592
677
  ```bash
593
678
  npm install --save-dev tare-mcp
@@ -644,15 +729,23 @@ Options:
644
729
 
645
730
  ## Roadmap
646
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
+
647
737
  v0.2:
738
+
648
739
  - [x] PR diff/regression mode for JSON reports
649
740
  - [x] Threshold flags for token, tool, server, and overlap growth
650
741
 
651
742
  Next:
743
+
652
744
  - [ ] Per-tool schema breakdown
653
745
  - [ ] Context budget config file (`tare.config.json`)
654
746
 
655
747
  Later:
748
+
656
749
  - [ ] Better SSE fallback
657
750
  - [ ] Improved Claude local token estimator
658
751
  - [ ] Opt-in API-backed token counting improvements
@@ -661,7 +754,7 @@ Later:
661
754
  - [ ] MCP profile generator
662
755
  - [ ] `tare-mcp --fix` to generate lean MCP profiles
663
756
 
664
- 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.
665
758
 
666
759
  ## License
667
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.0";
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.0";
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.0",
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": {