hf-papers-mcp 1.0.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/server.js ADDED
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // Standalone MCP server exposing the Hugging Face papers tools over stdio.
5
+ //
6
+ // The tool definitions live in src/papers.js, which shells out to
7
+ // scripts/paper_manager.py. Nothing here holds state: every call is a fresh
8
+ // subprocess against the public Hugging Face API.
9
+
10
+ const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
11
+ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
12
+ const {
13
+ ListToolsRequestSchema,
14
+ CallToolRequestSchema,
15
+ } = require('@modelcontextprotocol/sdk/types.js');
16
+
17
+ const papers = require('./src/papers.js');
18
+
19
+ // The domain signature takes an `engines` bag in its original host; this tool
20
+ // declares `requires: []`, so an empty object is the whole contract.
21
+ const tools = papers.tools({});
22
+ const byName = new Map(tools.map((t) => [t.def.name, t]));
23
+
24
+ const server = new Server(
25
+ { name: 'hf-papers-mcp', version: '1.0.0' },
26
+ { capabilities: { tools: {} } },
27
+ );
28
+
29
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
30
+ tools: tools.map((t) => t.def),
31
+ }));
32
+
33
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
34
+ const tool = byName.get(request.params.name);
35
+ if (!tool) {
36
+ return {
37
+ isError: true,
38
+ content: [{ type: 'text', text: `Unknown tool: ${request.params.name}` }],
39
+ };
40
+ }
41
+ try {
42
+ const result = await tool.handler(request.params.arguments || {});
43
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
44
+ } catch (err) {
45
+ return {
46
+ isError: true,
47
+ content: [{ type: 'text', text: `${request.params.name} failed: ${err.message}` }],
48
+ };
49
+ }
50
+ });
51
+
52
+ async function main() {
53
+ // stdout is the MCP transport — anything written there that is not a protocol
54
+ // message corrupts the stream. Keep diagnostics on stderr.
55
+ await server.connect(new StdioServerTransport());
56
+ process.stderr.write(`hf-papers-mcp ready — ${tools.length} tools\n`);
57
+ }
58
+
59
+ main().catch((err) => {
60
+ process.stderr.write(`fatal: ${err.stack || err.message}\n`);
61
+ process.exit(1);
62
+ });
package/src/papers.js ADDED
@@ -0,0 +1,180 @@
1
+ 'use strict';
2
+
3
+ const { execFile } = require('child_process');
4
+ const path = require('path');
5
+
6
+ const SCRIPT = path.resolve(__dirname, '..', 'scripts', 'paper_manager.py');
7
+ // A warm `uv run` already takes ~12.5s; the first (cold) call also resolves the
8
+ // PEP 723 dependency set, which can take far longer. The old 15s budget timed
9
+ // out on cold starts and silently degraded to a depless python3 — so allow real
10
+ // headroom here.
11
+ const TIMEOUT_MS = 60_000;
12
+
13
+ /**
14
+ * Shell out to paper_manager.py and return parsed JSON.
15
+ * Prefers `uv run` (auto-resolves PEP 723 deps). Falls back to `python3` ONLY
16
+ * when uv is genuinely absent — a uv timeout or runtime failure is surfaced
17
+ * rather than masked, because python3 here lacks huggingface_hub and would
18
+ * return misleading/empty data.
19
+ *
20
+ * IMPORTANT: --json must come BEFORE the subcommand (argparse global flag).
21
+ */
22
+ function runPaperManager(args) {
23
+ return new Promise((resolve) => {
24
+ const tryRun = (cmd, cmdArgs) => {
25
+ execFile(cmd, cmdArgs, {
26
+ timeout: TIMEOUT_MS,
27
+ maxBuffer: 1024 * 1024,
28
+ }, (err, stdout, stderr) => {
29
+ if (err && cmd === 'uv' && err.code === 'ENOENT') {
30
+ // uv binary not installed — fall back to python3
31
+ tryRun('python3', [SCRIPT, '--json', ...args]);
32
+ return;
33
+ }
34
+ if (err) {
35
+ // Surface real failures (timeout, runtime error) instead of degrading.
36
+ const reason = err.killed ? `timed out after ${TIMEOUT_MS}ms` : err.message;
37
+ resolve({ error: reason, stderr: stderr?.trim() || '' });
38
+ return;
39
+ }
40
+ // The python3 path has no huggingface_hub, so anything it does return is
41
+ // partial. Label it rather than let it read as a complete answer.
42
+ const degraded = cmd === 'python3'
43
+ ? { degraded: 'uv not installed; ran bare python3 without huggingface_hub — results may be incomplete' }
44
+ : {};
45
+ try {
46
+ const parsed = JSON.parse(stdout);
47
+ // search/daily/index return arrays. Object-spreading an array yields
48
+ // {0:…,1:…}, so the caller receives a map keyed by index instead of a
49
+ // list. Keep arrays intact; only wrap when there is a label to attach.
50
+ if (Array.isArray(parsed)) {
51
+ resolve(degraded.degraded ? { results: parsed, ...degraded } : parsed);
52
+ return;
53
+ }
54
+ resolve({ ...parsed, ...degraded });
55
+ } catch {
56
+ // Output wasn't JSON — return raw text (e.g., citation command)
57
+ resolve({ result: stdout.trim(), ...degraded });
58
+ }
59
+ });
60
+ };
61
+ // Try uv first (auto-resolves PEP 723 dependencies), then python3
62
+ tryRun('uv', ['run', SCRIPT, '--json', ...args]);
63
+ });
64
+ }
65
+
66
+ module.exports = {
67
+ name: 'papers',
68
+
69
+ // No engine dependencies — this is a standalone dev tool
70
+ requires: [],
71
+
72
+ tools(engines) {
73
+ return [
74
+ {
75
+ def: {
76
+ name: 'hf_papers_search',
77
+ description: 'Search Hugging Face papers by keyword. Use to find research relevant to current engine work.',
78
+ inputSchema: {
79
+ type: 'object',
80
+ properties: {
81
+ query: { type: 'string', description: 'Search query (e.g., "speculative decoding")' },
82
+ limit: { type: 'number', description: 'Max results to return', default: 10 },
83
+ },
84
+ required: ['query'],
85
+ },
86
+ },
87
+ handler: async (args) => {
88
+ return runPaperManager(['search', '--query', args.query, '--limit', String(args.limit || 10)]);
89
+ },
90
+ },
91
+ {
92
+ def: {
93
+ name: 'hf_papers_daily',
94
+ description: 'Fetch daily curated papers from Hugging Face. Check what is trending in AI research.',
95
+ inputSchema: {
96
+ type: 'object',
97
+ properties: {
98
+ date: { type: 'string', description: 'Date in YYYY-MM-DD format (default: today)' },
99
+ limit: { type: 'number', description: 'Max papers to return', default: 15 },
100
+ },
101
+ },
102
+ },
103
+ handler: async (args) => {
104
+ const cmdArgs = ['daily', '--limit', String(args.limit || 15)];
105
+ if (args.date) cmdArgs.push('--date', args.date);
106
+ return runPaperManager(cmdArgs);
107
+ },
108
+ },
109
+ {
110
+ def: {
111
+ name: 'hf_papers_info',
112
+ description: 'Get full metadata for a paper from Hugging Face — title, authors, abstract, URLs.',
113
+ inputSchema: {
114
+ type: 'object',
115
+ properties: {
116
+ arxiv_id: { type: 'string', description: 'arXiv paper ID (e.g., "2301.12345")' },
117
+ },
118
+ required: ['arxiv_id'],
119
+ },
120
+ },
121
+ handler: async (args) => {
122
+ return runPaperManager(['info', '--arxiv-id', args.arxiv_id]);
123
+ },
124
+ },
125
+ {
126
+ def: {
127
+ name: 'hf_papers_citation',
128
+ description: 'Generate a citation for a paper. Returns raw text in BibTeX, APA, or MLA format as { result: "..." }.',
129
+ inputSchema: {
130
+ type: 'object',
131
+ properties: {
132
+ arxiv_id: { type: 'string', description: 'arXiv paper ID' },
133
+ format: { type: 'string', description: 'Citation format', default: 'bibtex', enum: ['bibtex', 'apa', 'mla'] },
134
+ },
135
+ required: ['arxiv_id'],
136
+ },
137
+ },
138
+ handler: async (args) => {
139
+ return runPaperManager(['citation', '--arxiv-id', args.arxiv_id, '--format', args.format || 'bibtex']);
140
+ },
141
+ },
142
+ {
143
+ def: {
144
+ name: 'hf_papers_check',
145
+ description: 'Check if a paper is indexed on Hugging Face and get its metadata.',
146
+ inputSchema: {
147
+ type: 'object',
148
+ properties: {
149
+ arxiv_id: { type: 'string', description: 'arXiv paper ID' },
150
+ },
151
+ required: ['arxiv_id'],
152
+ },
153
+ },
154
+ handler: async (args) => {
155
+ return runPaperManager(['check', '--arxiv-id', args.arxiv_id]);
156
+ },
157
+ },
158
+ {
159
+ def: {
160
+ name: 'hf_papers_index',
161
+ description: 'Trigger indexing of a paper on Hugging Face from arXiv.',
162
+ inputSchema: {
163
+ type: 'object',
164
+ properties: {
165
+ arxiv_id: { type: 'string', description: 'arXiv paper ID' },
166
+ },
167
+ required: ['arxiv_id'],
168
+ },
169
+ },
170
+ handler: async (args) => {
171
+ return runPaperManager(['index', '--arxiv-id', args.arxiv_id]);
172
+ },
173
+ },
174
+ ];
175
+ },
176
+
177
+ resources(engines) {
178
+ return [];
179
+ },
180
+ };