arxivsub-mcp 0.1.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +69 -0
  3. package/dist/index.js +434 -0
  4. package/package.json +56 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 arXivSub
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # arxivsub-mcp
2
+
3
+ An [MCP](https://modelcontextprotocol.io) server for **[arXivSub](https://arxivsub.comfyai.app)** — search arXiv and the top AI / CV / ML / robotics / NLP conferences, follow any paper to its related work, build research analytics, and manage a personal library, straight from your AI agent (Claude Desktop, Cursor, Cline, Claude Code, Windsurf, …).
4
+
5
+ arXivSub summarizes every paper into a structured 6-part breakdown (what it's about, innovations, techniques, datasets, results, limitations) and indexes it for semantic search across **arXiv + CVPR, ICCV, ECCV, ICLR, ICML, NeurIPS, AAAI, MICCAI, CoRL, RSS, ACL, EMNLP, ICRA, IROS, IJCAI, SIGGRAPH, SIGGRAPH Asia**. This server puts that engine directly in your agent's hands.
6
+
7
+ ## Get a free key (7-day trial, no credit card)
8
+
9
+ 1. Sign in at **https://arxivsub.comfyai.app/skills** with email or Google.
10
+ 2. Copy your API key from the Skills page.
11
+ 3. Put it in the server config below as `ARXIVSUB_SKILL_KEY`.
12
+
13
+ New accounts get a **7-day free trial** of the full skill. After that it's part of arXivSub Pro.
14
+
15
+ ## Install
16
+
17
+ The server runs via `npx` — no manual install needed. Add it to your MCP client's config.
18
+
19
+ **Claude Desktop** (`claude_desktop_config.json`), **Cursor** (`~/.cursor/mcp.json`), **Windsurf**, **Cline**, etc.:
20
+
21
+ ```json
22
+ {
23
+ "mcpServers": {
24
+ "arxivsub": {
25
+ "command": "npx",
26
+ "args": ["-y", "arxivsub-mcp"],
27
+ "env": { "ARXIVSUB_SKILL_KEY": "your_key_here" }
28
+ }
29
+ }
30
+ }
31
+ ```
32
+
33
+ **Claude Code:**
34
+
35
+ ```bash
36
+ claude mcp add arxivsub --env ARXIVSUB_SKILL_KEY=your_key_here -- npx -y arxivsub-mcp
37
+ ```
38
+
39
+ Restart your client and the tools below become available.
40
+
41
+ ## Tools
42
+
43
+ | Tool | What it does |
44
+ |------|--------------|
45
+ | `search_papers` | Semantic + keyword search over arXiv and conferences. Filters: venues, recency, year, `has_code`. Returns each paper's 6-part summary, authors, PDF, and code link. |
46
+ | `find_similar_papers` | Nearest-neighbour "related work" for a paper you already found. Chainable for a mini literature review. |
47
+ | `research_insight` | Pivot analytics (the website's Custom Chart engine): trends, rankings, breakdowns. *Pro / trial.* |
48
+ | `library_save` | Save/update a paper in your personal library — notes, reading status, collections. |
49
+ | `library_list` | List your saved papers and collections. |
50
+
51
+ Just ask your agent naturally — *"find recent papers on LLM safety with code"*, *"more work like this one"*, *"how has interest in diffusion models trended by month"*, *"save this to my "to read" collection"* — and it will pick the right tool.
52
+
53
+ ## Notes
54
+
55
+ - **Case-sensitive venues:** `arxiv`, `CVPR`, `ICCV`, `ECCV`, `ICLR`, `ICML`, `NeurIPS`, `AAAI`, `MICCAI`, `CoRL`, `RSS`, `ACL`, `EMNLP`, `ICRA`, `IROS`, `IJCAI`, `SIGGRAPH`, `SIGGRAPH Asia`.
56
+ - **Quota:** each search / similar / insight call uses one daily quota unit; library calls are free. The remaining quota is returned with every result.
57
+ - **Privacy / security:** the server only holds your skill key and talks to the arXivSub gateway over HTTPS. It has no direct database access and no service credentials.
58
+
59
+ ## Development
60
+
61
+ ```bash
62
+ npm install
63
+ npm run build # tsc -> dist/
64
+ ARXIVSUB_SKILL_KEY=... node dist/index.js # run over stdio
65
+ ```
66
+
67
+ ## License
68
+
69
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,434 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * arxivsub-mcp — Model Context Protocol server for arXivSub.
4
+ *
5
+ * A thin client over the arXivSub agent-skills gateway (the same endpoint the
6
+ * downloadable skill scripts use). It holds the user's ARXIVSUB_SKILL_KEY and
7
+ * exposes the gateway's actions as MCP tools. All auth, quota, and Pro/trial
8
+ * gating happen server-side in the gateway — this process never sees the
9
+ * database or any service-role credential.
10
+ *
11
+ * Tools:
12
+ * search_papers — search arXiv + conferences (semantic + keyword)
13
+ * find_similar_papers — nearest-neighbour "related work" for a paper
14
+ * research_insight — pivot analytics over the corpora (Custom Chart engine)
15
+ * library_save — save/update a paper in the personal library
16
+ * library_list — list the personal library
17
+ */
18
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
19
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
20
+ import { z } from "zod";
21
+ import { realpathSync } from "node:fs";
22
+ import { fileURLToPath } from "node:url";
23
+ const API_URL = "https://qtevnmgyobilaanrzidq.supabase.co/functions/v1/agent-skills-gateway";
24
+ const SKILLS_URL = "https://arxivsub.comfyai.app/skills";
25
+ const PRICING_URL = "https://arxivsub.comfyai.app/pricing";
26
+ const NO_KEY_MESSAGE = `No arXivSub API key found. arxivsub-mcp needs your ARXIVSUB_SKILL_KEY.\n\n` +
27
+ `It's free: sign in at ${SKILLS_URL} with email or Google (no credit card) to ` +
28
+ `get your key and a 7-day free trial, then add it to this server's config, e.g.:\n\n` +
29
+ ` "arxivsub": {\n` +
30
+ ` "command": "npx",\n` +
31
+ ` "args": ["-y", "arxivsub-mcp"],\n` +
32
+ ` "env": { "ARXIVSUB_SKILL_KEY": "your_key_here" }\n` +
33
+ ` }\n`;
34
+ /** A user-facing error whose message is safe to surface directly to the agent. */
35
+ class GatewayError extends Error {
36
+ }
37
+ function getApiKey() {
38
+ return (process.env.ARXIVSUB_SKILL_KEY || "").trim();
39
+ }
40
+ /**
41
+ * POST a body to the gateway with the skill key header. Maps non-2xx responses
42
+ * to friendly, actionable GatewayError messages (missing key, invalid key,
43
+ * quota exhausted, trial ended, bad request).
44
+ */
45
+ async function callGateway(body) {
46
+ const key = getApiKey();
47
+ if (!key)
48
+ throw new GatewayError(NO_KEY_MESSAGE);
49
+ let resp;
50
+ try {
51
+ resp = await fetch(API_URL, {
52
+ method: "POST",
53
+ headers: {
54
+ "Content-Type": "application/json",
55
+ "x-agent-skill-key": key,
56
+ },
57
+ body: JSON.stringify(body),
58
+ });
59
+ }
60
+ catch (e) {
61
+ throw new GatewayError(`Could not reach arXivSub (${e instanceof Error ? e.message : String(e)}). ` +
62
+ `This is usually a temporary network issue — try again.`);
63
+ }
64
+ const raw = await resp.text();
65
+ let data = null;
66
+ try {
67
+ data = raw ? JSON.parse(raw) : null;
68
+ }
69
+ catch {
70
+ data = null;
71
+ }
72
+ if (!resp.ok) {
73
+ const errMsg = (data && data.error) || raw || `HTTP ${resp.status}`;
74
+ if (resp.status === 401) {
75
+ throw new GatewayError(`Invalid arXivSub API key. Check ARXIVSUB_SKILL_KEY in this server's ` +
76
+ `config — get or copy your key at ${SKILLS_URL}.`);
77
+ }
78
+ if (resp.status === 403 && /quota/i.test(errMsg)) {
79
+ throw new GatewayError(`Your arXivSub daily quota is used up. It resets tomorrow — no need to retry today.`);
80
+ }
81
+ if (resp.status === 403) {
82
+ throw new GatewayError(`This needs an active arXivSub Pro subscription or free-trial days. ` +
83
+ `Your free trial may have ended — you can upgrade at ${PRICING_URL}.`);
84
+ }
85
+ throw new GatewayError(errMsg);
86
+ }
87
+ return data;
88
+ }
89
+ // ---------------------------------------------------------------------------
90
+ // Summary parsing — mirrors the bundled skill scripts so tool output matches.
91
+ // summary_content is a <SEG>-delimited string. When it has 11 segments the
92
+ // first is a duplicated title and is dropped; otherwise the segments are used
93
+ // as-is. Layout (0-indexed, after the optional drop):
94
+ // [0] what it's about [1] innovations [2] techniques
95
+ // [3] datasets [4] results [5] limitations
96
+ // [6] first_author [7] first_aff [8] last_author [9] last_aff
97
+ // ---------------------------------------------------------------------------
98
+ function segmentsOf(summaryContent) {
99
+ const segments = (summaryContent || "").split("<SEG>");
100
+ return segments.length === 11 ? segments.slice(1) : segments;
101
+ }
102
+ export function parseSummary(summaryContent) {
103
+ const content = segmentsOf(summaryContent);
104
+ const g = (i) => (i < content.length ? content[i].trim() : "");
105
+ if (content.length < 6) {
106
+ return {
107
+ what_about: (summaryContent || "").slice(0, 300),
108
+ innovations: "",
109
+ techniques: "",
110
+ datasets: "",
111
+ results: "",
112
+ limitations: "",
113
+ };
114
+ }
115
+ return {
116
+ what_about: g(0),
117
+ innovations: g(1),
118
+ techniques: g(2),
119
+ datasets: g(3),
120
+ results: g(4),
121
+ limitations: g(5),
122
+ };
123
+ }
124
+ /** Like parseSummary but also pulls the author tail (segments 6..9) — used by
125
+ * find_similar_papers, whose rows carry authors inside summary_content rather
126
+ * than a structured authors[] array. */
127
+ export function parseSummaryWithAuthors(summaryContent) {
128
+ const content = segmentsOf(summaryContent);
129
+ const g = (i) => (i < content.length ? content[i].trim() : "");
130
+ const base = parseSummary(summaryContent);
131
+ if (content.length < 6) {
132
+ return { ...base, first_author: "", first_aff: "", last_author: "", last_aff: "" };
133
+ }
134
+ return {
135
+ ...base,
136
+ first_author: g(6),
137
+ first_aff: g(7),
138
+ last_author: g(8),
139
+ last_aff: g(9),
140
+ };
141
+ }
142
+ export function shapeSearchPaper(p, source) {
143
+ const authors = Array.isArray(p.authors) ? p.authors : [];
144
+ const first = authors.find((a) => a && a.is_first_author) || authors[0] || {};
145
+ const last = authors.find((a) => a && a.is_last_author) ||
146
+ authors[authors.length - 1] ||
147
+ {};
148
+ return {
149
+ id: p.id,
150
+ title: p.title,
151
+ source,
152
+ conference: p.conference_name ?? "arXiv",
153
+ year: p.publish_year ?? null,
154
+ arxiv_id: p.arxiv_id ?? null,
155
+ pdf_url: p.pdf_url ?? null,
156
+ github_url: p.github_url ?? null,
157
+ first_author: first.name || "",
158
+ first_aff: first.affiliation || "",
159
+ last_author: last.name || "",
160
+ last_aff: last.affiliation || "",
161
+ keywords: Array.isArray(p.keywords)
162
+ ? p.keywords.map((k) => k?.name).filter(Boolean)
163
+ : [],
164
+ ...parseSummary(p.summary_content || ""),
165
+ };
166
+ }
167
+ export function shapeSimilarPaper(p, itemType) {
168
+ const source = itemType === "paper" ? "arxiv" : "conferences";
169
+ return {
170
+ id: p.id,
171
+ title: p.title,
172
+ source,
173
+ conference: p.conference_name ?? "arXiv",
174
+ year: p.publish_year ?? null,
175
+ pdf_url: p.pdf_url ?? null,
176
+ github_url: p.github_url ?? null,
177
+ ...parseSummaryWithAuthors(p.summary_content || ""),
178
+ };
179
+ }
180
+ function textResult(obj) {
181
+ return { content: [{ type: "text", text: JSON.stringify(obj, null, 2) }] };
182
+ }
183
+ function errorResult(e) {
184
+ const msg = e instanceof Error ? e.message : String(e);
185
+ return { content: [{ type: "text", text: msg }], isError: true };
186
+ }
187
+ // ---------------------------------------------------------------------------
188
+ // Server + tools
189
+ // ---------------------------------------------------------------------------
190
+ const server = new McpServer({ name: "arxivsub", version: "0.1.0" });
191
+ const VENUES = "arxiv, CVPR, ICCV, ECCV, ICLR, ICML, NeurIPS, AAAI, MICCAI, CoRL, RSS, ACL, EMNLP, ICRA, IROS, IJCAI, SIGGRAPH, 'SIGGRAPH Asia'";
192
+ server.tool("search_papers", "Search academic papers from arXiv and major AI/CV/ML/robotics/NLP conferences via arXivSub " +
193
+ "(semantic + keyword search over a structured, summarized corpus). Use for 'find papers on X', " +
194
+ "'latest research about Y', 'recent conference work on Z', or to start a literature review. " +
195
+ "Returns each paper with a 6-part summary (what it's about, innovations, techniques, datasets, " +
196
+ "results, limitations), authors + affiliations, pdf_url, and github_url when code is available.", {
197
+ query: z.string().describe("Natural-language search query, e.g. 'LLM safety alignment'."),
198
+ locations: z
199
+ .array(z.string())
200
+ .optional()
201
+ .describe(`Venues to search (CASE-SENSITIVE): ${VENUES}. Include 'arxiv' for arXiv preprints. Default: ['arxiv'].`),
202
+ limit: z
203
+ .number()
204
+ .int()
205
+ .positive()
206
+ .optional()
207
+ .describe("Max papers per source (server caps at 100). Default 10."),
208
+ arxiv_days: z
209
+ .number()
210
+ .int()
211
+ .positive()
212
+ .optional()
213
+ .describe("Only arXiv papers from the last N days. Default 30."),
214
+ conference_years: z
215
+ .array(z.number().int())
216
+ .optional()
217
+ .describe("Conference publication years to include, e.g. [2024, 2025]. Default: last two years."),
218
+ has_code: z
219
+ .boolean()
220
+ .optional()
221
+ .describe("If true, only return papers that ship a public code repository (a reproducibility signal)."),
222
+ language: z
223
+ .string()
224
+ .optional()
225
+ .describe("Summary language, 'en' or 'zh'. Default 'en'."),
226
+ }, async (args) => {
227
+ try {
228
+ const body = {
229
+ action: "search",
230
+ query: args.query,
231
+ language: args.language ?? "en",
232
+ locations: args.locations ?? ["arxiv"],
233
+ limit: args.limit ?? 10,
234
+ };
235
+ if (args.arxiv_days != null)
236
+ body.arxiv_days = args.arxiv_days;
237
+ if (args.conference_years != null)
238
+ body.conference_years = args.conference_years;
239
+ if (args.has_code)
240
+ body.has_code = true;
241
+ const data = await callGateway(body);
242
+ const papers = [
243
+ ...(data?.arxiv || []).map((p) => shapeSearchPaper(p, "arxiv")),
244
+ ...(data?.conferences || []).map((p) => shapeSearchPaper(p, "conferences")),
245
+ ];
246
+ return textResult({
247
+ total_papers: papers.length,
248
+ quota_remaining: data?.quota_remaining ?? null,
249
+ papers,
250
+ });
251
+ }
252
+ catch (e) {
253
+ return errorResult(e);
254
+ }
255
+ });
256
+ server.tool("find_similar_papers", "Given ONE paper you already have (from a prior search result), return the most similar papers " +
257
+ "from the same corpus — semantic nearest neighbours, each with the full summary and code link. " +
258
+ "Use for 'more like this', 'related work', 'what else is in this line of work', or to expand a " +
259
+ "literature review. You can chain it: search -> pick a seed -> find_similar_papers -> repeat on a " +
260
+ "strong neighbour.", {
261
+ item_type: z
262
+ .enum(["paper", "ciiina"])
263
+ .describe("The seed paper's type, from its search 'source': 'arxiv' -> 'paper', 'conferences' -> 'ciiina'."),
264
+ item_id: z
265
+ .string()
266
+ .describe("The seed paper's arXivSub id (uuid) from a prior search result. Never invent one."),
267
+ limit: z
268
+ .number()
269
+ .int()
270
+ .positive()
271
+ .optional()
272
+ .describe("How many neighbours to return (server caps at 20). Default 8."),
273
+ language: z.string().optional().describe("Summary language, 'en' or 'zh'. Default 'en'."),
274
+ }, async (args) => {
275
+ try {
276
+ const data = await callGateway({
277
+ action: "similar",
278
+ item_type: args.item_type,
279
+ item_id: args.item_id,
280
+ limit: args.limit ?? 8,
281
+ language: args.language ?? "en",
282
+ });
283
+ const papers = (data?.results || []).map((p) => shapeSimilarPaper(p, args.item_type));
284
+ return textResult({
285
+ item_type: args.item_type,
286
+ item_id: args.item_id,
287
+ total_similar: papers.length,
288
+ quota_remaining: data?.quota_remaining ?? null,
289
+ papers,
290
+ });
291
+ }
292
+ catch (e) {
293
+ return errorResult(e);
294
+ }
295
+ });
296
+ server.tool("research_insight", "Build aggregate pivot statistics over the arXivSub corpora — the same engine as the website's " +
297
+ "Custom Chart. Use for trends, rankings, breakdowns and comparisons: 'how has interest in diffusion " +
298
+ "models trended', 'top institutions in RL', 'papers per month by keyword', 'avg citations by " +
299
+ "affiliation'. Returns rows of {dim, breakdown, value} for you to interpret and visualize. " +
300
+ "Requires Pro or active trial days.", {
301
+ corpus: z
302
+ .enum(["arxiv", "ciiina", "conference"])
303
+ .optional()
304
+ .describe("'arxiv' (recent arXiv) or 'ciiina' (conferences; 'conference' is an alias). Default 'arxiv'."),
305
+ dim: z
306
+ .string()
307
+ .describe("REQUIRED axis. arxiv: time:day|week|month|year, keyword, category, affiliation, author, citation_bin. " +
308
+ "ciiina: time:year, keyword, conference, award, affiliation, author, citation_bin."),
309
+ measure: z
310
+ .string()
311
+ .optional()
312
+ .describe("count_papers (default) | count_authors | count_affiliations | count_keywords | avg_citation | " +
313
+ "median_citation | award_rate (ciiina only)."),
314
+ breakdown: z
315
+ .string()
316
+ .nullable()
317
+ .optional()
318
+ .describe("Optional second split, same value set as dim. e.g. dim=time:month, breakdown=keyword."),
319
+ filters: z
320
+ .record(z.any())
321
+ .optional()
322
+ .describe("Optional object: search, start_date, end_date, keywords[], categories[] (arxiv), " +
323
+ "conferences[]/publish_years[]/awards[] (ciiina), affiliations[]."),
324
+ options: z
325
+ .record(z.any())
326
+ .optional()
327
+ .describe("Optional object: top_n_dim, top_n_breakdown, min_count, show_others, sort."),
328
+ }, async (args) => {
329
+ try {
330
+ const data = await callGateway({
331
+ action: "insight",
332
+ corpus: args.corpus ?? "arxiv",
333
+ dim: args.dim,
334
+ measure: args.measure ?? "count_papers",
335
+ breakdown: args.breakdown ?? null,
336
+ filters: args.filters ?? {},
337
+ options: args.options ?? {},
338
+ });
339
+ return textResult(data);
340
+ }
341
+ catch (e) {
342
+ return errorResult(e);
343
+ }
344
+ });
345
+ server.tool("library_save", "Save (or update) a paper in the user's personal arXivSub library. Use for 'save this', 'bookmark " +
346
+ "that paper', 'add it to my <X> collection', 'note that ...', 'mark as read'. Idempotent: saving the " +
347
+ "same paper again updates only the fields you pass and never duplicates. Cannot delete — removals " +
348
+ "are done on the website.", {
349
+ item_type: z
350
+ .enum(["paper", "ciiina"])
351
+ .describe("From the search 'source': 'arxiv' -> 'paper', 'conferences' -> 'ciiina'."),
352
+ item_id: z.string().describe("The item's arXivSub id (uuid) from a prior search result."),
353
+ note: z.string().optional().describe("Optional note (<=2000 chars)."),
354
+ reading_status: z
355
+ .enum(["unread", "reading", "read"])
356
+ .optional()
357
+ .describe("Optional reading status."),
358
+ collection_name: z
359
+ .string()
360
+ .optional()
361
+ .describe("Optional collection/folder name; created automatically if it doesn't exist."),
362
+ }, async (args) => {
363
+ try {
364
+ const data = await callGateway({
365
+ action: "library_save",
366
+ item_type: args.item_type,
367
+ item_id: args.item_id,
368
+ note: args.note ?? null,
369
+ reading_status: args.reading_status ?? null,
370
+ collection_name: args.collection_name ?? null,
371
+ });
372
+ return textResult(data);
373
+ }
374
+ catch (e) {
375
+ return errorResult(e);
376
+ }
377
+ });
378
+ server.tool("library_list", "List the user's personal arXivSub library — collections and saved items, optionally filtered. " +
379
+ "Use for 'what have I saved', 'show my library', 'which papers are in my <name> collection', " +
380
+ "'what am I reading'.", {
381
+ status: z
382
+ .enum(["unread", "reading", "read"])
383
+ .optional()
384
+ .describe("Only items with this reading status."),
385
+ item_type: z
386
+ .enum(["paper", "ciiina"])
387
+ .optional()
388
+ .describe("Only papers ('paper') or conference items ('ciiina')."),
389
+ collection_name: z.string().optional().describe("Only items in this collection."),
390
+ limit: z
391
+ .number()
392
+ .int()
393
+ .positive()
394
+ .optional()
395
+ .describe("Max items (server caps at 200). Default 100."),
396
+ }, async (args) => {
397
+ try {
398
+ const data = await callGateway({
399
+ action: "library_list",
400
+ status: args.status ?? null,
401
+ item_type: args.item_type ?? null,
402
+ collection_name: args.collection_name ?? null,
403
+ limit: args.limit ?? 100,
404
+ });
405
+ return textResult(data);
406
+ }
407
+ catch (e) {
408
+ return errorResult(e);
409
+ }
410
+ });
411
+ async function main() {
412
+ const transport = new StdioServerTransport();
413
+ await server.connect(transport);
414
+ // stdout is the JSON-RPC channel — only log to stderr.
415
+ console.error("arxivsub-mcp server running on stdio");
416
+ }
417
+ /** True only when this file is the process entry point (run directly or via the
418
+ * npx bin shim) — so importing it for tests does NOT start the stdio server. */
419
+ function isEntrypoint() {
420
+ try {
421
+ const self = realpathSync(fileURLToPath(import.meta.url));
422
+ const argv1 = process.argv[1] ? realpathSync(process.argv[1]) : "";
423
+ return self === argv1;
424
+ }
425
+ catch {
426
+ return false;
427
+ }
428
+ }
429
+ if (isEntrypoint()) {
430
+ main().catch((err) => {
431
+ console.error("Fatal:", err);
432
+ process.exit(1);
433
+ });
434
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "arxivsub-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for arXivSub — search arXiv and top AI / CV / ML / robotics / NLP conference papers, follow a paper to its related work, build research analytics, and manage a personal library, straight from your AI agent.",
5
+ "keywords": [
6
+ "mcp",
7
+ "model-context-protocol",
8
+ "arxiv",
9
+ "research",
10
+ "papers",
11
+ "academic",
12
+ "literature-review",
13
+ "conferences",
14
+ "llm",
15
+ "claude",
16
+ "cursor",
17
+ "ai-agent"
18
+ ],
19
+ "homepage": "https://arxivsub.comfyai.app",
20
+ "bugs": {
21
+ "url": "https://github.com/lujiazho/arxivsub-mcp/issues"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/lujiazho/arxivsub-mcp.git"
26
+ },
27
+ "license": "MIT",
28
+ "author": "arXivSub",
29
+ "type": "module",
30
+ "bin": {
31
+ "arxivsub-mcp": "dist/index.js"
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "engines": {
39
+ "node": ">=18"
40
+ },
41
+ "scripts": {
42
+ "build": "tsc",
43
+ "prepublishOnly": "npm run build",
44
+ "start": "node dist/index.js",
45
+ "test": "npm run build && node parse.test.mjs && node smoke.mjs",
46
+ "probe": "node probe.mjs"
47
+ },
48
+ "dependencies": {
49
+ "@modelcontextprotocol/sdk": "^1.12.0",
50
+ "zod": "^3.23.8"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^20.14.0",
54
+ "typescript": "^5.5.0"
55
+ }
56
+ }