@tensorfeed/mcp-server 2.0.1 → 2.0.2

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,16 @@
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.2 - 2026-07-06
6
+
7
+ Output hardening. Caller-supplied identifiers that a tool echoes back in its
8
+ own message (a service, model, provider, benchmark, publisher domain, CVE id,
9
+ or search query that did not match) are now stripped of angle brackets and
10
+ length-capped before they are placed in the response text. This is a
11
+ defense-in-depth layer on top of the existing output sanitizer, so a value a
12
+ caller passes in cannot be reflected back as markup. No tool signatures,
13
+ endpoints, or behavior for valid inputs change.
14
+
5
15
  ## 2.0.1 - 2026-07-03
6
16
 
7
17
  Docs release, no code changes to the stdio server. The README now documents
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 } from './sanitize.js';
8
+ import { sanitizeToolResponse, sanitizeForLLM, sanitizeReflectedValue } 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
@@ -235,10 +235,10 @@ registerTool('find_tensorfeed_data', 'Discover which TensorFeed endpoint answers
235
235
  const rows = flattenCatalog(meta);
236
236
  const top = scoreEndpoints(rows, query, limit ?? 3);
237
237
  if (top.length === 0) {
238
- return { content: [{ type: 'text', text: `No TensorFeed endpoint matched "${query}". Browse the full catalog at https://tensorfeed.ai/api/meta or https://tensorfeed.ai/developers.` }] };
238
+ return { content: [{ type: 'text', text: `No TensorFeed endpoint matched "${sanitizeReflectedValue(query)}". Browse the full catalog at https://tensorfeed.ai/api/meta or https://tensorfeed.ai/developers.` }] };
239
239
  }
240
240
  const lines = top.map((r) => `- ${r.path}\n ${r.description}`).join('\n');
241
- return { content: [{ type: 'text', text: `Top TensorFeed endpoints for "${query}":\n${lines}\n\nCall free endpoints directly over HTTP. Paid endpoints (marked with a credit cost) accept x402 payment or a TENSORFEED_TOKEN bearer; see https://tensorfeed.ai/developers/agent-payments.` }] };
241
+ return { content: [{ type: 'text', text: `Top TensorFeed endpoints for "${sanitizeReflectedValue(query)}":\n${lines}\n\nCall free endpoints directly over HTTP. Paid endpoints (marked with a credit cost) accept x402 payment or a TENSORFEED_TOKEN bearer; see https://tensorfeed.ai/developers/agent-payments.` }] };
242
242
  });
243
243
  // ── Tool: get_ai_status ─────────────────────────────────────────────
244
244
  registerTool('get_ai_status', 'Get the real-time operational status of major AI services (Claude, OpenAI, Gemini, Mistral, Cohere, Replicate, Hugging Face), including per-component breakdowns and an operational/degraded/down rollup per provider. One cross-provider status call instead of checking each vendor\'s status page, useful before an agent routes a request to a model that may be impaired. Free, no auth.', {}, async () => {
@@ -261,7 +261,7 @@ registerTool('is_service_down', 'Check whether one named AI service (e.g. "claud
261
261
  const match = data.services.find(s => s.name.toLowerCase().includes(service.toLowerCase()) ||
262
262
  s.provider.toLowerCase().includes(service.toLowerCase()));
263
263
  if (!match) {
264
- return { content: [{ type: 'text', text: `Service "${service}" not found. Available services: ${data.services.map(s => s.name).join(', ')}` }] };
264
+ return { content: [{ type: 'text', text: `Service "${sanitizeReflectedValue(service)}" not found. Available services: ${data.services.map(s => s.name).join(', ')}` }] };
265
265
  }
266
266
  const statusEmoji = match.status === 'operational' ? 'OK' : match.status === 'degraded' ? 'DEGRADED' : 'DOWN';
267
267
  const components = match.components.length > 0
@@ -563,7 +563,7 @@ registerTool('x402_publisher_verdict', "TensorFeed's signed trust verdict on one
563
563
  content: [
564
564
  {
565
565
  type: 'text',
566
- text: `x402 Publisher Verdict for ${data.domain}: ${data.verdict}\n` +
566
+ text: `x402 Publisher Verdict for ${sanitizeReflectedValue(data.domain)}: ${data.verdict}\n` +
567
567
  `Momentum: ${data.momentum}. Shared wallet: ${t.wallet_shared ? 'yes (risk flag)' : 'no'}` +
568
568
  (t.last_settled ? `. Last settled ${t.last_settled}` : '') +
569
569
  '\n' +
@@ -580,7 +580,7 @@ registerTool('x402_publisher_verdict', "TensorFeed's signed trust verdict on one
580
580
  content: [
581
581
  {
582
582
  type: 'text',
583
- text: `x402 Publisher Verdict for ${data.domain}: ${data.verdict}\n` +
583
+ text: `x402 Publisher Verdict for ${sanitizeReflectedValue(data.domain)}: ${data.verdict}\n` +
584
584
  `Claim: ${data.claim}` +
585
585
  (data.captured_at ? `\nIndex captured ${data.captured_at}` : '') +
586
586
  '\n' +
@@ -711,15 +711,16 @@ registerTool('failover_verdict', "Provider A is degraded; TensorFeed's signed ru
711
711
  if ((tier ?? 'preview') === 'full') {
712
712
  const data = (await fetchJSON(`/premium/failover-verdict?${params}`, { auth: true }));
713
713
  const dest = data.failover_to;
714
+ const fromProvider = sanitizeReflectedValue(data.from.provider);
714
715
  const incidentLine = data.from.incident
715
- ? `Incident on ${data.from.provider}: ${data.from.incident.title} (action ${data.from.incident.recommended_action})\n`
716
+ ? `Incident on ${fromProvider}: ${data.from.incident.title} (action ${data.from.incident.recommended_action})\n`
716
717
  : '';
717
718
  if (!dest) {
718
719
  return {
719
720
  content: [
720
721
  {
721
722
  type: 'text',
722
- text: `Failover Verdict from ${data.from.provider}: no operational failover target found.\n` +
723
+ text: `Failover Verdict from ${fromProvider}: no operational failover target found.\n` +
723
724
  incidentLine +
724
725
  `Why: ${data.why}\nClaim: ${data.claim}` +
725
726
  verdictBilling(data.billing),
@@ -734,7 +735,7 @@ registerTool('failover_verdict', "Provider A is degraded; TensorFeed's signed ru
734
735
  content: [
735
736
  {
736
737
  type: 'text',
737
- text: `Failover Verdict from ${data.from.provider}: fail over to ${dest.model.name} (${dest.model.provider})\n` +
738
+ text: `Failover Verdict from ${fromProvider}: fail over to ${dest.model.name} (${dest.model.provider})\n` +
738
739
  `Why: ${dest.why}\n` +
739
740
  `Blended $${dest.pricing.blended}/1M, p95 ${dest.latency.measured_p95_ms ?? 'n/a'}ms, status ${dest.operational.status}\n` +
740
741
  incidentLine +
@@ -748,13 +749,14 @@ registerTool('failover_verdict', "Provider A is degraded; TensorFeed's signed ru
748
749
  }
749
750
  const data = (await fetchJSON(`/preview/failover-verdict?${params}`));
750
751
  const dest = data.failover_to;
752
+ const fromProvider = sanitizeReflectedValue(data.from.provider);
751
753
  const rl = previewRateLine(data.rate_limit);
752
754
  if (!dest) {
753
755
  return {
754
756
  content: [
755
757
  {
756
758
  type: 'text',
757
- text: `Failover Verdict from ${data.from.provider}: no operational failover target found.\n` +
759
+ text: `Failover Verdict from ${fromProvider}: no operational failover target found.\n` +
758
760
  `Why: ${data.why}\n${data.claim}\n` +
759
761
  verdictUpsell('failover_verdict', 'the full failover candidate (pricing, measured latency, quality) and the ranked alternatives', rl),
760
762
  },
@@ -765,7 +767,7 @@ registerTool('failover_verdict', "Provider A is degraded; TensorFeed's signed ru
765
767
  content: [
766
768
  {
767
769
  type: 'text',
768
- text: `Failover Verdict from ${data.from.provider}` +
770
+ text: `Failover Verdict from ${fromProvider}` +
769
771
  (data.from.in_incident ? ' (confirmed in incident)' : '') +
770
772
  `: fail over to ${dest.model.name} (${dest.model.provider})\n` +
771
773
  `Why: ${data.why}\n` +
@@ -797,7 +799,7 @@ registerTool('ssvc_verdict', "TensorFeed's signed SSVC patch-urgency decision fo
797
799
  content: [
798
800
  {
799
801
  type: 'text',
800
- text: `SSVC Verdict for ${data.cve}: ${data.decision_primary} (Mission and Well-being assumed medium)\n` +
802
+ text: `SSVC Verdict for ${sanitizeReflectedValue(data.cve)}: ${data.decision_primary} (Mission and Well-being assumed medium)\n` +
801
803
  `Envelope: low ${env.low}, medium ${env.medium}, high ${env.high}\n` +
802
804
  `Decision points: exploitation ${dp.exploitation}, automatable ${dp.automatable}, technical impact ${dp.technical_impact}\n` +
803
805
  `${kevLine}\n` +
@@ -815,7 +817,7 @@ registerTool('ssvc_verdict', "TensorFeed's signed SSVC patch-urgency decision fo
815
817
  content: [
816
818
  {
817
819
  type: 'text',
818
- text: `SSVC decision points for ${data.cve}:\n` +
820
+ text: `SSVC decision points for ${sanitizeReflectedValue(data.cve)}:\n` +
819
821
  ` exploitation: ${dp.exploitation}\n` +
820
822
  ` automatable: ${dp.automatable}\n` +
821
823
  ` technical impact: ${dp.technical_impact}\n` +
@@ -852,7 +854,7 @@ registerTool('pricing_series', 'Daily price points for one AI model over a windo
852
854
  content: [
853
855
  {
854
856
  type: 'text',
855
- text: `${data.model} (${data.provider ?? 'unknown'}) ${data.points.length} points\n${summary}\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
857
+ text: `${sanitizeReflectedValue(data.model)} (${sanitizeReflectedValue(data.provider ?? 'unknown')}) ${data.points.length} points\n${summary}\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
856
858
  },
857
859
  ],
858
860
  };
@@ -867,7 +869,7 @@ registerTool('pricing_series', 'Daily price points for one AI model over a windo
867
869
  content: [
868
870
  {
869
871
  type: 'text',
870
- text: `${data.model} (${data.provider ?? 'unknown'}) ${data.points.length} points over ${data.points.length} days\n${summary}\nFor up to 90 days, pass days 8 to 90 (1 credit).`,
872
+ text: `${sanitizeReflectedValue(data.model)} (${sanitizeReflectedValue(data.provider ?? 'unknown')}) ${data.points.length} points over ${data.points.length} days\n${summary}\nFor up to 90 days, pass days 8 to 90 (1 credit).`,
871
873
  },
872
874
  ],
873
875
  };
@@ -890,7 +892,7 @@ registerTool('benchmark_series', 'Daily benchmark scores for one model+benchmark
890
892
  content: [
891
893
  {
892
894
  type: 'text',
893
- text: `${data.model} on ${data.benchmark}: ${data.points.length} points\n${summary}\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
895
+ text: `${sanitizeReflectedValue(data.model)} on ${sanitizeReflectedValue(data.benchmark)}: ${data.points.length} points\n${summary}\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
894
896
  },
895
897
  ],
896
898
  };
@@ -905,7 +907,7 @@ registerTool('benchmark_series', 'Daily benchmark scores for one model+benchmark
905
907
  content: [
906
908
  {
907
909
  type: 'text',
908
- text: `${data.model} on ${data.benchmark}: ${data.points.length} points\n${summary}`,
910
+ text: `${sanitizeReflectedValue(data.model)} on ${sanitizeReflectedValue(data.benchmark)}: ${data.points.length} points\n${summary}`,
909
911
  },
910
912
  ],
911
913
  };
@@ -926,7 +928,7 @@ registerTool('status_uptime', 'Daily uptime rollup for one provider over a windo
926
928
  content: [
927
929
  {
928
930
  type: 'text',
929
- text: `${data.provider} uptime: ${data.uptime_pct ?? 'n/a'}% over ${data.days_with_data} measured days (of ${data.days_total} in range)\n` +
931
+ text: `${sanitizeReflectedValue(data.provider)} uptime: ${data.uptime_pct ?? 'n/a'}% over ${data.days_with_data} measured days (of ${data.days_total} in range)\n` +
930
932
  ` operational: ${data.days_operational}, degraded: ${data.days_degraded}, down: ${data.days_down}` +
931
933
  incidents +
932
934
  `\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
@@ -940,7 +942,7 @@ registerTool('status_uptime', 'Daily uptime rollup for one provider over a windo
940
942
  content: [
941
943
  {
942
944
  type: 'text',
943
- text: `${data.provider} over ${data.days_total} days: ${data.uptime_pct ?? '?'}% uptime (${data.days_operational} operational, ${data.days_degraded} degraded, ${data.days_down} down)`,
945
+ text: `${sanitizeReflectedValue(data.provider)} over ${data.days_total} days: ${data.uptime_pct ?? '?'}% uptime (${data.days_operational} operational, ${data.days_degraded} degraded, ${data.days_down} down)`,
944
946
  },
945
947
  ],
946
948
  };
@@ -1030,7 +1032,7 @@ registerTool('compare_models', 'Pick between models in one call: pricing, benchm
1030
1032
  const data = (await fetchJSON(`/premium/compare/models?ids=${encodeURIComponent(ids)}`, { auth: true }));
1031
1033
  const lines = data.models.map(m => {
1032
1034
  if (!m.matched)
1033
- return ` ${m.query}: not found`;
1035
+ return ` ${sanitizeReflectedValue(m.query)}: not found`;
1034
1036
  const benches = data.benchmark_keys
1035
1037
  .map(k => `${k}=${m.benchmarks[k] ?? '-'}`)
1036
1038
  .join(', ');
@@ -1209,9 +1211,10 @@ registerTool('create_watch', 'Register a webhook watch. type selects what to wat
1209
1211
  if (secret !== undefined)
1210
1212
  body.secret = secret;
1211
1213
  const data = (await fetchJSON('/premium/watches', { method: 'POST', body, auth: true }));
1214
+ const safeProvider = sanitizeReflectedValue(provider);
1212
1215
  const desc = op === 'changes'
1213
- ? `${provider} rank changes`
1214
- : `${provider} ${(op ?? '').replace('_', ' ')} #${threshold}`;
1216
+ ? `${safeProvider} rank changes`
1217
+ : `${safeProvider} ${(op ?? '').replace('_', ' ')} #${threshold}`;
1215
1218
  return {
1216
1219
  content: [
1217
1220
  {
@@ -28,6 +28,21 @@
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
+ * Neutralize a caller-supplied identifier before echoing it back inside a
33
+ * human-readable message (for example `Service "X" not found`). Model,
34
+ * provider, service and benchmark names are the only caller text that lands
35
+ * in our response strings, and a well-formed one never contains angle
36
+ * brackets. Stripping `<` and `>` means a value like
37
+ * `<script>alert(1)</script>` cannot be reflected as live markup into a
38
+ * downstream web renderer, nor masquerade as a tag in an LLM's context. The
39
+ * length cap stops a caller padding the message. This runs on echoed caller
40
+ * input only, never on the structured data we return, so arrow notation
41
+ * (`->`) and other formatted output elsewhere is untouched. The full
42
+ * response still passes through sanitizeForLLM afterward for control-char
43
+ * and role-token scrubbing.
44
+ */
45
+ export declare function sanitizeReflectedValue(input: unknown): string;
31
46
  /**
32
47
  * Recursively scrub the `text` field of every text-content item in an
33
48
  * MCP tool response. Non-text items (resources, images) pass through
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.1",
3
+ "version": "2.0.2",
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.1",
6
+ "version": "2.0.2",
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.1",
23
+ "version": "2.0.2",
24
24
  "runtimeHint": "npx",
25
25
  "transport": {
26
26
  "type": "stdio"