UNPKG

@memory-bank/mcp

Version:

Memory-enabled Co-Pilot (MCP) server for managing project documentation and context

397 lines 18.7 kB
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { logger } from '../shared/utils/logger.js'; import { isValidLanguage } from '@memory-bank/schemas'; /** * Generates tool definitions based on current environment settings * * This function dynamically creates tool definitions for the MCP server, * taking into account environment variables and configuration settings. * It allows hiding parameters that are already defined via CLI options. * * Note: Uses inputSchema instead of parameters to comply with MCP SDK 1.9.0+ requirements */ function generateToolDefinitions(configProvider = null) { // Determine which parameters should be required vs. optional const isProjectMode = configProvider?.getConfig().isProjectMode || false; const hasDocsPathEnv = !!process.env.MEMORY_BANK_ROOT || !!process.env.DOCS_ROOT; const hasLanguageEnv = !!process.env.LANGUAGE; logger.debug(`Generating tool definitions:`, { isProjectMode, hasDocsPathEnv, hasLanguageEnv }); // Common tool properties const branchRequired = !isProjectMode; const docsRequired = !hasDocsPathEnv; const languageRequired = !hasLanguageEnv; // Read Context Tool: Dynamic required parameters const readContextRequired = ['docs', 'language', 'branch'].filter(param => { if (param === 'branch') return branchRequired; if (param === 'docs') return docsRequired; if (param === 'language') return languageRequired; return true; }); logger.debug(`Read context required parameters:`, readContextRequired); return [ { name: 'read_document', description: 'Read a document from a branch or global memory bank', inputSchema: { type: 'object', properties: { scope: { type: 'string', enum: ['branch', 'global'], description: 'Scope to read from (branch or global)' }, branch: { type: 'string', description: 'Branch name (required if scope is "branch", auto-detected in project mode)' }, path: { type: 'string', description: 'Document path (e.g. "config.json")' }, docs: { type: 'string', description: 'Path to docs directory' } }, required: ['scope', 'path', ...(docsRequired ? ['docs'] : [])] } }, { name: 'write_document', description: 'Write a document to a branch or global memory bank', inputSchema: { type: 'object', properties: { scope: { type: 'string', enum: ['branch', 'global'], description: 'Scope to write to (branch or global)' }, branch: { type: 'string', description: 'Branch name (required if scope is "branch", auto-detected in project mode)' }, path: { type: 'string', description: 'Document path (e.g. "config.json")' }, content: { type: 'string', description: 'Document content (mutually exclusive with patches)' }, patches: { type: 'array', description: 'JSON Patch operations (RFC 6902, mutually exclusive with content)' }, tags: { type: 'array', items: { type: 'string' }, description: 'Tags to assign to the document' }, docs: { type: 'string', description: 'Path to docs directory' }, returnContent: { type: 'boolean', description: 'If true, return the full document content in output', default: false } }, required: ['scope', 'path', ...(docsRequired ? ['docs'] : [])] } }, { name: 'read_context', description: 'Read all context information (rules, branch memory bank, global memory bank) at once', inputSchema: { type: 'object', properties: { branch: { type: 'string' }, docs: { type: 'string' }, language: { type: 'string', enum: ['en', 'ja', 'zh'] } }, required: readContextRequired } }, { name: 'search_documents_by_tags', description: 'Search documents in memory banks by tags', inputSchema: { type: 'object', properties: { tags: { type: 'array', items: { type: 'string' }, description: 'Array of tags to search for (e.g., ["architecture", "refactoring"])' }, match: { type: 'string', enum: ['and', 'or'], default: 'or', description: 'Match type: "and" requires all tags, "or" requires any tag' }, scope: { type: 'string', enum: ['branch', 'global', 'all'], default: 'all', description: 'Search scope: "branch", "global", or "all"' }, branch: { type: 'string', description: 'Branch name. Required if scope is "branch" or "all", but optional in project mode (will be auto-detected).' }, docs: { type: 'string', description: 'Path to docs directory' } }, required: ['tags', ...(docsRequired ? ['docs'] : [])] } } ]; } export function resolveDocsRoot(toolDocs, defaultDocsPath = './docs') { if (toolDocs) { return toolDocs; } if (process.env.MEMORY_BANK_ROOT) { return process.env.MEMORY_BANK_ROOT; } if (process.env.DOCS_ROOT) { return process.env.DOCS_ROOT; } return defaultDocsPath; } function getMergedApplicationOptions(appInstance, docs, language = 'ja') { if (!appInstance) { return { docsRoot: docs || resolveDocsRoot(), language, verbose: false }; } const originalOptions = appInstance.options || {}; const docsRoot = docs ? docs : resolveDocsRoot(); return { ...originalOptions, ...(docs ? { docsRoot } : {}), language: originalOptions.language || language, verbose: originalOptions.verbose || false }; } /** * Configure MCP server routes * @param server MCP server instance * @param app Application instance */ export function setupRoutes(server, app = null) { // Get config provider from the app if available const configProvider = app ? app.configProvider : null; // Generate tool definitions dynamically based on environment const dynamicTools = generateToolDefinitions(configProvider); server.setRequestHandler(ListToolsRequestSchema, async () => { // ツール定義はすでにinputSchemaを使用するように更新済み const result = { tools: dynamicTools, // PaginatedResultSchemaの必須プロパティを追加 _meta: {} // 空のオブジェクトでも良い }; // デバッグ用のログ出力(JSON-RPCの外部に出力されるので注意) logger.debug('ListToolsRequestSchema response:', JSON.stringify(result, null, 2)); return result; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; logger.debug('Tool call received:', { name, args }); if (!args || typeof args !== 'object') { logger.error('Invalid arguments:', { name, args }); throw new Error(`No arguments provided for tool: ${name}`); } const params = args; logger.debug('Parsed params:', params); switch (name) { // 旧APIのケースハンドラは削除済み - v3.0.0から統合APIのみサポート case 'read_document': { const scope = params.scope; const path = params.path; const branch = params.branch; const docs = params.docs; if (!path || !scope) { throw new Error('Invalid arguments for read_document: path and scope are required'); } // ブランチスコープの場合のみブランチ名が必要(プロジェクトモードでは自動検出可能) if (scope === 'branch' && !branch) { const isProjectMode = app?.configProvider.getConfig().isProjectMode || false; if (!isProjectMode) { throw new Error('Branch name is required when not running in project mode'); } // プロジェクトモードの場合は、ブランチ名はコントローラー内で自動検出される logger.debug('Branch name not provided but in project mode, will use auto-detection'); } if (!app) { throw new Error('Application not initialized'); } const docsRoot = docs || resolveDocsRoot(); let controllerApp = app; if (docs) { logger.debug(`Creating new application instance with docsRoot: ${docsRoot}`); const appOptions = getMergedApplicationOptions(app, docsRoot, 'ja'); logger.debug(`Using merged application options: ${JSON.stringify(appOptions)}`); // Normally create a new app instance here, using existing for simplicity } // scopeに応じて適切なコントローラーを呼び出す let response; if (scope === 'branch') { response = await controllerApp.getDocumentController().readDocument({ scope, branchName: branch, path }); } else if (scope === 'global') { response = await controllerApp.getDocumentController().readDocument({ scope, path }); } else { throw new Error(`Invalid scope: ${scope}, must be 'branch' or 'global'`); } if (!response.success) { throw new Error(response.error?.message || 'Failed to read document'); } return { content: [{ type: 'text', text: response.data }], _meta: { lastModified: new Date().toISOString() } }; } case 'write_document': { const scope = params.scope; const path = params.path; const branch = params.branch; const content = params.content; const patches = params.patches; const tags = params.tags; const returnContent = params.returnContent; const docs = params.docs; if (!path || !scope) { throw new Error('Invalid arguments for write_document: path and scope are required'); } // ブランチスコープの場合のみブランチ名が必要(プロジェクトモードでは自動検出可能) if (scope === 'branch' && !branch) { const isProjectMode = app?.configProvider.getConfig().isProjectMode || false; if (!isProjectMode) { throw new Error('Branch name is required when not running in project mode'); } // プロジェクトモードの場合は、ブランチ名はコントローラー内で自動検出される logger.debug('Branch name not provided but in project mode, will use auto-detection'); } if (content && patches) { throw new Error('Content and patches cannot be provided at the same time'); } if (!app) { throw new Error('Application not initialized'); } const docsRoot = docs || resolveDocsRoot(); let controllerApp = app; if (docs) { logger.debug(`Creating new application instance with docsRoot: ${docsRoot}`); const appOptions = getMergedApplicationOptions(app, docsRoot, 'ja'); logger.debug(`Using merged application options: ${JSON.stringify(appOptions)}`); // Normally create a new app instance here, using existing for simplicity } const response = await controllerApp.getDocumentController().writeDocument({ scope, branchName: branch, path, content, patches, tags, returnContent }); if (!response.success) { throw new Error(response.error?.message || 'Failed to write document'); } return { content: [{ type: 'text', text: response.data ? JSON.stringify(response.data, null, 2) : 'Document written successfully' }], _meta: { lastModified: new Date().toISOString() } }; } case 'read_context': { // Get parameters from request or use environment/config defaults if available const providedBranch = params.branch; const providedLanguage = params.language; const providedDocs = params.docs; // Resolve final values with fallbacks const docsRoot = providedDocs || resolveDocsRoot(); const language = providedLanguage || process.env.LANGUAGE || (app ? app.options.language : 'ja'); // Branch can be auto-detected in project mode const isProjectMode = app?.configProvider.getConfig().isProjectMode || false; let branch = providedBranch; logger.info(`Reading context (providedBranch: ${providedBranch || 'none'}, providedLanguage: ${providedLanguage || 'none'}, docsRoot: ${docsRoot}, isProjectMode: ${isProjectMode})`); // Check if we need branch auto-detection if (!branch && isProjectMode) { logger.debug('Branch not provided but in project mode, will use auto-detection'); // Actual auto-detection happens in the UseCase, no need to do it here } else if (!branch && !isProjectMode) { // Only throw if branch is truly required (not in project mode and not provided) throw new Error('Branch name is required for read_context when not in project mode'); } if (!app) { throw new Error('Application not initialized'); } try { logger.debug('Requesting context from ContextController'); let contextApp = app; if (providedDocs) { logger.debug(`Creating new application instance with docsRoot: ${docsRoot}`); const appOptions = getMergedApplicationOptions(app, docsRoot, isValidLanguage(language) ? language : 'en'); logger.debug(`Using merged application options: ${JSON.stringify(appOptions)}`); // Normally create a new app instance here, using existing for simplicity } // Pass parameters to readContext, even if some are undefined // The ContextController and ReadContextUseCase will handle auto-detection const response = await contextApp.getContextController().readContext({ branch, // This might be undefined, which is OK for auto-detection language: language || 'en' // Provide default language if undefined }); if (!response.success) { throw new Error(response.error || 'Failed to read context'); } // Format the response data properly for MCP protocol const formattedResponse = { rules: response.data?.rules, branchMemory: response.data?.branchMemory, globalMemory: response.data?.globalMemory }; return { content: [ { type: 'text', text: JSON.stringify(formattedResponse, null, 2), }, ], }; } catch (error) { logger.error('Error reading context:', error); throw error; } } default: throw new Error(`Unknown tool: ${name}`); } }); } //# sourceMappingURL=routes.js.map