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:
co-authored by
Claude Opus 4.6
parent
3b04a1bad0
commit
143587ebf1
@@ -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,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` +
|
|
||||||
`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.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
db = new Database(dbPath);
|
db = new Database(dbPath);
|
||||||
|
console.error('[RA-H] Creating new database at:', 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');
|
||||||
|
|||||||
Reference in New Issue
Block a user