@tensorfeed/mcp-server 2.0.2 → 2.0.3

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
@@ -2,6 +2,24 @@
2
2
 
3
3
  All notable changes to the [TensorFeed.ai MCP server](https://github.com/RipperMercs/tensorfeed-mcp). Free tools work without configuration; premium tools require a bearer token via the `TENSORFEED_TOKEN` env var. Buy credits at [tensorfeed.ai/developers/agent-payments](https://tensorfeed.ai/developers/agent-payments).
4
4
 
5
+ ## 2.0.3 - 2026-07-06
6
+
7
+ Error-path and input hardening, completing the 2.0.2 output work.
8
+
9
+ - Tool errors now pass through the same scrub as tool results. Previously a
10
+ thrown error's message (which can embed an upstream response body) went to
11
+ the host raw; every throw is now converted to a sanitized MCP tool error
12
+ with secrets redacted (the live bearer token, plus anything shaped like a
13
+ tf_live_ token), the standard control-char and role-token scrub applied,
14
+ and the text capped at 4k chars.
15
+ - Upstream error bodies embedded in error messages are capped at 1.5k chars
16
+ at the source.
17
+ - Every free-text tool parameter now carries a schema-level max length
18
+ (200 for identifiers, larger for comma lists, URLs, and search queries),
19
+ so oversized inputs are rejected by validation before reaching any code.
20
+
21
+ No tool signatures change and no behavior changes for valid inputs.
22
+
5
23
  ## 2.0.2 - 2026-07-06
6
24
 
7
25
  Output hardening. Caller-supplied identifiers that a tool echoes back in its
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { z } from 'zod';
5
5
  import { readFileSync } from 'node:fs';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { dirname, join } from 'node:path';
8
- import { sanitizeToolResponse, sanitizeForLLM, sanitizeReflectedValue } from './sanitize.js';
8
+ import { sanitizeToolResponse, sanitizeForLLM, sanitizeReflectedValue, sanitizeErrorText } from './sanitize.js';
9
9
  const API_BASE = 'https://tensorfeed.ai/api';
10
10
  // Per-request upstream timeout. fetchJSON is the single HTTP helper behind every
11
11
  // MCP tool and the latest_news resource, so a missing timeout would let one
@@ -67,16 +67,35 @@ async function fetchJSON(path, opts = {}) {
67
67
  catch {
68
68
  errPayload = await res.text().catch(() => '');
69
69
  }
70
+ // The upstream error body is untrusted input. Cap it here so a hostile
71
+ // or malfunctioning upstream (or a middlebox returning an HTML error
72
+ // page) cannot inflate the error message. The registerTool wrapper
73
+ // additionally runs the full error scrub before anything reaches the
74
+ // host LLM; this cap just bounds what gets embedded at the source.
75
+ const detail = errDetail(errPayload);
70
76
  if (res.status === 402) {
71
- throw new Error(`Payment required (402). Your token may be out of credits. Top up at https://tensorfeed.ai/developers/agent-payments. Detail: ${JSON.stringify(errPayload)}`);
77
+ throw new Error(`Payment required (402). Your token may be out of credits. Top up at https://tensorfeed.ai/developers/agent-payments. Detail: ${detail}`);
72
78
  }
73
79
  if (res.status === 401) {
74
- throw new Error(`Token rejected (401). Check that TENSORFEED_TOKEN is set to a valid tf_live_... token. Detail: ${JSON.stringify(errPayload)}`);
80
+ throw new Error(`Token rejected (401). Check that TENSORFEED_TOKEN is set to a valid tf_live_... token. Detail: ${detail}`);
75
81
  }
76
- throw new Error(`API error ${res.status}: ${JSON.stringify(errPayload)}`);
82
+ throw new Error(`API error ${res.status}: ${detail}`);
77
83
  }
78
84
  return res.json();
79
85
  }
86
+ // Stringify an upstream error payload for embedding in an Error message,
87
+ // bounded so an oversized body cannot balloon the error text.
88
+ const MAX_ERR_DETAIL_CHARS = 1500;
89
+ function errDetail(payload) {
90
+ let s;
91
+ try {
92
+ s = JSON.stringify(payload) ?? String(payload);
93
+ }
94
+ catch {
95
+ s = String(payload);
96
+ }
97
+ return s.length > MAX_ERR_DETAIL_CHARS ? s.slice(0, MAX_ERR_DETAIL_CHARS) + '...' : s;
98
+ }
80
99
  // ── Server setup ────────────────────────────────────────────────────
81
100
  const server = new McpServer({
82
101
  name: 'tensorfeed',
@@ -141,8 +160,25 @@ function registerTool(name, description, paramsSchema, cb, annotations) {
141
160
  // explicitly and bypass this inference.
142
161
  const finalAnnotations = annotations ?? (PAID_TOOL_RE.test(description) ? PREMIUM_READ_TOOL : READ_TOOL);
143
162
  const wrapped = (async (args, extra) => {
144
- const result = await cb(args, extra);
145
- return sanitizeToolResponse(result);
163
+ try {
164
+ const result = await cb(args, extra);
165
+ return sanitizeToolResponse(result);
166
+ }
167
+ catch (err) {
168
+ // The error path used to bypass the sanitizer entirely: a thrown
169
+ // Error's message (which can embed an upstream response body via
170
+ // fetchJSON) went to the SDK raw. Convert every throw into a
171
+ // sanitized MCP tool error instead: secrets redacted (the live
172
+ // bearer token, plus anything token-shaped), the standard
173
+ // control-char and role-token scrub applied, and the text capped,
174
+ // so the error channel offers no unsanitized path into the host
175
+ // LLM's context.
176
+ const raw = err instanceof Error ? err.message : String(err);
177
+ return {
178
+ content: [{ type: 'text', text: sanitizeErrorText(raw, [process.env.TENSORFEED_TOKEN]) }],
179
+ isError: true,
180
+ };
181
+ }
146
182
  });
147
183
  // Direct dispatch to the SDK's underlying registration. Do not call
148
184
  // registerTool() here or it will recurse forever.
@@ -188,7 +224,7 @@ server.registerResource('latest_news', 'tensorfeed://news/latest', {
188
224
  });
189
225
  // ── Tool: get_ai_news ───────────────────────────────────────────────
190
226
  registerTool('get_ai_news', 'Get the latest AI news from TensorFeed.ai as a ranked list with title, source, URL, snippet, and publish time, filterable by category (e.g. "anthropic", "openai", "research", "tools"). Aggregates 15+ sources (Anthropic, OpenAI, Google, TechCrunch, The Verge, arXiv, and more) into one normalized feed, so an agent reads one schema instead of polling each outlet. Free, no auth.', {
191
- category: z.string().optional().describe('Filter by category (e.g. "anthropic", "openai", "research", "tools")'),
227
+ category: z.string().max(100).optional().describe('Filter by category (e.g. "anthropic", "openai", "research", "tools")'),
192
228
  limit: z.number().min(1).max(50).optional().describe('Number of articles to return (default 10, max 50)'),
193
229
  digest: z.boolean().optional().describe('When true, return a headline-only digest (title, source, URL per story) instead of the full article set.'),
194
230
  }, async ({ category, limit, digest }) => {
@@ -219,7 +255,7 @@ registerTool('get_ai_news', 'Get the latest AI news from TensorFeed.ai as a rank
219
255
  });
220
256
  // ── Tool: find_tensorfeed_data (discovery, long-tail surface) ───────
221
257
  registerTool('find_tensorfeed_data', 'Discover which TensorFeed endpoint answers a data need. Describe what you want in plain language (e.g. "trending AI papers", "is OpenAI down", "model price history") and this returns the 2 to 3 best-matching TensorFeed endpoints, each with its HTTP path, what it returns, and whether it is free or paid. TensorFeed exposes 100+ AI-ecosystem data and signed-verdict endpoints; the core ones are also dedicated tools, but the full catalog is reachable here and callable over HTTP (paid ones via x402 or a credits token). Free, no auth. Use this first when no dedicated tool obviously fits.', {
222
- query: z.string().describe('Plain-language description of the data or decision you need.'),
258
+ query: z.string().max(500).describe('Plain-language description of the data or decision you need.'),
223
259
  limit: z.number().int().min(1).max(5).optional().describe('Max endpoints to return (default 3).'),
224
260
  }, async ({ query, limit }) => {
225
261
  const { flattenCatalog, scoreEndpoints } = await import('./discovery.js');
@@ -255,7 +291,7 @@ registerTool('get_ai_status', 'Get the real-time operational status of major AI
255
291
  });
256
292
  // ── Tool: is_service_down ───────────────────────────────────────────
257
293
  registerTool('is_service_down', 'Check whether one named AI service (e.g. "claude", "openai", "gemini", "mistral", "cohere", "hugging face", "replicate") is currently operational, degraded, or down, with its component-level breakdown. Matches on service or provider name and lists available services if there is no match, so an agent can gate a call on live status before sending traffic. Free, no auth.', {
258
- service: z.string().describe('Service name to check (e.g. "claude", "openai", "gemini", "mistral", "cohere", "hugging face", "replicate")'),
294
+ service: z.string().max(200).describe('Service name to check (e.g. "claude", "openai", "gemini", "mistral", "cohere", "hugging face", "replicate")'),
259
295
  }, async ({ service }) => {
260
296
  const data = await fetchJSON('/status');
261
297
  const match = data.services.find(s => s.name.toLowerCase().includes(service.toLowerCase()) ||
@@ -332,7 +368,7 @@ registerTool('account_status', 'Check the configured TensorFeed token: current c
332
368
  registerTool('route_verdict', "TensorFeed's signed model-routing decision: the single best model for a task or named model, fused from live pricing, contamination-discounted benchmarks, real production usage, measured p95 latency, incident state, and deprecation flags, with the reasoning. tier='preview' (default) is free (10 calls per day per IP), top verdict only. tier='full' costs 1 credit ($0.02), adds ranked runners-up, constraint filters, and an AFTA-signed receipt you can audit, and needs a TENSORFEED_TOKEN. Get credits at tensorfeed.ai/developers/agent-payments.", {
333
369
  tier: z.enum(['preview', 'full']).optional().describe("'preview' (default, free) or 'full' (1 credit; adds runners-up, filters, signed receipt)."),
334
370
  task: z.enum(['code', 'reasoning', 'creative', 'general']).optional().describe('Task type to route for (code, reasoning, creative, general). Provide task or model.'),
335
- model: z.string().optional().describe('Model id or display name to narrow the verdict to one model (e.g. "Claude Opus 4.7" or "claude-opus-4-7"). Provide task or model.'),
371
+ model: z.string().max(200).optional().describe('Model id or display name to narrow the verdict to one model (e.g. "Claude Opus 4.7" or "claude-opus-4-7"). Provide task or model.'),
336
372
  max_latency_p95_ms: z.number().optional().describe('Full tier only. Drop candidates whose measured p95 latency exceeds this value (ms).'),
337
373
  budget: z.number().optional().describe('Full tier only. Max blended USD per 1M tokens.'),
338
374
  min_quality: z.number().min(0).max(1).optional().describe('Full tier only. Minimum trust-discounted quality score in [0, 1].'),
@@ -549,7 +585,7 @@ registerTool('x402_settlement_verdict', "TensorFeed's signed ruling on the state
549
585
  // ── Tool: x402_publisher_verdict (preview free / full 1 credit) ──────
550
586
  registerTool('x402_publisher_verdict', "TensorFeed's signed trust verdict on one x402 publisher, over its OWN on-chain settlement index: whether a named publisher domain is actively settling, recently quiet, registered with no settlement, unreachable, missing a Base payTo, or not indexed. Requires a domain. tier='preview' (default) is free (10 calls per day per IP), the trust verdict only. tier='full' costs 1 credit ($0.02), adds the 30-day settlement momentum, the shared-wallet risk flag, the settlement evidence (volume, count, last settled), and an AFTA-signed receipt, and needs a TENSORFEED_TOKEN. Get credits at tensorfeed.ai/developers/agent-payments.", {
551
587
  tier: z.enum(['preview', 'full']).optional().describe("'preview' (default, free) or 'full' (1 credit; adds momentum, shared-wallet flag, evidence, signed receipt)."),
552
- domain: z.string().describe('Publisher domain to rule on, e.g. "x402.tavily.com". Required.'),
588
+ domain: z.string().max(253).describe('Publisher domain to rule on, e.g. "x402.tavily.com". Required.'),
553
589
  }, async ({ tier, domain }) => {
554
590
  if (!domain || !domain.trim()) {
555
591
  return { content: [{ type: 'text', text: 'Provide a publisher domain (e.g. domain "x402.tavily.com") to get a trust verdict.' }] };
@@ -591,7 +627,7 @@ registerTool('x402_publisher_verdict', "TensorFeed's signed trust verdict on one
591
627
  });
592
628
  registerTool('stack_safety_verdict', "TensorFeed's deploy gate for an AI software stack: pass each package as comma-separated name@version and get the overall BLOCK / HOLD / PASS / UNKNOWN gate plus a per-package verdict, fusing the ingested AI-stack CVE batch with the CISA KEV catalog. Conservative by design: BLOCK only on an exploited CVE with no fix, HOLD when a known CVE applies and you must verify your version, PASS on no match, UNKNOWN outside the curated AI-stack cohort. tier='preview' (default) is free (10 calls per day per IP), caps at 3 packages, gate plus worst offender. tier='full' costs 1 credit ($0.02), raises the cap to 10 packages, adds the matched-CVE evidence (ids, affected ranges, fixed versions, KEV status) and an AFTA-signed receipt, and needs a TENSORFEED_TOKEN. Get credits at tensorfeed.ai/developers/agent-payments.", {
593
629
  tier: z.enum(['preview', 'full']).optional().describe("'preview' (default, free, caps at 3 packages) or 'full' (1 credit; up to 10 packages, matched-CVE evidence, signed receipt)."),
594
- packages: z.string().describe('Comma-separated AI-stack packages as name@version (e.g. "vllm@0.5.0,transformers@4.40.0"). Required. Preview caps at 3, full up to 10.'),
630
+ packages: z.string().max(1000).describe('Comma-separated AI-stack packages as name@version (e.g. "vllm@0.5.0,transformers@4.40.0"). Required. Preview caps at 3, full up to 10.'),
595
631
  }, async ({ tier, packages }) => {
596
632
  if (!packages || !packages.trim()) {
597
633
  return { content: [{ type: 'text', text: 'Provide packages as a comma-separated name@version list (e.g. "vllm@0.5.0,transformers@4.40.0") to get a deploy gate.' }] };
@@ -643,8 +679,8 @@ registerTool('stack_safety_verdict', "TensorFeed's deploy gate for an AI softwar
643
679
  });
644
680
  registerTool('benchmark_trust_verdict', "TensorFeed's signed ruling on whether an AI benchmark is still a trustworthy capability signal or saturated, contaminated, or near ceiling so a high score should be down-weighted: a trust band (reliable, use_with_caution, saturated, contaminated, deprecated) and a 0-100 trust score per benchmark. Pass benchmark to narrow to one, or category to filter, or neither for the registry. tier='preview' (default) is free (10 calls per day per IP), top verdict and bands only. tier='full' costs 1 credit ($0.02), adds the per-signal detail (ceiling proximity, frontier compression, contamination), a down-weight recommendation with an alternative benchmark, and an AFTA-signed receipt, and needs a TENSORFEED_TOKEN. Get credits at tensorfeed.ai/developers/agent-payments.", {
645
681
  tier: z.enum(['preview', 'full']).optional().describe("'preview' (default, free) or 'full' (1 credit; adds per-signal detail, recommendation, signed receipt)."),
646
- benchmark: z.string().optional().describe('Benchmark registry id or name to narrow to one (e.g. "mmlu", "swe-bench"). Optional.'),
647
- category: z.string().optional().describe('Category to filter the benchmarks (e.g. "coding", "reasoning"). Optional.'),
682
+ benchmark: z.string().max(200).optional().describe('Benchmark registry id or name to narrow to one (e.g. "mmlu", "swe-bench"). Optional.'),
683
+ category: z.string().max(100).optional().describe('Category to filter the benchmarks (e.g. "coding", "reasoning"). Optional.'),
648
684
  }, async ({ tier, benchmark, category }) => {
649
685
  const params = new URLSearchParams();
650
686
  if (benchmark)
@@ -699,7 +735,7 @@ registerTool('benchmark_trust_verdict', "TensorFeed's signed ruling on whether a
699
735
  });
700
736
  registerTool('failover_verdict', "Provider A is degraded; TensorFeed's signed ruling on the single best operational provider to fail over to for a task right now. Confirms A against the live incident-triage feed, then runs the route-verdict fusion with A (and any provider already in failover) excluded. Requires from. tier='preview' (default) is free (10 calls per day per IP), the failover target only. tier='full' costs 1 credit ($0.02), adds the full candidate (pricing, measured p95 latency, quality), the ranked alternatives, the confirmed incident on A, and an AFTA-signed receipt, and needs a TENSORFEED_TOKEN. Get credits at tensorfeed.ai/developers/agent-payments.", {
701
737
  tier: z.enum(['preview', 'full']).optional().describe("'preview' (default, free) or 'full' (1 credit; adds candidate detail, ranked alternatives, incident, signed receipt)."),
702
- from: z.string().describe('The degraded provider to fail over FROM (e.g. "anthropic", "openai"). Required.'),
738
+ from: z.string().max(200).describe('The degraded provider to fail over FROM (e.g. "anthropic", "openai"). Required.'),
703
739
  task: z.enum(['code', 'reasoning', 'creative', 'general']).optional().describe('Task type to optimize the failover target for. Optional.'),
704
740
  }, async ({ tier, from, task }) => {
705
741
  if (!from || !from.trim()) {
@@ -781,7 +817,7 @@ registerTool('failover_verdict', "Provider A is degraded; TensorFeed's signed ru
781
817
  // ── Tool: ssvc_verdict (preview free / full 1 credit) ────────────────
782
818
  registerTool('ssvc_verdict', "TensorFeed's signed SSVC patch-urgency decision for one CVE, applying the CISA SSVC Coordinator decision tree to the recorded Vulnrichment decision points (exploitation, automatable, technical impact). Requires a CVE id. tier='preview' (default) is free (10 calls per day per IP): the three decision points and the decision-tree provenance, WITHOUT the computed Act / Attend / Track / Track* decision. tier='full' costs 1 credit ($0.02), adds the computed decision across the full low/medium/high Mission and Well-being envelope, the per-level reasoning, a live CISA KEV cross-check, and an AFTA-signed receipt, and needs a TENSORFEED_TOKEN. Get credits at tensorfeed.ai/developers/agent-payments.", {
783
819
  tier: z.enum(['preview', 'full']).optional().describe("'preview' (default, free) or 'full' (1 credit; adds the computed decision, envelope, reasoning, KEV cross-check, signed receipt)."),
784
- cve: z.string().describe('CVE id to rule on, e.g. "CVE-2024-3094". Required.'),
820
+ cve: z.string().max(64).describe('CVE id to rule on, e.g. "CVE-2024-3094". Required.'),
785
821
  }, async ({ tier, cve }) => {
786
822
  if (!cve || !cve.trim()) {
787
823
  return { content: [{ type: 'text', text: 'Provide a CVE id (e.g. cve "CVE-2024-3094") to get the SSVC decision points.' }] };
@@ -839,7 +875,7 @@ function seriesFromDate(days) {
839
875
  }
840
876
  // ── Tool: pricing_series (free 1 to 7 days, paid 8 to 90 days) ──────
841
877
  registerTool('pricing_series', 'Daily price points for one AI model over a window. days 1 to 7 is free; days 8 to 90 costs 1 credit ($0.02) and needs a TENSORFEED_TOKEN, adding the min/max/delta summary over the longer window. Get credits at tensorfeed.ai/developers/agent-payments.', {
842
- model: z.string().describe('Model id or display name (e.g. "Claude Opus 4.7" or "claude-opus-4-7").'),
878
+ model: z.string().max(200).describe('Model id or display name (e.g. "Claude Opus 4.7" or "claude-opus-4-7").'),
843
879
  days: z.number().int().min(1).max(90).optional().describe('Window length (default 7). 1 to 7 free; 8 to 90 costs 1 credit.'),
844
880
  }, async ({ model, days }) => {
845
881
  const d = days ?? 7;
@@ -876,8 +912,8 @@ registerTool('pricing_series', 'Daily price points for one AI model over a windo
876
912
  });
877
913
  // ── Tool: benchmark_series (free 1 to 7 days, paid 8 to 90 days) ────
878
914
  registerTool('benchmark_series', 'Daily benchmark scores for one model+benchmark over a window. Benchmark keys: swe_bench, mmlu_pro, gpqa_diamond, math, human_eval. days 1 to 7 is free; days 8 to 90 costs 1 credit ($0.02) and needs a TENSORFEED_TOKEN, tracking score evolution over the longer window. Get credits at tensorfeed.ai/developers/agent-payments.', {
879
- model: z.string().describe('Model id or display name.'),
880
- benchmark: z.string().describe('Benchmark key (e.g. swe_bench, mmlu_pro, gpqa_diamond, math, human_eval).'),
915
+ model: z.string().max(200).describe('Model id or display name.'),
916
+ benchmark: z.string().max(200).describe('Benchmark key (e.g. swe_bench, mmlu_pro, gpqa_diamond, math, human_eval).'),
881
917
  days: z.number().int().min(1).max(90).optional().describe('Window length (default 7). 1 to 7 free; 8 to 90 costs 1 credit.'),
882
918
  }, async ({ model, benchmark, days }) => {
883
919
  const d = days ?? 7;
@@ -914,7 +950,7 @@ registerTool('benchmark_series', 'Daily benchmark scores for one model+benchmark
914
950
  });
915
951
  // ── Tool: status_uptime (free 1 to 7 days, paid 8 to 90 days) ───────
916
952
  registerTool('status_uptime', 'Daily uptime rollup for one provider over a window with operational/degraded/down day counts and uptime % (degraded counts as half-credit). days 1 to 7 is free; days 8 to 90 costs 1 credit ($0.02) and needs a TENSORFEED_TOKEN, adding per-incident-day detail over the longer window. Get credits at tensorfeed.ai/developers/agent-payments.', {
917
- provider: z.string().describe('Provider name (e.g. anthropic, openai, google).'),
953
+ provider: z.string().max(200).describe('Provider name (e.g. anthropic, openai, google).'),
918
954
  days: z.number().int().min(1).max(90).optional().describe('Window length (default 7). 1 to 7 free; 8 to 90 costs 1 credit.'),
919
955
  }, async ({ provider, days }) => {
920
956
  const d = days ?? 7;
@@ -1027,7 +1063,7 @@ registerTool('whats_new', 'Catch up on everything that changed in AI in one call
1027
1063
  });
1028
1064
  // ── Tool: compare_models (1 credit) ─────────────────────────────────
1029
1065
  registerTool('compare_models', 'Pick between models in one call: pricing, benchmarks, status, and recent news for 2 to 5 models side by side, with cheapest-blended and per-benchmark rankings, so you choose without scraping each provider. Costs 1 credit ($0.02).', {
1030
- ids: z.string().describe('Comma-separated list of 2-5 model ids or display names. Examples: "Claude Opus 4.8,GPT-5.5,Gemini 3.5 Flash" or "opus-4-8,gpt-5-5"'),
1066
+ ids: z.string().max(500).describe('Comma-separated list of 2-5 model ids or display names. Examples: "Claude Opus 4.8,GPT-5.5,Gemini 3.5 Flash" or "opus-4-8,gpt-5-5"'),
1031
1067
  }, async ({ ids }) => {
1032
1068
  const data = (await fetchJSON(`/premium/compare/models?ids=${encodeURIComponent(ids)}`, { auth: true }));
1033
1069
  const lines = data.models.map(m => {
@@ -1054,7 +1090,7 @@ registerTool('compare_models', 'Pick between models in one call: pricing, benchm
1054
1090
  });
1055
1091
  // ── Tool: provider_deepdive (1 credit) ──────────────────────────────
1056
1092
  registerTool('provider_deepdive', 'Everything about one AI provider in a single call: live status, every model with pricing, tier, and benchmarks joined in, recent news, and agent traffic, replacing about four separate lookups. Costs 3 credits ($0.06). Strict premium, no free trial.', {
1057
- provider: z.string().describe('Provider id or display name (case-insensitive). Examples: anthropic, openai, google, mistral, cohere'),
1093
+ provider: z.string().max(200).describe('Provider id or display name (case-insensitive). Examples: anthropic, openai, google, mistral, cohere'),
1058
1094
  }, async ({ provider }) => {
1059
1095
  const data = (await fetchJSON(`/premium/providers/${encodeURIComponent(provider)}`, { auth: true }));
1060
1096
  const modelLines = data.models
@@ -1105,12 +1141,12 @@ registerTool('list_watches', 'List the active webhook watches owned by the confi
1105
1141
  // create_*_watch tool used, against the same /premium/watches endpoint.
1106
1142
  registerTool('create_watch', 'Register a webhook watch. type selects what to watch: "price" (a model price change), "status" (a service status transition), "digest" (a scheduled daily or weekly pricing summary), or "leaderboard_rank" (a provider crossing an uptime-rank threshold). Costs 1 credit ($0.02) at registration; the watch lives 90 days and each fire is an HMAC-signed POST to callback_url. Needs a TENSORFEED_TOKEN.', {
1107
1143
  type: z.enum(['price', 'status', 'digest', 'leaderboard_rank']).describe('What to watch.'),
1108
- callback_url: z.string().describe('HTTPS URL that receives the HMAC-signed POST when the watch fires.'),
1109
- secret: z.string().optional().describe('Optional shared secret used to HMAC-sign delivery bodies.'),
1110
- model: z.string().optional().describe('type=price only. Model name (e.g. "Claude Opus 4.7").'),
1144
+ callback_url: z.string().max(2048).describe('HTTPS URL that receives the HMAC-signed POST when the watch fires.'),
1145
+ secret: z.string().max(256).optional().describe('Optional shared secret used to HMAC-sign delivery bodies.'),
1146
+ model: z.string().max(200).optional().describe('type=price only. Model name (e.g. "Claude Opus 4.7").'),
1111
1147
  field: z.enum(['inputPrice', 'outputPrice', 'blended']).optional().describe('type=price only. Which price field to watch.'),
1112
1148
  cadence: z.enum(['daily', 'weekly']).optional().describe('type=digest only. How often the digest fires.'),
1113
- provider: z.string().optional().describe('type=status or type=leaderboard_rank only. Provider name or slug (case-insensitive). e.g. anthropic, openai, gemini, bedrock, azure.'),
1149
+ provider: z.string().max(200).optional().describe('type=status or type=leaderboard_rank only. Provider name or slug (case-insensitive). e.g. anthropic, openai, gemini, bedrock, azure.'),
1114
1150
  op: z
1115
1151
  .enum(['lt', 'gt', 'changes', 'becomes', 'drops_below', 'rises_above'])
1116
1152
  .optional()
@@ -28,6 +28,25 @@
28
28
  * Scrub a single string for return to a host LLM. Idempotent and pure.
29
29
  */
30
30
  export declare function sanitizeForLLM(input: string): string;
31
+ /**
32
+ * Scrub an error message before it is returned to the host LLM as a tool
33
+ * error. Errors are the one path that used to bypass sanitizeToolResponse:
34
+ * a thrown Error's message (which may embed an upstream response body) went
35
+ * to the SDK raw. This applies, in order:
36
+ *
37
+ * 1. Redact any caller-provided secret values (e.g. the live
38
+ * TENSORFEED_TOKEN) wherever they appear in the text.
39
+ * 2. Redact anything shaped like a TensorFeed bearer token
40
+ * (tf_live_...), even if the exact env value was not passed, in case
41
+ * an upstream or proxy ever echoes an Authorization header back.
42
+ * 3. Run the standard sanitizeForLLM scrub (control chars, bidi and
43
+ * zero-width chars, role-confusion tokens).
44
+ * 4. Cap the result at MAX_ERROR_CHARS.
45
+ *
46
+ * Secrets shorter than 8 chars are ignored to avoid redacting common
47
+ * substrings if a trivial value is ever passed by mistake.
48
+ */
49
+ export declare function sanitizeErrorText(input: string, secrets?: (string | undefined)[]): string;
31
50
  /**
32
51
  * Neutralize a caller-supplied identifier before echoing it back inside a
33
52
  * human-readable message (for example `Service "X" not found`). Model,
package/dist/sanitize.js CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tensorfeed/mcp-server",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "mcpName": "ai.tensorfeed/mcp-server",
5
5
  "description": "MCP server for TensorFeed.ai - AI news, service status, model pricing, signed decision verdicts, time series, model comparison, webhook watches, and a discovery tool for the full TensorFeed data catalog, for AI agents",
6
6
  "main": "dist/index.js",
package/server.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "name": "ai.tensorfeed/mcp-server",
4
4
  "title": "TensorFeed",
5
5
  "description": "AI news, model pricing, service status, and machine-payable premium tools. AFTA-certified.",
6
- "version": "2.0.2",
6
+ "version": "2.0.3",
7
7
  "websiteUrl": "https://tensorfeed.ai/developers/agent-payments",
8
8
  "repository": {
9
9
  "url": "https://github.com/RipperMercs/tensorfeed-mcp",
@@ -20,7 +20,7 @@
20
20
  "registryType": "npm",
21
21
  "registryBaseUrl": "https://registry.npmjs.org",
22
22
  "identifier": "@tensorfeed/mcp-server",
23
- "version": "2.0.2",
23
+ "version": "2.0.3",
24
24
  "runtimeHint": "npx",
25
25
  "transport": {
26
26
  "type": "stdio"