diff --git a/projects/arabica/src/sprint2/src/app.ts b/projects/arabica/src/sprint2/src/app.ts index 78c607b..0cbf06d 100644 --- a/projects/arabica/src/sprint2/src/app.ts +++ b/projects/arabica/src/sprint2/src/app.ts @@ -3,10 +3,11 @@ * Copyright (c) 2025-2026 Gitconomy Research * * SPDX-License-Identifier: MIT - * + * * Contributors: * - 郭晧 (Initial Author) */ + import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; @@ -18,64 +19,65 @@ import { listObsidianNotes, readObsidianNote } from './services/resourceService' // 初始化 MCP Server // ========================================== const server = new McpServer({ - name: 'Project-Caffeine-S2-Prompt-Strategy', - version: '2.0.0' + name: 'Project-Caffeine-Arabica-Edition', + version: '0.0.2' }); +// ========================================== +// 辅助函数 +// ========================================== + +/** + * 处理可选参数的默认值转换与补全。 + * + * 该函数执行两个主要任务: + * 1. 遍历传入的参数对象,将空值(undefined/null/"")转换为字符串 "无"。 + * 2. 根据提供的 expectedKeys 列表,补全缺失的可选参数键,值为 "无",确保模板替换不会失败。 + * + * @param {Record} args - 客户端传入的原始参数对象 + * @param {string[]} expectedKeys - 该框架预期的所有参数键名列表(用于补全缺失的键) + * @returns {Record} 补全且清洗后的参数对象,所有值均为非空字符串 + */ +function sanitizeArgs(args: Record, expectedKeys: string[] = []): Record { + const safeArgs: Record = {}; + + // 处理已有的参数,确保值不为空 + for (const [key, value] of Object.entries(args)) { + safeArgs[key] = value !== undefined && value !== null && value !== "" ? String(value) : "无"; + } + + // 补全缺失的可选参数键,确保模板替换不会失败 + expectedKeys.forEach(key => { + if (!(key in safeArgs)) { + safeArgs[key] = "无"; + } + }); + + return safeArgs; +} + // ========================================== // 注册 Prompts 原语(多维思维框架模板) // ========================================== /** - * 处理 SCQA 框架的 prompts/get 请求。 - * 调用 handlePromptsGet 获取消息序列,并进行清洗(过滤非法 role、补全 annotations/_meta)。 - * @param args - 包含 situation, complication, question, answer 的对象 - * @param extra - MCP 额外上下文(未使用) - * @returns 符合 MCP 规范的 prompt 响应对象 + * 注册并处理 SCQA 框架的 prompts/get 请求。 + * + * @param {object} args - 客户端传入的框架参数 + * @param {string} args.situation - 当前的客观背景或情境描述(必填) + * @param {string} [args.context] - 补充的约束条件、行业背景或已有资源(可选) + * @param {string} [args.objective] - 期望达成的最终业务目标(可选) + * @returns {Promise} 符合 MCP 协议规范的 prompt 响应对象,包含过滤和补全元数据后的 messages 序列 */ - server.prompt( - 'scqa', // 示例:SCQA 框架 + 'scqa', { - situation: z.string().describe('情境'), - complication: z.string().describe('复杂性'), - question: z.string().describe('问题'), - answer: z.string().describe('答案') + situation: z.string().describe('当前的客观背景或情境描述'), + context: z.string().optional().describe('补充的约束条件、行业背景或已有资源(可选)'), + objective: z.string().optional().describe('期望达成的最终业务目标(可选)') }, - async (args, extra) => { - const result = await handlePromptsGet('scqa', args as Record); - return { - ...result, - messages: Array.isArray(result.messages) - ? result.messages - .filter((msg: any) => msg.role === "user" || msg.role === "assistant") - .map((msg: any) => ({ - ...msg, - // Optionally ensure content has required structure - content: { - ...msg.content, - // Add default annotations/_meta if missing - annotations: msg.content.annotations ?? undefined, - _meta: msg.content._meta ?? undefined - } - })) - : [] - }; - } -); - -/** - * 处理 5 Whys 框架的 prompts/get 请求。 - * 调用 handlePromptsGet 获取消息序列,并进行清洗。 - * @param args - 包含 problem 的对象 - * @returns 符合 MCP 规范的 prompt 响应对象 - */ - -server.prompt( - '5whys', // 5 Whys 框架 - { problem: z.string().describe('需要分析的问题或现象') }, async (args) => { - const result = await handlePromptsGet('5whys', args); + const result = await handlePromptsGet('scqa', sanitizeArgs(args, ['situation', 'context', 'objective'])); return { ...result, messages: Array.isArray(result.messages) @@ -95,17 +97,23 @@ server.prompt( ); /** - * 处理 5W3H 框架的 prompts/get 请求。 - * 调用 handlePromptsGet 获取消息序列,并进行清洗。 - * @param args - 包含 topic 的对象 - * @returns 符合 MCP 规范的 prompt 响应对象 + * 注册并处理 5 Whys 框架的 prompts/get 请求。 + * + * @param {object} args - 客户端传入的框架参数 + * @param {string} args.problem - 需要分析的核心问题、故障或不良现象(必填) + * @param {string} [args.context] - 问题发生的背景信息、前置条件或受影响的范围(可选) + * @param {string} [args.goal] - 期望通过解决此问题达成的最终目标(可选) + * @returns {Promise} 符合 MCP 协议规范的 prompt 响应对象 */ - server.prompt( - '5w3h', // 5W3H 框架 - { topic: z.string().describe('需要分析的主题') }, + '5whys', + { + problem: z.string().describe('需要分析的核心问题、故障或不良现象'), + context: z.string().optional().describe('问题发生的背景信息、前置条件或受影响的范围(可选)'), + goal: z.string().optional().describe('期望通过解决此问题达成的最终目标(可选)') + }, async (args) => { - const result = await handlePromptsGet('5w3h', args); + const result = await handlePromptsGet('5whys', sanitizeArgs(args, ['problem', 'context', 'goal'])); return { ...result, messages: Array.isArray(result.messages) @@ -125,17 +133,23 @@ server.prompt( ); /** - * 处理 SWOT 框架的 prompts/get 请求。 - * 调用 handlePromptsGet 获取消息序列,并进行清洗。 - * @param args - 包含 entity 的对象 - * @returns 符合 MCP 规范的 prompt 响应对象 + * 注册并处理 5W3H 框架的 prompts/get 请求。 + * + * @param {object} args - 客户端传入的框架参数 + * @param {string} args.topic - 需要分析的核心主题、事件或问题(必填) + * @param {string} [args.context] - 该主题发生的特定背景、前置条件或行业环境(可选) + * @param {string} [args.objective] - 期望通过此次分析达成的核心目标(可选) + * @returns {Promise} 符合 MCP 协议规范的 prompt 响应对象 */ - server.prompt( - 'swot', // SWOT 框架 - { entity: z.string().describe('分析对象(企业、项目等)') }, + '5w3h', + { + topic: z.string().describe('需要分析的核心主题、事件或问题'), + context: z.string().optional().describe('该主题发生的特定背景、前置条件或行业环境(可选)'), + objective: z.string().optional().describe('期望通过此次分析达成的核心目标(可选)') + }, async (args) => { - const result = await handlePromptsGet('swot', args); + const result = await handlePromptsGet('5w3h', sanitizeArgs(args, ['topic', 'context', 'objective'])); return { ...result, messages: Array.isArray(result.messages) @@ -155,17 +169,59 @@ server.prompt( ); /** - * 处理 PESTLE 框架的 prompts/get 请求。 - * 调用 handlePromptsGet 获取消息序列,并进行清洗。 - * @param args - 包含 domain 的对象 - * @returns 符合 MCP 规范的 prompt 响应对象 + * 注册并处理 SWOT 框架的 prompts/get 请求。 + * + * @param {object} args - 客户端传入的框架参数 + * @param {string} args.entity - 分析对象(如企业、产品、项目、个人等)(必填) + * @param {string} [args.context] - 补充的行业背景、当前阶段或面临的核心挑战(可选) + * @param {string} [args.competitors] - 主要竞争对手或对标对象(可选) + * @returns {Promise} 符合 MCP 协议规范的 prompt 响应对象 */ - server.prompt( - 'pestle', // PESTLE 框架 - { domain: z.string().describe('行业或领域') }, + 'swot', + { + entity: z.string().describe('分析对象(如企业、产品、项目、个人等)'), + context: z.string().optional().describe('补充的行业背景、当前阶段或面临的核心挑战(可选)'), + competitors: z.string().optional().describe('主要竞争对手或对标对象(可选)') + }, async (args) => { - const result = await handlePromptsGet('pestle', args); + const result = await handlePromptsGet('swot', sanitizeArgs(args, ['entity', 'context', 'competitors'])); + return { + ...result, + messages: Array.isArray(result.messages) + ? result.messages + .filter((msg: any) => msg.role === "user" || msg.role === "assistant") + .map((msg: any) => ({ + ...msg, + content: { + ...msg.content, + annotations: msg.content.annotations ?? undefined, + _meta: msg.content._meta ?? undefined + } + })) + : [] + }; + } +); + +/** + * 注册并处理 PESTLE 框架的 prompts/get 请求。 + * + * @param {object} args - 客户端传入的框架参数 + * @param {string} args.domain - 需要分析的具体行业、市场或业务领域(必填) + * @param {string} [args.region] - 目标地域范围(可选) + * @param {string} [args.timeframe] - 分析的时间跨度(可选) + * @returns {Promise} 符合 MCP 协议规范的 prompt 响应对象 + */ +server.prompt( + 'pestle', + { + domain: z.string().describe('需要分析的具体行业、市场或业务领域'), + region: z.string().optional().describe('目标地域范围(可选)'), + timeframe: z.string().optional().describe('分析的时间跨度(可选)') + }, + async (args) => { + const result = await handlePromptsGet('pestle', sanitizeArgs(args, ['domain', 'region', 'timeframe'])); return { ...result, messages: Array.isArray(result.messages) @@ -185,23 +241,22 @@ server.prompt( ); // ========================================== -// 注册工具:generate_search_queries +// 注册工具(Tools) // ========================================== /** - * 处理 generate_search_queries 工具调用。 - * 调用 handleToolCall 获取结果,并确保返回的每个 content 项具有 type: "text"。 - * @param args - 包含 query 的对象 - * @param extra - MCP 额外上下文(未使用) - * @returns MCP 工具响应对象 + * 注册意图拆解工具 generate_search_queries。 + * 接收用户的原始自然语言查询,调用意图拆解服务生成 3-5 个专业检索词。 + * + * @param {object} args - 工具参数对象 + * @param {string} args.query - 用户的原始查询语句 + * @returns {Promise} 符合 MCP 协议的工具响应对象,包含 type 为 "text" 的内容格式化结果 */ - server.tool( 'generate_search_queries', { query: z.string().describe('用户的原始查询语句') }, - async (args, extra) => { + async (args) => { const result = await handleToolCall('generate_search_queries', args); - // Ensure each content item has type: "text" (not a generic string) return { ...result, content: result.content.map((item: any) => ({ @@ -212,17 +267,13 @@ server.tool( } ); -// ===================================================== -// 注册工具:list_local_notes, read_local_note, save_note -// ===================================================== - /** - * 处理 list_local_notes 工具调用。 - * 调用 handleToolCall 获取笔记列表,并标准化 content 格式。 - * @param args - 空对象(无需参数) - * @returns MCP 工具响应对象,包含笔记列表文本 + * 注册获取本地笔记列表工具 list_local_notes。 + * 允许大模型获取当前知识库目录下所有存在的 Markdown 笔记文件名。 + * + * @param {object} args - 无需额外参数(保留以符合 MCP 签名) + * @returns {Promise} 包含笔记文件名列表的文本响应对象 */ - server.tool( 'list_local_notes', {}, @@ -238,14 +289,14 @@ server.tool( } ); - /** - * 处理 read_local_note 工具调用。 - * 调用 handleToolCall 读取指定笔记内容,并标准化 content 格式。 - * @param args - 包含 filename 的对象 - * @returns MCP 工具响应对象,包含笔记内容文本 + * 注册读取单一本地笔记工具 read_local_note。 + * 允许大模型根据指定文件名获取该 Markdown 文件的具体文本内容。 + * + * @param {object} args - 工具参数对象 + * @param {string} args.filename - 需要读取的笔记文件名,必须包含 .md 后缀 + * @returns {Promise} 包含目标文件全量文本内容的响应对象 */ - server.tool( 'read_local_note', { filename: z.string().describe('需要读取的笔记文件名,必须包含 .md 后缀') }, @@ -262,12 +313,14 @@ server.tool( ); /** - * 处理 save_note 工具调用。 - * 调用 handleToolCall 保存笔记到本地知识库,并标准化 content 格式。 - * @param args - 包含 filename 和 content 的对象 - * @returns MCP 工具响应对象,包含保存结果信息 + * 注册保存本地笔记工具 save_note。 + * 允许大模型将分析结果或摘要撰写并保存为本地知识库中的 Markdown 文件。 + * + * @param {object} args - 工具参数对象 + * @param {string} args.filename - 笔记文件名,必须以 .md 结尾 + * @param {string} args.content - 需要持久化写入的笔记内容(Markdown 格式) + * @returns {Promise} 保存成功的状态回执或报错信息的响应对象 */ - server.tool( 'save_note', { @@ -287,14 +340,27 @@ server.tool( ); // ========================================== -// 注册 Resources 原语(暴露本地笔记供客户端勾选) +// 注册资源(Resources) // ========================================== -// 使用 ResourceTemplate 注册动态资源 +/** + * 注册 local-notes 资源模板原语。 + * 提供给支持 Resources 的 MCP 客户端,使用户可以直接在界面上勾选并注入本地知识库笔记。 + * + * - list 钩子:返回所有有效的 note://local/{filename} 资源列表。 + * - 读取钩子:根据解析出的 filename 读取本地文件内容,组装为符合 MCP 规范的响应。 + * + * @type {import('@modelcontextprotocol/sdk/server/mcp.js').ResourceTemplate} + */ server.resource( - "local-notes", // 资源名称 - new ResourceTemplate("note://local/{filename}", { - // 实现列表功能:返回所有可用的笔记资源 + "local-notes", + new ResourceTemplate("note://local/{filename}", { + /** + * 列出所有可用的笔记资源。 + * + * @returns {Promise<{ resources: Array<{ name: string; uri: string; mimeType: string; description: string }> }>} + * 资源列表,每个资源包含名称、URI、MIME 类型和描述 + */ list: async () => { try { const notes = await listObsidianNotes(); @@ -312,22 +378,21 @@ server.resource( } } }), - - /** + /** * 处理资源读取请求:根据 URI 中的 filename 参数读取对应笔记内容。 - * @param uri - 请求的 URI 对象 - * @param params - 包含从 URI 提取的 filename 参数 - * @returns 符合 MCP 规范的资源内容对象 - * @throws 当读取失败时抛出错误 + * + * @param {URL} uri - 请求的完整 URI 对象 + * @param {{ filename: string | string[] }} params - 从 URI 中提取的参数,包含 filename + * @returns {Promise<{ contents: Array<{ uri: string; mimeType: string; text: string }> }>} + * 符合 MCP 规范的内容响应对象 + * @throws {Error} 当读取失败时抛出错误,错误信息将被 MCP 客户端捕获 */ - async (uri, { filename }) => { try { - // filename 参数由 ResourceTemplate 自动从 URI 中提取 const filenameStr = Array.isArray(filename) ? filename[0] : filename; const decodedFilename = decodeURIComponent(filenameStr); const content = await readObsidianNote(decodedFilename); - + return { contents: [{ uri: uri.href, @@ -336,25 +401,25 @@ server.resource( }] }; } catch (error: any) { - // 错误处理:返回错误信息 throw new Error(`读取笔记失败: ${error.message}`); } } ); // ========================================== -// 启动 STDIO 传输层 +// 启动服务器 // ========================================== - /** - * 启动 MCP 服务器,连接 STDIO 传输层。 - * 该函数创建 StdioServerTransport 实例并连接到 server。 - * @returns {Promise} 无返回值 + * 启动 MCP 服务器的主入口函数。 + * 建立与客户端(如 Cherry Studio)的标准输入/输出 (STDIO) 传输通信通道, + * 将服务器配置挂载至进程,并就绪等待各类协议请求。 + * + * @returns {Promise} 异步的启动过程,无返回值 + * @throws {Error} 如果连接失败,错误会被捕获并记录,进程退出 */ - async function start() { - console.error('[S2] 正在启动 MCP Server (Prompts + 检索词工具)...'); + console.error('[S2] 正在启动 MCP Server (Prompts + Tools + Resources)...'); const transport = new StdioServerTransport(); await server.connect(transport); console.error('[S2] MCP Server 已就绪,等待 Cherry Studio 连接'); diff --git a/projects/arabica/src/sprint2/tsconfig.json b/projects/arabica/src/sprint2/tsconfig.json index 0cad0ed..240fec7 100644 --- a/projects/arabica/src/sprint2/tsconfig.json +++ b/projects/arabica/src/sprint2/tsconfig.json @@ -1,4 +1,5 @@ { + "$schema": "https://json.schemastore.org/tsconfig", // Uncomment if you want schema validation and the URL is reachable "compilerOptions": { "target": "ES2022", "module": "CommonJS",