From 143587ebf1d1b44b1c8e13dcd550c3bf59b72246 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CBeeRad=E2=80=9D?= Date: Sun, 8 Feb 2026 09:59:27 +1100 Subject: [PATCH] =?UTF-8?q?feat:=20MCP=20server=20v1.1.0=20=E2=80=94=20pro?= =?UTF-8?q?active=20knowledge=20capture,=20context=20tool,=20auto-create?= =?UTF-8?q?=20DB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrite instructions to encourage proactive capture, add rah_get_context orientation tool (hub nodes, dimensions, stats), add rah_update_edge, improve tool descriptions, auto-create database on first connection. Co-Authored-By: Claude Opus 4.6 --- apps/mcp-server-standalone/index.js | 86 ++++++++++++++++--- apps/mcp-server-standalone/package.json | 2 +- .../services/nodeService.js | 41 ++++++++- .../services/sqlite-client.js | 73 ++++++++++++++-- 4 files changed, 179 insertions(+), 23 deletions(-) diff --git a/apps/mcp-server-standalone/index.js b/apps/mcp-server-standalone/index.js index d943c75..1b2376e 100644 --- a/apps/mcp-server-standalone/index.js +++ b/apps/mcp-server-standalone/index.js @@ -13,23 +13,25 @@ const dimensionService = require('./services/dimensionService'); // Server info const serverInfo = { name: 'ra-h-standalone', - version: '1.0.0' + version: '1.1.0' }; const instructions = [ - 'RA-H Knowledge Base - Direct SQLite access.', - 'Use rah_add_node to create nodes with dimensions.', - 'Use rah_search_nodes to find existing content before creating duplicates.', - 'All operations happen locally on this device.' + "RA-H is the user's personal knowledge graph — local SQLite, fully on-device.", + 'Call rah_get_context first to see what\'s in the graph.', + 'Proactively identify valuable information in conversations and offer to save it.', + 'Search before creating to avoid duplicates.', + 'Every edge needs an explanation — why does this connection exist?', + 'All data stays on this device.' ].join(' '); // Tool schemas const addNodeInputSchema = { - title: z.string().min(1).max(160).describe('Node title'), + title: z.string().min(1).max(160).describe('Clear, descriptive title'), content: z.string().max(20000).optional().describe('Node content/notes'), link: z.string().url().optional().describe('Source URL'), - description: z.string().max(2000).optional().describe('Brief description'), - dimensions: z.array(z.string()).min(1).max(5).describe('Categories/tags (1-5 required)'), + description: z.string().max(2000).optional().describe('One-sentence summary. Helps search and AI understanding.'), + dimensions: z.array(z.string()).min(1).max(5).describe('1-5 categories. Call rah_list_dimensions first to use existing ones.'), metadata: z.record(z.any()).optional().describe('Additional metadata'), chunk: z.string().max(50000).optional().describe('Full source text') }; @@ -56,9 +58,14 @@ const updateNodeInputSchema = { }; const createEdgeInputSchema = { - sourceId: z.number().int().positive().describe('Source node ID'), + sourceId: z.number().int().positive().describe("The 'subject' node (reads: source [explanation] target)"), targetId: z.number().int().positive().describe('Target node ID'), - explanation: z.string().min(1).describe('Why does this connection exist?') + explanation: z.string().min(1).describe("Human-readable explanation. Should read as a sentence: 'Alice invented this technique'") +}; + +const updateEdgeInputSchema = { + id: z.number().int().positive().describe('Edge ID'), + explanation: z.string().min(1).describe('Updated explanation for this connection') }; const queryEdgesInputSchema = { @@ -120,13 +127,45 @@ async function main() { const server = new McpServer(serverInfo, { instructions }); + // ========== CONTEXT TOOL ========== + + server.registerTool( + 'rah_get_context', + { + title: 'Get RA-H context', + description: 'Get knowledge graph overview: stats, hub nodes (most connected), dimensions, and recent activity. Call this first to understand the user\'s graph.', + inputSchema: {} + }, + async () => { + const context = nodeService.getContext(); + + // First-run welcome message + if (context.stats.nodeCount === 0) { + return { + content: [{ type: 'text', text: 'Empty knowledge graph. This is a fresh start! Suggest adding the first node about something the user is working on or interested in.' }], + structuredContent: { + ...context, + welcome: true, + suggestion: 'Ask the user what they\'re working on or interested in, then create the first node.' + } + }; + } + + const summary = `Graph: ${context.stats.nodeCount} nodes, ${context.stats.edgeCount} edges, ${context.stats.dimensionCount} dimensions.`; + return { + content: [{ type: 'text', text: summary }], + structuredContent: context + }; + } + ); + // ========== NODE TOOLS ========== server.registerTool( 'rah_add_node', { title: 'Add RA-H node', - description: 'Create a new node in the local knowledge base.', + description: 'Create a new node in the knowledge graph. Always search first (rah_search_nodes) to avoid duplicates.', inputSchema: addNodeInputSchema }, async ({ title, content, link, description, dimensions, metadata, chunk }) => { @@ -163,7 +202,7 @@ async function main() { 'rah_search_nodes', { title: 'Search RA-H nodes', - description: 'Find existing nodes by keyword search.', + description: 'Search the knowledge graph by keyword. Call before creating nodes to check for duplicates.', inputSchema: searchNodesInputSchema }, async ({ query, limit = 10, dimensions }) => { @@ -267,7 +306,7 @@ async function main() { 'rah_create_edge', { title: 'Create RA-H edge', - description: 'Create a connection between two nodes.', + description: 'Connect two nodes. Edges are the most valuable part of the graph — they represent understanding, not just proximity.', inputSchema: createEdgeInputSchema }, async ({ sourceId, targetId, explanation }) => { @@ -289,6 +328,27 @@ async function main() { } ); + server.registerTool( + 'rah_update_edge', + { + title: 'Update RA-H edge', + description: 'Update an edge explanation. Use when a connection needs a better or corrected explanation.', + inputSchema: updateEdgeInputSchema + }, + async ({ id, explanation }) => { + const edge = edgeService.updateEdge(id, { explanation: explanation.trim() }); + + return { + content: [{ type: 'text', text: `Updated edge #${id}` }], + structuredContent: { + success: true, + edgeId: edge.id, + message: `Updated edge #${id}` + } + }; + } + ); + server.registerTool( 'rah_query_edges', { diff --git a/apps/mcp-server-standalone/package.json b/apps/mcp-server-standalone/package.json index c0a04b8..38d4845 100644 --- a/apps/mcp-server-standalone/package.json +++ b/apps/mcp-server-standalone/package.json @@ -1,6 +1,6 @@ { "name": "ra-h-mcp-server", - "version": "1.0.0", + "version": "1.1.0", "description": "Connect Claude Code/Desktop to your RA-H knowledge base. Direct SQLite access - no web app required.", "main": "index.js", "bin": { diff --git a/apps/mcp-server-standalone/services/nodeService.js b/apps/mcp-server-standalone/services/nodeService.js index 668e5c5..8d3a8b0 100644 --- a/apps/mcp-server-standalone/services/nodeService.js +++ b/apps/mcp-server-standalone/services/nodeService.js @@ -241,11 +241,50 @@ function getNodeCount() { return Number(rows[0].count); } +/** + * Get knowledge graph context overview. + * Returns stats, hub nodes, dimensions, and recent activity. + */ +function getContext() { + const nodeCount = query('SELECT COUNT(*) as count FROM nodes')[0].count; + const edgeCount = query('SELECT COUNT(*) as count FROM edges')[0].count; + + const dimensionService = require('./dimensionService'); + const dimensions = dimensionService.getDimensions(); + + const recentNodes = query(` + SELECT n.id, n.title, n.description, + GROUP_CONCAT(nd.dimension) as dimensions + FROM nodes n + LEFT JOIN node_dimensions nd ON n.id = nd.node_id + GROUP BY n.id + ORDER BY n.created_at DESC + LIMIT 5 + `); + + const hubNodes = query(` + SELECT n.id, n.title, n.description, COUNT(e.id) as edge_count + FROM nodes n + LEFT JOIN edges e ON n.id = e.from_node_id OR n.id = e.to_node_id + GROUP BY n.id + ORDER BY edge_count DESC + LIMIT 5 + `); + + return { + stats: { nodeCount, edgeCount, dimensionCount: dimensions.length }, + dimensions, + recentNodes, + hubNodes + }; +} + module.exports = { getNodes, getNodeById, createNode, updateNode, deleteNode, - getNodeCount + getNodeCount, + getContext }; diff --git a/apps/mcp-server-standalone/services/sqlite-client.js b/apps/mcp-server-standalone/services/sqlite-client.js index 21c7bea..0a8332d 100644 --- a/apps/mcp-server-standalone/services/sqlite-client.js +++ b/apps/mcp-server-standalone/services/sqlite-client.js @@ -38,16 +38,73 @@ function initDatabase() { const dbPath = getDatabasePath(); - // Check if database exists + // Auto-create database if it doesn't exist if (!fs.existsSync(dbPath)) { - throw new Error( - `Database not found at: ${dbPath}\n\n` + - `Have you run RA-H at least once? The database is created when you first launch the app.\n\n` + - `If your database is in a different location, set the RAH_DB_PATH environment variable.` - ); - } + fs.mkdirSync(path.dirname(dbPath), { recursive: true }); + db = new Database(dbPath); + console.error('[RA-H] Creating new database at:', dbPath); - db = new Database(dbPath); + // Create core schema + db.exec(` + CREATE TABLE IF NOT EXISTS nodes ( + id INTEGER PRIMARY KEY, + title TEXT, + description TEXT, + content TEXT, + link TEXT, + type TEXT, + created_at TEXT, + updated_at TEXT, + metadata TEXT, + chunk TEXT, + embedding BLOB, + embedding_updated_at TEXT, + embedding_text TEXT, + chunk_status TEXT DEFAULT 'not_chunked', + is_pinned INTEGER DEFAULT 0 + ); + + CREATE TABLE IF NOT EXISTS edges ( + id INTEGER PRIMARY KEY, + from_node_id INTEGER NOT NULL, + to_node_id INTEGER NOT NULL, + source TEXT, + created_at TEXT, + context TEXT, + user_feedback INTEGER, + FOREIGN KEY (from_node_id) REFERENCES nodes(id) ON DELETE CASCADE, + FOREIGN KEY (to_node_id) REFERENCES nodes(id) ON DELETE CASCADE + ); + CREATE INDEX IF NOT EXISTS idx_edges_from ON edges(from_node_id); + CREATE INDEX IF NOT EXISTS idx_edges_to ON edges(to_node_id); + + CREATE TABLE IF NOT EXISTS node_dimensions ( + node_id INTEGER NOT NULL, + dimension TEXT NOT NULL, + PRIMARY KEY (node_id, dimension), + FOREIGN KEY (node_id) REFERENCES nodes(id) ON DELETE CASCADE + ) WITHOUT ROWID; + CREATE INDEX IF NOT EXISTS idx_dim_by_dimension ON node_dimensions(dimension, node_id); + CREATE INDEX IF NOT EXISTS idx_dim_by_node ON node_dimensions(node_id, dimension); + + CREATE TABLE IF NOT EXISTS dimensions ( + name TEXT PRIMARY KEY, + is_priority INTEGER DEFAULT 0, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP + ); + + -- Seed default dimensions + INSERT OR IGNORE INTO dimensions (name, is_priority) VALUES ('research', 1); + INSERT OR IGNORE INTO dimensions (name, is_priority) VALUES ('ideas', 1); + INSERT OR IGNORE INTO dimensions (name, is_priority) VALUES ('projects', 1); + INSERT OR IGNORE INTO dimensions (name, is_priority) VALUES ('memory', 1); + INSERT OR IGNORE INTO dimensions (name, is_priority) VALUES ('preferences', 1); + `); + + console.error('[RA-H] Database created successfully'); + } else { + db = new Database(dbPath); + } // Configure SQLite for performance db.pragma('journal_mode = WAL');