@learningnodes/elen-mcp 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ import { z } from 'zod';
2
+ import type { Elen } from '@learningnodes/elen';
3
+ import { commitInputSchema } from './commit';
4
+
5
+ export const elenSupersedeTool = {
6
+ name: 'elen_supersede',
7
+ description: 'Explicitly revise and supersede an older decision with a new one.',
8
+ inputSchema: {
9
+ type: 'object',
10
+ properties: {
11
+ oldDecisionId: { type: 'string', description: 'The old decision_id to supersede' },
12
+ question: { type: 'string', description: 'The question or problem statement' },
13
+ domain: { type: 'string', description: 'Domain of decision' },
14
+ decisionText: { type: 'string', description: 'The new revised answer/decision' },
15
+ constraints: {
16
+ type: 'array',
17
+ items: { type: 'string' },
18
+ description: 'Plain-text constraint rules'
19
+ },
20
+ refs: {
21
+ type: 'array',
22
+ items: { type: 'string' }
23
+ }
24
+ },
25
+ required: ['oldDecisionId', 'question', 'domain', 'decisionText', 'constraints']
26
+ }
27
+ };
28
+
29
+ export const supersedeInputSchema = commitInputSchema.extend({
30
+ oldDecisionId: z.string().min(1)
31
+ });
32
+
33
+ export async function handleSupersede(elen: Elen, args: unknown): Promise<unknown> {
34
+ const parsed = supersedeInputSchema.parse(args);
35
+ const result = await elen.supersedeDecision(parsed.oldDecisionId, {
36
+ question: parsed.question,
37
+ domain: parsed.domain,
38
+ decisionText: parsed.decisionText,
39
+ constraints: parsed.constraints,
40
+ refs: parsed.refs
41
+ });
42
+ return result;
43
+ }
@@ -0,0 +1,43 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { parseCliArgs } from '../src/index';
3
+ import { defaultStoragePath, routeToolCall } from '../src/server';
4
+
5
+ describe('MCP server CLI', () => {
6
+ it('parses --agent-id and --storage args', () => {
7
+ const parsed = parseCliArgs(['--agent-id', 'my-agent', '--storage', '/tmp/elen.db']);
8
+
9
+ expect(parsed.agentId).toBe('my-agent');
10
+ expect(parsed.storagePath).toBe('/tmp/elen.db');
11
+ });
12
+
13
+ it('uses defaults when args are omitted', () => {
14
+ const parsed = parseCliArgs([]);
15
+
16
+ expect(parsed.agentId).toBe('default-agent');
17
+ expect(defaultStoragePath().replace(/\\/g, '/')).toContain('.elen/decisions.db');
18
+ expect(parsed.storagePath).toBeUndefined();
19
+ });
20
+ });
21
+
22
+ describe('routeToolCall', () => {
23
+ it('routes known tools and throws for unknown tool', async () => {
24
+ const elen = {
25
+ logDecision: vi.fn(async () => ({ record_id: 'rec-1' })),
26
+ searchRecords: vi.fn(async () => [{ record_id: 'rec-1' }]),
27
+ getCompetencyProfile: vi.fn(async () => ({ domains: [] }))
28
+ } as any;
29
+
30
+ await expect(routeToolCall(elen, 'agent-a', 'elen_log_decision', {
31
+ question: 'Q',
32
+ domain: 'infrastructure',
33
+ constraints: ['C'],
34
+ evidence: ['E'],
35
+ answer: 'A'
36
+ })).resolves.toMatchObject({ record_id: 'rec-1', next_suggested_action: expect.any(String) });
37
+
38
+ await expect(routeToolCall(elen, 'agent-a', 'elen_search_precedents', {})).resolves.toEqual([{ record_id: 'rec-1' }]);
39
+ await expect(routeToolCall(elen, 'agent-a', 'elen_get_competency', {})).resolves.toEqual({ domains: [] });
40
+
41
+ await expect(routeToolCall(elen, 'agent-a', 'unknown_tool', {})).rejects.toThrow('Unknown tool');
42
+ });
43
+ });
@@ -0,0 +1,82 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import {
3
+ elenGetCompetencyTool,
4
+ elenLogDecisionTool,
5
+ elenSearchPrecedentsTool,
6
+ handleGetCompetency,
7
+ handleLogDecision,
8
+ handleSearchPrecedents,
9
+ validateCompetencyInput,
10
+ validateLogDecisionInput,
11
+ validateSearchInput
12
+ } from '../src/tools';
13
+
14
+ describe('MCP tool schemas', () => {
15
+ it('tool descriptions match expected format', () => {
16
+ expect(elenLogDecisionTool.description).toContain('validated Decision Record');
17
+ expect(elenSearchPrecedentsTool.description).toContain('validated decisions');
18
+ expect(elenGetCompetencyTool.description).toContain('competency profile');
19
+ });
20
+
21
+ it('elen_log_decision schema has required fields', () => {
22
+ expect(elenLogDecisionTool.inputSchema.required).toEqual(['question', 'constraints', 'evidence', 'answer']);
23
+ });
24
+
25
+ it('elen_search_precedents schema includes optional filters', () => {
26
+ expect(elenSearchPrecedentsTool.inputSchema.properties).toHaveProperty('query');
27
+ expect(elenSearchPrecedentsTool.inputSchema.properties).toHaveProperty('domain');
28
+ expect(elenSearchPrecedentsTool.inputSchema.properties).toHaveProperty('minConfidence');
29
+ expect(elenSearchPrecedentsTool.inputSchema.properties).toHaveProperty('limit');
30
+ });
31
+
32
+ it('elen_get_competency schema supports optional agentId', () => {
33
+ expect(elenGetCompetencyTool.inputSchema.properties).toHaveProperty('agentId');
34
+ });
35
+ });
36
+
37
+ describe('MCP tool handlers', () => {
38
+ it('elen_log_decision creates a record via SDK', async () => {
39
+ const elen = {
40
+ logDecision: vi.fn(async (input: unknown) => ({ record_id: 'rec-1', ...((input ?? {}) as object) }))
41
+ } as any;
42
+
43
+ const result = await handleLogDecision(elen, {
44
+ question: 'Which DB?',
45
+ domain: 'infrastructure',
46
+ constraints: ['Must scale'],
47
+ evidence: ['benchmark measured 3200 TPS'],
48
+ answer: 'PostgreSQL'
49
+ });
50
+
51
+ expect(elen.logDecision).toHaveBeenCalledOnce();
52
+ expect((result as { record_id: string }).record_id).toBe('rec-1');
53
+ });
54
+
55
+ it('elen_search_precedents returns matching records', async () => {
56
+ const elen = {
57
+ searchRecords: vi.fn(async () => [{ record_id: 'rec-a' }, { record_id: 'rec-b' }])
58
+ } as any;
59
+
60
+ const result = await handleSearchPrecedents(elen, { query: 'database choice', limit: 2 });
61
+
62
+ expect(elen.searchRecords).toHaveBeenCalledWith({ query: 'database choice', domain: undefined, minConfidence: undefined, limit: 2 });
63
+ expect(result).toHaveLength(2);
64
+ });
65
+
66
+ it('elen_get_competency returns profile', async () => {
67
+ const elen = {
68
+ getCompetencyProfile: vi.fn(async () => ({ domains: ['infrastructure'] }))
69
+ } as any;
70
+
71
+ const result = await handleGetCompetency(elen, {}, 'agent-a');
72
+
73
+ expect(elen.getCompetencyProfile).toHaveBeenCalledOnce();
74
+ expect((result as { domains: string[] }).domains).toContain('infrastructure');
75
+ });
76
+
77
+ it('throws on invalid tool inputs', () => {
78
+ expect(() => validateLogDecisionInput({ question: 'Q' })).toThrow('missing required field');
79
+ expect(() => validateSearchInput({ limit: '5' })).toThrow('limit must be a number');
80
+ expect(() => validateCompetencyInput({ agentId: 42 })).toThrow('agentId must be a string');
81
+ });
82
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "CommonJS",
5
+ "moduleResolution": "Node",
6
+ "declaration": true,
7
+ "outDir": "dist",
8
+ "rootDir": "src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true
13
+ },
14
+ "include": [
15
+ "src"
16
+ ]
17
+ }