import express from 'express';
import cors from 'cors';
import { v4 as uuidv4 } from 'uuid';
import fetch from 'node-fetch';
import fs from 'fs';
import path from 'path';
import {
Document,
Packer,
Paragraph,
TextRun,
HeadingLevel,
AlignmentType,
Table,
TableRow,
TableCell,
WidthType,
BorderStyle,
LevelFormat,
NumberFormat,
convertInchesToTwip,
convertMillimetersToTwip,
SectionType,
PageOrientation,
PageNumber,
Footer,
Header,
UnderlineType
} from 'docx';
import mammoth from 'mammoth';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { createServer } from 'http';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import crypto from 'crypto';
const app = express();
const PORT = process.env.PORT || 3500;
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// 生成API Key
function generateApiKey() {
return 'mcp_' + crypto.randomBytes(32).toString('hex');
}
// 存储API Key
let currentApiKey = generateApiKey();
let apiKeyExpiry = Date.now() + (24 * 60 * 60 * 1000); // 24小时后过期
console.log('🔑 API Key 已生成:', currentApiKey);
console.log('⏰ 过期时间:', new Date(apiKeyExpiry).toLocaleString());
// 启用CORS
app.use(cors({
origin: true,
credentials: true
}));
app.use(express.json());
// 静态文件服务
app.use(express.static(join(__dirname, '..', 'public')));
// 路由配置
app.get('/', (req, res) => {
res.sendFile(join(__dirname, '..', 'public', 'chat.html'));
});
app.get('/start', (req, res) => {
res.sendFile(join(__dirname, '..', 'public', 'start.html'));
});
app.get('/mcp', (req, res) => {
res.sendFile(join(__dirname, '..', 'public', 'index.html'));
});
// 存储SSE连接
const connections = new Map();
// 加载天气配置
function loadWeatherConfig() {
try {
const configPath = path.join(process.cwd(), 'config', 'weather.json');
const configData = fs.readFileSync(configPath, 'utf8');
return JSON.parse(configData);
} catch (error) {
console.warn('无法加载天气配置文件,使用默认配置:', error.message);
return {
openweathermap: { enabled: false },
mockData: { enabled: true, cities: {} }
};
}
}
// 创建MCP服务器实例
const server = new Server(
{
name: "mcp-sse-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// 注册工具
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "create_docx",
description: "创建新的Word文档",
inputSchema: {
type: "object",
properties: {
fileName: {
type: "string",
description: "文件名(不包含扩展名)",
},
title: {
type: "string",
description: "文档标题",
},
content: {
type: "string",
description: "文档内容(支持换行符\\n)",
}
},
required: ["fileName", "title", "content"],
},
},
{
name: "read_docx",
description: "读取Word文档内容",
inputSchema: {
type: "object",
properties: {
filePath: {
type: "string",
description: "Word文档文件路径(相对于documents目录)",
},
format: {
type: "string",
description: "输出格式",
enum: ["text", "html", "markdown"],
default: "text"
}
},
required: ["filePath"],
},
},
{
name: "replace_text_in_docx",
description: "替换Word文档中的文本",
inputSchema: {
type: "object",
properties: {
filePath: {
type: "string",
description: "Word文档文件路径",
},
searchText: {
type: "string",
description: "要搜索替换的文本",
},
replaceText: {
type: "string",
description: "替换后的文本",
},
outputFileName: {
type: "string",
description: "输出文件名(不包含扩展名)",
}
},
required: ["filePath", "searchText", "replaceText", "outputFileName"],
},
},
{
name: "create_formatted_docx",
description: "创建带格式的Word文档",
inputSchema: {
type: "object",
properties: {
fileName: {
type: "string",
description: "文件名(不包含扩展名)",
},
title: {
type: "string",
description: "文档标题",
},
content: {
type: "array",
description: "格式化内容数组",
items: {
type: "object",
properties: {
text: { type: "string", description: "文本内容" },
fontFamily: { type: "string", description: "字体" },
fontSize: { type: "number", description: "字号" },
bold: { type: "boolean", description: "是否粗体" },
italic: { type: "boolean", description: "是否斜体" },
underline: { type: "boolean", description: "是否下划线" },
color: { type: "string", description: "文字颜色" },
alignment: { type: "string", description: "对齐方式" },
heading: { type: "number", description: "标题级别" },
},
required: ["text"],
},
}
},
required: ["fileName", "title", "content"],
},
},
{
name: "create_table_in_docx",
description: "创建包含表格的Word文档",
inputSchema: {
type: "object",
properties: {
fileName: {
type: "string",
description: "文件名(不包含扩展名)",
},
title: {
type: "string",
description: "文档标题",
},
tableData: {
type: "object",
description: "表格数据",
properties: {
headers: {
type: "array",
items: { type: "string" },
description: "表格标题行",
},
rows: {
type: "array",
items: {
type: "array",
items: { type: "string" },
},
description: "表格数据行",
},
style: {
type: "object",
description: "表格样式设置",
},
},
required: ["headers", "rows"],
}
},
required: ["fileName", "title", "tableData"],
},
}
],
};
});
// 处理工具调用
server.setRequestHandler(CallToolRequestSchema, async (request) => {
switch (request.params.name) {
case "create_docx":
try {
const { createDocument } = await import('./docxHandler.js');
const result = await createDocument(
request.params.arguments.fileName,
request.params.arguments.title,
request.params.arguments.content
);
return {
content: [
{
type: "text",
text: result,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `创建文档失败: ${error.message}`,
},
],
};
}
case "read_docx":
try {
const { readDocument } = await import('./docxHandler.js');
const result = await readDocument(
request.params.arguments.filePath,
request.params.arguments.format || 'text'
);
return {
content: [
{
type: "text",
text: result,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `读取文档失败: ${error.message}`,
},
],
};
}
case "replace_text_in_docx":
try {
const { replaceTextInDocument } = await import('./docxHandler.js');
const result = await replaceTextInDocument(
request.params.arguments.filePath,
request.params.arguments.searchText,
request.params.arguments.replaceText,
request.params.arguments.outputFileName
);
return {
content: [
{
type: "text",
text: result,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `替换文本失败: ${error.message}`,
},
],
};
}
case "create_formatted_docx":
try {
const { createFormattedDocument } = await import('./docxHandler.js');
const result = await createFormattedDocument(
request.params.arguments.fileName,
request.params.arguments.title,
request.params.arguments.content
);
return {
content: [
{
type: "text",
text: result,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `创建格式化文档失败: ${error.message}`,
},
],
};
}
case "create_table_in_docx":
try {
const { createTableDocument } = await import('./docxHandler.js');
const result = await createTableDocument(
request.params.arguments.fileName,
request.params.arguments.title,
request.params.arguments.tableData
);
return {
content: [
{
type: "text",
text: result,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `创建表格文档失败: ${error.message}`,
},
],
};
}
default:
throw new Error(`未知工具: ${request.params.name}`);
}
});
// SSE端点
app.get('/sse', (req, res) => {
// 设置SSE响应头
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Cache-Control',
});
const connectionId = uuidv4();
connections.set(connectionId, res);
console.log(`新的SSE连接: ${connectionId}`);
// 发送连接确认
res.write(`data: ${JSON.stringify({
type: 'connection',
id: connectionId,
message: 'SSE连接已建立'
})}\n\n`);
// 处理连接关闭
req.on('close', () => {
connections.delete(connectionId);
console.log(`SSE连接关闭: ${connectionId}`);
});
req.on('error', (err) => {
console.error('SSE连接错误:', err);
connections.delete(connectionId);
});
});
// MCP消息处理端点
app.post('/mcp', async (req, res) => {
try {
const mcpRequest = req.body;
console.log('收到MCP请求:', JSON.stringify(mcpRequest, null, 2));
let response;
if (mcpRequest.method === 'tools/list') {
// 直接返回工具列表
response = {
jsonrpc: '2.0',
id: mcpRequest.id,
result: {
tools: [
{
name: "create_docx",
description: "创建新的Word文档",
inputSchema: {
type: "object",
properties: {
fileName: {
type: "string",
description: "文件名(不包含扩展名)",
},
title: {
type: "string",
description: "文档标题",
},
content: {
type: "string",
description: "文档内容(支持换行符\\n)",
}
},
required: ["fileName", "title", "content"],
},
},
{
name: "read_docx",
description: "读取Word文档内容",
inputSchema: {
type: "object",
properties: {
filePath: {
type: "string",
description: "Word文档文件路径(相对于documents目录)",
},
format: {
type: "string",
description: "输出格式",
enum: ["text", "html", "markdown"],
default: "text"
}
},
required: ["filePath"],
},
},
{
name: "replace_text_in_docx",
description: "替换Word文档中的文本",
inputSchema: {
type: "object",
properties: {
filePath: {
type: "string",
description: "Word文档文件路径",
},
searchText: {
type: "string",
description: "要搜索替换的文本",
},
replaceText: {
type: "string",
description: "替换后的文本",
},
outputFileName: {
type: "string",
description: "输出文件名(不包含扩展名)",
}
},
required: ["filePath", "searchText", "replaceText", "outputFileName"],
},
},
{
name: "create_formatted_docx",
description: "创建带格式的Word文档",
inputSchema: {
type: "object",
properties: {
fileName: {
type: "string",
description: "文件名(不包含扩展名)",
},
title: {
type: "string",
description: "文档标题",
},
content: {
type: "array",
description: "格式化内容数组",
items: {
type: "object",
properties: {
text: { type: "string", description: "文本内容" },
fontFamily: { type: "string", description: "字体" },
fontSize: { type: "number", description: "字号" },
bold: { type: "boolean", description: "是否粗体" },
italic: { type: "boolean", description: "是否斜体" },
underline: { type: "boolean", description: "是否下划线" },
color: { type: "string", description: "文字颜色" },
alignment: { type: "string", description: "对齐方式" },
heading: { type: "number", description: "标题级别" },
},
required: ["text"],
},
}
},
required: ["fileName", "title", "content"],
},
},
{
name: "create_table_in_docx",
description: "创建包含表格的Word文档",
inputSchema: {
type: "object",
properties: {
fileName: {
type: "string",
description: "文件名(不包含扩展名)",
},
title: {
type: "string",
description: "文档标题",
},
tableData: {
type: "object",
description: "表格数据",
properties: {
headers: {
type: "array",
items: { type: "string" },
description: "表格标题行",
},
rows: {
type: "array",
items: {
type: "array",
items: { type: "string" },
},
description: "表格数据行",
},
style: {
type: "object",
description: "表格样式设置",
},
},
required: ["headers", "rows"],
}
},
required: ["fileName", "title", "tableData"],
},
}
]
}
};
} else if (mcpRequest.method === 'tools/call') {
// 处理工具调用
const toolName = mcpRequest.params.name;
const args = mcpRequest.params.arguments;
let content;
switch (toolName) {
case "echo":
content = [{
type: "text",
text: `回显: ${args.text}`,
}];
break;
case "get_current_time":
content = [{
type: "text",
text: `当前时间: ${new Date().toLocaleString('zh-CN')}`,
}];
break;
case "calculate":
try {
const expression = args.expression;
const result = eval(expression.replace(/[^0-9+\-*/().\s]/g, ''));
content = [{
type: "text",
text: `计算结果: ${expression} = ${result}`,
}];
} catch (error) {
content = [{
type: "text",
text: `计算错误: ${error.message}`,
}];
}
break;
case "get_weather":
try {
const city = args.city;
const units = args.units || 'metric';
const weatherConfig = loadWeatherConfig();
let weatherData;
let isRealData = false;
// 尝试使用真实的 API
if (weatherConfig.openweathermap.enabled && weatherConfig.openweathermap.apiKey !== 'your_api_key_here') {
try {
const API_KEY = weatherConfig.openweathermap.apiKey;
const url = `${weatherConfig.openweathermap.baseUrl}/weather?q=${encodeURIComponent(city)}&appid=${API_KEY}&units=${units}&lang=zh_cn`;
const response = await fetch(url);
if (response.ok) {
weatherData = await response.json();
isRealData = true;
} else {
throw new Error(`API请求失败: ${response.status}`);
}
} catch (apiError) {
console.warn('真实API调用失败,使用模拟数据:', apiError.message);
}
}
// 如果真实API失败或未配置,使用模拟数据
if (!weatherData) {
const mockCity = weatherConfig.mockData.cities[city];
if (mockCity) {
// 根据单位转换温度
let temp = mockCity.temp;
let feels_like = mockCity.feels_like;
let wind_speed = mockCity.wind_speed;
if (units === 'imperial') {
temp = Math.round(temp * 9/5 + 32);
feels_like = Math.round(feels_like * 9/5 + 32);
wind_speed = Math.round(wind_speed * 2.237 * 100) / 100;
} else if (units === 'kelvin') {
temp = Math.round(temp + 273.15);
feels_like = Math.round(feels_like + 273.15);
}
weatherData = {
name: city,
main: {
temp: temp,
feels_like: feels_like,
humidity: mockCity.humidity,
pressure: mockCity.pressure
},
weather: [
{
main: "Mock",
description: mockCity.description,
icon: "01d"
}
],
wind: {
speed: wind_speed
},
visibility: mockCity.visibility
};
} else {
// 默认数据
weatherData = {
name: city,
main: {
temp: units === 'metric' ? 20 : units === 'imperial' ? 68 : 293,
feels_like: units === 'metric' ? 22 : units === 'imperial' ? 72 : 295,
humidity: 60,
pressure: 1013
},
weather: [
{
main: "Unknown",
description: "未知天气",
icon: "01d"
}
],
wind: {
speed: units === 'metric' ? 3.0 : 6.7
},
visibility: 10000
};
}
}
const tempUnit = units === 'metric' ? '°C' : units === 'imperial' ? '°F' : 'K';
const speedUnit = units === 'metric' ? 'm/s' : 'mph';
const dataSource = isRealData ? '🌐 实时数据' : '🎭 模拟数据';
const weatherReport = `🌤️ ${weatherData.name} 天气信息 (${dataSource}):
📊 温度: ${weatherData.main.temp}${tempUnit} (体感 ${weatherData.main.feels_like}${tempUnit})
🌤️ 天气: ${weatherData.weather[0].description}
💧 湿度: ${weatherData.main.humidity}%
🌬️ 风速: ${weatherData.wind.speed} ${speedUnit}
🔍 能见度: ${weatherData.visibility / 1000} km
📈 气压: ${weatherData.main.pressure} hPa
${isRealData ? '✅ 数据来源: OpenWeatherMap API' : '💡 提示: 这是模拟数据,要获取实时数据请在 config/weather.json 中配置 OpenWeatherMap API key'}`;
content = [{
type: "text",
text: weatherReport,
}];
} catch (error) {
content = [{
type: "text",
text: `天气查询错误: ${error.message}`,
}];
}
break;
case "read_docx":
try {
const filePath = args.filePath;
const format = args.format || 'text';
const fullPath = path.join(process.cwd(), 'documents', filePath);
if (!fs.existsSync(fullPath)) {
throw new Error(`文件不存在: ${filePath}`);
}
const result = await mammoth.convertToHtml(fs.readFileSync(fullPath));
let output;
if (format === 'html') {
output = result.value;
} else if (format === 'markdown') {
// 简单的HTML到Markdown转换
output = result.value
.replace(/<h([1-6])>(.*?)<\/h[1-6]>/g, (match, level, text) => '#'.repeat(parseInt(level)) + ' ' + text + '\n')
.replace(/<p>(.*?)<\/p>/g, '$1\n\n')
.replace(/<strong>(.*?)<\/strong>/g, '**$1**')
.replace(/<em>(.*?)<\/em>/g, '*$1*')
.replace(/<br\s*\/?>/g, '\n')
.replace(/<[^>]*>/g, '');
} else {
output = result.value.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim();
}
content = [{
type: "text",
text: `📄 文档读取成功 (${format}格式):\n\n${output}`,
}];
} catch (error) {
content = [{
type: "text",
text: `❌ 文档读取失败: ${error.message}`,
}];
}
break;
case "create_docx":
try {
const fileName = args.fileName;
const title = args.title;
const docContent = args.content;
// 创建文档段落
const paragraphs = [
new Paragraph({
children: [
new TextRun({
text: title,
bold: true,
size: 32,
}),
],
heading: HeadingLevel.TITLE,
alignment: AlignmentType.CENTER,
}),
new Paragraph({
children: [new TextRun("")],
}),
];
// 处理内容(支持换行)
const contentLines = docContent.split('\n');
contentLines.forEach(line => {
paragraphs.push(new Paragraph({
children: [new TextRun({
text: line,
size: 24,
})],
}));
});
const doc = new Document({
sections: [{
properties: {},
children: paragraphs,
}],
});
// 确保输出目录存在
const outputDir = path.join(process.cwd(), 'documents', 'output');
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// 保存文档
const outputPath = path.join(process.cwd(), 'documents', 'output', `${fileName}.docx`);
const buffer = await Packer.toBuffer(doc);
fs.writeFileSync(outputPath, buffer);
content = [{
type: "text",
text: `✅ Word文档创建成功!\n📁 文件路径: documents/output/${fileName}.docx\n📝 标题: ${title}\n📄 内容长度: ${docContent.length} 字符`,
}];
} catch (error) {
content = [{
type: "text",
text: `❌ 文档创建失败: ${error.message}`,
}];
}
break;
case "replace_text_in_docx":
try {
const filePath = args.filePath;
const searchText = args.searchText;
const replaceText = args.replaceText;
const outputFileName = args.outputFileName;
const fullPath = path.join(process.cwd(), 'documents', filePath);
if (!fs.existsSync(fullPath)) {
throw new Error(`文件不存在: ${filePath}`);
}
// 读取原文档内容
const result = await mammoth.convertToHtml(fs.readFileSync(fullPath));
let htmlContent = result.value;
// 执行文本替换
const originalCount = (htmlContent.match(new RegExp(searchText, 'g')) || []).length;
htmlContent = htmlContent.replace(new RegExp(searchText, 'g'), replaceText);
// 简单转换回文档格式(这里简化处理)
const textContent = htmlContent.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim();
const lines = textContent.split(/[.!?]+/).filter(line => line.trim());
const paragraphs = [
new Paragraph({
children: [
new TextRun({
text: "已处理的文档",
bold: true,
size: 28,
}),
],
heading: HeadingLevel.HEADING_1,
}),
];
lines.forEach(line => {
if (line.trim()) {
paragraphs.push(new Paragraph({
children: [new TextRun({
text: line.trim() + '.',
size: 24,
})],
}));
}
});
const doc = new Document({
sections: [{
properties: {},
children: paragraphs,
}],
});
// 确保输出目录存在
const outputDir = path.join(process.cwd(), 'documents', 'output');
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// 保存文档
const outputPath = path.join(process.cwd(), 'documents', 'output', `${outputFileName}.docx`);
const buffer = await Packer.toBuffer(doc);
fs.writeFileSync(outputPath, buffer);
content = [{
type: "text",
text: `✅ 文本替换完成!\n🔍 查找: "${searchText}"\n✏️ 替换为: "${replaceText}"\n📊 替换次数: ${originalCount}\n📁 输出文件: documents/output/${outputFileName}.docx`,
}];
} catch (error) {
content = [{
type: "text",
text: `❌ 文本替换失败: ${error.message}`,
}];
}
break;
case "create_formatted_docx":
try {
const fileName = args.fileName;
const title = args.title;
const contentArray = args.content;
// 创建文档段落
const paragraphs = [
new Paragraph({
children: [
new TextRun({
text: title,
bold: true,
size: 32,
}),
],
heading: HeadingLevel.TITLE,
alignment: AlignmentType.CENTER,
}),
new Paragraph({
children: [new TextRun("")],
}),
];
// 处理格式化内容
contentArray.forEach(item => {
const textRun = new TextRun({
text: item.text,
fontFamily: item.fontFamily,
size: item.fontSize ? item.fontSize * 2 : 24,
bold: item.bold || false,
italics: item.italic || false,
underline: item.underline ? { color: item.color || '000000', type: UnderlineType.SINGLE } : undefined,
color: item.color || '000000',
});
const paragraph = new Paragraph({
children: [textRun],
alignment: item.alignment === 'center' ? AlignmentType.CENTER :
item.alignment === 'right' ? AlignmentType.RIGHT :
item.alignment === 'justify' ? AlignmentType.JUSTIFIED :
AlignmentType.LEFT,
heading: item.heading ?
(item.heading === 1 ? HeadingLevel.HEADING_1 :
item.heading === 2 ? HeadingLevel.HEADING_2 :
item.heading === 3 ? HeadingLevel.HEADING_3 :
item.heading === 4 ? HeadingLevel.HEADING_4 :
item.heading === 5 ? HeadingLevel.HEADING_5 :
item.heading === 6 ? HeadingLevel.HEADING_6 : undefined) : undefined,
});
paragraphs.push(paragraph);
});
const doc = new Document({
sections: [{
properties: {},
children: paragraphs,
}],
});
// 确保输出目录存在
const outputDir = path.join(process.cwd(), 'documents', 'output');
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// 保存文档
const outputPath = path.join(process.cwd(), 'documents', 'output', `${fileName}.docx`);
const buffer = await Packer.toBuffer(doc);
fs.writeFileSync(outputPath, buffer);
content = [{
type: "text",
text: `✅ 格式化Word文档创建成功!\n📁 文件路径: documents/output/${fileName}.docx\n📝 标题: ${title}\n📄 内容段落数: ${contentArray.length}`,
}];
} catch (error) {
content = [{
type: "text",
text: `❌ 格式化文档创建失败: ${error.message}`,
}];
}
break;
case "create_table_in_docx":
try {
const fileName = args.fileName;
const title = args.title;
const tableData = args.tableData;
// 创建标题段落
const paragraphs = [
new Paragraph({
children: [
new TextRun({
text: title,
bold: true,
size: 32,
}),
],
heading: HeadingLevel.TITLE,
alignment: AlignmentType.CENTER,
}),
new Paragraph({
children: [new TextRun("")],
}),
];
// 创建表格行
const tableRows = [];
// 添加标题行
const headerRow = new TableRow({
children: tableData.headers.map(header =>
new TableCell({
children: [
new Paragraph({
children: [
new TextRun({
text: header,
bold: true,
size: 24,
}),
],
alignment: AlignmentType.CENTER,
}),
],
shading: {
fill: "e5f3ff",
},
})
),
});
tableRows.push(headerRow);
// 添加数据行
tableData.rows.forEach(row => {
const tableRow = new TableRow({
children: row.map(cell =>
new TableCell({
children: [
new Paragraph({
children: [
new TextRun({
text: cell,
size: 22,
}),
],
}),
],
})
),
});
tableRows.push(tableRow);
});
// 创建表格
const table = new Table({
rows: tableRows,
width: {
size: 100,
type: WidthType.PERCENTAGE,
},
borders: {
top: { style: BorderStyle.SINGLE, size: 1, color: "000000" },
bottom: { style: BorderStyle.SINGLE, size: 1, color: "000000" },
left: { style: BorderStyle.SINGLE, size: 1, color: "000000" },
right: { style: BorderStyle.SINGLE, size: 1, color: "000000" },
insideHorizontal: { style: BorderStyle.SINGLE, size: 1, color: "000000" },
insideVertical: { style: BorderStyle.SINGLE, size: 1, color: "000000" },
},
});
paragraphs.push(new Paragraph({ children: [new TextRun("")] }));
const doc = new Document({
sections: [{
properties: {},
children: [...paragraphs, table],
}],
});
// 确保输出目录存在
const outputDir = path.join(process.cwd(), 'documents', 'output');
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// 保存文档
const outputPath = path.join(process.cwd(), 'documents', 'output', `${fileName}.docx`);
const buffer = await Packer.toBuffer(doc);
fs.writeFileSync(outputPath, buffer);
content = [{
type: "text",
text: `✅ 表格Word文档创建成功!\n📁 文件路径: documents/output/${fileName}.docx\n📝 标题: ${title}\n📊 表格大小: ${tableData.headers.length} 列 × ${tableData.rows.length + 1} 行`,
}];
} catch (error) {
content = [{
type: "text",
text: `❌ 表格文档创建失败: ${error.message}`,
}];
}
break;
default:
throw new Error(`未知工具: ${toolName}`);
}
response = {
jsonrpc: '2.0',
id: mcpRequest.id,
result: {
content: content
}
};
} else {
throw new Error(`不支持的方法: ${mcpRequest.method}`);
}
// 通过SSE广播响应
broadcastToConnections({
type: 'mcp-response',
data: response
});
res.json(response);
} catch (error) {
console.error('MCP请求处理错误:', error);
const errorResponse = {
jsonrpc: '2.0',
id: req.body.id || null,
error: {
code: -32603,
message: error.message
}
};
res.status(500).json(errorResponse);
}
});
// 广播消息到所有SSE连接
function broadcastToConnections(message) {
const messageStr = `data: ${JSON.stringify(message)}\n\n`;
for (const [connectionId, connection] of connections) {
try {
connection.write(messageStr);
} catch (error) {
console.error(`向连接 ${connectionId} 发送消息失败:`, error);
connections.delete(connectionId);
}
}
}
// 健康检查端点
app.get('/health', (req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
connections: connections.size
});
});
// API Key管理路由
app.get('/api/key', (req, res) => {
// 检查API Key是否过期
if (Date.now() > apiKeyExpiry) {
currentApiKey = generateApiKey();
apiKeyExpiry = Date.now() + (24 * 60 * 60 * 1000);
console.log('🔄 API Key 已更新:', currentApiKey);
}
res.json({
apiKey: currentApiKey,
expiry: new Date(apiKeyExpiry).toISOString(),
timeRemaining: Math.max(0, apiKeyExpiry - Date.now())
});
});
// 重新生成API Key
app.post('/api/key/regenerate', (req, res) => {
currentApiKey = generateApiKey();
apiKeyExpiry = Date.now() + (24 * 60 * 60 * 1000);
console.log('🔄 API Key 已重新生成:', currentApiKey);
res.json({
apiKey: currentApiKey,
expiry: new Date(apiKeyExpiry).toISOString(),
message: 'API Key已重新生成'
});
});
// AI对话路由(简化版,仅用于内部测试)
app.post('/api/chat', async (req, res) => {
try {
const { message, history = [] } = req.body;
if (!message) {
return res.status(400).json({ error: '消息不能为空' });
}
// 简单的消息处理,主要用于测试MCP工具
const response = await processSimpleMessage(message, history);
res.json({
response: response.text,
toolCalls: response.toolCalls || [],
timestamp: new Date().toISOString()
});
} catch (error) {
console.error('消息处理错误:', error);
res.status(500).json({ error: error.message || '处理消息时发生错误' });
}
});
// 简单的消息处理(用于内部测试)
async function processSimpleMessage(message, history) {
const lowerMessage = message.toLowerCase();
let response = { text: '', toolCalls: [] };
// 创建Word文档
if (lowerMessage.includes('创建文档') || lowerMessage.includes('生成文档') || lowerMessage.includes('写文档')) {
const titleMatch = message.match(/(?:标题|题目|名称)[::]?\s*(.+?)(?:\s|$|,|。)/);
const contentMatch = message.match(/(?:内容|正文)[::]?\s*(.+)/);
const title = titleMatch ? titleMatch[1] : '文档标题';
const content = contentMatch ? contentMatch[1] : message;
const fileName = `文档_${Date.now()}`;
try {
const toolResult = await callInternalTool('create_docx', {
fileName,
title,
content
});
response.toolCalls.push({
tool: 'create_docx',
arguments: { fileName, title, content },
result: toolResult
});
response.text = `已为您创建文档:${toolResult}`;
} catch (error) {
response.text = '创建文档时出现错误。';
}
}
// 读取Word文档
else if (lowerMessage.includes('读取文档') || lowerMessage.includes('打开文档') || lowerMessage.includes('查看文档')) {
const fileMatch = message.match(/(?:文件|文档)[::]?\s*(.+?\.docx)/i);
if (fileMatch) {
const filePath = fileMatch[1];
try {
const toolResult = await callInternalTool('read_docx', {
filePath,
format: 'text'
});
response.toolCalls.push({
tool: 'read_docx',
arguments: { filePath, format: 'text' },
result: toolResult
});
response.text = `文档读取结果:${toolResult}`;
} catch (error) {
response.text = `读取文档"${filePath}"时出现错误。`;
}
} else {
response.text = '请指定要读取的文档文件名,例如:"读取文档 test.docx"';
}
}
// 替换文档文本
else if (lowerMessage.includes('替换文本') || lowerMessage.includes('修改文档')) {
const fileMatch = message.match(/(?:文件|文档)[::]?\s*(.+?\.docx)/i);
const searchMatch = message.match(/(?:查找|替换|原文)[::]?\s*"([^"]+)"/);
const replaceMatch = message.match(/(?:替换为|新文)[::]?\s*"([^"]+)"/);
if (fileMatch && searchMatch && replaceMatch) {
const filePath = fileMatch[1];
const searchText = searchMatch[1];
const replaceText = replaceMatch[1];
const outputFileName = `修改后_${Date.now()}`;
try {
const toolResult = await callInternalTool('replace_text_in_docx', {
filePath,
searchText,
replaceText,
outputFileName
});
response.toolCalls.push({
tool: 'replace_text_in_docx',
arguments: { filePath, searchText, replaceText, outputFileName },
result: toolResult
});
response.text = `文本替换结果:${toolResult}`;
} catch (error) {
response.text = '替换文本时出现错误。';
}
} else {
response.text = '请按格式指定:替换文档 test.docx 查找"旧文本" 替换为"新文本"';
}
}
// 创建格式化文档
else if (lowerMessage.includes('格式化文档') || lowerMessage.includes('带格式文档')) {
const title = '格式化文档示例';
const fileName = `格式化文档_${Date.now()}`;
const content = [
{ text: '一级标题', heading: 1, bold: true },
{ text: '这是普通段落文本。', fontSize: 12 },
{ text: '二级标题', heading: 2, bold: true },
{ text: '这是加粗文本。', bold: true },
{ text: '这是斜体文本。', italic: true },
{ text: '这是下划线文本。', underline: true }
];
try {
const toolResult = await callInternalTool('create_formatted_docx', {
fileName,
title,
content
});
response.toolCalls.push({
tool: 'create_formatted_docx',
arguments: { fileName, title, content },
result: toolResult
});
response.text = `已为您创建格式化文档:${toolResult}`;
} catch (error) {
response.text = '创建格式化文档时出现错误。';
}
}
// 创建表格文档
else if (lowerMessage.includes('创建表格') || lowerMessage.includes('生成表格') || lowerMessage.includes('表格文档')) {
const title = '表格文档示例';
const fileName = `表格文档_${Date.now()}`;
const tableData = {
headers: ['姓名', '年龄', '职位'],
rows: [
['张三', '25', '工程师'],
['李四', '30', '设计师'],
['王五', '28', '产品经理']
]
};
try {
const toolResult = await callInternalTool('create_table_in_docx', {
fileName,
title,
tableData
});
response.toolCalls.push({
tool: 'create_table_in_docx',
arguments: { fileName, title, tableData },
result: toolResult
});
response.text = `已为您创建表格文档:${toolResult}`;
} catch (error) {
response.text = '创建表格文档时出现错误。';
}
}
// 默认回复
else {
response.text = `您好!这是专业的Word文档处理MCP工具服务器。我可以帮您:
📄 创建文档 - 说"创建文档,标题:会议记录,内容:..."
👀 读取文档 - 说"读取文档 example.docx"
✏️ 替换文本 - 说"替换文档 test.docx 查找"旧文本" 替换为"新文本""
🎨 格式化文档 - 说"创建格式化文档"
📊 表格文档 - 说"创建表格文档"
专为Cursor等IDE提供Word文档处理的MCP工具调用服务。`;
}
return response;
}
// 启动服务器
app.listen(PORT, () => {
console.log(`MCP SSE服务器运行在端口 ${PORT}`);
console.log(`🔑 API Key: ${currentApiKey}`);
console.log(`🌐 启动页面: http://localhost:${PORT}/start`);
console.log(`💬 对话页面: http://localhost:${PORT}`);
console.log(`📡 SSE端点: http://localhost:${PORT}/sse`);
console.log(`🔧 MCP端点: http://localhost:${PORT}/mcp`);
// 自动打开浏览器到启动页面
const open = import('open');
open.then(({ default: openBrowser }) => {
openBrowser(`http://localhost:${PORT}/start`);
}).catch(() => {
console.log('无法自动打开浏览器,请手动访问 http://localhost:' + PORT + '/start');
});
});
// 优雅关闭
process.on('SIGTERM', () => {
console.log('收到SIGTERM信号,正在关闭服务器...');
process.exit(0);
});
process.on('SIGINT', () => {
console.log('收到SIGINT信号,正在关闭服务器...');
process.exit(0);
});
// 处理工具调用的统一函数
async function handleToolCall(request) {
switch (request.params.name) {
case "create_docx":
try {
const { createDocument } = await import('./docxHandler.js');
const result = await createDocument(
request.params.arguments.fileName,
request.params.arguments.title,
request.params.arguments.content
);
return {
content: [
{
type: "text",
text: result,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `创建文档失败: ${error.message}`,
},
],
};
}
case "read_docx":
try {
const { readDocument } = await import('./docxHandler.js');
const result = await readDocument(
request.params.arguments.filePath,
request.params.arguments.format || 'text'
);
return {
content: [
{
type: "text",
text: result,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `读取文档失败: ${error.message}`,
},
],
};
}
case "replace_text_in_docx":
try {
const { replaceTextInDocument } = await import('./docxHandler.js');
const result = await replaceTextInDocument(
request.params.arguments.filePath,
request.params.arguments.searchText,
request.params.arguments.replaceText,
request.params.arguments.outputFileName
);
return {
content: [
{
type: "text",
text: result,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `替换文本失败: ${error.message}`,
},
],
};
}
case "create_formatted_docx":
try {
const { createFormattedDocument } = await import('./docxHandler.js');
const result = await createFormattedDocument(
request.params.arguments.fileName,
request.params.arguments.title,
request.params.arguments.content
);
return {
content: [
{
type: "text",
text: result,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `创建格式化文档失败: ${error.message}`,
},
],
};
}
case "create_table_in_docx":
try {
const { createTableDocument } = await import('./docxHandler.js');
const result = await createTableDocument(
request.params.arguments.fileName,
request.params.arguments.title,
request.params.arguments.tableData
);
return {
content: [
{
type: "text",
text: result,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `创建表格文档失败: ${error.message}`,
},
],
};
}
default:
throw new Error(`未知工具: ${request.params.name}`);
}
}
// 内部工具调用函数
async function callInternalTool(toolName, args) {
const request = {
params: {
name: toolName,
arguments: args
}
};
const result = await handleToolCall(request);
return result.content[0].text;
}