项目背景

在写论文的时候感觉Introduction写起来很有难度,要合理的将段落和文献结合起来相对比较繁琐。虽然之前也有阅读大量相关文献,但是总感觉写起来很奇怪很别扭。因此,想着能否有一款AI应用能够帮助我自动生成Introduction,并且能够自动生成参考文献列表。同时,我还希望它能够参考其他文献的行文风格来创作,因为每一类期刊的文章都有自己相对应的风格。

同时考虑到我使用的是Vscode + Latex这样一个编辑环境,因此我希望直接可以使用mcp工具来帮助我进行辅助创作,因此我使用了之前文章提到的Vscode + Roo Code + MCP的方案来进行实现。

项目目标

  • 实现一个基于MCP协议的Latex论文写作助手。
  • 能够参考其他文献的行文风格来创作。
  • 能够将生成的论文内容保存到本地。
  • 在生成相应章节内容时懂得自动引用相关文献,并且能够将其和行文合理串联起来。

具体实现

1. 创建MCP服务器

和传统服务一样,都需要创建服务器进行交互。

// 创建 MCP 服务器
const server = new Server(
{
name: "mdpi-server-mcp-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);

2.注册工具列表处理器

MCP中的ListToolsRequestSchema主要做两件事:

  • 描述工具清单

  • name:工具的唯一标识

  • description:用途说明(给大模型看,帮助它判断何时调用)

  • inputSchema:参数定义(JSON Schema 格式,客户端据此校验输入)

  • 按协议响应

MCP 定义了一个标准请求码 ListToolsRequestSchema,客户端连接后会发送这个请求。服务器的处理器必须返回符合规范的 JSON。

// 注册工具列表处理器
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "search_arxiv",
description: "搜索 arXiv 论文",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "搜索英文关键词"
},
maxResults: {
type: "number",
description: "最大结果数量",
default: 5
}
},
required: ["query"]
}
},
{
name: "revise_section",
description: "对已有章节内容进行学术化润色、扩写或调整,保持 MDPI LaTeX 格式。",
inputSchema: {
type: "object",
properties: {
content: { type: "string", description: "需要修改的原始文本(LaTeX 片段)" },
instruction: { type: "string", description: "修改要求,如 '扩写至 500 字'、'加强逻辑衔接'、'转为被动语态'" },
autoRefs: {
"type": "boolean",
"description": "是否自动搜索并引用相关 arXiv 论文(默认 false)",
"default": false
},
refQuery: {
"type": "string",
"description": "自定义文献搜索关键词(若不提供,则从章节主题中自动生成)"
},
refCount: {
"type": "number",
"description": "期望引用的参考文献数量(默认 5)",
"default": 5
}
},
required: ["content", "instruction"]
}
},
{
name: "search_and_format_references",
description: "根据关键词搜索 arXiv 论文,返回格式化的 BibTeX 条目,可直接用于 LaTeX 文档。",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "搜索关键词(如 'sEMG joint moment prediction')" },
maxResults: { type: "number", description: "返回结果数量(默认 5)", default: 5 }
},
required: ["query"]
}
},
{
name: "generate_paper_section",
description: "根据用户指定的章节类型(如 introduction、related_work、experiments、conclusion)和自定义要点,生成 LaTeX 格式的章节草稿。可指定参考文献目录以模仿其写作风格。",
inputSchema: {
type: "object",
properties: {
section: {
type: "string",
enum: ["abstract", "introduction", "related_work", "method", "experiments", "results", "discussion", "conclusion"],
description: "要生成的章节类型"
},
additionalInstructions: { type: "string", description: "额外要求(如强调创新点、包含对比方法等)" },
previousContent: { type: "string", description: "已有的前文内容(用于上下文连贯)" },
referenceDir: { type: "string", description: "包含参考文献文件的本地目录路径,用于模仿其写作风格(可选)" },
autoRefs: {
"type": "boolean",
"description": "是否自动搜索并引用相关 arXiv 论文(默认 false)",
"default": false
},
refQuery: {
"type": "string",
"description": "自定义文献搜索关键词(若不提供,则从章节主题中自动生成)"
},
refCount: {
"type": "number",
"description": "期望引用的参考文献数量(默认 5)",
"default": 5
}
},
required: ["section"]
}
}
]
};
});

3.注册工具调用处理器

CallToolRequestSchema负责处理客户端请求。具体职责有:

  • 解析请求:提取工具名和参数。
  • 分发执行:根据工具名跳转到对应的业务逻辑(通常是一个 switch分支)。
  • 返回结果:将执行结果封装成 MCP 规定的响应格式({ content: [...] })返回给客户端。

server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;

try {
switch (name) {

case "revise_section": {
const { content, instruction, autoRefs, refQuery, refCount = 5 } = args as any;

let refContext = "";
let bibPaths: string[] = [];

// 自动搜索文献
if (autoRefs) {
const query = refQuery || buildRefQuery("", instruction, content);
try {
const { refPrompt, bibContent } = await buildRefContext(query, refCount);
if (refPrompt.trim()) {
refContext = refPrompt;
const bibFileName = `refs_${Date.now()}.bib`;
fs.writeFileSync(path.join(WORK_DIR, bibFileName), bibContent, "utf-8");
bibPaths.push(bibFileName);
}
} catch (err) {
console.warn("自动参考文献搜索失败:", err);
}
}

// 构建提示
const systemPrompt = `You are a meticulous academic editor. Revise the provided LaTeX text according to the user's instruction. Maintain the LaTeX formatting and existing citation commands. ${autoRefs ? 'If you add new statements, cite the provided references where appropriate using \\cite{id}.' : ''}`;
let userPrompt = `Original text:\n${content}\n\nRevision instruction: ${instruction}`;
if (refContext) {
userPrompt += `\n\n${refContext}\n\nAdd or keep citations as appropriate.`;
}

const revised = await callOpenRouterAPI(userPrompt, systemPrompt);
const outPath = path.join(WORK_DIR, `revised_${Date.now()}.tex`);
fs.writeFileSync(outPath, revised, "utf-8");

let resultText = revised;
if (bibPaths.length > 0) {
resultText += `\n\n📚 参考文献文件已保存: ${bibPaths.join(", ")}`;
}

return {
content: [{
type: "text",
text: resultText,
file: path.basename(outPath),
...(bibPaths.length > 0 && { bibFiles: bibPaths })
}]
};
}

case "search_and_format_references": {
const { query, maxResults = 5 } = args as any;
const bib = await searchAndFormatReferences(query, maxResults);
const outPath = path.join(WORK_DIR, `refs_${Date.now()}.bib`);
fs.writeFileSync(outPath, bib, "utf-8");
return {
content: [{ type: "text", text: bib, file: path.basename(outPath) }]
};
}

case "generate_paper_section": {
const { section, additionalInstructions, previousContent, autoRefs, refQuery, refCount = 5 } = args as any;

let refContext = "";
let bibPaths: string[] = [];

// 自动搜索文献
if (autoRefs) {
const query = refQuery || buildRefQuery(section, additionalInstructions, previousContent);
try {
const { refPrompt, bibContent } = await buildRefContext(query, refCount);
if (refPrompt.trim()) {
refContext = refPrompt;
// 保存 BibTeX 文件
const bibFileName = `refs_${Date.now()}.bib`;
fs.writeFileSync(path.join(WORK_DIR, bibFileName), bibContent, "utf-8");
bibPaths.push(bibFileName);
}
} catch (err) {
console.warn("自动参考文献搜索失败:", err);
}
}

// 构建系统提示
const systemPrompt = `You are an academic writer specializing in lower-limb joint moment estimation using deep learning. Write a ${section} section in LaTeX format suitable for MDPI template. Use professional, coherent English. ${autoRefs ? 'You MUST cite the provided references at appropriate places using \\cite{id}.' : ''}`;

let userPrompt = `Generate the ${section} section for a paper on deep learning-based joint moment estimation.`;
if (previousContent) {
userPrompt += `\n\nContext from previous sections:\n${previousContent.slice(0, 1000)}`;
}
if (additionalInstructions) {
userPrompt += `\n\nAdditional instructions: ${additionalInstructions}`;
}
if (refContext) {
userPrompt += `\n\n${refContext}\n\nPlease incorporate citations naturally in the generated text.`;
}
userPrompt += `\n\nOutput only the LaTeX content for this section.`;

const sectionText = await callOpenRouterAPI(userPrompt, systemPrompt);
const outPath = path.join(WORK_DIR, `${section}_${Date.now()}.tex`);
fs.writeFileSync(outPath, sectionText, "utf-8");

let resultText = sectionText;
if (bibPaths.length > 0) {
resultText += `\n\n📚 参考文献文件已保存: ${bibPaths.join(", ")}`;
}

return {
content: [{
type: "text",
text: resultText,
file: path.basename(outPath),
...(bibPaths.length > 0 && { bibFiles: bibPaths })
}]
};
}

case "search_arxiv": {
const { query, maxResults = 5 } = args as { query: string; maxResults?: number };
const results = await searchArxivPapers(query, maxResults);

return {
content: [{
type: "text",
text: `找到 ${results.papers.length} 篇相关论文(总计 ${results.totalResults} 篇):\n\n${results.papers.map((paper, index) =>
`${index + 1}. **${paper.title}**\n ID: ${paper.id}\n 发布日期: ${paper.published}\n 作者: ${paper.authors.map((author: any) => author.name || author).join(', ')}\n 摘要: ${paper.summary.substring(0, 300)}...\n URL: ${paper.url}\n`
).join('\n')}`
}]
};
}

default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
return {
content: [{
type: "text",
text: `工具执行失败: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
});

4.一些工具函数和辅助函数


// 搜索文献并且格式化为 BibTeX 条目
async function searchAndFormatReferences(query: string, maxResults: number): Promise<string> {
const results = await searchArxivPapers(query, maxResults);
const bibEntries: string[] = [];
for (const paper of results.papers) {
const entry = `@article{${paper.id},
title = {${paper.title}},
author = {${paper.authors.map((a: any) => a.name).join(' and ')}},
journal = {arXiv preprint},
year = {${paper.published.slice(0, 4)}},
eprint = {${paper.id}},
archivePrefix = {arXiv},
primaryClass = {cs.LG},
url = {${paper.url}}
}`;
bibEntries.push(entry);
}
return bibEntries.join('\n\n');
}

// 调用 OpenRouter API
async function callOpenRouterAPI(prompt: string, systemPrompt?: string): Promise<string> {
try {
const messages: Array<{ role: string, content: string }> = [];
if (systemPrompt) {
messages.push({ role: "system", content: systemPrompt });
}
messages.push({ role: "user", content: prompt });

const response = await axios.post(OPENROUTER_API_URL, {
model: OPENROUTER_MODEL,
messages: messages,
stream: false,
max_tokens: 8192,
temperature: 0.7,
top_p: 0.7,
}, {
headers: {
"Authorization": `Bearer ${OPENROUTER_API_KEY}`,
"Content-Type": "application/json",
"HTTP-Referer": "http://localhost", // 根据 OpenRouter 要求添加
"X-Title": "Mdpi Writer MCP Server"
}
});

return response.data.choices[0].message.content;
} catch (error) {
console.error("调用 OpenRouter API 时出错:", error);
throw new Error(`AI 调用失败: ${error instanceof Error ? error.message : String(error)}`);
}
}

// 构建文献查询
function buildRefQuery(
section: string,
additionalInstructions?: string,
textContent?: string
): string {
const baseQuery = "lower limb joint moment estimation deep learning";
const sectionKeywords: Record<string, string> = {
abstract: "summary abstract",
introduction: "introduction background",
related_work: "related work literature review",
method: "method model architecture",
experiments: "experiment dataset evaluation",
results: "results analysis",
discussion: "discussion implications",
conclusion: "conclusion future work"
};
let query = baseQuery;
if (sectionKeywords[section]) query += " " + sectionKeywords[section];
if (additionalInstructions) query += " " + additionalInstructions;
if (textContent) {
// 取前 200 个字符作为补充
const snippet = textContent.slice(0, 200).replace(/[^a-zA-Z0-9\s]/g, " ");
query += " " + snippet;
}
return query.slice(0, 300); // 限制长度
}

// 构建引用bib格式
async function buildRefContext(query: string, maxResults: number): Promise<{
refPrompt: string;
bibContent: string;
}> {
const results = await searchArxivPapers(query, maxResults);
const bibEntries: string[] = [];
const summaries: string[] = [];

for (const paper of results.papers) {
bibEntries.push(`@article{${paper.id},
title = {${paper.title}},
author = {${paper.authors.map((a: any) => a.name).join(' and ')}},
journal = {arXiv preprint},
year = {${paper.published.slice(0,4)}},
eprint = {${paper.id}},
archivePrefix = {arXiv},
primaryClass = {cs.LG},
url = {${paper.url}}
}`);
summaries.push(`- \\cite{${paper.id}} ${paper.title}. ${paper.summary.substring(0, 200)}...`);
}

const refPrompt = `
**Relevant literature for citation (use \\cite{id} in the text):**
${summaries.join('\n')}
`;
const bibContent = bibEntries.join('\n\n');
return { refPrompt, bibContent };
}

// 支持的文献文件扩展名
const REFERENCE_EXTS = ['.txt', '.md', '.tex', '.bib', '.pdf'];

// 读取文献内容(这里截取了字符长度,可以按需修改)
async function readReferenceContents(dirPath: string): Promise<string> {
if (!fs.existsSync(dirPath)) {
throw new Error(`目录不存在: ${dirPath}`);
}
const files = fs.readdirSync(dirPath);
let combinedText = "";
for (const file of files) {
const ext = path.extname(file).toLowerCase();
if (!REFERENCE_EXTS.includes(ext)) continue;
const filePath = path.join(dirPath, file);
const content = fs.readFileSync(filePath, "utf-8");
// 截取前 5000 字符以免 token 过多
combinedText += `\n\n--- 文件: ${file} ---\n${content.slice(0, 5000)}`;
}
if (!combinedText) {
throw new Error("目录下没有可读取的文献文件(支持 .txt, .md, .tex, .bib, .pdf(文本内容))");
}
return combinedText;
}

// 分析写作风格
async function analyzeWritingStyle(referenceText: string): Promise<string> {
const systemPrompt = "You are an expert in academic writing analysis.";
const userPrompt = `Analyze the following academic writing samples and summarize the key characteristics of the writing style. Include aspects such as:
- Sentence structure (e.g., long and complex vs. short and direct)
- Use of technical terminology and jargon
- Tone (e.g., formal, objective, cautious)
- Paragraph organization and flow
- Typical expressions or phrase patterns

Provide a concise paragraph describing the style, which can be used as guidance for generating new text in the same style.

Samples:
${referenceText}`;

return await callOpenRouterAPI(userPrompt, systemPrompt);
}

// 工具函数:搜索 arXiv 论文
async function searchArxivPapers(query: string, maxResults: number = 5): Promise<{ totalResults: number, papers: any[] }> {
try {
const results = await arxivClient.search({
start: 0,
searchQuery: {
include: [
{ field: "all", value: query }
]
},
maxResults: maxResults
});

const papers = results.entries.map(entry => {
const urlParts = entry.url.split('/');
const arxivId = urlParts[urlParts.length - 1];

return {
id: arxivId,
url: entry.url,
title: entry.title.replace(/\s+/g, ' ').trim(),
summary: entry.summary.replace(/\s+/g, ' ').trim(),
published: entry.published,
authors: entry.authors || []
};
});

return {
totalResults: results.totalResults,
papers: papers
};
} catch (error) {
console.error("搜索 arXiv 论文时出错:", error);
throw new Error(`搜索失败: ${error instanceof Error ? error.message : String(error)}`);
}
}

导入MCP服务

详细可见我之前的一篇文章,详细介绍了在Vscode中如何导入MCP服务。

链接:在Vscode中导入MCP服务

效果样例

  • 生成摘要

生成摘要

  • 文献引用

文献引用

  • bib文献列表生成

bib文献列表生成

结语

这一套流程下来还是挺花费token的,并且基本上只能生成一个大概的introduction,可能不如直接使用cli或者网页版交互更快。不过大部分文献还是可以用的,文献和引用的对应大多还是比较精准的。如果对项目感兴趣可以访问项目链接

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐