@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 +18 -0
- package/dist/index.js +62 -26
- package/dist/sanitize.d.ts +19 -0
- package/dist/sanitize.js +0 -0
- package/package.json +1 -1
- package/server.json +2 -2
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: ${
|
|
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: ${
|
|
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}: ${
|
|
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
|
-
|
|
145
|
-
|
|
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()
|
package/dist/sanitize.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
23
|
+
"version": "2.0.3",
|
|
24
24
|
"runtimeHint": "npx",
|
|
25
25
|
"transport": {
|
|
26
26
|
"type": "stdio"
|