Add tool documentation MCP tools for queryable TOOLS.md replacement
- Created tool_docs table with pgvector support (migration 015) - Added 5 MCP tools: tool_doc_add, tool_doc_search, tool_doc_get, tool_doc_list, tool_doc_export - Imported 268 tools from TOOLS.md to database - Enables semantic search for tool documentation (when embeddings available) - Reduces context pollution (TOOLS.md is 11,769 lines, now queryable) Task: CF-249 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
36
migrations/015_tool_docs.sql
Normal file
36
migrations/015_tool_docs.sql
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
-- Migration 015: Tool Documentation
|
||||||
|
-- Creates tool_docs table for queryable tool documentation with semantic search
|
||||||
|
-- Dependencies: 001_base_schema.sql (pgvector extension)
|
||||||
|
|
||||||
|
-- Tool documentation table
|
||||||
|
CREATE TABLE IF NOT EXISTS tool_docs (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
tool_name TEXT NOT NULL,
|
||||||
|
category TEXT NOT NULL CHECK (category IN ('mcp', 'cli', 'script', 'internal', 'deprecated')),
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
usage_example TEXT,
|
||||||
|
parameters JSONB, -- Structured parameter definitions
|
||||||
|
notes TEXT, -- Additional notes, gotchas, tips
|
||||||
|
tags TEXT[], -- Searchable tags (e.g., ['backup', 'database', 'postgresql'])
|
||||||
|
source_file TEXT, -- Original source file (TOOLS.md, script path, etc.)
|
||||||
|
embedding vector(1024), -- Semantic search embedding
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes for fast lookups
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tool_docs_name ON tool_docs(tool_name);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tool_docs_category ON tool_docs(category);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tool_docs_tags ON tool_docs USING gin(tags);
|
||||||
|
|
||||||
|
-- HNSW index for semantic similarity search
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tool_docs_embedding ON tool_docs USING hnsw (embedding vector_cosine_ops);
|
||||||
|
|
||||||
|
-- Full-text search on title + description
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tool_docs_fts ON tool_docs
|
||||||
|
USING gin(to_tsvector('english', title || ' ' || description || ' ' || COALESCE(notes, '')));
|
||||||
|
|
||||||
|
-- Record migration
|
||||||
|
INSERT INTO schema_migrations (version) VALUES ('015_tool_docs')
|
||||||
|
ON CONFLICT (version) DO NOTHING;
|
||||||
39
src/index.ts
39
src/index.ts
@@ -38,6 +38,7 @@ import {
|
|||||||
componentGraph,
|
componentGraph,
|
||||||
} from './tools/impact.js';
|
} from './tools/impact.js';
|
||||||
import { memoryAdd, memorySearch, memoryList, memoryContext } from './tools/memories.js';
|
import { memoryAdd, memorySearch, memoryList, memoryContext } from './tools/memories.js';
|
||||||
|
import { toolDocAdd, toolDocSearch, toolDocGet, toolDocList, toolDocExport } from './tools/tool-docs.js';
|
||||||
import {
|
import {
|
||||||
sessionStart,
|
sessionStart,
|
||||||
sessionUpdate,
|
sessionUpdate,
|
||||||
@@ -374,6 +375,44 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|||||||
result = await memoryContext(a.project, a.task_description);
|
result = await memoryContext(a.project, a.task_description);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// Tool Documentation
|
||||||
|
case 'tool_doc_add':
|
||||||
|
result = await toolDocAdd({
|
||||||
|
tool_name: a.tool_name,
|
||||||
|
category: a.category,
|
||||||
|
title: a.title,
|
||||||
|
description: a.description,
|
||||||
|
usage_example: a.usage_example,
|
||||||
|
parameters: a.parameters,
|
||||||
|
notes: a.notes,
|
||||||
|
tags: a.tags,
|
||||||
|
source_file: a.source_file,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'tool_doc_search':
|
||||||
|
result = await toolDocSearch({
|
||||||
|
query: a.query,
|
||||||
|
category: a.category,
|
||||||
|
tags: a.tags,
|
||||||
|
limit: a.limit,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'tool_doc_get':
|
||||||
|
result = await toolDocGet({
|
||||||
|
tool_name: a.tool_name,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'tool_doc_list':
|
||||||
|
result = await toolDocList({
|
||||||
|
category: a.category,
|
||||||
|
tag: a.tag,
|
||||||
|
limit: a.limit,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'tool_doc_export':
|
||||||
|
result = await toolDocExport();
|
||||||
|
break;
|
||||||
|
|
||||||
// Sessions
|
// Sessions
|
||||||
case 'session_start':
|
case 'session_start':
|
||||||
result = await sessionStart({
|
result = await sessionStart({
|
||||||
|
|||||||
@@ -644,6 +644,72 @@ export const toolDefinitions = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Tool Documentation Tools
|
||||||
|
{
|
||||||
|
name: 'tool_doc_add',
|
||||||
|
description: 'Add a new tool documentation entry',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
tool_name: { type: 'string', description: 'Tool or command name' },
|
||||||
|
category: { type: 'string', enum: ['mcp', 'cli', 'script', 'internal', 'deprecated'], description: 'Tool category' },
|
||||||
|
title: { type: 'string', description: 'Short descriptive title' },
|
||||||
|
description: { type: 'string', description: 'Detailed description of what the tool does' },
|
||||||
|
usage_example: { type: 'string', description: 'Usage example (optional)' },
|
||||||
|
parameters: { type: 'object', description: 'Parameter definitions (optional)' },
|
||||||
|
notes: { type: 'string', description: 'Additional notes, gotchas, tips (optional)' },
|
||||||
|
tags: { type: 'array', items: { type: 'string' }, description: 'Searchable tags (optional)' },
|
||||||
|
source_file: { type: 'string', description: 'Original source file (optional)' },
|
||||||
|
},
|
||||||
|
required: ['tool_name', 'category', 'title', 'description'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tool_doc_search',
|
||||||
|
description: 'Search tool documentation semantically',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
query: { type: 'string', description: 'Search query' },
|
||||||
|
category: { type: 'string', enum: ['mcp', 'cli', 'script', 'internal', 'deprecated'], description: 'Filter by category (optional)' },
|
||||||
|
tags: { type: 'array', items: { type: 'string' }, description: 'Filter by tags (optional)' },
|
||||||
|
limit: { type: 'number', description: 'Max results (default: 5)' },
|
||||||
|
},
|
||||||
|
required: ['query'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tool_doc_get',
|
||||||
|
description: 'Get specific tool documentation by name',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
tool_name: { type: 'string', description: 'Tool or command name' },
|
||||||
|
},
|
||||||
|
required: ['tool_name'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tool_doc_list',
|
||||||
|
description: 'List tool documentation entries',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
category: { type: 'string', enum: ['mcp', 'cli', 'script', 'internal', 'deprecated'], description: 'Filter by category (optional)' },
|
||||||
|
tag: { type: 'string', description: 'Filter by tag (optional)' },
|
||||||
|
limit: { type: 'number', description: 'Max results (default: 20)' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tool_doc_export',
|
||||||
|
description: 'Export all tool documentation as markdown (for backup/migration)',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
// Session Management Tools
|
// Session Management Tools
|
||||||
{
|
{
|
||||||
name: 'session_start',
|
name: 'session_start',
|
||||||
|
|||||||
336
src/tools/tool-docs.ts
Normal file
336
src/tools/tool-docs.ts
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
// Tool documentation operations for queryable TOOLS.md replacement
|
||||||
|
|
||||||
|
import { query, queryOne, execute } from '../db.js';
|
||||||
|
import { getEmbedding, formatEmbedding } from '../embeddings.js';
|
||||||
|
|
||||||
|
type ToolCategory = 'mcp' | 'cli' | 'script' | 'internal' | 'deprecated';
|
||||||
|
|
||||||
|
interface ToolDoc {
|
||||||
|
id: number;
|
||||||
|
tool_name: string;
|
||||||
|
category: ToolCategory;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
usage_example: string | null;
|
||||||
|
parameters: Record<string, unknown> | null;
|
||||||
|
notes: string | null;
|
||||||
|
tags: string[];
|
||||||
|
source_file: string | null;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolDocAddArgs {
|
||||||
|
tool_name: string;
|
||||||
|
category: ToolCategory;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
usage_example?: string;
|
||||||
|
parameters?: Record<string, unknown>;
|
||||||
|
notes?: string;
|
||||||
|
tags?: string[];
|
||||||
|
source_file?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolDocSearchArgs {
|
||||||
|
query: string;
|
||||||
|
category?: ToolCategory;
|
||||||
|
tags?: string[];
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolDocGetArgs {
|
||||||
|
tool_name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolDocListArgs {
|
||||||
|
category?: ToolCategory;
|
||||||
|
tag?: string;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new tool documentation entry
|
||||||
|
*/
|
||||||
|
export async function toolDocAdd(args: ToolDocAddArgs): Promise<string> {
|
||||||
|
const {
|
||||||
|
tool_name,
|
||||||
|
category,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
usage_example,
|
||||||
|
parameters,
|
||||||
|
notes,
|
||||||
|
tags = [],
|
||||||
|
source_file
|
||||||
|
} = args;
|
||||||
|
|
||||||
|
// Generate embedding for semantic search
|
||||||
|
const embedText = `${title}. ${description}. ${tags.join(' ')}. ${notes || ''}`;
|
||||||
|
const embedding = await getEmbedding(embedText);
|
||||||
|
const embeddingValue = embedding ? formatEmbedding(embedding) : null;
|
||||||
|
|
||||||
|
if (embeddingValue) {
|
||||||
|
await execute(
|
||||||
|
`INSERT INTO tool_docs (tool_name, category, title, description, usage_example, parameters, notes, tags, source_file, embedding)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
|
||||||
|
[
|
||||||
|
tool_name,
|
||||||
|
category,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
usage_example || null,
|
||||||
|
parameters ? JSON.stringify(parameters) : null,
|
||||||
|
notes || null,
|
||||||
|
tags,
|
||||||
|
source_file || null,
|
||||||
|
embeddingValue
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await execute(
|
||||||
|
`INSERT INTO tool_docs (tool_name, category, title, description, usage_example, parameters, notes, tags, source_file)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
||||||
|
[
|
||||||
|
tool_name,
|
||||||
|
category,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
usage_example || null,
|
||||||
|
parameters ? JSON.stringify(parameters) : null,
|
||||||
|
notes || null,
|
||||||
|
tags,
|
||||||
|
source_file || null
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Added tool documentation: ${tool_name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search tool documentation semantically
|
||||||
|
*/
|
||||||
|
export async function toolDocSearch(args: ToolDocSearchArgs): Promise<string> {
|
||||||
|
const { query: searchQuery, category, tags, limit = 5 } = args;
|
||||||
|
|
||||||
|
// Generate embedding for search
|
||||||
|
const embedding = await getEmbedding(searchQuery);
|
||||||
|
|
||||||
|
if (!embedding) {
|
||||||
|
return 'Error: Could not generate embedding for search';
|
||||||
|
}
|
||||||
|
|
||||||
|
const embeddingStr = formatEmbedding(embedding);
|
||||||
|
|
||||||
|
let whereClause = 'WHERE embedding IS NOT NULL';
|
||||||
|
const params: unknown[] = [embeddingStr, limit];
|
||||||
|
let paramIndex = 3;
|
||||||
|
|
||||||
|
if (category) {
|
||||||
|
whereClause += ` AND category = $${paramIndex++}`;
|
||||||
|
params.splice(params.length - 1, 0, category);
|
||||||
|
}
|
||||||
|
if (tags && tags.length > 0) {
|
||||||
|
whereClause += ` AND tags && $${paramIndex++}`;
|
||||||
|
params.splice(params.length - 1, 0, tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
const docs = await query<ToolDoc & { similarity: number }>(
|
||||||
|
`SELECT id, tool_name, category, title, description, usage_example, notes, tags,
|
||||||
|
1 - (embedding <=> $1) as similarity
|
||||||
|
FROM tool_docs
|
||||||
|
${whereClause}
|
||||||
|
ORDER BY embedding <=> $1
|
||||||
|
LIMIT $2`,
|
||||||
|
params
|
||||||
|
);
|
||||||
|
|
||||||
|
if (docs.length === 0) {
|
||||||
|
return 'No relevant tool documentation found';
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = ['Relevant tool documentation:\n'];
|
||||||
|
for (const doc of docs) {
|
||||||
|
const sim = Math.round(doc.similarity * 100);
|
||||||
|
const tagStr = doc.tags.length > 0 ? ` [${doc.tags.join(', ')}]` : '';
|
||||||
|
lines.push(`**${doc.tool_name}** (${doc.category}, ${sim}% match)${tagStr}`);
|
||||||
|
lines.push(` ${doc.description}`);
|
||||||
|
|
||||||
|
if (doc.usage_example) {
|
||||||
|
lines.push(`\n Usage:\n \`\`\`\n ${doc.usage_example}\n \`\`\``);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc.notes) {
|
||||||
|
lines.push(` _Notes: ${doc.notes}_`);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get specific tool documentation by name
|
||||||
|
*/
|
||||||
|
export async function toolDocGet(args: ToolDocGetArgs): Promise<string> {
|
||||||
|
const { tool_name } = args;
|
||||||
|
|
||||||
|
const doc = await queryOne<ToolDoc>(
|
||||||
|
`SELECT * FROM tool_docs WHERE tool_name = $1`,
|
||||||
|
[tool_name]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!doc) {
|
||||||
|
return `Tool documentation not found: ${tool_name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
`# ${doc.tool_name}`,
|
||||||
|
`**Category:** ${doc.category}`,
|
||||||
|
`**Title:** ${doc.title}`,
|
||||||
|
'',
|
||||||
|
`**Description:**`,
|
||||||
|
doc.description,
|
||||||
|
''
|
||||||
|
];
|
||||||
|
|
||||||
|
if (doc.usage_example) {
|
||||||
|
lines.push('**Usage:**');
|
||||||
|
lines.push('```');
|
||||||
|
lines.push(doc.usage_example);
|
||||||
|
lines.push('```');
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc.parameters) {
|
||||||
|
lines.push('**Parameters:**');
|
||||||
|
lines.push('```json');
|
||||||
|
lines.push(JSON.stringify(doc.parameters, null, 2));
|
||||||
|
lines.push('```');
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc.notes) {
|
||||||
|
lines.push('**Notes:**');
|
||||||
|
lines.push(doc.notes);
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc.tags.length > 0) {
|
||||||
|
lines.push(`**Tags:** ${doc.tags.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc.source_file) {
|
||||||
|
lines.push(`**Source:** ${doc.source_file}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List tool documentation entries
|
||||||
|
*/
|
||||||
|
export async function toolDocList(args: ToolDocListArgs): Promise<string> {
|
||||||
|
const { category, tag, limit = 20 } = args;
|
||||||
|
|
||||||
|
let whereClause = 'WHERE 1=1';
|
||||||
|
const params: unknown[] = [];
|
||||||
|
let paramIndex = 1;
|
||||||
|
|
||||||
|
if (category) {
|
||||||
|
whereClause += ` AND category = $${paramIndex++}`;
|
||||||
|
params.push(category);
|
||||||
|
}
|
||||||
|
if (tag) {
|
||||||
|
whereClause += ` AND $${paramIndex++} = ANY(tags)`;
|
||||||
|
params.push(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
params.push(limit);
|
||||||
|
|
||||||
|
const docs = await query<ToolDoc>(
|
||||||
|
`SELECT tool_name, category, title, tags
|
||||||
|
FROM tool_docs
|
||||||
|
${whereClause}
|
||||||
|
ORDER BY category, tool_name
|
||||||
|
LIMIT $${paramIndex}`,
|
||||||
|
params
|
||||||
|
);
|
||||||
|
|
||||||
|
if (docs.length === 0) {
|
||||||
|
return `No tool documentation found${category ? ` for category ${category}` : ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = [`Tool Documentation${category ? ` (${category})` : ''}:\n`];
|
||||||
|
|
||||||
|
let currentCategory = '';
|
||||||
|
for (const doc of docs) {
|
||||||
|
if (doc.category !== currentCategory) {
|
||||||
|
currentCategory = doc.category;
|
||||||
|
lines.push(`\n**${currentCategory.toUpperCase()}:**`);
|
||||||
|
}
|
||||||
|
const tagStr = doc.tags.length > 0 ? ` [${doc.tags.join(', ')}]` : '';
|
||||||
|
lines.push(` • ${doc.tool_name}: ${doc.title}${tagStr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export all tool documentation as markdown (for backup/migration)
|
||||||
|
*/
|
||||||
|
export async function toolDocExport(): Promise<string> {
|
||||||
|
const docs = await query<ToolDoc>(
|
||||||
|
`SELECT * FROM tool_docs ORDER BY category, tool_name`,
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (docs.length === 0) {
|
||||||
|
return 'No tool documentation to export';
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = ['# Tool Documentation Database Export\n'];
|
||||||
|
|
||||||
|
let currentCategory = '';
|
||||||
|
for (const doc of docs) {
|
||||||
|
if (doc.category !== currentCategory) {
|
||||||
|
currentCategory = doc.category;
|
||||||
|
lines.push(`\n## ${currentCategory.toUpperCase()}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(`### ${doc.tool_name}`);
|
||||||
|
lines.push(`**${doc.title}**\n`);
|
||||||
|
lines.push(doc.description);
|
||||||
|
lines.push('');
|
||||||
|
|
||||||
|
if (doc.usage_example) {
|
||||||
|
lines.push('**Usage:**');
|
||||||
|
lines.push('```');
|
||||||
|
lines.push(doc.usage_example);
|
||||||
|
lines.push('```\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc.parameters) {
|
||||||
|
lines.push('**Parameters:**');
|
||||||
|
lines.push('```json');
|
||||||
|
lines.push(JSON.stringify(doc.parameters, null, 2));
|
||||||
|
lines.push('```\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc.notes) {
|
||||||
|
lines.push('**Notes:**');
|
||||||
|
lines.push(doc.notes);
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc.tags.length > 0) {
|
||||||
|
lines.push(`**Tags:** ${doc.tags.join(', ')}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push('---\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user