@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 +10 -0
- package/dist/index.js +25 -22
- package/dist/sanitize.d.ts +15 -0
- package/dist/sanitize.js +0 -0
- package/package.json +1 -1
- package/server.json +2 -2
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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
|
-
? `${
|
|
1214
|
-
: `${
|
|
1216
|
+
? `${safeProvider} rank changes`
|
|
1217
|
+
: `${safeProvider} ${(op ?? '').replace('_', ' ')} #${threshold}`;
|
|
1215
1218
|
return {
|
|
1216
1219
|
content: [
|
|
1217
1220
|
{
|
package/dist/sanitize.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
23
|
+
"version": "2.0.2",
|
|
24
24
|
"runtimeHint": "npx",
|
|
25
25
|
"transport": {
|
|
26
26
|
"type": "stdio"
|