feat: sync shared skills contract and graph-first discipline
This commit is contained in:
+159
-19
@@ -44,12 +44,14 @@ let logger = (message) => console.log(`[mcp] ${message}`);
|
||||
|
||||
const instructions = [
|
||||
'RA-H is a personal knowledge graph — local-first, vendor-neutral.',
|
||||
'Core concepts: nodes (knowledge units) and edges (connections with explanations).',
|
||||
'The graph is the default working memory for substantive turns.',
|
||||
'Core concepts: nodes (knowledge units), edges (connections with explanations), and shared editable skills.',
|
||||
'If the user is trying to find a specific existing node, use rah_search_nodes first.',
|
||||
'If graph context would help with a broader task, use rah_retrieve_query_context.',
|
||||
'If graph context would help with a broader task, use rah_retrieve_query_context before answering.',
|
||||
'Use rah_get_context only when high-level graph orientation would actually help.',
|
||||
'Do not keep re-running retrieval if you already have enough relevant graph context in play.',
|
||||
'Search before creating: use rah_search_nodes to check if content already exists.',
|
||||
'Search before creating, and prefer rah_update_node when the artifact is clearly the same thing.',
|
||||
'Use rah_list_skills and rah_read_skill for non-trivial workflows that need operating doctrine. Use rah_write_skill and rah_delete_skill when the user explicitly wants to change that shared skill set.',
|
||||
'Only suggest saving durable knowledge when it is unusually valuable. Keep the ask brief, for example: Add "X" as a node?',
|
||||
'Do not create edges autonomously. Surface likely edge candidates briefly, then call edge-write tools only after the user explicitly confirms.',
|
||||
'Every edge needs an explanation: why does this connection exist?',
|
||||
@@ -151,7 +153,8 @@ const updateNodeInputSchema = {
|
||||
source: z.string().optional().describe('Canonical source text for embedding.'),
|
||||
link: z.string().optional().describe('New link'),
|
||||
metadata: z.record(z.any()).optional().describe('Metadata patch. This now merges with existing metadata. Prefer canonical keys: type, state, captured_method, captured_by, source_metadata.')
|
||||
}).describe('Fields to update')
|
||||
}).describe('Fields to update'),
|
||||
source_update_basis: z.string().optional().describe('When rewriting source on a node that already has source text, include a short exact excerpt from the current source you inspected first.')
|
||||
};
|
||||
|
||||
const updateNodeOutputSchema = {
|
||||
@@ -172,12 +175,56 @@ const getNodesOutputSchema = {
|
||||
id: z.number(),
|
||||
title: z.string(),
|
||||
source: z.string().nullable(),
|
||||
description: z.string().nullable(),
|
||||
link: z.string().nullable(),
|
||||
updated_at: z.string()
|
||||
})
|
||||
)
|
||||
};
|
||||
|
||||
const listSkillsOutputSchema = {
|
||||
count: z.number(),
|
||||
skills: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
immutable: z.boolean().optional(),
|
||||
})
|
||||
)
|
||||
};
|
||||
|
||||
const readSkillInputSchema = {
|
||||
name: z.string().min(1).describe('Skill name')
|
||||
};
|
||||
|
||||
const readSkillOutputSchema = {
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
immutable: z.boolean(),
|
||||
content: z.string()
|
||||
};
|
||||
|
||||
const writeSkillInputSchema = {
|
||||
name: z.string().min(1).describe('Skill name to create or update'),
|
||||
content: z.string().min(1).describe('Full markdown content, including frontmatter when needed')
|
||||
};
|
||||
|
||||
const writeSkillOutputSchema = {
|
||||
success: z.boolean(),
|
||||
name: z.string(),
|
||||
message: z.string()
|
||||
};
|
||||
|
||||
const deleteSkillInputSchema = {
|
||||
name: z.string().min(1).describe('Skill name to delete')
|
||||
};
|
||||
|
||||
const deleteSkillOutputSchema = {
|
||||
success: z.boolean(),
|
||||
name: z.string(),
|
||||
message: z.string()
|
||||
};
|
||||
|
||||
// rah_create_edge schemas
|
||||
const createEdgeInputSchema = {
|
||||
sourceId: z.number().int().positive().describe('Source node ID'),
|
||||
@@ -327,7 +374,7 @@ mcpServer.registerTool(
|
||||
'rah_add_node',
|
||||
{
|
||||
title: 'Add RA-H node',
|
||||
description: 'Create a new node in the local RA-H knowledge base after you have already decided a net-new write is correct. If the user explicitly asked to save or import something and the target artifact is clear, write after duplicate/update checks. If you are only suggesting a save, propose the node first and wait for confirmation.',
|
||||
description: 'Create a new node after you have already decided a net-new write is correct. Search first with rah_search_nodes, and prefer rah_update_node if the artifact is clearly the same thing. If the user explicitly asked to save or import something and the target artifact is clear, write after duplicate/update checks. If you are only suggesting a save, propose the node first and wait for confirmation.',
|
||||
inputSchema: addNodeInputSchema,
|
||||
outputSchema: addNodeOutputSchema
|
||||
},
|
||||
@@ -363,7 +410,7 @@ mcpServer.registerTool(
|
||||
'rah_search_nodes',
|
||||
{
|
||||
title: 'Search RA-H nodes',
|
||||
description: 'Find existing RA-H entries that mention a topic before adding new ones. Use this first when the user is trying to locate a specific node they already created. For full current-turn grounding of a substantive request, prefer rah_retrieve_query_context.',
|
||||
description: 'Find existing RA-H entries that mention a topic before adding new ones. Use this first for direct lookup, duplicate checks, or when the user seems to be referring to an existing node. For broader current-turn grounding, prefer rah_retrieve_query_context.',
|
||||
inputSchema: searchNodesInputSchema,
|
||||
outputSchema: searchNodesOutputSchema
|
||||
},
|
||||
@@ -406,7 +453,7 @@ mcpServer.registerTool(
|
||||
'rah_retrieve_query_context',
|
||||
{
|
||||
title: 'Retrieve RA-H query context',
|
||||
description: 'Given the raw user query plus optional focused node state, retrieve the most relevant graph context for the current turn. It starts with direct graph search and broadens only if useful. Use this when graph context could help answer or complete a broader task. For explicit node lookup, use rah_search_nodes.',
|
||||
description: 'Given the raw user query plus optional focused node state, retrieve the most relevant graph context for the current turn. Use this when graph context could help answer, plan, or complete a broader task. For explicit node lookup or duplicate checks, use rah_search_nodes first.',
|
||||
inputSchema: retrieveQueryContextInputSchema,
|
||||
outputSchema: retrieveQueryContextOutputSchema
|
||||
},
|
||||
@@ -431,11 +478,11 @@ mcpServer.registerTool(
|
||||
'rah_update_node',
|
||||
{
|
||||
title: 'Update RA-H node',
|
||||
description: 'Update an existing node when it is clearly the same artifact and a net-new node would be redundant. Explicit user-directed updates can proceed once the target node is clear.',
|
||||
description: 'Update an existing node when it is clearly the same artifact and a net-new node would be redundant. Inspect current state with rah_get_nodes first when accuracy matters. When rewriting source on a node that already has source text, inspect that source first and include source_update_basis as a short exact excerpt you actually read.',
|
||||
inputSchema: updateNodeInputSchema,
|
||||
outputSchema: updateNodeOutputSchema
|
||||
},
|
||||
async ({ id, updates }) => {
|
||||
async ({ id, updates, source_update_basis }) => {
|
||||
if (!updates || Object.keys(updates).length === 0) {
|
||||
throw new McpError(ErrorCode.InvalidParams, 'At least one field must be provided in updates.');
|
||||
}
|
||||
@@ -453,7 +500,7 @@ mcpServer.registerTool(
|
||||
|
||||
const result = await callRaHApi(`/api/nodes/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(mappedUpdates)
|
||||
body: JSON.stringify({ ...mappedUpdates, source_update_basis })
|
||||
});
|
||||
|
||||
const node = result.node || result.data;
|
||||
@@ -472,7 +519,7 @@ mcpServer.registerTool(
|
||||
'rah_get_nodes',
|
||||
{
|
||||
title: 'Get RA-H nodes by ID',
|
||||
description: 'Load full node records by their IDs.',
|
||||
description: 'Load node records by ID, including current source text and description. Use this before rewriting source or when a focused-node excerpt is not enough.',
|
||||
inputSchema: getNodesInputSchema,
|
||||
outputSchema: getNodesOutputSchema
|
||||
},
|
||||
@@ -491,6 +538,7 @@ mcpServer.registerTool(
|
||||
id: result.node.id,
|
||||
title: result.node.title,
|
||||
source: result.node.source ?? null,
|
||||
description: result.node.description ?? null,
|
||||
link: result.node.link ?? null,
|
||||
updated_at: result.node.updated_at
|
||||
});
|
||||
@@ -514,7 +562,7 @@ mcpServer.registerTool(
|
||||
'rah_create_edge',
|
||||
{
|
||||
title: 'Create RA-H edge',
|
||||
description: 'Create a connection between two nodes only after the user has explicitly confirmed the proposed relationship.',
|
||||
description: 'Create a connection between two nodes only after the user has explicitly confirmed the proposed relationship. Check existing edges first when you are not already sure the relationship is new.',
|
||||
inputSchema: createEdgeInputSchema,
|
||||
outputSchema: createEdgeOutputSchema
|
||||
},
|
||||
@@ -587,7 +635,7 @@ mcpServer.registerTool(
|
||||
'rah_update_edge',
|
||||
{
|
||||
title: 'Update RA-H edge',
|
||||
description: 'Update an existing edge connection only after the user explicitly confirmed the corrected relationship.',
|
||||
description: 'Update an existing edge connection only after the user explicitly confirmed the corrected relationship. Use this when the connection already exists and only the explanation needs correction.',
|
||||
inputSchema: updateEdgeInputSchema,
|
||||
outputSchema: updateEdgeOutputSchema
|
||||
},
|
||||
@@ -614,6 +662,94 @@ mcpServer.registerTool(
|
||||
}
|
||||
);
|
||||
|
||||
mcpServer.registerTool(
|
||||
'rah_list_skills',
|
||||
{
|
||||
title: 'List RA-H skills',
|
||||
description: 'List the shared skills available to internal and external RA-H agents. Use this to see the current operating doctrine before reading or editing a specific skill.',
|
||||
inputSchema: {},
|
||||
outputSchema: listSkillsOutputSchema
|
||||
},
|
||||
async () => {
|
||||
const result = await callRaHApi('/api/skills', { method: 'GET' });
|
||||
const skills = Array.isArray(result.data) ? result.data : [];
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: `Found ${skills.length} skill(s).` }],
|
||||
structuredContent: {
|
||||
count: skills.length,
|
||||
skills
|
||||
}
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
mcpServer.registerTool(
|
||||
'rah_read_skill',
|
||||
{
|
||||
title: 'Read RA-H skill',
|
||||
description: 'Read one shared RA-H skill by name. Use this before executing a non-trivial workflow that matches the skill trigger.',
|
||||
inputSchema: readSkillInputSchema,
|
||||
outputSchema: readSkillOutputSchema
|
||||
},
|
||||
async ({ name }) => {
|
||||
const result = await callRaHApi(`/api/skills/${encodeURIComponent(name)}`, { method: 'GET' });
|
||||
return {
|
||||
content: [{ type: 'text', text: result.data.content }],
|
||||
structuredContent: result.data
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
mcpServer.registerTool(
|
||||
'rah_write_skill',
|
||||
{
|
||||
title: 'Write RA-H skill',
|
||||
description: 'Create or update a shared RA-H skill when the user explicitly wants to change the doctrine surface. Content should be the full markdown body for that skill.',
|
||||
inputSchema: writeSkillInputSchema,
|
||||
outputSchema: writeSkillOutputSchema
|
||||
},
|
||||
async ({ name, content }) => {
|
||||
const result = await callRaHApi('/api/skills', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name, content })
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: `Skill "${name}" saved.` }],
|
||||
structuredContent: {
|
||||
success: true,
|
||||
name,
|
||||
message: result.message || `Skill "${name}" saved.`,
|
||||
}
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
mcpServer.registerTool(
|
||||
'rah_delete_skill',
|
||||
{
|
||||
title: 'Delete RA-H skill',
|
||||
description: 'Delete a shared RA-H skill when the user explicitly wants it removed from the shared skill set.',
|
||||
inputSchema: deleteSkillInputSchema,
|
||||
outputSchema: deleteSkillOutputSchema
|
||||
},
|
||||
async ({ name }) => {
|
||||
const result = await callRaHApi(`/api/skills/${encodeURIComponent(name)}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: `Skill "${name}" deleted.` }],
|
||||
structuredContent: {
|
||||
success: true,
|
||||
name,
|
||||
message: result.message || `Skill "${name}" deleted.`,
|
||||
}
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
mcpServer.registerTool(
|
||||
'rah_search_embeddings',
|
||||
{
|
||||
@@ -734,12 +870,16 @@ mcpServer.registerTool(
|
||||
'rah_get_context',
|
||||
{
|
||||
title: 'Get RA-H context',
|
||||
description: 'Get orientation context: high-level graph state, hub nodes, stats, and available guides. Use this for orientation only, not as the default retrieval path for substantive requests.',
|
||||
description: 'Get orientation context: high-level graph state, hub nodes, stats, and available skills. Use this for orientation only, not as the default retrieval path for substantive requests.',
|
||||
inputSchema: {},
|
||||
outputSchema: {
|
||||
stats: z.object({ nodeCount: z.number(), edgeCount: z.number() }),
|
||||
hubNodes: z.array(z.object({ id: z.number(), title: z.string(), description: z.string().nullable(), edgeCount: z.number() })),
|
||||
guides: z.array(z.string())
|
||||
skills: z.array(z.object({
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
immutable: z.boolean().optional(),
|
||||
}))
|
||||
}
|
||||
},
|
||||
async () => {
|
||||
@@ -748,8 +888,8 @@ mcpServer.registerTool(
|
||||
id: n.id, title: n.title, description: n.description ?? null, edgeCount: n.edge_count ?? 0
|
||||
})) : [];
|
||||
|
||||
const guideResult = await callRaHApi('/api/guides', { method: 'GET' });
|
||||
const guides = Array.isArray(guideResult.data) ? guideResult.data.map(g => g.name) : [];
|
||||
const skillResult = await callRaHApi('/api/skills', { method: 'GET' });
|
||||
const skills = Array.isArray(skillResult.data) ? skillResult.data : [];
|
||||
|
||||
const stats = { nodeCount: 0, edgeCount: 0 };
|
||||
try {
|
||||
@@ -763,8 +903,8 @@ mcpServer.registerTool(
|
||||
} catch { /* use defaults */ }
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: `Knowledge graph: ${stats.nodeCount} nodes, ${stats.edgeCount} edges, ${hubNodes.length} hub nodes, ${guides.length} guides available.` }],
|
||||
structuredContent: { stats, hubNodes, guides }
|
||||
content: [{ type: 'text', text: `Knowledge graph: ${stats.nodeCount} nodes, ${stats.edgeCount} edges, ${hubNodes.length} hub nodes, ${skills.length} skills available.` }],
|
||||
structuredContent: { stats, hubNodes, skills }
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user