feat: standalone MCP server (ra-h-mcp-server)
- Added apps/mcp-server-standalone/ with direct SQLite access - Works without RA-OS app running - Install via: npx ra-h-mcp-server - Updated docs/8_mcp.md with setup instructions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
5c480b01d6
commit
bcea76800b
@@ -0,0 +1,206 @@
|
||||
'use strict';
|
||||
|
||||
const { query, getDb } = require('./sqlite-client');
|
||||
|
||||
/**
|
||||
* Get all edges.
|
||||
*/
|
||||
function getEdges(filters = {}) {
|
||||
const { nodeId, limit = 50 } = filters;
|
||||
|
||||
let sql = 'SELECT * FROM edges';
|
||||
const params = [];
|
||||
|
||||
if (nodeId) {
|
||||
sql += ' WHERE from_node_id = ? OR to_node_id = ?';
|
||||
params.push(nodeId, nodeId);
|
||||
}
|
||||
|
||||
sql += ' ORDER BY created_at DESC LIMIT ?';
|
||||
params.push(limit);
|
||||
|
||||
const rows = query(sql, params);
|
||||
|
||||
return rows.map(row => ({
|
||||
...row,
|
||||
context: parseContext(row.context)
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get edge by ID.
|
||||
*/
|
||||
function getEdgeById(id) {
|
||||
const rows = query('SELECT * FROM edges WHERE id = ?', [id]);
|
||||
if (rows.length === 0) return null;
|
||||
|
||||
const row = rows[0];
|
||||
return {
|
||||
...row,
|
||||
context: parseContext(row.context)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new edge.
|
||||
* Note: This is a simplified version without AI inference.
|
||||
* The main app handles edge type inference.
|
||||
*/
|
||||
function createEdge(edgeData) {
|
||||
const { from_node_id, to_node_id, explanation, source = 'mcp' } = edgeData;
|
||||
const now = new Date().toISOString();
|
||||
const db = getDb();
|
||||
|
||||
if (!from_node_id || !to_node_id) {
|
||||
throw new Error('from_node_id and to_node_id are required');
|
||||
}
|
||||
|
||||
if (!explanation || !explanation.trim()) {
|
||||
throw new Error('Edge explanation is required');
|
||||
}
|
||||
|
||||
// Simple context without AI inference
|
||||
// The main app can re-infer types when it loads
|
||||
const context = {
|
||||
type: 'related_to',
|
||||
confidence: 0.5,
|
||||
inferred_at: now,
|
||||
explanation: explanation.trim(),
|
||||
created_via: 'mcp'
|
||||
};
|
||||
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO edges (from_node_id, to_node_id, context, source, created_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = stmt.run(
|
||||
from_node_id,
|
||||
to_node_id,
|
||||
JSON.stringify(context),
|
||||
source,
|
||||
now
|
||||
);
|
||||
|
||||
const edgeId = Number(result.lastInsertRowid);
|
||||
return getEdgeById(edgeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an edge.
|
||||
*/
|
||||
function updateEdge(id, updates) {
|
||||
const { explanation, context: contextUpdates } = updates;
|
||||
const db = getDb();
|
||||
|
||||
const existing = getEdgeById(id);
|
||||
if (!existing) {
|
||||
throw new Error(`Edge with ID ${id} not found`);
|
||||
}
|
||||
|
||||
// If explanation changed, update context
|
||||
if (explanation && explanation.trim()) {
|
||||
const now = new Date().toISOString();
|
||||
const newContext = {
|
||||
...existing.context,
|
||||
explanation: explanation.trim(),
|
||||
inferred_at: now,
|
||||
created_via: 'mcp'
|
||||
};
|
||||
|
||||
const stmt = db.prepare('UPDATE edges SET context = ? WHERE id = ?');
|
||||
stmt.run(JSON.stringify(newContext), id);
|
||||
} else if (contextUpdates) {
|
||||
const newContext = {
|
||||
...existing.context,
|
||||
...contextUpdates
|
||||
};
|
||||
const stmt = db.prepare('UPDATE edges SET context = ? WHERE id = ?');
|
||||
stmt.run(JSON.stringify(newContext), id);
|
||||
}
|
||||
|
||||
return getEdgeById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an edge.
|
||||
*/
|
||||
function deleteEdge(id) {
|
||||
const result = query('DELETE FROM edges WHERE id = ?', [id]);
|
||||
if (result.changes === 0) {
|
||||
throw new Error(`Edge with ID ${id} not found`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connections for a node.
|
||||
*/
|
||||
function getNodeConnections(nodeId) {
|
||||
const sql = `
|
||||
SELECT
|
||||
e.*,
|
||||
CASE
|
||||
WHEN e.from_node_id = ? THEN n_to.id
|
||||
ELSE n_from.id
|
||||
END as connected_node_id,
|
||||
CASE
|
||||
WHEN e.from_node_id = ? THEN n_to.title
|
||||
ELSE n_from.title
|
||||
END as connected_node_title,
|
||||
CASE
|
||||
WHEN e.from_node_id = ? THEN n_to.description
|
||||
ELSE n_from.description
|
||||
END as connected_node_description
|
||||
FROM edges e
|
||||
LEFT JOIN nodes n_from ON e.from_node_id = n_from.id
|
||||
LEFT JOIN nodes n_to ON e.to_node_id = n_to.id
|
||||
WHERE e.from_node_id = ? OR e.to_node_id = ?
|
||||
ORDER BY e.created_at DESC
|
||||
`;
|
||||
|
||||
const rows = query(sql, [nodeId, nodeId, nodeId, nodeId, nodeId]);
|
||||
|
||||
return rows.map(row => ({
|
||||
edgeId: row.id,
|
||||
from_node_id: row.from_node_id,
|
||||
to_node_id: row.to_node_id,
|
||||
context: parseContext(row.context),
|
||||
connected_node: {
|
||||
id: row.connected_node_id,
|
||||
title: row.connected_node_title,
|
||||
description: row.connected_node_description
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get edge count.
|
||||
*/
|
||||
function getEdgeCount() {
|
||||
const rows = query('SELECT COUNT(*) as count FROM edges');
|
||||
return Number(rows[0].count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse context JSON safely.
|
||||
*/
|
||||
function parseContext(context) {
|
||||
if (!context) return null;
|
||||
if (typeof context === 'object') return context;
|
||||
try {
|
||||
return JSON.parse(context);
|
||||
} catch {
|
||||
return context;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getEdges,
|
||||
getEdgeById,
|
||||
createEdge,
|
||||
updateEdge,
|
||||
deleteEdge,
|
||||
getNodeConnections,
|
||||
getEdgeCount
|
||||
};
|
||||
Reference in New Issue
Block a user