@tensorfeed/mcp-server 1.11.1 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,25 @@
1
1
  # Changelog
2
2
 
3
- All notable changes to the [TensorFeed.ai MCP server](https://github.com/RipperMercs/tensorfeed/tree/main/mcp-server). 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).
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
+
5
+ ## 1.13.0 - 2026-05-04
6
+
7
+ ### Added
8
+ - `get_ai_papers_trending` tool. Daily curated AI/ML research papers ranked by citation count, sourced from Semantic Scholar. Five fan-out queries (LLM, transformer, RLHF, AI agents, diffusion), deduped, top 30. Free.
9
+ - `get_arxiv_recent` tool. Most recent arXiv submissions in cs.AI / cs.LG / cs.CL / cs.CV, top 50 by submission date. Optional `limit` arg for the rendered list. Free.
10
+ - `get_hf_trending` tool. Top 30 most-downloaded models and datasets on Hugging Face. Optional `section` (models / datasets / both) and `limit` args. Free.
11
+
12
+ All three are backed by the worker endpoints shipped in the same monorepo commit and pass through the 1.12.0 output sanitizer automatically.
13
+
14
+ ## 1.12.0 - 2026-05-04
15
+
16
+ ### Added
17
+ - Defense-in-depth output sanitizer (`src/sanitize.ts`). Every tool's text output passes through a scrub pass before reaching the host LLM (Claude Desktop, Cursor, Cline, Roo, etc): strips ASCII control chars (preserves newline, carriage return, tab), strips bidi control + zero-width spoofing chars, and neutralizes role-confusion markers (ChatML / Llama / Mistral chat tokens, "ignore previous instructions", line-leading "system:" / "developer:" / "assistant:" preambles). Wired via a new `registerTool()` helper that wraps `server.tool()`. The TensorFeed worker already runs the same scrub on its agent-facing endpoints; this layer protects against any new endpoint that might forget it, plus drift between worker rules and what the MCP server should consider safe. No behavior change for legitimate text. No new dependencies.
18
+
19
+ ## 1.11.1 - 2026-05-04
20
+
21
+ ### Changed
22
+ - Repository URL updated to point at the dedicated [`RipperMercs/tensorfeed-mcp`](https://github.com/RipperMercs/tensorfeed-mcp) repo. The MCP server now has its own GitHub home for issues, stars, and discoverability. Source-of-truth for builds remains in the main `tensorfeed` monorepo's `mcp-server/` folder; the standalone repo is mirrored on release. No code changes; no behavior changes; npm install / npx unaffected.
4
23
 
5
24
  ## 1.11.0 - 2026-04-29
6
25
 
package/README.md CHANGED
@@ -17,13 +17,31 @@ cd mcp-server
17
17
  npm run build
18
18
  npm publish --access public
19
19
 
20
- # 3. Republish to the official MCP registry
20
+ # 3. Republish to the official MCP registry. The script lives at
21
+ # repo-root/scripts/, not mcp-server/scripts/, so step back up first.
22
+ cd ..
21
23
  .\scripts\mcp-publish.ps1
22
24
 
23
- # 4. Mirror the new version to the standalone repo
24
- # (manual until we wire it as a release-time GitHub Action)
25
+ # 4. Mirror to the standalone repo - automated. The
26
+ # .github/workflows/mirror-mcp-server.yml workflow runs on every
27
+ # push to main that touches mcp-server/. To trigger a manual sync,
28
+ # go to the Actions tab and run "Mirror MCP server to standalone
29
+ # repo" via workflow_dispatch.
25
30
  ```
26
31
 
32
+ ### Automated mirror setup (one-time)
33
+
34
+ The mirror workflow needs a personal access token with `contents: write`
35
+ permission on `RipperMercs/tensorfeed-mcp`. Set it once:
36
+
37
+ 1. Generate a fine-grained PAT at github.com/settings/personal-access-tokens
38
+ scoped to the standalone repo only, with Repository permissions
39
+ `Contents: Read and write` and `Metadata: Read-only`.
40
+ 2. Add it to this monorepo as a repository secret named
41
+ `STANDALONE_REPO_TOKEN` (Settings -> Secrets and variables -> Actions).
42
+ 3. The workflow will pick it up on the next push to main that changes
43
+ `mcp-server/**`, or via the manual "Run workflow" button.
44
+
27
45
  ## Quick links
28
46
 
29
47
  - **User-facing repo (please star):** https://github.com/RipperMercs/tensorfeed-mcp
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
4
  import { z } from 'zod';
5
+ import { sanitizeToolResponse } from './sanitize.js';
5
6
  const API_BASE = 'https://tensorfeed.ai/api';
6
7
  const SDK_VERSION = '1.9.0';
7
8
  async function fetchJSON(path, opts = {}) {
@@ -46,8 +47,26 @@ const server = new McpServer({
46
47
  name: 'tensorfeed',
47
48
  version: SDK_VERSION,
48
49
  });
50
+ // ── Defense-in-depth tool wrapper ───────────────────────────────────
51
+ // Every tool's text output passes through sanitizeToolResponse before
52
+ // reaching the host LLM. Strips control chars, bidi/zero-width spoofing,
53
+ // and neutralizes role-confusion tokens (ChatML / Llama / Mistral chat
54
+ // markers, "ignore previous instructions", etc). The TensorFeed worker
55
+ // already runs the same scrub on its agent-facing endpoints; this layer
56
+ // covers any new endpoint that might forget it, plus protects against
57
+ // drift between worker rules and what the MCP server should consider
58
+ // safe. See ./sanitize.ts.
59
+ function registerTool(name, description, paramsSchema, cb) {
60
+ const wrapped = (async (args, extra) => {
61
+ const result = await cb(args, extra);
62
+ return sanitizeToolResponse(result);
63
+ });
64
+ // Direct dispatch to the SDK's underlying registration. Do not call
65
+ // registerTool() here or it will recurse forever.
66
+ return server.tool(name, description, paramsSchema, wrapped);
67
+ }
49
68
  // ── Tool: get_ai_news ───────────────────────────────────────────────
50
- server.tool('get_ai_news', 'Get the latest AI news articles from TensorFeed.ai. Aggregates from 15+ sources including Anthropic, OpenAI, Google, TechCrunch, The Verge, arXiv, and more.', {
69
+ registerTool('get_ai_news', 'Get the latest AI news articles from TensorFeed.ai. Aggregates from 15+ sources including Anthropic, OpenAI, Google, TechCrunch, The Verge, arXiv, and more.', {
51
70
  category: z.string().optional().describe('Filter by category (e.g. "anthropic", "openai", "research", "tools")'),
52
71
  limit: z.number().min(1).max(50).optional().describe('Number of articles to return (default 10, max 50)'),
53
72
  }, async ({ category, limit }) => {
@@ -62,7 +81,7 @@ server.tool('get_ai_news', 'Get the latest AI news articles from TensorFeed.ai.
62
81
  return { content: [{ type: 'text', text: text || 'No articles found.' }] };
63
82
  });
64
83
  // ── Tool: get_ai_status ─────────────────────────────────────────────
65
- server.tool('get_ai_status', 'Get real-time operational status of major AI services (Claude, OpenAI, Gemini, Mistral, Cohere, Replicate, Hugging Face).', {}, async () => {
84
+ registerTool('get_ai_status', 'Get real-time operational status of major AI services (Claude, OpenAI, Gemini, Mistral, Cohere, Replicate, Hugging Face).', {}, async () => {
66
85
  const data = await fetchJSON('/status');
67
86
  const text = data.services
68
87
  .map(s => {
@@ -75,7 +94,7 @@ server.tool('get_ai_status', 'Get real-time operational status of major AI servi
75
94
  return { content: [{ type: 'text', text: `AI Service Status:\n${text}` }] };
76
95
  });
77
96
  // ── Tool: is_service_down ───────────────────────────────────────────
78
- server.tool('is_service_down', 'Check if a specific AI service is currently down or experiencing issues.', {
97
+ registerTool('is_service_down', 'Check if a specific AI service is currently down or experiencing issues.', {
79
98
  service: z.string().describe('Service name to check (e.g. "claude", "openai", "gemini", "mistral", "cohere", "hugging face", "replicate")'),
80
99
  }, async ({ service }) => {
81
100
  const data = await fetchJSON('/status');
@@ -96,7 +115,7 @@ server.tool('is_service_down', 'Check if a specific AI service is currently down
96
115
  };
97
116
  });
98
117
  // ── Tool: get_model_pricing ─────────────────────────────────────────
99
- server.tool('get_model_pricing', 'Get AI model pricing comparison across all major providers (Anthropic, OpenAI, Google, Meta, Mistral, Cohere). Prices per 1M tokens.', {}, async () => {
118
+ registerTool('get_model_pricing', 'Get AI model pricing comparison across all major providers (Anthropic, OpenAI, Google, Meta, Mistral, Cohere). Prices per 1M tokens.', {}, async () => {
100
119
  const data = await fetchJSON('/models');
101
120
  const text = data.providers
102
121
  .map(p => {
@@ -116,7 +135,7 @@ server.tool('get_model_pricing', 'Get AI model pricing comparison across all maj
116
135
  return { content: [{ type: 'text', text: `AI Model Pricing (per 1M tokens):\n\n${text}` }] };
117
136
  });
118
137
  // ── Tool: get_ai_today ──────────────────────────────────────────────
119
- server.tool('get_ai_today', 'Get a summary of what happened in AI today. Returns the top stories from the last 24 hours.', {
138
+ registerTool('get_ai_today', 'Get a summary of what happened in AI today. Returns the top stories from the last 24 hours.', {
120
139
  limit: z.number().min(1).max(20).optional().describe('Number of stories (default 5)'),
121
140
  }, async ({ limit }) => {
122
141
  const data = await fetchJSON(`/news?limit=${limit || 5}`);
@@ -126,7 +145,7 @@ server.tool('get_ai_today', 'Get a summary of what happened in AI today. Returns
126
145
  return { content: [{ type: 'text', text: `Today in AI:\n\n${text}` }] };
127
146
  });
128
147
  // ── Tool: get_agent_activity ────────────────────────────────────────
129
- server.tool('get_agent_activity', 'Get live AI bot traffic on TensorFeed.ai. Returns today\'s bot hit count, the most recent 50 hits with bot name + endpoint + timestamp, and a derived top-bots breakdown. Useful when an agent wants to see which crawlers are pulling AI ecosystem data and how that distribution shifts. Free, no auth.', {}, async () => {
148
+ registerTool('get_agent_activity', 'Get live AI bot traffic on TensorFeed.ai. Returns today\'s bot hit count, the most recent 50 hits with bot name + endpoint + timestamp, and a derived top-bots breakdown. Useful when an agent wants to see which crawlers are pulling AI ecosystem data and how that distribution shifts. Free, no auth.', {}, async () => {
130
149
  const data = (await fetchJSON('/agents/activity'));
131
150
  const tally = new Map();
132
151
  for (const hit of data.recent) {
@@ -158,7 +177,7 @@ server.tool('get_agent_activity', 'Get live AI bot traffic on TensorFeed.ai. Ret
158
177
  // PREMIUM TOOLS (require TENSORFEED_TOKEN env var, paid in USDC on Base)
159
178
  // ════════════════════════════════════════════════════════════════════
160
179
  // ── Tool: get_account_balance ───────────────────────────────────────
161
- server.tool('get_account_balance', 'Check the credit balance for the configured TensorFeed bearer token. Free, but requires TENSORFEED_TOKEN to be set.', {}, async () => {
180
+ registerTool('get_account_balance', 'Check the credit balance for the configured TensorFeed bearer token. Free, but requires TENSORFEED_TOKEN to be set.', {}, async () => {
162
181
  const data = (await fetchJSON('/payment/balance', { auth: true }));
163
182
  return {
164
183
  content: [
@@ -170,7 +189,7 @@ server.tool('get_account_balance', 'Check the credit balance for the configured
170
189
  };
171
190
  });
172
191
  // ── Tool: get_account_usage ─────────────────────────────────────────
173
- server.tool('get_account_usage', 'Show per-endpoint usage for the configured TensorFeed token (last 100 calls aggregated). Free, but requires TENSORFEED_TOKEN.', {}, async () => {
192
+ registerTool('get_account_usage', 'Show per-endpoint usage for the configured TensorFeed token (last 100 calls aggregated). Free, but requires TENSORFEED_TOKEN.', {}, async () => {
174
193
  const data = (await fetchJSON('/payment/usage', { auth: true }));
175
194
  if (data.total_calls === 0) {
176
195
  return { content: [{ type: 'text', text: 'No premium API calls on this token yet.' }] };
@@ -189,7 +208,7 @@ server.tool('get_account_usage', 'Show per-endpoint usage for the configured Ten
189
208
  };
190
209
  });
191
210
  // ── Tool: premium_routing (1 credit) ────────────────────────────────
192
- server.tool('premium_routing', 'Get a ranked list of recommended AI models for a task with full score breakdown (quality, availability, cost, latency). Costs 1 credit.', {
211
+ registerTool('premium_routing', 'Get a ranked list of recommended AI models for a task with full score breakdown (quality, availability, cost, latency). Costs 1 credit.', {
193
212
  task: z.enum(['code', 'reasoning', 'creative', 'general']).optional().describe('Task type the model needs to be good at (default: general)'),
194
213
  budget: z.number().optional().describe('Max blended USD per 1M tokens'),
195
214
  min_quality: z.number().min(0).max(1).optional().describe('Minimum quality score in [0, 1]'),
@@ -217,7 +236,7 @@ server.tool('premium_routing', 'Get a ranked list of recommended AI models for a
217
236
  return { content: [{ type: 'text', text: `Routing for "${data.task}":\n\n${list}${billing}` }] };
218
237
  });
219
238
  // ── Tool: pricing_series (1 credit) ─────────────────────────────────
220
- server.tool('pricing_series', 'Daily price points for one AI model with min/max/delta summary. Default range = last 30 days, max 90 days. Costs 1 credit.', {
239
+ registerTool('pricing_series', 'Daily price points for one AI model with min/max/delta summary. Default range = last 30 days, max 90 days. Costs 1 credit.', {
221
240
  model: z.string().describe('Model id or display name (e.g. "Claude Opus 4.7" or "claude-opus-4-7")'),
222
241
  from: z.string().optional().describe('Start date YYYY-MM-DD UTC (default: 30 days ago)'),
223
242
  to: z.string().optional().describe('End date YYYY-MM-DD UTC (default: today)'),
@@ -242,7 +261,7 @@ server.tool('pricing_series', 'Daily price points for one AI model with min/max/
242
261
  };
243
262
  });
244
263
  // ── Tool: benchmark_series (1 credit) ───────────────────────────────
245
- server.tool('benchmark_series', 'Score evolution for a single benchmark on one AI model. Costs 1 credit. Benchmark keys: swe_bench, mmlu_pro, gpqa_diamond, math, human_eval.', {
264
+ registerTool('benchmark_series', 'Score evolution for a single benchmark on one AI model. Costs 1 credit. Benchmark keys: swe_bench, mmlu_pro, gpqa_diamond, math, human_eval.', {
246
265
  model: z.string().describe('Model id or display name'),
247
266
  benchmark: z.string().describe('Benchmark key (e.g. swe_bench, mmlu_pro, gpqa_diamond, math, human_eval)'),
248
267
  from: z.string().optional().describe('Start date YYYY-MM-DD UTC (default: 30 days ago)'),
@@ -268,7 +287,7 @@ server.tool('benchmark_series', 'Score evolution for a single benchmark on one A
268
287
  };
269
288
  });
270
289
  // ── Tool: status_uptime (1 credit) ──────────────────────────────────
271
- server.tool('status_uptime', 'Daily uptime rollup for one provider with operational/degraded/down day counts and uptime % (degraded counts as half-credit). Costs 1 credit.', {
290
+ registerTool('status_uptime', 'Daily uptime rollup for one provider with operational/degraded/down day counts and uptime % (degraded counts as half-credit). Costs 1 credit.', {
272
291
  provider: z.string().describe('Provider name (e.g. anthropic, openai, google)'),
273
292
  from: z.string().optional().describe('Start date YYYY-MM-DD UTC (default: 30 days ago)'),
274
293
  to: z.string().optional().describe('End date YYYY-MM-DD UTC (default: today)'),
@@ -295,7 +314,7 @@ server.tool('status_uptime', 'Daily uptime rollup for one provider with operatio
295
314
  };
296
315
  });
297
316
  // ── Tool: premium_agents_directory (1 credit) ───────────────────────
298
- server.tool('premium_agents_directory', 'Enriched AI agents catalog joined with live status, recent news, agent traffic, flagship pricing, and a 0-100 trending_score. Costs 1 credit.', {
317
+ registerTool('premium_agents_directory', 'Enriched AI agents catalog joined with live status, recent news, agent traffic, flagship pricing, and a 0-100 trending_score. Costs 1 credit.', {
299
318
  category: z.string().optional().describe('coding, research, general, creative, frameworks'),
300
319
  status: z.enum(['operational', 'degraded', 'down', 'unknown']).optional(),
301
320
  open_source: z.boolean().optional(),
@@ -328,7 +347,7 @@ server.tool('premium_agents_directory', 'Enriched AI agents catalog joined with
328
347
  };
329
348
  });
330
349
  // ── Tool: whats_new (1 credit) ──────────────────────────────────────
331
- server.tool('whats_new', 'Agent morning brief: pricing changes, new/removed models, status incidents, and top news from the last 1-7 days. The tool to call when your agent boots up. Costs 1 credit.', {
350
+ registerTool('whats_new', 'Agent morning brief: pricing changes, new/removed models, status incidents, and top news from the last 1-7 days. The tool to call when your agent boots up. Costs 1 credit.', {
332
351
  days: z.number().min(1).max(7).optional().describe('Window length in days (default 1)'),
333
352
  news_limit: z.number().min(1).max(25).optional().describe('Max news headlines (default 10)'),
334
353
  }, async ({ days, news_limit }) => {
@@ -364,7 +383,7 @@ server.tool('whats_new', 'Agent morning brief: pricing changes, new/removed mode
364
383
  };
365
384
  });
366
385
  // ── Tool: compare_models (1 credit) ─────────────────────────────────
367
- server.tool('compare_models', 'Side-by-side comparison of 2-5 AI models. Returns pricing, benchmarks (normalized to a union of keys with null for missing scores), provider status, and recent news per model, plus rankings (cheapest blended, most context, per-benchmark leaderboard). Costs 1 credit.', {
386
+ registerTool('compare_models', 'Side-by-side comparison of 2-5 AI models. Returns pricing, benchmarks (normalized to a union of keys with null for missing scores), provider status, and recent news per model, plus rankings (cheapest blended, most context, per-benchmark leaderboard). Costs 1 credit.', {
368
387
  ids: z.string().describe('Comma-separated list of 2-5 model ids or display names. Examples: "Claude Opus 4.7,GPT-5.5,Gemini 3" or "opus-4-7,gpt-5-5"'),
369
388
  }, async ({ ids }) => {
370
389
  const data = (await fetchJSON(`/premium/compare/models?ids=${encodeURIComponent(ids)}`, { auth: true }));
@@ -391,7 +410,7 @@ server.tool('compare_models', 'Side-by-side comparison of 2-5 AI models. Returns
391
410
  };
392
411
  });
393
412
  // ── Tool: provider_deepdive (1 credit) ──────────────────────────────
394
- server.tool('provider_deepdive', 'Everything about an AI provider in one call: live status, all models with pricing + tier + benchmark scores joined in, recent news mentions, and agent traffic. Costs 1 credit. Aggregation IS the value; doing this from free endpoints would take 4 round-trips.', {
413
+ registerTool('provider_deepdive', 'Everything about an AI provider in one call: live status, all models with pricing + tier + benchmark scores joined in, recent news mentions, and agent traffic. Costs 1 credit. Aggregation IS the value; doing this from free endpoints would take 4 round-trips.', {
395
414
  provider: z.string().describe('Provider id or display name (case-insensitive). Examples: anthropic, openai, google, mistral, cohere'),
396
415
  }, async ({ provider }) => {
397
416
  const data = (await fetchJSON(`/premium/providers/${encodeURIComponent(provider)}`, { auth: true }));
@@ -421,7 +440,7 @@ server.tool('provider_deepdive', 'Everything about an AI provider in one call: l
421
440
  };
422
441
  });
423
442
  // ── Tool: cost_projection (1 credit) ────────────────────────────────
424
- server.tool('cost_projection', 'Project the cost of a token-usage workload across 1-10 AI models. Returns daily/weekly/monthly/yearly totals per model and a ranking by cheapest monthly. Costs 1 credit.', {
443
+ registerTool('cost_projection', 'Project the cost of a token-usage workload across 1-10 AI models. Returns daily/weekly/monthly/yearly totals per model and a ranking by cheapest monthly. Costs 1 credit.', {
425
444
  models: z.string().describe('One model or comma-separated list of up to 10 (e.g. "Claude Opus 4.7,GPT-5.5,Gemini 3"). Names or ids both work.'),
426
445
  input_tokens_per_day: z.number().min(0).describe('Expected daily input token volume'),
427
446
  output_tokens_per_day: z.number().min(0).describe('Expected daily output token volume'),
@@ -456,7 +475,7 @@ server.tool('cost_projection', 'Project the cost of a token-usage workload acros
456
475
  };
457
476
  });
458
477
  // ── Tool: news_search (1 credit) ────────────────────────────────────
459
- server.tool('news_search', 'Full-text search over the TensorFeed news article corpus with optional date range, provider, and category filters. Relevance scoring with recency boost. Costs 1 credit.', {
478
+ registerTool('news_search', 'Full-text search over the TensorFeed news article corpus with optional date range, provider, and category filters. Relevance scoring with recency boost. Costs 1 credit.', {
460
479
  q: z.string().optional().describe('Free-text query, e.g. "claude opus pricing". Omit to browse latest filtered articles.'),
461
480
  from: z.string().optional().describe('Start date YYYY-MM-DD UTC (inclusive)'),
462
481
  to: z.string().optional().describe('End date YYYY-MM-DD UTC (inclusive end-of-day)'),
@@ -494,7 +513,7 @@ server.tool('news_search', 'Full-text search over the TensorFeed news article co
494
513
  };
495
514
  });
496
515
  // ── Tool: list_watches ──────────────────────────────────────────────
497
- server.tool('list_watches', 'List the active webhook watches owned by the configured TensorFeed token. Free, requires TENSORFEED_TOKEN.', {}, async () => {
516
+ registerTool('list_watches', 'List the active webhook watches owned by the configured TensorFeed token. Free, requires TENSORFEED_TOKEN.', {}, async () => {
498
517
  const data = (await fetchJSON('/premium/watches', { auth: true }));
499
518
  if (data.count === 0) {
500
519
  return { content: [{ type: 'text', text: 'No active watches.' }] };
@@ -510,7 +529,7 @@ server.tool('list_watches', 'List the active webhook watches owned by the config
510
529
  return { content: [{ type: 'text', text: `${data.count} active watches:\n\n${rows}` }] };
511
530
  });
512
531
  // ── Tool: create_price_watch (1 credit) ─────────────────────────────
513
- server.tool('create_price_watch', 'Register a webhook watch on a model price change. Costs 1 credit. Watch lives 90 days. Each fire is an HMAC-signed POST to callback_url.', {
532
+ registerTool('create_price_watch', 'Register a webhook watch on a model price change. Costs 1 credit. Watch lives 90 days. Each fire is an HMAC-signed POST to callback_url.', {
514
533
  model: z.string().describe('Model name (e.g. "Claude Opus 4.7")'),
515
534
  field: z.enum(['inputPrice', 'outputPrice', 'blended']).describe('Which price field to watch'),
516
535
  op: z.enum(['lt', 'gt', 'changes']).describe('Trigger: lt = below threshold, gt = above, changes = any change'),
@@ -535,7 +554,7 @@ server.tool('create_price_watch', 'Register a webhook watch on a model price cha
535
554
  };
536
555
  });
537
556
  // ── Tool: create_status_watch (1 credit) ────────────────────────────
538
- server.tool('create_status_watch', 'Register a webhook watch on a service status transition (e.g. anthropic becomes down). Costs 1 credit. Watch lives 90 days.', {
557
+ registerTool('create_status_watch', 'Register a webhook watch on a service status transition (e.g. anthropic becomes down). Costs 1 credit. Watch lives 90 days.', {
539
558
  provider: z.string().describe('Provider name (e.g. anthropic, openai)'),
540
559
  op: z.enum(['becomes', 'changes']).describe('becomes = transitions to a specific value; changes = any transition'),
541
560
  value: z.enum(['operational', 'degraded', 'down']).optional().describe('Required when op is becomes'),
@@ -559,7 +578,7 @@ server.tool('create_status_watch', 'Register a webhook watch on a service status
559
578
  };
560
579
  });
561
580
  // ── Tool: create_digest_watch (1 credit) ────────────────────────────
562
- server.tool('create_digest_watch', 'Register a scheduled digest webhook that fires daily or weekly with a curated summary of pricing changes (regardless of whether anything dramatic happened). Costs 1 credit. Watch lives 90 days. Set-and-forget for agents that want a periodic snapshot without subscribing to realtime transitions.', {
581
+ registerTool('create_digest_watch', 'Register a scheduled digest webhook that fires daily or weekly with a curated summary of pricing changes (regardless of whether anything dramatic happened). Costs 1 credit. Watch lives 90 days. Set-and-forget for agents that want a periodic snapshot without subscribing to realtime transitions.', {
563
582
  cadence: z.enum(['daily', 'weekly']).describe('How often the digest fires'),
564
583
  callback_url: z.string().describe('HTTPS URL to POST to when the digest fires'),
565
584
  secret: z.string().optional().describe('Optional HMAC shared secret'),
@@ -581,14 +600,14 @@ server.tool('create_digest_watch', 'Register a scheduled digest webhook that fir
581
600
  };
582
601
  });
583
602
  // ── Tool: delete_watch ──────────────────────────────────────────────
584
- server.tool('delete_watch', 'Delete one of your active webhook watches by id. Free, requires TENSORFEED_TOKEN.', {
603
+ registerTool('delete_watch', 'Delete one of your active webhook watches by id. Free, requires TENSORFEED_TOKEN.', {
585
604
  watch_id: z.string().describe('The wat_... id from create_price_watch / create_status_watch / list_watches'),
586
605
  }, async ({ watch_id }) => {
587
606
  await fetchJSON(`/premium/watches/${watch_id}`, { method: 'DELETE', auth: true });
588
607
  return { content: [{ type: 'text', text: `Deleted watch ${watch_id}.` }] };
589
608
  });
590
609
  // ── Tool: probe_latest (free) ───────────────────────────────────────
591
- server.tool('probe_latest', 'Last 24 hours of measured LLM endpoint latency and availability per provider (Anthropic, OpenAI, Google, Mistral, Cohere). TensorFeed pings each provider\'s chat completion endpoint every 15 min and records time-to-first-byte, total response time, and HTTP status. Returns per-provider success rate and ttfb/total p50/p95/p99 latency. The data is unique because we measure it ourselves, not self-reported by the providers. Useful when an agent needs to pick a provider whose SLA you can verify, or to detect ongoing incidents before they hit a status page. Free, no auth.', {}, async () => {
610
+ registerTool('probe_latest', 'Last 24 hours of measured LLM endpoint latency and availability per provider (Anthropic, OpenAI, Google, Mistral, Cohere). TensorFeed pings each provider\'s chat completion endpoint every 15 min and records time-to-first-byte, total response time, and HTTP status. Returns per-provider success rate and ttfb/total p50/p95/p99 latency. The data is unique because we measure it ourselves, not self-reported by the providers. Useful when an agent needs to pick a provider whose SLA you can verify, or to detect ongoing incidents before they hit a status page. Free, no auth.', {}, async () => {
592
611
  const data = (await fetchJSON('/probe/latest'));
593
612
  const s = data.summary;
594
613
  if (s.providers.length === 0) {
@@ -614,7 +633,7 @@ server.tool('probe_latest', 'Last 24 hours of measured LLM endpoint latency and
614
633
  };
615
634
  });
616
635
  // ── Tool: probe_series (1 credit) ───────────────────────────────────
617
- server.tool('probe_series', 'Daily SLA series for one LLM provider, measured by TensorFeed. Returns per-day count, success rate, ttfb p50/p95/p99, total p50/p95/p99, and incident-hour count across the requested window. Provider status pages are politically managed; this is the measured truth. Pairs naturally with premium_routing for picking a model whose SLA you can verify. Costs 1 credit.', {
636
+ registerTool('probe_series', 'Daily SLA series for one LLM provider, measured by TensorFeed. Returns per-day count, success rate, ttfb p50/p95/p99, total p50/p95/p99, and incident-hour count across the requested window. Provider status pages are politically managed; this is the measured truth. Pairs naturally with premium_routing for picking a model whose SLA you can verify. Costs 1 credit.', {
618
637
  provider: z.enum(['anthropic', 'openai', 'google', 'mistral', 'cohere']).describe('LLM provider key'),
619
638
  from: z.string().optional().describe('Inclusive start YYYY-MM-DD (default: 30 days before to)'),
620
639
  to: z.string().optional().describe('Inclusive end YYYY-MM-DD (default: today UTC)'),
@@ -645,7 +664,7 @@ server.tool('probe_series', 'Daily SLA series for one LLM provider, measured by
645
664
  };
646
665
  });
647
666
  // ── Tool: mcp_registry_snapshot (free) ──────────────────────────────
648
- server.tool('mcp_registry_snapshot', 'Today\'s summary of the official Model Context Protocol server registry. Returns total servers, by-status breakdown, top namespaces, and 1-day deltas (newly added, reactivated, deprecated). Captured daily at 9:30 AM UTC from registry.modelcontextprotocol.io. Useful when an agent wants to see how the MCP ecosystem is growing or detect freshly-deprecated servers it may be using. Free, no auth.', {}, async () => {
667
+ registerTool('mcp_registry_snapshot', 'Today\'s summary of the official Model Context Protocol server registry. Returns total servers, by-status breakdown, top namespaces, and 1-day deltas (newly added, reactivated, deprecated). Captured daily at 9:30 AM UTC from registry.modelcontextprotocol.io. Useful when an agent wants to see how the MCP ecosystem is growing or detect freshly-deprecated servers it may be using. Free, no auth.', {}, async () => {
649
668
  const data = (await fetchJSON('/mcp/registry/snapshot'));
650
669
  const s = data.summary;
651
670
  const statusLine = Object.entries(s.by_status)
@@ -676,7 +695,7 @@ server.tool('mcp_registry_snapshot', 'Today\'s summary of the official Model Con
676
695
  };
677
696
  });
678
697
  // ── Tool: mcp_registry_series (1 credit) ────────────────────────────
679
- server.tool('mcp_registry_series', 'Multi-day series of MCP server registry growth and churn. Returns per-day total servers, active count, and daily added/removed counts across the requested window. The registry itself is open data, but a 30/90-day trend requires daily capture started weeks ago, which TensorFeed has been running. Costs 1 credit.', {
698
+ registerTool('mcp_registry_series', 'Multi-day series of MCP server registry growth and churn. Returns per-day total servers, active count, and daily added/removed counts across the requested window. The registry itself is open data, but a 30/90-day trend requires daily capture started weeks ago, which TensorFeed has been running. Costs 1 credit.', {
680
699
  from: z.string().optional().describe('Inclusive start date YYYY-MM-DD (default: 30 days before to)'),
681
700
  to: z.string().optional().describe('Inclusive end date YYYY-MM-DD (default: today UTC)'),
682
701
  }, async ({ from, to }) => {
@@ -705,6 +724,85 @@ server.tool('mcp_registry_series', 'Multi-day series of MCP server registry grow
705
724
  ],
706
725
  };
707
726
  });
727
+ // ── Tool: get_ai_papers_trending (free) ─────────────────────────────
728
+ registerTool('get_ai_papers_trending', 'Daily curated AI/ML research papers ranked by citation count. Sourced from Semantic Scholar across five fan-out queries (large language model, transformer, RLHF, AI agents, diffusion model), deduped by paperId, top 30 returned. Each paper carries title, abstract, authors, year, venue, citation count, arxivId, doi, and fields of study. Refreshed daily at 11:00 UTC. Use this when an agent wants the citation-ranked top of the AI research literature. Free, no auth.', {}, async () => {
729
+ const data = (await fetchJSON('/papers/ai-trending'));
730
+ const s = data.snapshot;
731
+ const lines = s.papers.map((p, i) => {
732
+ const authors = p.authors.slice(0, 3).join(', ') + (p.authors.length > 3 ? ' et al.' : '');
733
+ const year = p.year ? ` (${p.year})` : '';
734
+ const venue = p.venue ? ` ${p.venue}.` : '';
735
+ const arxiv = p.arxivId ? ` arXiv:${p.arxivId}` : '';
736
+ const link = p.url ? `\n ${p.url}` : '';
737
+ return `${i + 1}. ${p.title}${year}\n ${authors}.${venue} Citations: ${p.citationCount}.${arxiv}${link}`;
738
+ }).join('\n\n');
739
+ const venues = s.summary.top_venues.slice(0, 5).map(v => `${v.venue} (${v.count})`).join(', ');
740
+ return {
741
+ content: [
742
+ {
743
+ type: 'text',
744
+ text: `AI Papers, citation-ranked. ${s.date}, ${s.total_papers} papers.\n` +
745
+ (venues ? `Top venues: ${venues}\n\n` : '\n') +
746
+ lines +
747
+ `\n\nSource: Semantic Scholar Graph API. Captured ${s.capturedAt}.`,
748
+ },
749
+ ],
750
+ };
751
+ });
752
+ // ── Tool: get_arxiv_recent (free) ───────────────────────────────────
753
+ registerTool('get_arxiv_recent', 'Most recent arXiv submissions in cs.AI / cs.LG / cs.CL / cs.CV. Single Atom API call, deduped by arxivId, top 50 by submission date. The firehose pair to get_ai_papers_trending: this shows what just dropped, ai_papers_trending ranks by citation count. Each paper carries arxivId, title, abstract, authors, primary category, publication date, html and pdf URLs. Refreshed daily at 11:30 UTC. Free, no auth.', {
754
+ limit: z.number().min(1).max(50).optional().describe('Max papers to include in the rendered list (default 25, max 50)'),
755
+ }, async ({ limit }) => {
756
+ const data = (await fetchJSON('/papers/arxiv-recent'));
757
+ const s = data.snapshot;
758
+ const cap = Math.min(limit ?? 25, s.papers.length);
759
+ const lines = s.papers.slice(0, cap).map((p, i) => {
760
+ const authors = p.authors.slice(0, 3).join(', ') + (p.authors.length > 3 ? ' et al.' : '');
761
+ const cat = p.primaryCategory ? ` [${p.primaryCategory}]` : '';
762
+ const date = p.publishedAt ? ` ${p.publishedAt.slice(0, 10)}` : '';
763
+ return `${i + 1}. ${p.title}${cat}${date}\n ${authors}\n ${p.htmlUrl}`;
764
+ }).join('\n\n');
765
+ return {
766
+ content: [
767
+ {
768
+ type: 'text',
769
+ text: `arXiv recent submissions. ${s.date}, showing ${cap} of ${s.total_papers}.\n` +
770
+ `Categories: ${s.categories_queried.join(', ')}\n\n` +
771
+ lines +
772
+ `\n\nSource: arxiv.org. Captured ${s.capturedAt}.`,
773
+ },
774
+ ],
775
+ };
776
+ });
777
+ // ── Tool: get_hf_trending (free) ────────────────────────────────────
778
+ registerTool('get_hf_trending', 'Top 30 most-downloaded models and top 30 most-downloaded datasets on Hugging Face. Captured daily at 12:00 UTC against the public HF API (no auth). Each entry carries id, downloads, likes, pipeline_tag (models), tags, lastModified, private, gated. Use this when an agent wants the current shape of the open model/dataset ecosystem. Once enough daily snapshots accumulate, day-over-day download deltas become a real trending signal. Free, no auth.', {
779
+ section: z.enum(['models', 'datasets', 'both']).optional().describe('Which to include in the rendered output (default both)'),
780
+ limit: z.number().min(1).max(30).optional().describe('Max items per section (default 15, max 30)'),
781
+ }, async ({ section, limit }) => {
782
+ const data = (await fetchJSON('/hf/trending'));
783
+ const s = data.snapshot;
784
+ const sec = section ?? 'both';
785
+ const n = Math.min(limit ?? 15, 30);
786
+ const fmtCount = (x) => x >= 1_000_000 ? `${(x / 1_000_000).toFixed(1)}M` : x >= 1_000 ? `${(x / 1_000).toFixed(1)}K` : String(x);
787
+ const modelLines = s.models.items.slice(0, n).map((m, i) => {
788
+ const tag = m.pipeline_tag ? ` [${m.pipeline_tag}]` : '';
789
+ return `${i + 1}. ${m.id}${tag} ${fmtCount(m.downloads)} downloads, ${fmtCount(m.likes)} likes`;
790
+ }).join('\n');
791
+ const datasetLines = s.datasets.items.slice(0, n).map((d, i) => `${i + 1}. ${d.id} ${fmtCount(d.downloads)} downloads, ${fmtCount(d.likes)} likes`).join('\n');
792
+ const pipelines = s.summary.top_pipeline_tags.slice(0, 5).map(t => `${t.tag} (${t.count})`).join(', ');
793
+ const namespaces = s.summary.top_namespaces.slice(0, 5).map(n => `${n.namespace} (${n.count})`).join(', ');
794
+ let text = `Hugging Face top-downloaded. ${s.date}.\n`;
795
+ if (pipelines)
796
+ text += `Top pipeline tags (models): ${pipelines}\n`;
797
+ if (namespaces)
798
+ text += `Top namespaces: ${namespaces}\n`;
799
+ if (sec === 'models' || sec === 'both')
800
+ text += `\nModels:\n${modelLines}\n`;
801
+ if (sec === 'datasets' || sec === 'both')
802
+ text += `\nDatasets:\n${datasetLines}\n`;
803
+ text += `\nSource: huggingface.co. Captured ${s.capturedAt}.`;
804
+ return { content: [{ type: 'text', text }] };
805
+ });
708
806
  // ── Start ───────────────────────────────────────────────────────────
709
807
  async function main() {
710
808
  const transport = new StdioServerTransport();
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Defense-in-depth output scrub for tool responses.
3
+ *
4
+ * The MCP server returns text from the TensorFeed worker into a host LLM
5
+ * (Claude Desktop, Cursor, Cline, Roo, etc). Any text we forward becomes
6
+ * untrusted input to that downstream model. The worker already runs
7
+ * sanitize.ts on its agent-facing endpoints, but this layer protects
8
+ * against:
9
+ *
10
+ * 1. New worker endpoints that forget the worker-side scrub
11
+ * 2. Future tools that aggregate or reformat upstream text
12
+ * 3. Drift between the worker's sanitization rules and what the MCP
13
+ * server should consider safe
14
+ *
15
+ * The rule set mirrors worker/src/sanitize.ts:
16
+ * - Strip ASCII control chars (preserves \n \r \t)
17
+ * - Strip bidi controls and zero-width chars used in spoofing
18
+ * - Neutralize role-confusion markers (ChatML / Llama / Mistral chat
19
+ * tokens, jailbreak preamble lines, "ignore previous instructions")
20
+ * - Collapse runs of whitespace
21
+ *
22
+ * No length cap is applied here. Tool output is conversational and may
23
+ * legitimately be tens of KB; upstream KV size limits already bound it.
24
+ */
25
+ /**
26
+ * Scrub a single string for return to a host LLM. Idempotent and pure.
27
+ */
28
+ export declare function sanitizeForLLM(input: string): string;
29
+ /**
30
+ * Recursively scrub the `text` field of every text-content item in an
31
+ * MCP tool response. Non-text items (resources, images) pass through
32
+ * untouched. Returns a new content array; the input is not mutated.
33
+ */
34
+ export declare function sanitizeToolResponse<T extends {
35
+ content: Array<{
36
+ type: string;
37
+ text?: string;
38
+ [k: string]: unknown;
39
+ }>;
40
+ }>(response: T): T;
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tensorfeed/mcp-server",
3
- "version": "1.11.1",
3
+ "version": "1.13.0",
4
4
  "mcpName": "ai.tensorfeed/mcp-server",
5
5
  "description": "MCP server for TensorFeed.ai - AI news, status, model data, and premium endpoints (routing, cost projection, forecast, history series, news search, webhook watches with digest tier, enriched directory) 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": "1.11.0",
6
+ "version": "1.13.0",
7
7
  "websiteUrl": "https://tensorfeed.ai/developers/agent-payments",
8
8
  "repository": {
9
9
  "url": "https://github.com/RipperMercs/tensorfeed-mcp",
@@ -14,7 +14,7 @@
14
14
  "registryType": "npm",
15
15
  "registryBaseUrl": "https://registry.npmjs.org",
16
16
  "identifier": "@tensorfeed/mcp-server",
17
- "version": "1.11.0",
17
+ "version": "1.13.0",
18
18
  "runtimeHint": "npx",
19
19
  "transport": {
20
20
  "type": "stdio"
@@ -44,7 +44,10 @@
44
44
  "get_ai_today",
45
45
  "get_agent_activity",
46
46
  "mcp_registry_snapshot",
47
- "probe_latest"
47
+ "probe_latest",
48
+ "get_ai_papers_trending",
49
+ "get_arxiv_recent",
50
+ "get_hf_trending"
48
51
  ],
49
52
  "premiumTools": [
50
53
  "premium_routing",