Skip to main content
Glama
shared-server.ts16 kB
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; import { writeFile, mkdir } from 'fs/promises'; import { dirname } from 'path'; import { GoogleGenAI } from '@google/genai'; // Resolve OpenRouter API key from env; support both common casings. function getOpenRouterKey(): string { return ( process.env.OPENROUTER_KEY || process.env.openrouter_key || process.env.OPENROUTER_API_KEY || process.env.openrouter_api_key || '' ); } // Resolve Gemini API key from env function getGeminiKey(): string { return ( process.env.GEMINI_API_KEY || process.env.gemini_api_key || process.env.GEMINI_KEY || process.env.gemini_key || '' ); } // Determine which LLM backend to use: 'gemini' or 'openrouter' function getLLMBackend(): 'gemini' | 'openrouter' { const backend = ( process.env.LLM_BACKEND || process.env.llm_backend || 'openrouter' ).toLowerCase(); if (backend === 'gemini') { return 'gemini'; } return 'openrouter'; } // Get custom writing rules from environment variable function getCustomWritingRules(): string { return ( process.env.BETTER_WRITER_CUSTOM_RULES || process.env.better_writer_custom_rules || process.env.CUSTOM_WRITING_RULES || process.env.custom_writing_rules || '' ); } // Check if Better Writer should be the default writing tool function isDefaultTool(): boolean { const value = ( process.env.BW_DEFAULT_TOOL || process.env.bw_default_tool || process.env.BETTER_WRITER_DEFAULT || process.env.better_writer_default || 'false' ).toLowerCase(); return value === 'true' || value === '1' || value === 'yes'; } // Build the system prompt (Chinese, aligned to the provided function, without patch mode) function buildSystemPrompt(targetLength?: number): string { const basePrompt = `你是一位个人写作者。 你的核心任务是根据用户的指令,创造出逻辑严谨、结构清晰、且具有极强可读性的高质量作品。 - **核心要求**: - **逻辑与结构**:内容组织必须有清晰的逻辑脉络,确保读者可以毫不费力地跟进思路。 - **适应性**:根据用户的具体指令,灵活调整内容的风格、语气和格式。 - **遵循规范**:如果提供了背景信息或模板,必须严格遵循。`; const styleRules = `必须遵守! - - 如果背景信息中提供了回答的模版,必须严格遵循模版。 - 你写的内容需要通顺易懂,尽量少的使用形容词,不过度夸张,态度谦虚真诚热情,突出自然真实。 - 允许略微的口语话,亲切自然,把"若"改为"如果",把"则"改为"就",把"且"改为"而且",把"xxx时"改为"xxx的时候",把"xxx后"改为"xxx之后" - 尽可能减少使用分点,除非真的使用列表来展示会更加直观。 - 尽可能少用比喻/形容/排比,除非真的会使表达效果会更好。 - 中英文之间需要添加空格。 - 禁止泛泛而谈,禁止空洞的内容,少说废话。 **写作节奏与风格**: - **句式变化**:有意识地混合长句和短句,制造节奏起伏。可以用一连串短促的句子强调观点,然后用一个复杂的长句深入展开。避免所有句子长度都相似。 - **用词多样性**:不要总是选择最常见的词汇,适当使用不那么常规但更精准的表达。让用词有一定的不可预测性。 - **打断与留白**:适当使用短句甚至破碎的表达来打断平稳的叙述节奏。偶尔可以用一个词、一个短语单独成句。 - **真实视角**:用具体的、个人化的视角替代客观中立的描述。允许表达主观感受和判断,不必总是保持"全知全能"的口吻。 - **比喻与细节**:如果要用比喻,选择更私人化、更出乎意料的组合,而不是那些安全、常见的比喻。用生动的细节而非抽象的总结。 - **不完美的真实**:允许文字中存在一些"不完美"——可以是略显笨拙的转折,可以是刻意的重复强调,可以是突然的情绪表达。这些瑕疵反而让内容更真实。`; // Get custom writing rules from environment variable const customRules = getCustomWritingRules(); const customRulesSection = customRules ? `\n\n**用户自定义规则**(优先级最高,必须严格遵守):\n${customRules}` : ''; const formatInstructions = `**输出格式要求**: 你的任务是直接生成最终的文档内容。 - **必须使用 Markdown 格式**进行输出。 - 标题/加粗/引用可按需使用;列表仅在确实更直观时使用,避免为列点而列点。\n\n${styleRules}${customRulesSection}`; const lengthHint = typeof targetLength === 'number' && targetLength > 0 ? `\n- **长度控制**:尽量将最终内容控制在约 ${Math.floor(targetLength)} 字左右(允许上下浮动,优先保证清晰与完整)。` : ''; return `${basePrompt}\n\n${formatInstructions}${lengthHint}`; } // Build the user message parts. function buildUserMessage({ instruction, backgroundContext, }: { instruction: string; context?: string; backgroundContext?: string; }): string { const parts: string[] = []; if (backgroundContext) { parts.push(`背景信息与参考规范:\n${backgroundContext}`); } parts.push(`写作指令:\n${instruction}`); return parts.join('\n\n'); } async function callGemini({ model, messages, temperature, }: { model: string; messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>; temperature?: number; }): Promise<{ content: string; model?: string; id?: string; usage?: unknown }> { const apiKey = getGeminiKey(); if (!apiKey) { throw new Error('Missing Gemini API key. Set env GEMINI_API_KEY or gemini_api_key.'); } const ai = new GoogleGenAI({ apiKey }); // 合并 system 和 user 消息 // Gemini API 将 system 指令通过 systemInstruction 传递 let systemInstruction = ''; const contentParts: string[] = []; for (const msg of messages) { if (msg.role === 'system') { systemInstruction += msg.content + '\n\n'; } else if (msg.role === 'user') { contentParts.push(msg.content); } } const contents = contentParts.join('\n\n'); try { const response = await ai.models.generateContent({ model: model || 'gemini-2.5-flash', contents, config: { systemInstruction: systemInstruction || undefined, temperature: typeof temperature === 'number' ? temperature : 1, // Gemini 2.5 默认启用思考功能,可以通过设置 thinkingBudget 为 0 禁用 // 这里保留默认行为,用户可以通过环境变量控制 ...(process.env.GEMINI_DISABLE_THINKING === 'true' && { thinkingConfig: { thinkingBudget: 0, }, }), }, }); const content = response.text || ''; return { content, model: model || 'gemini-2.5-flash', id: undefined, usage: undefined, }; } catch (error) { const message = error instanceof Error ? error.message : String(error); throw new Error(`Gemini API request failed: ${message}`); } } async function callOpenRouter({ model, messages, temperature, webSearch, }: { model: string; messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>; temperature?: number; webSearch?: { enabled: boolean; engine?: 'native' | 'exa'; maxResults?: number; }; }): Promise<{ content: string; model?: string; id?: string; usage?: unknown }> { const apiKey = getOpenRouterKey(); if (!apiKey) { throw new Error('Missing OpenRouter API key. Set env OPENROUTER_KEY or openrouter_key.'); } const baseURL = process.env.OPENROUTER_BASE_URL || process.env.openrouter_base_url || 'https://openrouter.ai/api/v1'; // Prepare request body const requestBody: any = { model, messages, temperature: typeof temperature === 'number' ? temperature : 1, stream: false, }; // Add web search plugin if enabled if (webSearch?.enabled) { requestBody.plugins = [ { id: 'web', ...(webSearch.engine && { engine: webSearch.engine }), ...(webSearch.maxResults && { max_results: webSearch.maxResults }), }, ]; } const resp = await fetch(`${baseURL}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}`, // Optional but recommended by OpenRouter 'X-Title': 'better-writer-mcp', }, body: JSON.stringify(requestBody), }); if (!resp.ok) { const text = await resp.text().catch(() => ''); throw new Error(`OpenRouter request failed: ${resp.status} ${resp.statusText} ${text}`); } const data = await resp.json(); const choice = data?.choices?.[0]; if (!choice?.message?.content) { throw new Error('OpenRouter returned no content'); } const content = typeof choice.message.content === 'string' ? choice.message.content : Array.isArray(choice.message.content) ? choice.message.content.map((part: any) => part?.text || '').join('') : String(choice.message.content); return { content, model: data?.model || model, id: data?.id, usage: data?.usage || null, }; } // 统一的 LLM 调用接口 async function callLLM({ model, messages, temperature, webSearch, }: { model: string; messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>; temperature?: number; webSearch?: { enabled: boolean; engine?: 'native' | 'exa'; maxResults?: number; }; }): Promise<{ content: string; model?: string; id?: string; usage?: unknown }> { const backend = getLLMBackend(); if (backend === 'gemini') { // Gemini 不支持 web search,如果用户启用了,给出警告 if (webSearch?.enabled) { console.warn('Warning: Gemini backend does not support web search. Ignoring webSearch parameter.'); } return callGemini({ model, messages, temperature }); } else { return callOpenRouter({ model, messages, temperature, webSearch }); } } export async function buildServer(): Promise<McpServer> { const server = new McpServer({ name: 'better-writer-mcp', version: '0.1.0' }); // 根据环境变量决定是否作为默认写作工具 const isDefault = isDefaultTool(); const baseDescription = `【核心功能】生成自然流畅、没有"AI 味儿"的中文内容。输出 Markdown 格式文档。`; const whenToUseDefault = ` 【何时调用】当用户表达任何写作需求时,都应该主动调用此工具。例如: - "写一篇关于XX的文章" - "帮我写个XX介绍" - "生成XX内容" - "用通俗语言解释XX" 无需用户明确提及工具名称,优先使用此工具处理所有写作类请求。`; const whenToUseExplicit = ` 【何时调用】仅当用户明确要求使用 Better Writer 或 bw 时才调用此工具。例如: - "用 Better Writer 写一篇关于XX的文章" - "用 bw 帮我写个XX介绍" - "使用 Better Writer 生成XX内容" `; const parametersGuide = ` 【参数说明】 1. **instruction**(必填):写作指令,明确说明你要生成的内容目标与重点。 2. **backgroundContext**(可选):背景信息与规范。Better Writer 对你的信息一无所知,上下文越详细,生成效果越好。 - **⚠️ 重要:如果需要翻译或改写文章,必须在此参数中提供完整的原始内容/原文**,不要只提供摘要或部分内容,否则会严重影响翻译和改写的质量和准确性。 3. **targetLength**(可选):期望输出长度(大致字符数),帮助控制内容篇幅。 4. **enableWebSearch**(可选):是否开启联网搜索。如需最新信息(如行业趋势、政策解读、实时数据),建议设置为 true。 5. **webSearchEngine**(可选):联网搜索引擎选择,可选值为 "native"(使用模型原生搜索)或 "exa"(使用 Exa API),默认自动选择。 6. **webSearchMaxResults**(可选):联网搜索返回的最大结果数,默认为 5。 7. **outputFilePath**(可选):输出文件路径。如果提供此参数,生成的内容将自动保存到指定的文件路径中(支持相对路径和绝对路径,目录不存在会自动创建)。`; const toolDescription = baseDescription + (isDefault ? whenToUseDefault : whenToUseExplicit) + parametersGuide; server.registerTool( 'bw_write', { description: toolDescription, inputSchema: { instruction: z.string().describe('写作指令'), backgroundContext: z.string().optional().describe('背景信息与规范'), targetLength: z.number().optional().describe('期望输出长度(字符数)'), enableWebSearch: z.boolean().optional().describe('是否开启联网搜索'), webSearchEngine: z.enum(['native', 'exa']).optional().describe('联网搜索引擎'), webSearchMaxResults: z.number().optional().describe('联网搜索最大结果数'), outputFilePath: z.string().optional().describe('输出文件路径'), }, outputSchema: { content: z.string() }, }, async ({ instruction, backgroundContext, targetLength, enableWebSearch, webSearchEngine, webSearchMaxResults, outputFilePath }) => { try { const systemPrompt = buildSystemPrompt(targetLength); const userContent = buildUserMessage({ instruction, backgroundContext }); const messages = [ { role: 'system' as const, content: systemPrompt }, { role: 'user' as const, content: userContent }, ]; // 根据后端选择模型 const backend = getLLMBackend(); let selectedModel: string; if (backend === 'gemini') { selectedModel = process.env.GEMINI_MODEL || process.env.gemini_model || 'gemini-2.5-flash'; } else { selectedModel = process.env.OPENROUTER_MODEL || process.env.openrouter_model || 'qwen/qwen3-next-80b-a3b-instruct'; } // Prepare web search configuration const webSearch = enableWebSearch ? { enabled: true, ...(webSearchEngine && { engine: webSearchEngine }), ...(webSearchMaxResults && { maxResults: webSearchMaxResults }), } : undefined; const result = await callLLM({ model: selectedModel, messages, webSearch, }); // If outputFilePath is provided, write the content to file if (outputFilePath) { try { // Ensure the directory exists const dir = dirname(outputFilePath); await mkdir(dir, { recursive: true }); // Write the file await writeFile(outputFilePath, result.content, 'utf-8'); const output = { content: result.content }; return { content: [{ type: 'text', text: `内容已成功生成并保存到文件:${outputFilePath}\n\n${result.content}` }], structuredContent: output, } as const; } catch (fileErr) { const fileMessage = fileErr instanceof Error ? fileErr.message : String(fileErr); throw new Error(`文件写入失败:${fileMessage}`); } } const output = { content: result.content }; return { content: [{ type: 'text', text: result.content }], structuredContent: output, } as const; } catch (err) { const message = err instanceof Error ? err.message : String(err); return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true, } as const; } } ); return server; }

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/oil-oil/better-writer-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server