@tensorfeed/mcp-server 1.12.0 → 1.20.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,52 @@
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.20.0 - 2026-05-04
6
+
7
+ ### Changed
8
+ - `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.
9
+
10
+ ## 1.19.0 - 2026-05-04
11
+
12
+ ### Added
13
+ - `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.
14
+
15
+ ## 1.18.0 - 2026-05-04
16
+
17
+ ### Added
18
+ - `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.
19
+
20
+ ## 1.17.0 - 2026-05-04
21
+
22
+ ### Added
23
+ - `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.
24
+
25
+ ## 1.16.0 - 2026-05-04
26
+
27
+ ### Added
28
+ - `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.
29
+
30
+ ## 1.15.0 - 2026-05-04
31
+
32
+ ### Changed
33
+ - `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.
34
+
35
+ ## 1.14.0 - 2026-05-04
36
+
37
+ ### Added
38
+ - `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.
39
+
40
+ Companion to `get_trending_repos`: that surfaces repos gaining stars, this surfaces repos where the active conversations are happening.
41
+
42
+ ## 1.13.0 - 2026-05-04
43
+
44
+ ### Added
45
+ - `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.
46
+ - `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.
47
+ - `get_hf_trending` tool. Top 30 most-downloaded models and datasets on Hugging Face. Optional `section` (models / datasets / both) and `limit` args. Free.
48
+
49
+ All three are backed by the worker endpoints shipped in the same monorepo commit and pass through the 1.12.0 output sanitizer automatically.
50
+
5
51
  ## 1.12.0 - 2026-05-04
6
52
 
7
53
  ### Added
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
@@ -724,6 +724,363 @@ registerTool('mcp_registry_series', 'Multi-day series of MCP server registry gro
724
724
  ],
725
725
  };
726
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 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.', {
779
+ 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
+ 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 ?? 'all';
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 spaceLines = (s.spaces?.items ?? []).slice(0, n).map((sp, i) => {
793
+ const sdk = sp.sdk ? ` [${sp.sdk}]` : '';
794
+ const hw = sp.hardware ? `, hw: ${sp.hardware}` : '';
795
+ const stage = sp.runtime_stage ? `, ${sp.runtime_stage.toLowerCase()}` : '';
796
+ return `${i + 1}. ${sp.id}${sdk} ${fmtCount(sp.likes)} likes${stage}${hw}`;
797
+ }).join('\n');
798
+ const pipelines = s.summary.top_pipeline_tags.slice(0, 5).map(t => `${t.tag} (${t.count})`).join(', ');
799
+ const namespaces = s.summary.top_namespaces.slice(0, 5).map(n => `${n.namespace} (${n.count})`).join(', ');
800
+ const sdks = (s.summary.top_space_sdks ?? []).slice(0, 5).map(t => `${t.sdk} (${t.count})`).join(', ');
801
+ let text = `Hugging Face top assets. ${s.date}.\n`;
802
+ if (pipelines)
803
+ text += `Top pipeline tags (models): ${pipelines}\n`;
804
+ if (sdks)
805
+ text += `Top Space SDKs: ${sdks}\n`;
806
+ if (namespaces)
807
+ text += `Top namespaces: ${namespaces}\n`;
808
+ const includeModels = sec === 'models' || sec === 'all' || sec === 'both';
809
+ const includeDatasets = sec === 'datasets' || sec === 'all' || sec === 'both';
810
+ const includeSpaces = sec === 'spaces' || sec === 'all';
811
+ if (includeModels)
812
+ text += `\nModels (by downloads):\n${modelLines}\n`;
813
+ if (includeDatasets)
814
+ text += `\nDatasets (by downloads):\n${datasetLines}\n`;
815
+ if (includeSpaces)
816
+ text += `\nSpaces (by likes):\n${spaceLines || ' (none returned)'}\n`;
817
+ text += `\nSource: huggingface.co. Captured ${s.capturedAt}.`;
818
+ return { content: [{ type: 'text', text }] };
819
+ });
820
+ // ── Tool: get_hot_issues (free) ─────────────────────────────────────
821
+ 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.', {
822
+ limit: z.number().min(1).max(30).optional().describe('Max issues to render (default 15, max 30)'),
823
+ }, async ({ limit }) => {
824
+ const data = (await fetchJSON('/issues/hot'));
825
+ const s = data.snapshot;
826
+ const cap = Math.min(limit ?? 15, s.issues.length);
827
+ const lines = s.issues.slice(0, cap).map((i, n) => {
828
+ const meta = `${i.comments} comments, ${i.reactions_total} reactions [${i.matched_topic}]`;
829
+ const author = i.author ? ` by ${i.author}` : '';
830
+ const updated = i.updated_at ? ` updated ${i.updated_at.slice(0, 10)}` : '';
831
+ return `${n + 1}. ${i.repo}: ${i.title}\n ${meta}${updated}${author}\n ${i.url}`;
832
+ }).join('\n\n');
833
+ const repos = s.summary.top_repos.slice(0, 5).map(r => `${r.repo} (${r.count})`).join(', ');
834
+ return {
835
+ content: [
836
+ {
837
+ type: 'text',
838
+ text: `Hot AI GitHub issues. ${s.date}, showing ${cap} of ${s.total_issues}.\n` +
839
+ `Window: last ${s.recent_window_days} days, comments threshold ${s.comments_threshold}.\n` +
840
+ (repos ? `Most-active repos: ${repos}\n\n` : '\n') +
841
+ lines +
842
+ `\n\nSource: GitHub Search API. Captured ${s.capturedAt}.`,
843
+ },
844
+ ],
845
+ };
846
+ });
847
+ // ── Tool: get_reddit_trending (free) ────────────────────────────────
848
+ 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.', {
849
+ subreddit: z.string().optional().describe('Optional filter to a single subreddit name (e.g. "LocalLLaMA"). Case-insensitive. If absent, returns posts from all 7.'),
850
+ limit: z.number().min(1).max(30).optional().describe('Max posts to render (default 15, max 30)'),
851
+ }, async ({ subreddit, limit }) => {
852
+ const data = (await fetchJSON('/reddit/trending'));
853
+ const s = data.snapshot;
854
+ const subFilter = subreddit?.trim().toLowerCase();
855
+ const filtered = subFilter
856
+ ? s.posts.filter(p => p.subreddit.toLowerCase() === subFilter)
857
+ : s.posts;
858
+ const cap = Math.min(limit ?? 15, filtered.length);
859
+ const fmtCount = (x) => x >= 1_000 ? `${(x / 1_000).toFixed(1)}K` : String(x);
860
+ const lines = filtered.slice(0, cap).map((p, i) => {
861
+ const author = p.author ? ` by ${p.author}` : '';
862
+ const flair = p.flair ? ` [${p.flair}]` : '';
863
+ return `${i + 1}. r/${p.subreddit}: ${p.title}${flair}\n ${fmtCount(p.score)} pts, ${fmtCount(p.num_comments)} comments${author}\n ${p.permalink}`;
864
+ }).join('\n\n');
865
+ const subs = Object.entries(s.summary.by_subreddit)
866
+ .sort((a, b) => b[1] - a[1])
867
+ .slice(0, 5)
868
+ .map(([sub, n]) => `r/${sub} (${n})`)
869
+ .join(', ');
870
+ let text = `Hot AI Reddit threads. ${s.date}, showing ${cap} of ${filtered.length}`;
871
+ if (subFilter)
872
+ text += ` (filtered to r/${subreddit})`;
873
+ text += '.\n';
874
+ if (subs && !subFilter)
875
+ text += `Most-active subreddits: ${subs}\n`;
876
+ text += '\n' + (lines || ' (no posts)');
877
+ text += `\n\nSource: reddit.com. Captured ${s.capturedAt}.`;
878
+ return { content: [{ type: 'text', text }] };
879
+ });
880
+ // ── Tool: get_openrouter_models (free) ──────────────────────────────
881
+ 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.', {
882
+ namespace: z.string().optional().describe('Optional filter to a single provider namespace (e.g. "anthropic", "openai", "meta-llama"). Case-insensitive.'),
883
+ free_only: z.boolean().optional().describe('If true, return only models with prompt+completion both 0 (free-tier).'),
884
+ cheapest: z.boolean().optional().describe('If true, sort by cheapest input price ascending. Free-tier models excluded from this sort.'),
885
+ limit: z.number().min(1).max(50).optional().describe('Max models to render (default 20, max 50)'),
886
+ }, async ({ namespace, free_only, cheapest, limit }) => {
887
+ const data = (await fetchJSON('/openrouter/models'));
888
+ const s = data.snapshot;
889
+ const ns = namespace?.trim().toLowerCase();
890
+ let list = s.models;
891
+ if (ns)
892
+ list = list.filter(m => m.id.toLowerCase().startsWith(ns + '/'));
893
+ if (free_only)
894
+ list = list.filter(m => m.pricing.prompt === 0 && m.pricing.completion === 0);
895
+ if (cheapest) {
896
+ list = list
897
+ .filter(m => typeof m.pricing.prompt === 'number' && m.pricing.prompt > 0)
898
+ .sort((a, b) => (a.pricing.prompt ?? 0) - (b.pricing.prompt ?? 0));
899
+ }
900
+ const cap = Math.min(limit ?? 20, list.length);
901
+ const fmtPrice = (p) => p === null ? '?' : p === 0 ? 'free' : `$${(p * 1_000_000).toFixed(2)}/Mtok`;
902
+ const fmtCtx = (c) => c === null ? 'unknown' : c >= 1_000_000 ? `${(c / 1_000_000).toFixed(0)}M` : `${(c / 1_000).toFixed(0)}K`;
903
+ const lines = list.slice(0, cap).map((m, i) => {
904
+ const modality = m.modality ? ` ${m.modality}` : '';
905
+ return `${i + 1}. ${m.id}${modality}\n in ${fmtPrice(m.pricing.prompt)}, out ${fmtPrice(m.pricing.completion)}, ctx ${fmtCtx(m.context_length)}`;
906
+ }).join('\n\n');
907
+ const namespaces = s.summary.by_namespace.slice(0, 5).map(n => `${n.namespace} (${n.count})`).join(', ');
908
+ let header = `OpenRouter catalog. ${s.date}. ${s.total_models} models total, ${list.length} after filters, showing ${cap}.\n`;
909
+ header += `Top provider namespaces: ${namespaces}\n`;
910
+ if (s.summary.cheapest_input)
911
+ header += `Cheapest input (paid): ${s.summary.cheapest_input.id} at $${s.summary.cheapest_input.usd_per_million.toFixed(2)}/Mtok\n`;
912
+ if (s.summary.largest_context)
913
+ header += `Largest context: ${s.summary.largest_context.id} at ${fmtCtx(s.summary.largest_context.tokens)} tokens\n`;
914
+ header += `Free-tier models available: ${s.summary.free_tier_count}\n`;
915
+ return {
916
+ content: [
917
+ {
918
+ type: 'text',
919
+ text: header + '\n' + (lines || ' (no models match the filters)') + `\n\nSource: openrouter.ai. Captured ${s.capturedAt}.`,
920
+ },
921
+ ],
922
+ };
923
+ });
924
+ // ── Tool: get_hf_daily_papers (free) ────────────────────────────────
925
+ 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.', {
926
+ limit: z.number().min(1).max(30).optional().describe('Max papers to render (default 15, max 30)'),
927
+ }, async ({ limit }) => {
928
+ const data = (await fetchJSON('/papers/hf-daily'));
929
+ const s = data.snapshot;
930
+ const cap = Math.min(limit ?? 15, s.papers.length);
931
+ const lines = s.papers.slice(0, cap).map((p, i) => {
932
+ const authors = p.authors.slice(0, 3).join(', ') + (p.authors.length > 3 ? ' et al.' : '');
933
+ const kw = p.ai_keywords.length > 0 ? `\n keywords: ${p.ai_keywords.slice(0, 5).join(', ')}` : '';
934
+ const arxiv = p.arxiv_url ? `\n arxiv: ${p.arxiv_url}` : '';
935
+ return `${i + 1}. ${p.title}\n ${authors}\n ${p.upvotes} upvotes, ${p.num_comments} comments\n ${p.hf_url}${arxiv}${kw}`;
936
+ }).join('\n\n');
937
+ const kw = s.summary.by_keyword.slice(0, 5).map(k => `${k.keyword} (${k.count})`).join(', ');
938
+ let text = `HF Daily Papers (editor-curated). ${s.date}, showing ${cap} of ${s.total_papers}.\n`;
939
+ if (kw)
940
+ text += `Top keywords: ${kw}\n`;
941
+ if (s.summary.most_upvoted)
942
+ text += `Most upvoted: "${s.summary.most_upvoted.title}" (${s.summary.most_upvoted.upvotes})\n`;
943
+ text += '\n' + lines + `\n\nSource: huggingface.co/papers. Captured ${s.capturedAt}.`;
944
+ return { content: [{ type: 'text', text }] };
945
+ });
946
+ // ── Tool: get_ai_ecosystem_today (free, composite) ──────────────────
947
+ // Single fetch to /api/today which fans out worker-side. The previous
948
+ // implementation made 9 separate fetches per agent invocation; now
949
+ // the worker's edge cache absorbs the load and serves one cached
950
+ // response to all concurrent agents. Output stays text-rendered
951
+ // (MCP returns text content) but the wire shape is the structured
952
+ // JSON the worker produces.
953
+ 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.', {
954
+ sections: z.array(z.enum([
955
+ 'news', 'papers', 'hf', 'community', 'inference', 'status',
956
+ ])).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.'),
957
+ 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.'),
958
+ }, async ({ sections, limit_per_section }) => {
959
+ const params = new URLSearchParams();
960
+ if (sections && sections.length > 0)
961
+ params.set('sections', sections.join(','));
962
+ if (typeof limit_per_section === 'number')
963
+ params.set('limit', String(limit_per_section));
964
+ const path = `/today${params.toString() ? `?${params.toString()}` : ''}`;
965
+ let brief;
966
+ try {
967
+ brief = (await fetchJSON(path));
968
+ }
969
+ catch (err) {
970
+ return {
971
+ content: [{ type: 'text', text: `Failed to fetch /api/today: ${err.message}` }],
972
+ };
973
+ }
974
+ const fmtCount = (x) => x >= 1_000_000 ? `${(x / 1_000_000).toFixed(1)}M`
975
+ : x >= 1_000 ? `${(x / 1_000).toFixed(1)}K`
976
+ : String(x);
977
+ const sectionTexts = [];
978
+ if (brief.news.available && brief.news.data && brief.news.data.items.length > 0) {
979
+ const lines = brief.news.data.items.map((a, i) => ` ${i + 1}. ${a.title} (${a.source})\n ${a.url}`).join('\n');
980
+ sectionTexts.push(`📰 NEWS\n${lines}`);
981
+ }
982
+ if (brief.papers.available && brief.papers.data) {
983
+ const bits = [];
984
+ const ai = brief.papers.data.ai_trending;
985
+ if (ai.available && ai.data && ai.data.items.length > 0) {
986
+ const lines = ai.data.items.map((p, i) => {
987
+ const authors = p.authors.slice(0, 2).join(', ') + (p.authors.length > 2 ? ' et al.' : '');
988
+ const venue = p.venue ? ` ${p.venue}.` : '';
989
+ return ` ${i + 1}. ${p.title}${venue} ${authors}. ${fmtCount(p.citationCount)} citations`;
990
+ }).join('\n');
991
+ bits.push(` By citation count (Semantic Scholar):\n${lines}`);
992
+ }
993
+ const ax = brief.papers.data.arxiv_recent;
994
+ if (ax.available && ax.data && ax.data.items.length > 0) {
995
+ const lines = ax.data.items.map((p, i) => {
996
+ const date = p.publishedAt ? p.publishedAt.slice(0, 10) : '';
997
+ const authors = p.authors.slice(0, 2).join(', ') + (p.authors.length > 2 ? ' et al.' : '');
998
+ return ` ${i + 1}. ${p.title} (arXiv:${p.arxivId}, ${date}). ${authors}`;
999
+ }).join('\n');
1000
+ bits.push(` Just submitted (arXiv recent):\n${lines}`);
1001
+ }
1002
+ const hd = brief.papers.data.hf_daily;
1003
+ if (hd.available && hd.data && hd.data.items.length > 0) {
1004
+ const lines = hd.data.items.map((p, i) => ` ${i + 1}. ${p.title} (${p.upvotes} upvotes, ${p.num_comments} comments)`).join('\n');
1005
+ bits.push(` HF editor picks today:\n${lines}`);
1006
+ }
1007
+ if (bits.length > 0)
1008
+ sectionTexts.push(`📄 PAPERS\n${bits.join('\n')}`);
1009
+ }
1010
+ if (brief.hf.available && brief.hf.data) {
1011
+ const bits = [];
1012
+ const { models, datasets, spaces } = brief.hf.data;
1013
+ if (models.length > 0) {
1014
+ bits.push(` Models (top by downloads):\n` + models.map((x, i) => ` ${i + 1}. ${x.id} (${fmtCount(x.downloads)} downloads)`).join('\n'));
1015
+ }
1016
+ if (datasets.length > 0) {
1017
+ bits.push(` Datasets (top by downloads):\n` + datasets.map((x, i) => ` ${i + 1}. ${x.id} (${fmtCount(x.downloads)} downloads)`).join('\n'));
1018
+ }
1019
+ if (spaces.length > 0) {
1020
+ bits.push(` Spaces (top by likes):\n` + spaces.map((x, i) => ` ${i + 1}. ${x.id} (${fmtCount(x.likes)} likes)`).join('\n'));
1021
+ }
1022
+ if (bits.length > 0)
1023
+ sectionTexts.push(`🤗 HUGGING FACE\n${bits.join('\n')}`);
1024
+ }
1025
+ if (brief.community.available && brief.community.data) {
1026
+ const bits = [];
1027
+ const { github_issues, reddit } = brief.community.data;
1028
+ if (github_issues.length > 0) {
1029
+ const lines = github_issues.map((it, i) => ` ${i + 1}. ${it.repo}: ${it.title} (${it.comments} comments)\n ${it.url}`).join('\n');
1030
+ bits.push(` GitHub hot issues:\n${lines}`);
1031
+ }
1032
+ if (reddit.length > 0) {
1033
+ 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');
1034
+ bits.push(` Reddit hot threads:\n${lines}`);
1035
+ }
1036
+ if (bits.length > 0)
1037
+ sectionTexts.push(`💬 COMMUNITY\n${bits.join('\n')}`);
1038
+ }
1039
+ if (brief.inference.available && brief.inference.data) {
1040
+ const d = brief.inference.data;
1041
+ const lines = [];
1042
+ lines.push(` ${d.total_models} models across providers (OpenRouter)`);
1043
+ if (d.cheapest_input)
1044
+ lines.push(` Cheapest input: ${d.cheapest_input.id} at $${d.cheapest_input.usd_per_million.toFixed(2)}/Mtok`);
1045
+ if (d.largest_context)
1046
+ lines.push(` Largest context: ${d.largest_context.id} at ${fmtCount(d.largest_context.tokens)} tokens`);
1047
+ lines.push(` Free-tier models available: ${d.free_tier_count}`);
1048
+ if (d.top_namespaces.length > 0) {
1049
+ lines.push(` Top namespaces: ${d.top_namespaces.map(n => `${n.namespace} (${n.count})`).join(', ')}`);
1050
+ }
1051
+ sectionTexts.push(`⚙️ INFERENCE CATALOG\n${lines.join('\n')}`);
1052
+ }
1053
+ if (brief.status.available && brief.status.data) {
1054
+ const d = brief.status.data;
1055
+ if (d.all_operational) {
1056
+ sectionTexts.push(`✓ STATUS: all tracked AI providers operational (${d.service_count} services)`);
1057
+ }
1058
+ else {
1059
+ const lines = d.issues.map(s => ` ${s.name} (${s.provider}): ${s.status.toUpperCase()}`).join('\n');
1060
+ sectionTexts.push(`⚠️ STATUS\n${lines}`);
1061
+ }
1062
+ }
1063
+ if (sectionTexts.length === 0) {
1064
+ return {
1065
+ content: [
1066
+ {
1067
+ type: 'text',
1068
+ text: 'No data available yet. The daily snapshots have not run, or all upstream feeds errored. Try again in a few minutes.',
1069
+ },
1070
+ ],
1071
+ };
1072
+ }
1073
+ const header = `TensorFeed AI ecosystem brief, generated ${brief.generated_at}`;
1074
+ const body = sectionTexts.join('\n\n');
1075
+ return {
1076
+ content: [
1077
+ {
1078
+ type: 'text',
1079
+ 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.`,
1080
+ },
1081
+ ],
1082
+ };
1083
+ });
727
1084
  // ── Start ───────────────────────────────────────────────────────────
728
1085
  async function main() {
729
1086
  const transport = new StdioServerTransport();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tensorfeed/mcp-server",
3
- "version": "1.12.0",
3
+ "version": "1.20.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.12.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.12.0",
17
+ "version": "1.20.0",
18
18
  "runtimeHint": "npx",
19
19
  "transport": {
20
20
  "type": "stdio"
@@ -44,7 +44,15 @@
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",
51
+ "get_hot_issues",
52
+ "get_reddit_trending",
53
+ "get_openrouter_models",
54
+ "get_hf_daily_papers",
55
+ "get_ai_ecosystem_today"
48
56
  ],
49
57
  "premiumTools": [
50
58
  "premium_routing",