@tensorfeed/mcp-server 1.13.0 → 1.21.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
@@ -2,6 +2,48 @@
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
+ ## 1.21.0 - 2026-05-04
6
+
7
+ ### Added
8
+ - Three free history-series tools: `pricing_series_free`, `benchmark_series_free`, `status_uptime_free`. Each returns up to 7 days of daily snapshots (price/score/uptime) for one model or provider. Max range capped at 7 days; for the full 90-day window keep using the paid `pricing_series`, `benchmark_series`, `status_uptime` tools (1 credit each). Reason for the split: agents discovering the data need a free way to see "yes, TensorFeed actually has historical pricing for this model" before deciding to spend a credit. The 7-day teaser shows the trend without giving away the long tail.
9
+
10
+ ## 1.20.0 - 2026-05-04
11
+
12
+ ### Changed
13
+ - `get_ai_ecosystem_today` now calls the new `/api/today` worker endpoint (single fetch) instead of fanning out 9 individual fetches client-side. Behavior is unchanged from the agent's perspective: same args, same text-rendered output. The win is that the worker's edge cache absorbs the load: a thousand agents calling `get_ai_ecosystem_today` per minute now hit the worker once for 5 minutes (cache TTL) instead of 9000 times. Net cost on the worker stays constant.
14
+
15
+ ## 1.19.0 - 2026-05-04
16
+
17
+ ### Added
18
+ - `get_ai_ecosystem_today` tool. One-shot composite AI morning brief that internally fans out across 9 free TensorFeed endpoints in parallel (news, 3 paper feeds, HF models/datasets/Spaces, hot GitHub issues, Reddit threads, OpenRouter catalog summary, provider status) and returns a single synthesized text response. Optional `sections` arg (any subset of news / papers / hf / community / inference / status) and `limit_per_section` arg. Graceful degradation: any one feed failing just drops its section. Designed for agents that want a fast snapshot without 8 separate tool calls. Free.
19
+
20
+ ## 1.18.0 - 2026-05-04
21
+
22
+ ### Added
23
+ - `get_hf_daily_papers` tool. Hugging Face Daily Papers (editor-curated AI/ML papers with community upvotes and discussion counts). Different signal from `get_arxiv_recent` and `get_ai_papers_trending`. Optional `limit` arg. Free.
24
+
25
+ ## 1.17.0 - 2026-05-04
26
+
27
+ ### Added
28
+ - `get_openrouter_models` tool. OpenRouter cross-provider model catalog (200+ models across 50+ inference providers) with comparable per-token pricing, context window, modality, and provider metadata. Optional `namespace` filter, `free_only` flag for free-tier models, `cheapest` sort by ascending input price, and `limit` for the rendered list. Companion to `get_model_pricing` (curated frontier-lab catalog) by surfacing the long tail of OSS models on cloud inference. Free.
29
+
30
+ ## 1.16.0 - 2026-05-04
31
+
32
+ ### Added
33
+ - `get_reddit_trending` tool. Currently-hot threads from 7 AI-relevant subreddits (LocalLLaMA, MachineLearning, ClaudeAI, OpenAI, singularity, artificial, AI_Agents), ranked by score, top 30. Optional `subreddit` arg to filter to one sub, optional `limit` for the rendered list. Companion to `get_hot_issues`: GitHub developer conversation vs Reddit community conversation. Titles pass through the worker's prompt-injection sanitizer at capture time. Free.
34
+
35
+ ## 1.15.0 - 2026-05-04
36
+
37
+ ### Changed
38
+ - `get_hf_trending` tool now covers Hugging Face Spaces in addition to models and datasets. Spaces are ranked by likes (the right signal for hosted apps where downloads is meaningless) and the rendered entry shows id, sdk, likes, runtime stage when available, and hardware tier when available. New `section` enum values: `'spaces'` (just spaces), `'all'` (all three sections, the new default). `'both'` continues to mean models+datasets for backward compat. Output also surfaces top Space SDKs (gradio / streamlit / docker / static) in the header.
39
+
40
+ ## 1.14.0 - 2026-05-04
41
+
42
+ ### Added
43
+ - `get_hot_issues` tool. Currently-hot GitHub issues across the AI ecosystem, ranked by comment count. Five fan-out search queries on AI topics (llm, ai-agents, large-language-models, machine-learning, transformer), filtered to is:issue is:open archived:false comments>=10 with activity in the last 7 days, deduped, top 30. Optional `limit` arg for the rendered list. Free.
44
+
45
+ Companion to `get_trending_repos`: that surfaces repos gaining stars, this surfaces repos where the active conversations are happening.
46
+
5
47
  ## 1.13.0 - 2026-05-04
6
48
 
7
49
  ### Added
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
4
4
  import { z } from 'zod';
5
5
  import { sanitizeToolResponse } from './sanitize.js';
6
6
  const API_BASE = 'https://tensorfeed.ai/api';
7
- const SDK_VERSION = '1.9.0';
7
+ const SDK_VERSION = '1.10.0';
8
8
  async function fetchJSON(path, opts = {}) {
9
9
  const headers = {
10
10
  'User-Agent': `TensorFeed-MCP/${SDK_VERSION}`,
@@ -235,6 +235,69 @@ registerTool('premium_routing', 'Get a ranked list of recommended AI models for
235
235
  : '';
236
236
  return { content: [{ type: 'text', text: `Routing for "${data.task}":\n\n${list}${billing}` }] };
237
237
  });
238
+ // ── Tool: pricing_series_free (free, 7-day cap) ─────────────────────
239
+ registerTool('pricing_series_free', 'Daily price points for one AI model over the last 1 to 7 days, free. For windows up to 90 days use pricing_series (1 credit).', {
240
+ model: z.string().describe('Model id or display name (e.g. "Claude Opus 4.7" or "claude-opus-4-7")'),
241
+ days: z.number().min(1).max(7).optional().describe('Rolling window length 1 to 7 days (default 7)'),
242
+ }, async ({ model, days }) => {
243
+ const params = new URLSearchParams({ model });
244
+ if (typeof days === 'number')
245
+ params.set('days', String(days));
246
+ const data = (await fetchJSON(`/history/pricing/series?${params}`));
247
+ const s = data.summary;
248
+ const summary = s.first && s.latest
249
+ ? `${s.first.date} blended $${s.first.blended} -> ${s.latest.date} blended $${s.latest.blended} (${s.delta_pct_blended}%, min $${s.min_blended}, max $${s.max_blended})`
250
+ : 'no data points in range';
251
+ return {
252
+ content: [
253
+ {
254
+ type: 'text',
255
+ text: `${data.model} (${data.provider ?? 'unknown'}) ${data.points.length} points over ${data.points.length} days\n${summary}\nFor up to 90 days, use the pricing_series tool (1 credit).`,
256
+ },
257
+ ],
258
+ };
259
+ });
260
+ // ── Tool: benchmark_series_free (free, 7-day cap) ───────────────────
261
+ registerTool('benchmark_series_free', 'Daily benchmark scores for one model+benchmark over the last 1 to 7 days, free. Benchmark keys: swe_bench, mmlu_pro, gpqa_diamond, math, human_eval. For windows up to 90 days use benchmark_series (1 credit).', {
262
+ model: z.string().describe('Model id or display name'),
263
+ benchmark: z.string().describe('Benchmark key'),
264
+ days: z.number().min(1).max(7).optional().describe('Rolling window length 1 to 7 days (default 7)'),
265
+ }, async ({ model, benchmark, days }) => {
266
+ const params = new URLSearchParams({ model, benchmark });
267
+ if (typeof days === 'number')
268
+ params.set('days', String(days));
269
+ const data = (await fetchJSON(`/history/benchmarks/series?${params}`));
270
+ const s = data.summary;
271
+ const summary = s.first && s.latest
272
+ ? `${s.first.date} score ${s.first.score} -> ${s.latest.date} score ${s.latest.score} (delta ${s.delta_pp} pp)`
273
+ : 'no data in range';
274
+ return {
275
+ content: [
276
+ {
277
+ type: 'text',
278
+ text: `${data.model} on ${data.benchmark}: ${data.points.length} points\n${summary}`,
279
+ },
280
+ ],
281
+ };
282
+ });
283
+ // ── Tool: status_uptime_free (free, 7-day cap) ──────────────────────
284
+ registerTool('status_uptime_free', 'Daily uptime rollup for one provider over the last 1 to 7 days, free. For windows up to 90 days use status_uptime (1 credit).', {
285
+ provider: z.string().describe('Provider name (e.g. anthropic, openai, google)'),
286
+ days: z.number().min(1).max(7).optional().describe('Rolling window length 1 to 7 days (default 7)'),
287
+ }, async ({ provider, days }) => {
288
+ const params = new URLSearchParams({ provider });
289
+ if (typeof days === 'number')
290
+ params.set('days', String(days));
291
+ const data = (await fetchJSON(`/history/status/uptime?${params}`));
292
+ return {
293
+ content: [
294
+ {
295
+ type: 'text',
296
+ text: `${data.provider} over ${data.days_total} days: ${data.uptime_pct ?? '?'}% uptime (${data.days_operational} operational, ${data.days_degraded} degraded, ${data.days_down} down)`,
297
+ },
298
+ ],
299
+ };
300
+ });
238
301
  // ── Tool: pricing_series (1 credit) ─────────────────────────────────
239
302
  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.', {
240
303
  model: z.string().describe('Model id or display name (e.g. "Claude Opus 4.7" or "claude-opus-4-7")'),
@@ -775,13 +838,13 @@ registerTool('get_arxiv_recent', 'Most recent arXiv submissions in cs.AI / cs.LG
775
838
  };
776
839
  });
777
840
  // ── 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)'),
841
+ registerTool('get_hf_trending', 'Top Hugging Face models, datasets, and Spaces. Models + datasets ranked by downloads (top 30 each); Spaces ranked by likes (top 30, since downloads is meaningless for hosted apps). Captured daily at 12:00 UTC against the public HF API (no auth). Each model entry carries id, downloads, likes, pipeline_tag, tags, lastModified, private, gated. Each space entry carries id, author, sdk (gradio/streamlit/static/docker), likes, tags, lastModified, private, runtime_stage, hardware. Use this when an agent wants the current shape of the open model + dataset + app ecosystem. Once enough daily snapshots accumulate, day-over-day deltas become a real trending signal. Free, no auth.', {
842
+ section: z.enum(['models', 'datasets', 'spaces', 'all', 'both']).optional().describe('Which to include in the rendered output (default all). "both" is accepted as alias for "models+datasets" for backward compat.'),
780
843
  limit: z.number().min(1).max(30).optional().describe('Max items per section (default 15, max 30)'),
781
844
  }, async ({ section, limit }) => {
782
845
  const data = (await fetchJSON('/hf/trending'));
783
846
  const s = data.snapshot;
784
- const sec = section ?? 'both';
847
+ const sec = section ?? 'all';
785
848
  const n = Math.min(limit ?? 15, 30);
786
849
  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
850
  const modelLines = s.models.items.slice(0, n).map((m, i) => {
@@ -789,20 +852,298 @@ registerTool('get_hf_trending', 'Top 30 most-downloaded models and top 30 most-d
789
852
  return `${i + 1}. ${m.id}${tag} ${fmtCount(m.downloads)} downloads, ${fmtCount(m.likes)} likes`;
790
853
  }).join('\n');
791
854
  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');
855
+ const spaceLines = (s.spaces?.items ?? []).slice(0, n).map((sp, i) => {
856
+ const sdk = sp.sdk ? ` [${sp.sdk}]` : '';
857
+ const hw = sp.hardware ? `, hw: ${sp.hardware}` : '';
858
+ const stage = sp.runtime_stage ? `, ${sp.runtime_stage.toLowerCase()}` : '';
859
+ return `${i + 1}. ${sp.id}${sdk} ${fmtCount(sp.likes)} likes${stage}${hw}`;
860
+ }).join('\n');
792
861
  const pipelines = s.summary.top_pipeline_tags.slice(0, 5).map(t => `${t.tag} (${t.count})`).join(', ');
793
862
  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`;
863
+ const sdks = (s.summary.top_space_sdks ?? []).slice(0, 5).map(t => `${t.sdk} (${t.count})`).join(', ');
864
+ let text = `Hugging Face top assets. ${s.date}.\n`;
795
865
  if (pipelines)
796
866
  text += `Top pipeline tags (models): ${pipelines}\n`;
867
+ if (sdks)
868
+ text += `Top Space SDKs: ${sdks}\n`;
797
869
  if (namespaces)
798
870
  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`;
871
+ const includeModels = sec === 'models' || sec === 'all' || sec === 'both';
872
+ const includeDatasets = sec === 'datasets' || sec === 'all' || sec === 'both';
873
+ const includeSpaces = sec === 'spaces' || sec === 'all';
874
+ if (includeModels)
875
+ text += `\nModels (by downloads):\n${modelLines}\n`;
876
+ if (includeDatasets)
877
+ text += `\nDatasets (by downloads):\n${datasetLines}\n`;
878
+ if (includeSpaces)
879
+ text += `\nSpaces (by likes):\n${spaceLines || ' (none returned)'}\n`;
803
880
  text += `\nSource: huggingface.co. Captured ${s.capturedAt}.`;
804
881
  return { content: [{ type: 'text', text }] };
805
882
  });
883
+ // ── Tool: get_hot_issues (free) ─────────────────────────────────────
884
+ registerTool('get_hot_issues', 'Currently-hot GitHub issues across the AI ecosystem. Five fan-out keyword-phrase searches ("large language model", "AI agent", transformer, "machine learning", LLM) for is:issue is:open archived:false comments>=10 updated within the last 7 days. Deduped by URL, top 30 by comment count. Each issue carries url, repo, number, title, author, state, comments, reactions_total, labels, created_at, updated_at, and matched_topic (the keyword phrase that found it). Refreshed daily at 12:30 UTC. Companion to get_trending_repos: that shows which AI repos are gaining stars, this shows where the active conversations are. Free, no auth.', {
885
+ limit: z.number().min(1).max(30).optional().describe('Max issues to render (default 15, max 30)'),
886
+ }, async ({ limit }) => {
887
+ const data = (await fetchJSON('/issues/hot'));
888
+ const s = data.snapshot;
889
+ const cap = Math.min(limit ?? 15, s.issues.length);
890
+ const lines = s.issues.slice(0, cap).map((i, n) => {
891
+ const meta = `${i.comments} comments, ${i.reactions_total} reactions [${i.matched_topic}]`;
892
+ const author = i.author ? ` by ${i.author}` : '';
893
+ const updated = i.updated_at ? ` updated ${i.updated_at.slice(0, 10)}` : '';
894
+ return `${n + 1}. ${i.repo}: ${i.title}\n ${meta}${updated}${author}\n ${i.url}`;
895
+ }).join('\n\n');
896
+ const repos = s.summary.top_repos.slice(0, 5).map(r => `${r.repo} (${r.count})`).join(', ');
897
+ return {
898
+ content: [
899
+ {
900
+ type: 'text',
901
+ text: `Hot AI GitHub issues. ${s.date}, showing ${cap} of ${s.total_issues}.\n` +
902
+ `Window: last ${s.recent_window_days} days, comments threshold ${s.comments_threshold}.\n` +
903
+ (repos ? `Most-active repos: ${repos}\n\n` : '\n') +
904
+ lines +
905
+ `\n\nSource: GitHub Search API. Captured ${s.capturedAt}.`,
906
+ },
907
+ ],
908
+ };
909
+ });
910
+ // ── Tool: get_reddit_trending (free) ────────────────────────────────
911
+ registerTool('get_reddit_trending', 'Currently-hot threads in 7 AI-relevant subreddits (LocalLLaMA, MachineLearning, ClaudeAI, OpenAI, singularity, artificial, AI_Agents). Stickied and NSFW posts filtered, deduped by post id, ranked by score, top 30. Each post carries id, subreddit, title, author, score, upvote_ratio, num_comments, permalink, url, created_utc, flair, is_self, is_video. Refreshed daily at 13:00 UTC. Companion to get_hot_issues: that surfaces developer conversation on GitHub, this surfaces community conversation on Reddit. Titles are sanitized at capture time against prompt-injection tokens. Free, no auth.', {
912
+ subreddit: z.string().optional().describe('Optional filter to a single subreddit name (e.g. "LocalLLaMA"). Case-insensitive. If absent, returns posts from all 7.'),
913
+ limit: z.number().min(1).max(30).optional().describe('Max posts to render (default 15, max 30)'),
914
+ }, async ({ subreddit, limit }) => {
915
+ const data = (await fetchJSON('/reddit/trending'));
916
+ const s = data.snapshot;
917
+ const subFilter = subreddit?.trim().toLowerCase();
918
+ const filtered = subFilter
919
+ ? s.posts.filter(p => p.subreddit.toLowerCase() === subFilter)
920
+ : s.posts;
921
+ const cap = Math.min(limit ?? 15, filtered.length);
922
+ const fmtCount = (x) => x >= 1_000 ? `${(x / 1_000).toFixed(1)}K` : String(x);
923
+ const lines = filtered.slice(0, cap).map((p, i) => {
924
+ const author = p.author ? ` by ${p.author}` : '';
925
+ const flair = p.flair ? ` [${p.flair}]` : '';
926
+ return `${i + 1}. r/${p.subreddit}: ${p.title}${flair}\n ${fmtCount(p.score)} pts, ${fmtCount(p.num_comments)} comments${author}\n ${p.permalink}`;
927
+ }).join('\n\n');
928
+ const subs = Object.entries(s.summary.by_subreddit)
929
+ .sort((a, b) => b[1] - a[1])
930
+ .slice(0, 5)
931
+ .map(([sub, n]) => `r/${sub} (${n})`)
932
+ .join(', ');
933
+ let text = `Hot AI Reddit threads. ${s.date}, showing ${cap} of ${filtered.length}`;
934
+ if (subFilter)
935
+ text += ` (filtered to r/${subreddit})`;
936
+ text += '.\n';
937
+ if (subs && !subFilter)
938
+ text += `Most-active subreddits: ${subs}\n`;
939
+ text += '\n' + (lines || ' (no posts)');
940
+ text += `\n\nSource: reddit.com. Captured ${s.capturedAt}.`;
941
+ return { content: [{ type: 'text', text }] };
942
+ });
943
+ // ── Tool: get_openrouter_models (free) ──────────────────────────────
944
+ registerTool('get_openrouter_models', 'OpenRouter cross-provider model catalog (200+ models normalized across 50+ inference providers). Each entry has per-token pricing (prompt + completion + image + request), context window, modality (e.g. text+image->text), instruct_type, tokenizer, top provider metadata (max_completion_tokens, moderation flag), and supported_parameters. Use this to find the long tail of OSS models on cloud inference, including the cheapest model for a given workload, the model with the largest context window, or the free-tier set. Pairs with get_model_pricing (curated frontier-lab catalog). Refreshed daily at 14:00 UTC. Free, no auth.', {
945
+ namespace: z.string().optional().describe('Optional filter to a single provider namespace (e.g. "anthropic", "openai", "meta-llama"). Case-insensitive.'),
946
+ free_only: z.boolean().optional().describe('If true, return only models with prompt+completion both 0 (free-tier).'),
947
+ cheapest: z.boolean().optional().describe('If true, sort by cheapest input price ascending. Free-tier models excluded from this sort.'),
948
+ limit: z.number().min(1).max(50).optional().describe('Max models to render (default 20, max 50)'),
949
+ }, async ({ namespace, free_only, cheapest, limit }) => {
950
+ const data = (await fetchJSON('/openrouter/models'));
951
+ const s = data.snapshot;
952
+ const ns = namespace?.trim().toLowerCase();
953
+ let list = s.models;
954
+ if (ns)
955
+ list = list.filter(m => m.id.toLowerCase().startsWith(ns + '/'));
956
+ if (free_only)
957
+ list = list.filter(m => m.pricing.prompt === 0 && m.pricing.completion === 0);
958
+ if (cheapest) {
959
+ list = list
960
+ .filter(m => typeof m.pricing.prompt === 'number' && m.pricing.prompt > 0)
961
+ .sort((a, b) => (a.pricing.prompt ?? 0) - (b.pricing.prompt ?? 0));
962
+ }
963
+ const cap = Math.min(limit ?? 20, list.length);
964
+ const fmtPrice = (p) => p === null ? '?' : p === 0 ? 'free' : `$${(p * 1_000_000).toFixed(2)}/Mtok`;
965
+ const fmtCtx = (c) => c === null ? 'unknown' : c >= 1_000_000 ? `${(c / 1_000_000).toFixed(0)}M` : `${(c / 1_000).toFixed(0)}K`;
966
+ const lines = list.slice(0, cap).map((m, i) => {
967
+ const modality = m.modality ? ` ${m.modality}` : '';
968
+ return `${i + 1}. ${m.id}${modality}\n in ${fmtPrice(m.pricing.prompt)}, out ${fmtPrice(m.pricing.completion)}, ctx ${fmtCtx(m.context_length)}`;
969
+ }).join('\n\n');
970
+ const namespaces = s.summary.by_namespace.slice(0, 5).map(n => `${n.namespace} (${n.count})`).join(', ');
971
+ let header = `OpenRouter catalog. ${s.date}. ${s.total_models} models total, ${list.length} after filters, showing ${cap}.\n`;
972
+ header += `Top provider namespaces: ${namespaces}\n`;
973
+ if (s.summary.cheapest_input)
974
+ header += `Cheapest input (paid): ${s.summary.cheapest_input.id} at $${s.summary.cheapest_input.usd_per_million.toFixed(2)}/Mtok\n`;
975
+ if (s.summary.largest_context)
976
+ header += `Largest context: ${s.summary.largest_context.id} at ${fmtCtx(s.summary.largest_context.tokens)} tokens\n`;
977
+ header += `Free-tier models available: ${s.summary.free_tier_count}\n`;
978
+ return {
979
+ content: [
980
+ {
981
+ type: 'text',
982
+ text: header + '\n' + (lines || ' (no models match the filters)') + `\n\nSource: openrouter.ai. Captured ${s.capturedAt}.`,
983
+ },
984
+ ],
985
+ };
986
+ });
987
+ // ── Tool: get_hf_daily_papers (free) ────────────────────────────────
988
+ registerTool('get_hf_daily_papers', 'Hugging Face Daily Papers: editor-curated AI/ML papers from huggingface.co/papers, layered with community upvotes and discussion counts. Different signal from get_arxiv_recent (firehose) and get_ai_papers_trending (citation-ranked all-time): this is editor picks of-the-day with engagement signal. Each paper carries paperId, title, summary, authors, upvotes, num_comments, hf_url, arxiv_url (when applicable), github_repo, github_stars, ai_keywords. Refreshed daily at 14:15 UTC. Titles sanitized at capture time. Free, no auth.', {
989
+ limit: z.number().min(1).max(30).optional().describe('Max papers to render (default 15, max 30)'),
990
+ }, async ({ limit }) => {
991
+ const data = (await fetchJSON('/papers/hf-daily'));
992
+ const s = data.snapshot;
993
+ const cap = Math.min(limit ?? 15, s.papers.length);
994
+ const lines = s.papers.slice(0, cap).map((p, i) => {
995
+ const authors = p.authors.slice(0, 3).join(', ') + (p.authors.length > 3 ? ' et al.' : '');
996
+ const kw = p.ai_keywords.length > 0 ? `\n keywords: ${p.ai_keywords.slice(0, 5).join(', ')}` : '';
997
+ const arxiv = p.arxiv_url ? `\n arxiv: ${p.arxiv_url}` : '';
998
+ return `${i + 1}. ${p.title}\n ${authors}\n ${p.upvotes} upvotes, ${p.num_comments} comments\n ${p.hf_url}${arxiv}${kw}`;
999
+ }).join('\n\n');
1000
+ const kw = s.summary.by_keyword.slice(0, 5).map(k => `${k.keyword} (${k.count})`).join(', ');
1001
+ let text = `HF Daily Papers (editor-curated). ${s.date}, showing ${cap} of ${s.total_papers}.\n`;
1002
+ if (kw)
1003
+ text += `Top keywords: ${kw}\n`;
1004
+ if (s.summary.most_upvoted)
1005
+ text += `Most upvoted: "${s.summary.most_upvoted.title}" (${s.summary.most_upvoted.upvotes})\n`;
1006
+ text += '\n' + lines + `\n\nSource: huggingface.co/papers. Captured ${s.capturedAt}.`;
1007
+ return { content: [{ type: 'text', text }] };
1008
+ });
1009
+ // ── Tool: get_ai_ecosystem_today (free, composite) ──────────────────
1010
+ // Single fetch to /api/today which fans out worker-side. The previous
1011
+ // implementation made 9 separate fetches per agent invocation; now
1012
+ // the worker's edge cache absorbs the load and serves one cached
1013
+ // response to all concurrent agents. Output stays text-rendered
1014
+ // (MCP returns text content) but the wire shape is the structured
1015
+ // JSON the worker produces.
1016
+ registerTool('get_ai_ecosystem_today', 'One-shot composite AI morning brief. Calls /api/today which fans out across all daily TensorFeed feeds (news, 3 paper feeds, HF models/datasets/Spaces, hot GitHub issues, Reddit threads, OpenRouter catalog summary, provider status) and returns a single synthesized text response. Optional sections filter (any subset of news, papers, hf, community, inference, status) and limit_per_section (1-10, default 3). Captured-at timestamps from each underlying snapshot are surfaced so the agent knows recency. Free, no auth.', {
1017
+ sections: z.array(z.enum([
1018
+ 'news', 'papers', 'hf', 'community', 'inference', 'status',
1019
+ ])).optional().describe('Which subsections to include (default: all six). "papers" covers all 3 paper feeds; "hf" covers models + datasets + spaces; "community" covers hot GitHub issues + Reddit threads; "inference" is the OpenRouter catalog summary.'),
1020
+ limit_per_section: z.number().min(1).max(10).optional().describe('Max items per subsection (default 3, max 10). The output is intentionally terse so an agent can call this without burning context.'),
1021
+ }, async ({ sections, limit_per_section }) => {
1022
+ const params = new URLSearchParams();
1023
+ if (sections && sections.length > 0)
1024
+ params.set('sections', sections.join(','));
1025
+ if (typeof limit_per_section === 'number')
1026
+ params.set('limit', String(limit_per_section));
1027
+ const path = `/today${params.toString() ? `?${params.toString()}` : ''}`;
1028
+ let brief;
1029
+ try {
1030
+ brief = (await fetchJSON(path));
1031
+ }
1032
+ catch (err) {
1033
+ return {
1034
+ content: [{ type: 'text', text: `Failed to fetch /api/today: ${err.message}` }],
1035
+ };
1036
+ }
1037
+ const fmtCount = (x) => x >= 1_000_000 ? `${(x / 1_000_000).toFixed(1)}M`
1038
+ : x >= 1_000 ? `${(x / 1_000).toFixed(1)}K`
1039
+ : String(x);
1040
+ const sectionTexts = [];
1041
+ if (brief.news.available && brief.news.data && brief.news.data.items.length > 0) {
1042
+ const lines = brief.news.data.items.map((a, i) => ` ${i + 1}. ${a.title} (${a.source})\n ${a.url}`).join('\n');
1043
+ sectionTexts.push(`📰 NEWS\n${lines}`);
1044
+ }
1045
+ if (brief.papers.available && brief.papers.data) {
1046
+ const bits = [];
1047
+ const ai = brief.papers.data.ai_trending;
1048
+ if (ai.available && ai.data && ai.data.items.length > 0) {
1049
+ const lines = ai.data.items.map((p, i) => {
1050
+ const authors = p.authors.slice(0, 2).join(', ') + (p.authors.length > 2 ? ' et al.' : '');
1051
+ const venue = p.venue ? ` ${p.venue}.` : '';
1052
+ return ` ${i + 1}. ${p.title}${venue} ${authors}. ${fmtCount(p.citationCount)} citations`;
1053
+ }).join('\n');
1054
+ bits.push(` By citation count (Semantic Scholar):\n${lines}`);
1055
+ }
1056
+ const ax = brief.papers.data.arxiv_recent;
1057
+ if (ax.available && ax.data && ax.data.items.length > 0) {
1058
+ const lines = ax.data.items.map((p, i) => {
1059
+ const date = p.publishedAt ? p.publishedAt.slice(0, 10) : '';
1060
+ const authors = p.authors.slice(0, 2).join(', ') + (p.authors.length > 2 ? ' et al.' : '');
1061
+ return ` ${i + 1}. ${p.title} (arXiv:${p.arxivId}, ${date}). ${authors}`;
1062
+ }).join('\n');
1063
+ bits.push(` Just submitted (arXiv recent):\n${lines}`);
1064
+ }
1065
+ const hd = brief.papers.data.hf_daily;
1066
+ if (hd.available && hd.data && hd.data.items.length > 0) {
1067
+ const lines = hd.data.items.map((p, i) => ` ${i + 1}. ${p.title} (${p.upvotes} upvotes, ${p.num_comments} comments)`).join('\n');
1068
+ bits.push(` HF editor picks today:\n${lines}`);
1069
+ }
1070
+ if (bits.length > 0)
1071
+ sectionTexts.push(`📄 PAPERS\n${bits.join('\n')}`);
1072
+ }
1073
+ if (brief.hf.available && brief.hf.data) {
1074
+ const bits = [];
1075
+ const { models, datasets, spaces } = brief.hf.data;
1076
+ if (models.length > 0) {
1077
+ bits.push(` Models (top by downloads):\n` + models.map((x, i) => ` ${i + 1}. ${x.id} (${fmtCount(x.downloads)} downloads)`).join('\n'));
1078
+ }
1079
+ if (datasets.length > 0) {
1080
+ bits.push(` Datasets (top by downloads):\n` + datasets.map((x, i) => ` ${i + 1}. ${x.id} (${fmtCount(x.downloads)} downloads)`).join('\n'));
1081
+ }
1082
+ if (spaces.length > 0) {
1083
+ bits.push(` Spaces (top by likes):\n` + spaces.map((x, i) => ` ${i + 1}. ${x.id} (${fmtCount(x.likes)} likes)`).join('\n'));
1084
+ }
1085
+ if (bits.length > 0)
1086
+ sectionTexts.push(`🤗 HUGGING FACE\n${bits.join('\n')}`);
1087
+ }
1088
+ if (brief.community.available && brief.community.data) {
1089
+ const bits = [];
1090
+ const { github_issues, reddit } = brief.community.data;
1091
+ if (github_issues.length > 0) {
1092
+ const lines = github_issues.map((it, i) => ` ${i + 1}. ${it.repo}: ${it.title} (${it.comments} comments)\n ${it.url}`).join('\n');
1093
+ bits.push(` GitHub hot issues:\n${lines}`);
1094
+ }
1095
+ if (reddit.length > 0) {
1096
+ const lines = reddit.map((p, i) => ` ${i + 1}. r/${p.subreddit}: ${p.title} (${p.score} pts, ${p.num_comments} comments)\n ${p.permalink}`).join('\n');
1097
+ bits.push(` Reddit hot threads:\n${lines}`);
1098
+ }
1099
+ if (bits.length > 0)
1100
+ sectionTexts.push(`💬 COMMUNITY\n${bits.join('\n')}`);
1101
+ }
1102
+ if (brief.inference.available && brief.inference.data) {
1103
+ const d = brief.inference.data;
1104
+ const lines = [];
1105
+ lines.push(` ${d.total_models} models across providers (OpenRouter)`);
1106
+ if (d.cheapest_input)
1107
+ lines.push(` Cheapest input: ${d.cheapest_input.id} at $${d.cheapest_input.usd_per_million.toFixed(2)}/Mtok`);
1108
+ if (d.largest_context)
1109
+ lines.push(` Largest context: ${d.largest_context.id} at ${fmtCount(d.largest_context.tokens)} tokens`);
1110
+ lines.push(` Free-tier models available: ${d.free_tier_count}`);
1111
+ if (d.top_namespaces.length > 0) {
1112
+ lines.push(` Top namespaces: ${d.top_namespaces.map(n => `${n.namespace} (${n.count})`).join(', ')}`);
1113
+ }
1114
+ sectionTexts.push(`⚙️ INFERENCE CATALOG\n${lines.join('\n')}`);
1115
+ }
1116
+ if (brief.status.available && brief.status.data) {
1117
+ const d = brief.status.data;
1118
+ if (d.all_operational) {
1119
+ sectionTexts.push(`✓ STATUS: all tracked AI providers operational (${d.service_count} services)`);
1120
+ }
1121
+ else {
1122
+ const lines = d.issues.map(s => ` ${s.name} (${s.provider}): ${s.status.toUpperCase()}`).join('\n');
1123
+ sectionTexts.push(`⚠️ STATUS\n${lines}`);
1124
+ }
1125
+ }
1126
+ if (sectionTexts.length === 0) {
1127
+ return {
1128
+ content: [
1129
+ {
1130
+ type: 'text',
1131
+ text: 'No data available yet. The daily snapshots have not run, or all upstream feeds errored. Try again in a few minutes.',
1132
+ },
1133
+ ],
1134
+ };
1135
+ }
1136
+ const header = `TensorFeed AI ecosystem brief, generated ${brief.generated_at}`;
1137
+ const body = sectionTexts.join('\n\n');
1138
+ return {
1139
+ content: [
1140
+ {
1141
+ type: 'text',
1142
+ text: `${header}\n\n${body}\n\nFor any subsection, call the underlying tool directly: get_ai_news, get_ai_papers_trending, get_arxiv_recent, get_hf_daily_papers, get_hf_trending, get_hot_issues, get_reddit_trending, get_openrouter_models, get_ai_status.`,
1143
+ },
1144
+ ],
1145
+ };
1146
+ });
806
1147
  // ── Start ───────────────────────────────────────────────────────────
807
1148
  async function main() {
808
1149
  const transport = new StdioServerTransport();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tensorfeed/mcp-server",
3
- "version": "1.13.0",
3
+ "version": "1.21.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.13.0",
6
+ "version": "1.20.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.13.0",
17
+ "version": "1.21.0",
18
18
  "runtimeHint": "npx",
19
19
  "transport": {
20
20
  "type": "stdio"
@@ -47,7 +47,15 @@
47
47
  "probe_latest",
48
48
  "get_ai_papers_trending",
49
49
  "get_arxiv_recent",
50
- "get_hf_trending"
50
+ "get_hf_trending",
51
+ "get_hot_issues",
52
+ "get_reddit_trending",
53
+ "get_openrouter_models",
54
+ "get_hf_daily_papers",
55
+ "get_ai_ecosystem_today",
56
+ "pricing_series_free",
57
+ "benchmark_series_free",
58
+ "status_uptime_free"
51
59
  ],
52
60
  "premiumTools": [
53
61
  "premium_routing",