@tensorfeed/mcp-server 2.0.1 → 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 +28 -0
- package/dist/index.js +86 -47
- package/dist/sanitize.d.ts +34 -0
- package/dist/sanitize.js +0 -0
- package/package.json +1 -1
- package/server.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
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
|
+
|
|
23
|
+
## 2.0.2 - 2026-07-06
|
|
24
|
+
|
|
25
|
+
Output hardening. Caller-supplied identifiers that a tool echoes back in its
|
|
26
|
+
own message (a service, model, provider, benchmark, publisher domain, CVE id,
|
|
27
|
+
or search query that did not match) are now stripped of angle brackets and
|
|
28
|
+
length-capped before they are placed in the response text. This is a
|
|
29
|
+
defense-in-depth layer on top of the existing output sanitizer, so a value a
|
|
30
|
+
caller passes in cannot be reflected back as markup. No tool signatures,
|
|
31
|
+
endpoints, or behavior for valid inputs change.
|
|
32
|
+
|
|
5
33
|
## 2.0.1 - 2026-07-03
|
|
6
34
|
|
|
7
35
|
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, 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');
|
|
@@ -235,10 +271,10 @@ registerTool('find_tensorfeed_data', 'Discover which TensorFeed endpoint answers
|
|
|
235
271
|
const rows = flattenCatalog(meta);
|
|
236
272
|
const top = scoreEndpoints(rows, query, limit ?? 3);
|
|
237
273
|
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.` }] };
|
|
274
|
+
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
275
|
}
|
|
240
276
|
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.` }] };
|
|
277
|
+
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
278
|
});
|
|
243
279
|
// ── Tool: get_ai_status ─────────────────────────────────────────────
|
|
244
280
|
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 () => {
|
|
@@ -255,13 +291,13 @@ 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()) ||
|
|
262
298
|
s.provider.toLowerCase().includes(service.toLowerCase()));
|
|
263
299
|
if (!match) {
|
|
264
|
-
return { content: [{ type: 'text', text: `Service "${service}" not found. Available services: ${data.services.map(s => s.name).join(', ')}` }] };
|
|
300
|
+
return { content: [{ type: 'text', text: `Service "${sanitizeReflectedValue(service)}" not found. Available services: ${data.services.map(s => s.name).join(', ')}` }] };
|
|
265
301
|
}
|
|
266
302
|
const statusEmoji = match.status === 'operational' ? 'OK' : match.status === 'degraded' ? 'DEGRADED' : 'DOWN';
|
|
267
303
|
const components = match.components.length > 0
|
|
@@ -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.' }] };
|
|
@@ -563,7 +599,7 @@ registerTool('x402_publisher_verdict', "TensorFeed's signed trust verdict on one
|
|
|
563
599
|
content: [
|
|
564
600
|
{
|
|
565
601
|
type: 'text',
|
|
566
|
-
text: `x402 Publisher Verdict for ${data.domain}: ${data.verdict}\n` +
|
|
602
|
+
text: `x402 Publisher Verdict for ${sanitizeReflectedValue(data.domain)}: ${data.verdict}\n` +
|
|
567
603
|
`Momentum: ${data.momentum}. Shared wallet: ${t.wallet_shared ? 'yes (risk flag)' : 'no'}` +
|
|
568
604
|
(t.last_settled ? `. Last settled ${t.last_settled}` : '') +
|
|
569
605
|
'\n' +
|
|
@@ -580,7 +616,7 @@ registerTool('x402_publisher_verdict', "TensorFeed's signed trust verdict on one
|
|
|
580
616
|
content: [
|
|
581
617
|
{
|
|
582
618
|
type: 'text',
|
|
583
|
-
text: `x402 Publisher Verdict for ${data.domain}: ${data.verdict}\n` +
|
|
619
|
+
text: `x402 Publisher Verdict for ${sanitizeReflectedValue(data.domain)}: ${data.verdict}\n` +
|
|
584
620
|
`Claim: ${data.claim}` +
|
|
585
621
|
(data.captured_at ? `\nIndex captured ${data.captured_at}` : '') +
|
|
586
622
|
'\n' +
|
|
@@ -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()) {
|
|
@@ -711,15 +747,16 @@ registerTool('failover_verdict', "Provider A is degraded; TensorFeed's signed ru
|
|
|
711
747
|
if ((tier ?? 'preview') === 'full') {
|
|
712
748
|
const data = (await fetchJSON(`/premium/failover-verdict?${params}`, { auth: true }));
|
|
713
749
|
const dest = data.failover_to;
|
|
750
|
+
const fromProvider = sanitizeReflectedValue(data.from.provider);
|
|
714
751
|
const incidentLine = data.from.incident
|
|
715
|
-
? `Incident on ${
|
|
752
|
+
? `Incident on ${fromProvider}: ${data.from.incident.title} (action ${data.from.incident.recommended_action})\n`
|
|
716
753
|
: '';
|
|
717
754
|
if (!dest) {
|
|
718
755
|
return {
|
|
719
756
|
content: [
|
|
720
757
|
{
|
|
721
758
|
type: 'text',
|
|
722
|
-
text: `Failover Verdict from ${
|
|
759
|
+
text: `Failover Verdict from ${fromProvider}: no operational failover target found.\n` +
|
|
723
760
|
incidentLine +
|
|
724
761
|
`Why: ${data.why}\nClaim: ${data.claim}` +
|
|
725
762
|
verdictBilling(data.billing),
|
|
@@ -734,7 +771,7 @@ registerTool('failover_verdict', "Provider A is degraded; TensorFeed's signed ru
|
|
|
734
771
|
content: [
|
|
735
772
|
{
|
|
736
773
|
type: 'text',
|
|
737
|
-
text: `Failover Verdict from ${
|
|
774
|
+
text: `Failover Verdict from ${fromProvider}: fail over to ${dest.model.name} (${dest.model.provider})\n` +
|
|
738
775
|
`Why: ${dest.why}\n` +
|
|
739
776
|
`Blended $${dest.pricing.blended}/1M, p95 ${dest.latency.measured_p95_ms ?? 'n/a'}ms, status ${dest.operational.status}\n` +
|
|
740
777
|
incidentLine +
|
|
@@ -748,13 +785,14 @@ registerTool('failover_verdict', "Provider A is degraded; TensorFeed's signed ru
|
|
|
748
785
|
}
|
|
749
786
|
const data = (await fetchJSON(`/preview/failover-verdict?${params}`));
|
|
750
787
|
const dest = data.failover_to;
|
|
788
|
+
const fromProvider = sanitizeReflectedValue(data.from.provider);
|
|
751
789
|
const rl = previewRateLine(data.rate_limit);
|
|
752
790
|
if (!dest) {
|
|
753
791
|
return {
|
|
754
792
|
content: [
|
|
755
793
|
{
|
|
756
794
|
type: 'text',
|
|
757
|
-
text: `Failover Verdict from ${
|
|
795
|
+
text: `Failover Verdict from ${fromProvider}: no operational failover target found.\n` +
|
|
758
796
|
`Why: ${data.why}\n${data.claim}\n` +
|
|
759
797
|
verdictUpsell('failover_verdict', 'the full failover candidate (pricing, measured latency, quality) and the ranked alternatives', rl),
|
|
760
798
|
},
|
|
@@ -765,7 +803,7 @@ registerTool('failover_verdict', "Provider A is degraded; TensorFeed's signed ru
|
|
|
765
803
|
content: [
|
|
766
804
|
{
|
|
767
805
|
type: 'text',
|
|
768
|
-
text: `Failover Verdict from ${
|
|
806
|
+
text: `Failover Verdict from ${fromProvider}` +
|
|
769
807
|
(data.from.in_incident ? ' (confirmed in incident)' : '') +
|
|
770
808
|
`: fail over to ${dest.model.name} (${dest.model.provider})\n` +
|
|
771
809
|
`Why: ${data.why}\n` +
|
|
@@ -779,7 +817,7 @@ registerTool('failover_verdict', "Provider A is degraded; TensorFeed's signed ru
|
|
|
779
817
|
// ── Tool: ssvc_verdict (preview free / full 1 credit) ────────────────
|
|
780
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.", {
|
|
781
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)."),
|
|
782
|
-
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.'),
|
|
783
821
|
}, async ({ tier, cve }) => {
|
|
784
822
|
if (!cve || !cve.trim()) {
|
|
785
823
|
return { content: [{ type: 'text', text: 'Provide a CVE id (e.g. cve "CVE-2024-3094") to get the SSVC decision points.' }] };
|
|
@@ -797,7 +835,7 @@ registerTool('ssvc_verdict', "TensorFeed's signed SSVC patch-urgency decision fo
|
|
|
797
835
|
content: [
|
|
798
836
|
{
|
|
799
837
|
type: 'text',
|
|
800
|
-
text: `SSVC Verdict for ${data.cve}: ${data.decision_primary} (Mission and Well-being assumed medium)\n` +
|
|
838
|
+
text: `SSVC Verdict for ${sanitizeReflectedValue(data.cve)}: ${data.decision_primary} (Mission and Well-being assumed medium)\n` +
|
|
801
839
|
`Envelope: low ${env.low}, medium ${env.medium}, high ${env.high}\n` +
|
|
802
840
|
`Decision points: exploitation ${dp.exploitation}, automatable ${dp.automatable}, technical impact ${dp.technical_impact}\n` +
|
|
803
841
|
`${kevLine}\n` +
|
|
@@ -815,7 +853,7 @@ registerTool('ssvc_verdict', "TensorFeed's signed SSVC patch-urgency decision fo
|
|
|
815
853
|
content: [
|
|
816
854
|
{
|
|
817
855
|
type: 'text',
|
|
818
|
-
text: `SSVC decision points for ${data.cve}:\n` +
|
|
856
|
+
text: `SSVC decision points for ${sanitizeReflectedValue(data.cve)}:\n` +
|
|
819
857
|
` exploitation: ${dp.exploitation}\n` +
|
|
820
858
|
` automatable: ${dp.automatable}\n` +
|
|
821
859
|
` technical impact: ${dp.technical_impact}\n` +
|
|
@@ -837,7 +875,7 @@ function seriesFromDate(days) {
|
|
|
837
875
|
}
|
|
838
876
|
// ── Tool: pricing_series (free 1 to 7 days, paid 8 to 90 days) ──────
|
|
839
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.', {
|
|
840
|
-
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").'),
|
|
841
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.'),
|
|
842
880
|
}, async ({ model, days }) => {
|
|
843
881
|
const d = days ?? 7;
|
|
@@ -852,7 +890,7 @@ registerTool('pricing_series', 'Daily price points for one AI model over a windo
|
|
|
852
890
|
content: [
|
|
853
891
|
{
|
|
854
892
|
type: 'text',
|
|
855
|
-
text: `${data.model} (${data.provider ?? 'unknown'}) ${data.points.length} points\n${summary}\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
893
|
+
text: `${sanitizeReflectedValue(data.model)} (${sanitizeReflectedValue(data.provider ?? 'unknown')}) ${data.points.length} points\n${summary}\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
856
894
|
},
|
|
857
895
|
],
|
|
858
896
|
};
|
|
@@ -867,15 +905,15 @@ registerTool('pricing_series', 'Daily price points for one AI model over a windo
|
|
|
867
905
|
content: [
|
|
868
906
|
{
|
|
869
907
|
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).`,
|
|
908
|
+
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
909
|
},
|
|
872
910
|
],
|
|
873
911
|
};
|
|
874
912
|
});
|
|
875
913
|
// ── Tool: benchmark_series (free 1 to 7 days, paid 8 to 90 days) ────
|
|
876
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.', {
|
|
877
|
-
model: z.string().describe('Model id or display name.'),
|
|
878
|
-
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).'),
|
|
879
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.'),
|
|
880
918
|
}, async ({ model, benchmark, days }) => {
|
|
881
919
|
const d = days ?? 7;
|
|
@@ -890,7 +928,7 @@ registerTool('benchmark_series', 'Daily benchmark scores for one model+benchmark
|
|
|
890
928
|
content: [
|
|
891
929
|
{
|
|
892
930
|
type: 'text',
|
|
893
|
-
text: `${data.model} on ${data.benchmark}: ${data.points.length} points\n${summary}\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
931
|
+
text: `${sanitizeReflectedValue(data.model)} on ${sanitizeReflectedValue(data.benchmark)}: ${data.points.length} points\n${summary}\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
894
932
|
},
|
|
895
933
|
],
|
|
896
934
|
};
|
|
@@ -905,14 +943,14 @@ registerTool('benchmark_series', 'Daily benchmark scores for one model+benchmark
|
|
|
905
943
|
content: [
|
|
906
944
|
{
|
|
907
945
|
type: 'text',
|
|
908
|
-
text: `${data.model} on ${data.benchmark}: ${data.points.length} points\n${summary}`,
|
|
946
|
+
text: `${sanitizeReflectedValue(data.model)} on ${sanitizeReflectedValue(data.benchmark)}: ${data.points.length} points\n${summary}`,
|
|
909
947
|
},
|
|
910
948
|
],
|
|
911
949
|
};
|
|
912
950
|
});
|
|
913
951
|
// ── Tool: status_uptime (free 1 to 7 days, paid 8 to 90 days) ───────
|
|
914
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.', {
|
|
915
|
-
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).'),
|
|
916
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.'),
|
|
917
955
|
}, async ({ provider, days }) => {
|
|
918
956
|
const d = days ?? 7;
|
|
@@ -926,7 +964,7 @@ registerTool('status_uptime', 'Daily uptime rollup for one provider over a windo
|
|
|
926
964
|
content: [
|
|
927
965
|
{
|
|
928
966
|
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` +
|
|
967
|
+
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
968
|
` operational: ${data.days_operational}, degraded: ${data.days_degraded}, down: ${data.days_down}` +
|
|
931
969
|
incidents +
|
|
932
970
|
`\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
@@ -940,7 +978,7 @@ registerTool('status_uptime', 'Daily uptime rollup for one provider over a windo
|
|
|
940
978
|
content: [
|
|
941
979
|
{
|
|
942
980
|
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)`,
|
|
981
|
+
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
982
|
},
|
|
945
983
|
],
|
|
946
984
|
};
|
|
@@ -1025,12 +1063,12 @@ registerTool('whats_new', 'Catch up on everything that changed in AI in one call
|
|
|
1025
1063
|
});
|
|
1026
1064
|
// ── Tool: compare_models (1 credit) ─────────────────────────────────
|
|
1027
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).', {
|
|
1028
|
-
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"'),
|
|
1029
1067
|
}, async ({ ids }) => {
|
|
1030
1068
|
const data = (await fetchJSON(`/premium/compare/models?ids=${encodeURIComponent(ids)}`, { auth: true }));
|
|
1031
1069
|
const lines = data.models.map(m => {
|
|
1032
1070
|
if (!m.matched)
|
|
1033
|
-
return ` ${m.query}: not found`;
|
|
1071
|
+
return ` ${sanitizeReflectedValue(m.query)}: not found`;
|
|
1034
1072
|
const benches = data.benchmark_keys
|
|
1035
1073
|
.map(k => `${k}=${m.benchmarks[k] ?? '-'}`)
|
|
1036
1074
|
.join(', ');
|
|
@@ -1052,7 +1090,7 @@ registerTool('compare_models', 'Pick between models in one call: pricing, benchm
|
|
|
1052
1090
|
});
|
|
1053
1091
|
// ── Tool: provider_deepdive (1 credit) ──────────────────────────────
|
|
1054
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.', {
|
|
1055
|
-
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'),
|
|
1056
1094
|
}, async ({ provider }) => {
|
|
1057
1095
|
const data = (await fetchJSON(`/premium/providers/${encodeURIComponent(provider)}`, { auth: true }));
|
|
1058
1096
|
const modelLines = data.models
|
|
@@ -1103,12 +1141,12 @@ registerTool('list_watches', 'List the active webhook watches owned by the confi
|
|
|
1103
1141
|
// create_*_watch tool used, against the same /premium/watches endpoint.
|
|
1104
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.', {
|
|
1105
1143
|
type: z.enum(['price', 'status', 'digest', 'leaderboard_rank']).describe('What to watch.'),
|
|
1106
|
-
callback_url: z.string().describe('HTTPS URL that receives the HMAC-signed POST when the watch fires.'),
|
|
1107
|
-
secret: z.string().optional().describe('Optional shared secret used to HMAC-sign delivery bodies.'),
|
|
1108
|
-
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").'),
|
|
1109
1147
|
field: z.enum(['inputPrice', 'outputPrice', 'blended']).optional().describe('type=price only. Which price field to watch.'),
|
|
1110
1148
|
cadence: z.enum(['daily', 'weekly']).optional().describe('type=digest only. How often the digest fires.'),
|
|
1111
|
-
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.'),
|
|
1112
1150
|
op: z
|
|
1113
1151
|
.enum(['lt', 'gt', 'changes', 'becomes', 'drops_below', 'rises_above'])
|
|
1114
1152
|
.optional()
|
|
@@ -1209,9 +1247,10 @@ registerTool('create_watch', 'Register a webhook watch. type selects what to wat
|
|
|
1209
1247
|
if (secret !== undefined)
|
|
1210
1248
|
body.secret = secret;
|
|
1211
1249
|
const data = (await fetchJSON('/premium/watches', { method: 'POST', body, auth: true }));
|
|
1250
|
+
const safeProvider = sanitizeReflectedValue(provider);
|
|
1212
1251
|
const desc = op === 'changes'
|
|
1213
|
-
? `${
|
|
1214
|
-
: `${
|
|
1252
|
+
? `${safeProvider} rank changes`
|
|
1253
|
+
: `${safeProvider} ${(op ?? '').replace('_', ' ')} #${threshold}`;
|
|
1215
1254
|
return {
|
|
1216
1255
|
content: [
|
|
1217
1256
|
{
|
package/dist/sanitize.d.ts
CHANGED
|
@@ -28,6 +28,40 @@
|
|
|
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;
|
|
50
|
+
/**
|
|
51
|
+
* Neutralize a caller-supplied identifier before echoing it back inside a
|
|
52
|
+
* human-readable message (for example `Service "X" not found`). Model,
|
|
53
|
+
* provider, service and benchmark names are the only caller text that lands
|
|
54
|
+
* in our response strings, and a well-formed one never contains angle
|
|
55
|
+
* brackets. Stripping `<` and `>` means a value like
|
|
56
|
+
* `<script>alert(1)</script>` cannot be reflected as live markup into a
|
|
57
|
+
* downstream web renderer, nor masquerade as a tag in an LLM's context. The
|
|
58
|
+
* length cap stops a caller padding the message. This runs on echoed caller
|
|
59
|
+
* input only, never on the structured data we return, so arrow notation
|
|
60
|
+
* (`->`) and other formatted output elsewhere is untouched. The full
|
|
61
|
+
* response still passes through sanitizeForLLM afterward for control-char
|
|
62
|
+
* and role-token scrubbing.
|
|
63
|
+
*/
|
|
64
|
+
export declare function sanitizeReflectedValue(input: unknown): string;
|
|
31
65
|
/**
|
|
32
66
|
* Recursively scrub the `text` field of every text-content item in an
|
|
33
67
|
* 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.
|
|
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"
|