Alpha Hub: Context Hub for research papers

CLI + MCP server for searching papers and building persistent knowledge.
Powered by alphaXiv for semantic search, paper reports, and PDF Q&A.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Advait Paliwal 2026-03-18 16:53:26 -07:00
commit 9a708a1ab9
20 changed files with 2212 additions and 0 deletions

84
cli/src/mcp/server.js Normal file
View file

@ -0,0 +1,84 @@
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { handleSearch, handleGet, handleAsk, handleAnnotate, handleCode } from './tools.js';
const _stderr = process.stderr;
console.log = (...args) => _stderr.write(args.join(' ') + '\n');
console.warn = (...args) => _stderr.write('[warn] ' + args.join(' ') + '\n');
console.info = (...args) => _stderr.write('[info] ' + args.join(' ') + '\n');
console.debug = (...args) => _stderr.write('[debug] ' + args.join(' ') + '\n');
const __dirname = dirname(fileURLToPath(import.meta.url));
const pkg = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf8'));
const server = new McpServer({
name: 'alpha',
version: pkg.version,
});
server.tool(
'alpha_search',
'Search research papers via alphaXiv. Supports semantic (embedding), keyword, and agentic search modes.',
{
query: z.string().describe('Search query — use 2-3 sentences for semantic mode, keywords for keyword mode'),
mode: z.enum(['semantic', 'keyword', 'agentic']).optional().describe('Search mode (default: semantic)'),
},
async (args) => handleSearch(args),
);
server.tool(
'alpha_get',
'Get paper content and local annotation. Accepts arXiv URL, alphaXiv URL, or arXiv ID.',
{
url: z.string().describe('arXiv/alphaXiv URL or arXiv ID (e.g. "2106.09685")'),
full_text: z.boolean().optional().describe('Get raw text instead of AI-generated report (default: false)'),
},
async (args) => handleGet(args),
);
server.tool(
'alpha_ask',
'Ask a question about a specific paper. Uses AI to analyze the PDF and answer.',
{
url: z.string().describe('arXiv/alphaXiv URL or arXiv ID'),
question: z.string().describe('Question about the paper'),
},
async (args) => handleAsk(args),
);
server.tool(
'alpha_annotate',
'Read, write, clear, or list local annotations on papers. Annotations persist across sessions and appear on future fetches.',
{
id: z.string().optional().describe('Paper ID (arXiv ID or URL). Required unless using list mode.'),
note: z.string().optional().describe('Annotation text to save. Omit to read existing.'),
clear: z.boolean().optional().describe('Remove annotation for this paper'),
list: z.boolean().optional().describe('List all annotations'),
},
async (args) => handleAnnotate(args),
);
server.tool(
'alpha_code',
"Read files from a paper's GitHub repository. Use path '/' for repo overview.",
{
github_url: z.string().describe('GitHub repository URL'),
path: z.string().optional().describe("File or directory path (default: '/')"),
},
async (args) => handleCode(args),
);
process.on('uncaughtException', (err) => {
_stderr.write(`[alpha-mcp] Uncaught exception: ${err.message}\n`);
});
process.on('unhandledRejection', (reason) => {
_stderr.write(`[alpha-mcp] Unhandled rejection: ${reason}\n`);
});
const transport = new StdioServerTransport();
await server.connect(transport);
_stderr.write(`[alpha-mcp] Server started (v${pkg.version})\n`);

94
cli/src/mcp/tools.js Normal file
View file

@ -0,0 +1,94 @@
import {
searchByEmbedding,
searchByKeyword,
agenticSearch,
getPaperContent,
answerPdfQuery,
readGithubRepo,
} from '../lib/alphaxiv.js';
import { writeAnnotation, readAnnotation, clearAnnotation, listAnnotations } from '../lib/annotations.js';
import { normalizePaperId, toArxivUrl } from '../lib/papers.js';
function textResult(data) {
return {
content: [{ type: 'text', text: typeof data === 'string' ? data : JSON.stringify(data, null, 2) }],
};
}
function errorResult(message) {
return {
content: [{ type: 'text', text: JSON.stringify({ error: message }, null, 2) }],
isError: true,
};
}
export async function handleSearch({ query, mode = 'semantic' }) {
try {
if (mode === 'keyword') return textResult(await searchByKeyword(query));
if (mode === 'agentic') return textResult(await agenticSearch(query));
return textResult(await searchByEmbedding(query));
} catch (err) {
return errorResult(`Search failed: ${err.message}`);
}
}
export async function handleGet({ url, full_text = false }) {
try {
const paperId = normalizePaperId(url);
const arxivUrl = toArxivUrl(url);
const content = await getPaperContent(arxivUrl, { fullText: full_text });
const annotation = readAnnotation(paperId);
return textResult({ content, annotation });
} catch (err) {
return errorResult(`Failed to fetch paper: ${err.message}`);
}
}
export async function handleAsk({ url, question }) {
try {
const answer = await answerPdfQuery(toArxivUrl(url), question);
return textResult(answer);
} catch (err) {
return errorResult(`Ask failed: ${err.message}`);
}
}
export async function handleAnnotate({ id, note, clear = false, list = false }) {
try {
if (list) {
const all = listAnnotations();
return textResult({ annotations: all, total: all.length });
}
if (!id) return errorResult('Provide a paper ID or use list mode.');
const paperId = normalizePaperId(id);
if (clear) {
const removed = clearAnnotation(paperId);
return textResult({ status: removed ? 'cleared' : 'not_found', id: paperId });
}
if (note) {
const saved = writeAnnotation(paperId, note);
return textResult({ status: 'saved', annotation: saved });
}
const existing = readAnnotation(paperId);
if (existing) return textResult({ annotation: existing });
return textResult({ status: 'no_annotation', id: paperId });
} catch (err) {
return errorResult(`Annotation failed: ${err.message}`);
}
}
export async function handleCode({ github_url, path = '/' }) {
try {
const result = await readGithubRepo(github_url, path);
return textResult(result);
} catch (err) {
return errorResult(`Code read failed: ${err.message}`);
}
}