feat: MCP server v1.1.0 — proactive knowledge capture, context tool, auto-create DB

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 <noreply@anthropic.com>
This commit is contained in:
“BeeRad”
2026-02-08 09:59:27 +11:00
co-authored by Claude Opus 4.6
parent 3b04a1bad0
commit 143587ebf1
4 changed files with 179 additions and 23 deletions
+73 -13
View File
@@ -13,23 +13,25 @@ const dimensionService = require('./services/dimensionService');
// Server info // Server info
const serverInfo = { const serverInfo = {
name: 'ra-h-standalone', name: 'ra-h-standalone',
version: '1.0.0' version: '1.1.0'
}; };
const instructions = [ const instructions = [
'RA-H Knowledge Base - Direct SQLite access.', "RA-H is the user's personal knowledge graph — local SQLite, fully on-device.",
'Use rah_add_node to create nodes with dimensions.', 'Call rah_get_context first to see what\'s in the graph.',
'Use rah_search_nodes to find existing content before creating duplicates.', 'Proactively identify valuable information in conversations and offer to save it.',
'All operations happen locally on this device.' 'Search before creating to avoid duplicates.',
'Every edge needs an explanation — why does this connection exist?',
'All data stays on this device.'
].join(' '); ].join(' ');
// Tool schemas // Tool schemas
const addNodeInputSchema = { 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'), content: z.string().max(20000).optional().describe('Node content/notes'),
link: z.string().url().optional().describe('Source URL'), link: z.string().url().optional().describe('Source URL'),
description: z.string().max(2000).optional().describe('Brief description'), 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('Categories/tags (1-5 required)'), 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'), metadata: z.record(z.any()).optional().describe('Additional metadata'),
chunk: z.string().max(50000).optional().describe('Full source text') chunk: z.string().max(50000).optional().describe('Full source text')
}; };
@@ -56,9 +58,14 @@ const updateNodeInputSchema = {
}; };
const createEdgeInputSchema = { 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'), 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 = { const queryEdgesInputSchema = {
@@ -120,13 +127,45 @@ async function main() {
const server = new McpServer(serverInfo, { instructions }); 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 ========== // ========== NODE TOOLS ==========
server.registerTool( server.registerTool(
'rah_add_node', 'rah_add_node',
{ {
title: 'Add RA-H 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 inputSchema: addNodeInputSchema
}, },
async ({ title, content, link, description, dimensions, metadata, chunk }) => { async ({ title, content, link, description, dimensions, metadata, chunk }) => {
@@ -163,7 +202,7 @@ async function main() {
'rah_search_nodes', 'rah_search_nodes',
{ {
title: 'Search RA-H 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 inputSchema: searchNodesInputSchema
}, },
async ({ query, limit = 10, dimensions }) => { async ({ query, limit = 10, dimensions }) => {
@@ -267,7 +306,7 @@ async function main() {
'rah_create_edge', 'rah_create_edge',
{ {
title: 'Create RA-H 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 inputSchema: createEdgeInputSchema
}, },
async ({ sourceId, targetId, explanation }) => { 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( server.registerTool(
'rah_query_edges', 'rah_query_edges',
{ {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ra-h-mcp-server", "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.", "description": "Connect Claude Code/Desktop to your RA-H knowledge base. Direct SQLite access - no web app required.",
"main": "index.js", "main": "index.js",
"bin": { "bin": {
@@ -241,11 +241,50 @@ function getNodeCount() {
return Number(rows[0].count); 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 = { module.exports = {
getNodes, getNodes,
getNodeById, getNodeById,
createNode, createNode,
updateNode, updateNode,
deleteNode, deleteNode,
getNodeCount getNodeCount,
getContext
}; };
@@ -38,16 +38,73 @@ function initDatabase() {
const dbPath = getDatabasePath(); const dbPath = getDatabasePath();
// Check if database exists // Auto-create database if it doesn't exist
if (!fs.existsSync(dbPath)) { if (!fs.existsSync(dbPath)) {
throw new Error( fs.mkdirSync(path.dirname(dbPath), { recursive: true });
`Database not found at: ${dbPath}\n\n` + db = new Database(dbPath);
`Have you run RA-H at least once? The database is created when you first launch the app.\n\n` + console.error('[RA-H] Creating new database at:', dbPath);
`If your database is in a different location, set the RAH_DB_PATH environment variable.`
);
}
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 // Configure SQLite for performance
db.pragma('journal_mode = WAL'); db.pragma('journal_mode = WAL');