// docs.ts — ツール詳細ドキュメント参照用ツール // // リポジトリ内 docs/tools/{name}.md を読み込んで返す。 // ワークスペース外の固定パスから読むため、Read ツールでは到達できない。 // Tool description にこのツールへのポインタを書いておくと、 // 詳細な使い方を必要に応じてエージェントが取得できる。 import * as fs from 'fs'; import * as path from 'path'; import { fileURLToPath } from 'url'; import { ToolDef } from '../../llm/openai-compat.js'; import type { ToolContext, ToolResult } from './core.js'; import { logger } from '../../logger.js'; type McpToolLookup = (serverId: string, toolName: string) => { description: string | null; input_schema: string | null } | null; let _mcpLookup: McpToolLookup | null = null; export function setMcpToolLookup(fn: McpToolLookup | null): void { _mcpLookup = fn; } // dist/engine/tools/docs.js または src/engine/tools/docs.ts から // リポジトリルートを解決し、docs/tools/ を指す const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const REPO_ROOT = path.resolve(__dirname, '..', '..', '..'); const DOCS_DIR = path.join(REPO_ROOT, 'docs', 'tools'); // 関連ツールが同じ doc を参照できるようエイリアスを定義 // キー・値ともに小文字 const TOOL_DOC_ALIASES: Record = { // checklist.md にまとめる createchecklist: 'checklist', checkitem: 'checklist', getchecklist: 'checklist', // searchknowledge.md にまとめる listnamespaces: 'searchknowledge', listdocuments: 'searchknowledge', ingestdocument: 'searchknowledge', ingeststatus: 'searchknowledge', // x.ts ツールをまとめる xuserposts: 'xsearch', xpostdetail: 'xsearch', xfetchcardmedia: 'xsearch', // youtube.ts をまとめる searchyoutube: 'getyoutubetranscript', // maps.ts をまとめる getdirections: 'searchplaces', reversegeocode: 'searchplaces', // office.ts をまとめる readpdf: 'office', readexcel: 'office', readdocx: 'office', readpptx: 'office', pdftoimages: 'office', splitexcelsheets: 'office', splitdocxsections: 'office', // pieces.ts をまとめる getpiece: 'listpieces', createpiece: 'listpieces', updatepiece: 'listpieces', // ms-learn.ts をまとめる fetchmicrosoftlearn: 'searchmicrosoftlearn', searchmicrosoftlearncache: 'searchmicrosoftlearn', refreshmicrosoftlearncache: 'searchmicrosoftlearn', // browser.ts: InteractiveBrowse / BrowseWithSession は browseweb.md にまとめる interactivebrowse: 'browseweb', browsewithsession: 'browseweb', // ssh.ts をまとめる sshexec: 'ssh-tools', sshupload: 'ssh-tools', sshdownload: 'ssh-tools', sshlistconnections: 'ssh-tools', // ssh-console.ts をまとめる sshconsoleensure: 'ssh-console-tools', sshconsolesend: 'ssh-console-tools', sshconsolesnapshot: 'ssh-console-tools', // slide.ts をまとめる settheme: 'slide', addslide: 'slide', buildpptx: 'slide', resetslides: 'slide', // notes.ts をまとめる searchnotes: 'notes', readnote: 'notes', writenote: 'notes', }; const READ_TOOL_DOC_DEF: ToolDef = { type: 'function', function: { name: 'ReadToolDoc', description: 'ツールの詳細な使い方ドキュメントを読み込む。各ツールの description は概要のみで、詳細な手順や例が必要なときはこれを呼ぶ。' + 'docs/tools/{name}.md(リポジトリ内固定パス)を参照する。', parameters: { type: 'object', properties: { name: { type: 'string', description: '読みたいツール名(例: "BrowseWeb", "SearchKnowledge")', }, }, required: ['name'], }, }, }; export const TOOL_DEFS: Record = { ReadToolDoc: READ_TOOL_DOC_DEF, }; export async function executeTool( name: string, input: Record, _ctx: ToolContext, ): Promise { if (name !== 'ReadToolDoc') return null; const toolName = input['name'] as string | undefined; if (!toolName || typeof toolName !== 'string') { return { output: 'ReadToolDoc error: name パラメータが必要です', isError: true }; } // MCP ツール名 (mcp____) の場合はキャッシュから返す if (toolName.startsWith('mcp__')) { // Lazy-import to avoid a hard dependency on mcp module at module-load time. const { parseToolName } = await import('../../mcp/tool-adapter.js'); const parsed = parseToolName(toolName); if (!parsed) { return { output: `ReadToolDoc: 不正な MCP ツール名 "${toolName}"`, isError: true }; } if (!_mcpLookup) { return { output: 'ReadToolDoc: MCP サブシステムが初期化されていません', isError: true }; } const row = _mcpLookup(parsed.serverId, parsed.toolName); if (!row) { return { output: `ReadToolDoc: ${toolName} のキャッシュ情報がありません`, isError: false }; } let schemaBlock = ''; if (row.input_schema) { try { const schema = JSON.parse(row.input_schema); schemaBlock = `\n\n## Input schema\n\n\`\`\`json\n${JSON.stringify(schema, null, 2)}\n\`\`\``; } catch { schemaBlock = `\n\n## Input schema (raw)\n\n\`\`\`\n${row.input_schema}\n\`\`\``; } } return { output: `# ${toolName}\n\n${row.description ?? '(no description)'}${schemaBlock}`, isError: false, }; } // パストラバーサル防止: 英数字とハイフン・アンダースコアのみ許可 if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(toolName)) { return { output: `ReadToolDoc error: 不正なツール名 "${toolName}"`, isError: true }; } const lowerName = toolName.toLowerCase(); const resolvedName = TOOL_DOC_ALIASES[lowerName] ?? lowerName; const docPath = path.join(DOCS_DIR, `${resolvedName}.md`); try { const content = await fs.promises.readFile(docPath, 'utf-8'); return { output: content, isError: false }; } catch (e: any) { if (e.code === 'ENOENT') { // 利用可能なドキュメント一覧を返す try { const files = await fs.promises.readdir(DOCS_DIR); const available = files .filter((f) => f.endsWith('.md')) .map((f) => f.replace(/\.md$/, '')) .sort(); return { output: `ReadToolDoc: "${toolName}" のドキュメントは存在しません。\n利用可能なドキュメント:\n${available.map((n) => `- ${n}`).join('\n')}`, isError: true, }; } catch { return { output: `ReadToolDoc: "${toolName}" のドキュメントは存在しません。`, isError: true }; } } logger.warn(`[ReadToolDoc] failed to read ${docPath}: ${e.message}`); return { output: `ReadToolDoc error: ${e.message}`, isError: true }; } }