suemo 0.0.1 → 0.0.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.
- package/README.md +37 -10
- package/package.json +1 -1
- package/src/cli/commands/believe.ts +22 -12
- package/src/cli/commands/consolidate.ts +18 -11
- package/src/cli/commands/doctor.ts +123 -0
- package/src/cli/commands/export-import.ts +58 -47
- package/src/cli/commands/goal.ts +52 -27
- package/src/cli/commands/health.ts +35 -17
- package/src/cli/commands/init.ts +155 -75
- package/src/cli/commands/observe.ts +25 -13
- package/src/cli/commands/query.ts +23 -7
- package/src/cli/commands/recall.ts +12 -6
- package/src/cli/commands/serve.ts +14 -5
- package/src/cli/commands/sync.ts +15 -6
- package/src/cli/commands/timeline.ts +30 -18
- package/src/cli/commands/wander.ts +27 -16
- package/src/cli/index.ts +3 -4
- package/src/cli/shared.ts +34 -0
- package/src/cognitive/consolidate.ts +48 -19
- package/src/cognitive/contradiction.ts +19 -7
- package/src/config.template.ts +36 -0
- package/src/config.ts +41 -12
- package/src/db/preflight.ts +32 -6
- package/src/db/schema.surql +11 -8
- package/src/db/schema.ts +6 -3
- package/src/embedding/index.ts +52 -0
- package/src/embedding/openai-compatible.ts +43 -0
- package/src/goal.ts +3 -1
- package/src/mcp/dispatch.ts +134 -0
- package/src/mcp/server.ts +25 -2
- package/src/mcp/stdio.ts +314 -0
- package/src/mcp/tools.ts +8 -89
- package/src/memory/read.ts +74 -19
- package/src/memory/write.ts +54 -18
- package/src/cli/commands/shared.ts +0 -20
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { Surreal } from 'surrealdb'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { consolidate } from '../cognitive/consolidate.ts'
|
|
4
|
+
import { healthReport, vitals } from '../cognitive/health.ts'
|
|
5
|
+
import type { SuemoConfig } from '../config.ts'
|
|
6
|
+
import { goalList, goalResolve, goalSet } from '../goal.ts'
|
|
7
|
+
import { getLogger } from '../logger.ts'
|
|
8
|
+
import { episodeEnd, episodeStart } from '../memory/episode.ts'
|
|
9
|
+
import { query, recall, timeline, wander } from '../memory/read.ts'
|
|
10
|
+
import { believe, invalidate, observe } from '../memory/write.ts'
|
|
11
|
+
import { ObserveInputSchema, QueryInputSchema } from '../types.ts'
|
|
12
|
+
|
|
13
|
+
const log = getLogger(['suemo', 'mcp', 'dispatch'])
|
|
14
|
+
|
|
15
|
+
export async function handleToolCall(
|
|
16
|
+
db: Surreal,
|
|
17
|
+
config: SuemoConfig,
|
|
18
|
+
method: string,
|
|
19
|
+
params: Record<string, unknown>,
|
|
20
|
+
): Promise<unknown> {
|
|
21
|
+
log.debug('Dispatching MCP tool call', { method, paramKeys: Object.keys(params) })
|
|
22
|
+
|
|
23
|
+
switch (method) {
|
|
24
|
+
case 'observe':
|
|
25
|
+
return observe(db, ObserveInputSchema.parse(params), config)
|
|
26
|
+
|
|
27
|
+
case 'believe': {
|
|
28
|
+
const parsed = z
|
|
29
|
+
.object({
|
|
30
|
+
content: z.string(),
|
|
31
|
+
scope: z.string().optional(),
|
|
32
|
+
confidence: z.number().optional(),
|
|
33
|
+
})
|
|
34
|
+
.parse(params)
|
|
35
|
+
return believe(db, parsed, config)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
case 'invalidate': {
|
|
39
|
+
const parsed = z.object({ nodeId: z.string(), reason: z.string().optional() }).parse(params)
|
|
40
|
+
await invalidate(db, parsed.nodeId, parsed.reason)
|
|
41
|
+
return { ok: true }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
case 'query':
|
|
45
|
+
return query(db, QueryInputSchema.parse(params), config)
|
|
46
|
+
|
|
47
|
+
case 'recall': {
|
|
48
|
+
const parsed = z.object({ nodeId: z.string() }).parse(params)
|
|
49
|
+
return recall(db, parsed.nodeId)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
case 'wander': {
|
|
53
|
+
const parsed = z
|
|
54
|
+
.object({ anchor: z.string().optional(), hops: z.number().optional(), scope: z.string().optional() })
|
|
55
|
+
.parse(params)
|
|
56
|
+
return wander(db, {
|
|
57
|
+
...(parsed.anchor ? { anchor: parsed.anchor } : {}),
|
|
58
|
+
...(parsed.hops ? { hops: parsed.hops } : {}),
|
|
59
|
+
...(parsed.scope ? { scope: parsed.scope } : {}),
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
case 'timeline': {
|
|
64
|
+
const parsed = z
|
|
65
|
+
.object({
|
|
66
|
+
scope: z.string().optional(),
|
|
67
|
+
from: z.string().optional(),
|
|
68
|
+
until: z.string().optional(),
|
|
69
|
+
limit: z.number().optional(),
|
|
70
|
+
})
|
|
71
|
+
.parse(params)
|
|
72
|
+
return timeline(db, {
|
|
73
|
+
...(parsed.scope ? { scope: parsed.scope } : {}),
|
|
74
|
+
...(parsed.from ? { from: parsed.from } : {}),
|
|
75
|
+
...(parsed.until ? { until: parsed.until } : {}),
|
|
76
|
+
...(parsed.limit ? { limit: parsed.limit } : {}),
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
case 'episode_start': {
|
|
81
|
+
const parsed = z.object({ sessionId: z.string() }).parse(params)
|
|
82
|
+
return episodeStart(db, parsed.sessionId)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
case 'episode_end': {
|
|
86
|
+
const parsed = z.object({ sessionId: z.string(), summary: z.string().optional() }).parse(params)
|
|
87
|
+
return episodeEnd(db, parsed.sessionId, parsed.summary)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
case 'goal_set': {
|
|
91
|
+
const parsed = z
|
|
92
|
+
.object({ content: z.string(), scope: z.string().optional(), tags: z.array(z.string()).optional() })
|
|
93
|
+
.parse(params)
|
|
94
|
+
return goalSet(db, parsed.content, config, {
|
|
95
|
+
...(parsed.scope ? { scope: parsed.scope } : {}),
|
|
96
|
+
tags: parsed.tags ?? [],
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
case 'goal_resolve': {
|
|
101
|
+
const parsed = z.object({ goalId: z.string() }).parse(params)
|
|
102
|
+
await goalResolve(db, parsed.goalId)
|
|
103
|
+
return { ok: true }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
case 'goal_list': {
|
|
107
|
+
const parsed = z.object({ scope: z.string().optional(), includeResolved: z.boolean().optional() }).parse(params)
|
|
108
|
+
return goalList(db, {
|
|
109
|
+
...(parsed.scope ? { scope: parsed.scope } : {}),
|
|
110
|
+
includeResolved: parsed.includeResolved ?? false,
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
case 'health':
|
|
115
|
+
return healthReport(db)
|
|
116
|
+
|
|
117
|
+
case 'vitals':
|
|
118
|
+
return vitals(db)
|
|
119
|
+
|
|
120
|
+
case 'consolidate': {
|
|
121
|
+
const parsed = z.object({ nremOnly: z.boolean().optional() }).parse(params)
|
|
122
|
+
return consolidate(db, {
|
|
123
|
+
nremOnly: parsed.nremOnly ?? false,
|
|
124
|
+
nremSimilarityThreshold: config.consolidation.nremSimilarityThreshold,
|
|
125
|
+
remRelationThreshold: config.consolidation.remRelationThreshold,
|
|
126
|
+
llm: config.consolidation.llm,
|
|
127
|
+
embedding: config.embedding,
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
default:
|
|
132
|
+
throw new Error(`Unknown MCP tool: ${method}`)
|
|
133
|
+
}
|
|
134
|
+
}
|
package/src/mcp/server.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// src/mcp/server.ts
|
|
2
2
|
import { Elysia } from 'elysia'
|
|
3
3
|
import type { SuemoConfig } from '../config.ts'
|
|
4
|
-
import { connect } from '../db/client.ts'
|
|
5
|
-
import { requireCompatibility } from '../db/preflight.ts'
|
|
4
|
+
import { connect, disconnect } from '../db/client.ts'
|
|
5
|
+
import { checkCompatibility, requireCompatibility } from '../db/preflight.ts'
|
|
6
6
|
import { runSchema } from '../db/schema.ts'
|
|
7
7
|
import { getLogger } from '../logger.ts'
|
|
8
|
+
import { runStdioServer } from './stdio.ts'
|
|
8
9
|
import { buildMcpRouter } from './tools.ts'
|
|
9
10
|
|
|
10
11
|
const log = getLogger(['suemo', 'mcp'])
|
|
@@ -21,3 +22,25 @@ export async function startMcpServer(config: SuemoConfig): Promise<void> {
|
|
|
21
22
|
|
|
22
23
|
log.info('MCP server listening', { port: config.mcp.port, host: config.mcp.host })
|
|
23
24
|
}
|
|
25
|
+
|
|
26
|
+
export async function startMcpStdioServer(config: SuemoConfig): Promise<void> {
|
|
27
|
+
const db = await connect(config.surreal)
|
|
28
|
+
try {
|
|
29
|
+
const compat = await checkCompatibility(db, {
|
|
30
|
+
requireEmbedding: config.embedding.provider === 'surreal',
|
|
31
|
+
context: 'mcp:stdio-startup',
|
|
32
|
+
})
|
|
33
|
+
if (!compat.ok) {
|
|
34
|
+
console.error('\n[suemo] Compatibility check failed:\n')
|
|
35
|
+
for (const err of compat.errors) {
|
|
36
|
+
console.error(` ✗ ${err}`)
|
|
37
|
+
}
|
|
38
|
+
console.error('\nFix the issues above and retry.\n')
|
|
39
|
+
process.exit(1)
|
|
40
|
+
}
|
|
41
|
+
await runSchema(db)
|
|
42
|
+
await runStdioServer(db, config)
|
|
43
|
+
} finally {
|
|
44
|
+
await disconnect()
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/mcp/stdio.ts
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline'
|
|
2
|
+
import type { Surreal } from 'surrealdb'
|
|
3
|
+
import type { SuemoConfig } from '../config.ts'
|
|
4
|
+
import { getLogger } from '../logger.ts'
|
|
5
|
+
import { handleToolCall } from './dispatch.ts'
|
|
6
|
+
|
|
7
|
+
const log = getLogger(['suemo', 'mcp', 'stdio'])
|
|
8
|
+
|
|
9
|
+
interface JsonRpcRequest {
|
|
10
|
+
jsonrpc?: string
|
|
11
|
+
id?: unknown
|
|
12
|
+
method?: unknown
|
|
13
|
+
params?: unknown
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface McpToolDefinition {
|
|
17
|
+
name: string
|
|
18
|
+
description: string
|
|
19
|
+
inputSchema: Record<string, unknown>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const SUPPORTED_PROTOCOL_VERSIONS = new Set(['2024-11-05', '2025-03-26'])
|
|
23
|
+
const DEFAULT_PROTOCOL_VERSION = '2024-11-05'
|
|
24
|
+
|
|
25
|
+
const MCP_TOOLS: McpToolDefinition[] = [
|
|
26
|
+
{
|
|
27
|
+
name: 'observe',
|
|
28
|
+
description: 'Store an observation/belief/question/hypothesis/goal memory node',
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {
|
|
32
|
+
content: { type: 'string' },
|
|
33
|
+
kind: { type: 'string' },
|
|
34
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
35
|
+
scope: { type: 'string' },
|
|
36
|
+
source: { type: 'string' },
|
|
37
|
+
confidence: { type: 'number' },
|
|
38
|
+
},
|
|
39
|
+
required: ['content'],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'believe',
|
|
44
|
+
description: 'Store a belief and run contradiction detection',
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: 'object',
|
|
47
|
+
properties: {
|
|
48
|
+
content: { type: 'string' },
|
|
49
|
+
scope: { type: 'string' },
|
|
50
|
+
confidence: { type: 'number' },
|
|
51
|
+
},
|
|
52
|
+
required: ['content'],
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'invalidate',
|
|
57
|
+
description: 'Invalidate a memory node (soft-delete via valid_until)',
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: 'object',
|
|
60
|
+
properties: {
|
|
61
|
+
nodeId: { type: 'string' },
|
|
62
|
+
reason: { type: 'string' },
|
|
63
|
+
},
|
|
64
|
+
required: ['nodeId'],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'query',
|
|
69
|
+
description: 'Hybrid semantic retrieval over memory nodes',
|
|
70
|
+
inputSchema: {
|
|
71
|
+
type: 'object',
|
|
72
|
+
properties: {
|
|
73
|
+
input: { type: 'string' },
|
|
74
|
+
scope: { type: 'string' },
|
|
75
|
+
kind: { type: 'array', items: { type: 'string' } },
|
|
76
|
+
topK: { type: 'number' },
|
|
77
|
+
activeOnly: { type: 'boolean' },
|
|
78
|
+
strategies: { type: 'array', items: { type: 'string' } },
|
|
79
|
+
},
|
|
80
|
+
required: ['input'],
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'recall',
|
|
85
|
+
description: 'Fetch one node and neighbors; tick review schedule',
|
|
86
|
+
inputSchema: {
|
|
87
|
+
type: 'object',
|
|
88
|
+
properties: { nodeId: { type: 'string' } },
|
|
89
|
+
required: ['nodeId'],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'wander',
|
|
94
|
+
description: 'Traverse graph via spreading activation',
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: 'object',
|
|
97
|
+
properties: {
|
|
98
|
+
anchor: { type: 'string' },
|
|
99
|
+
hops: { type: 'number' },
|
|
100
|
+
scope: { type: 'string' },
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'timeline',
|
|
106
|
+
description: 'Get chronological memory slice',
|
|
107
|
+
inputSchema: {
|
|
108
|
+
type: 'object',
|
|
109
|
+
properties: {
|
|
110
|
+
scope: { type: 'string' },
|
|
111
|
+
from: { type: 'string' },
|
|
112
|
+
until: { type: 'string' },
|
|
113
|
+
limit: { type: 'number' },
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'episode_start',
|
|
119
|
+
description: 'Start an episode for a session ID',
|
|
120
|
+
inputSchema: {
|
|
121
|
+
type: 'object',
|
|
122
|
+
properties: { sessionId: { type: 'string' } },
|
|
123
|
+
required: ['sessionId'],
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'episode_end',
|
|
128
|
+
description: 'End an episode for a session ID',
|
|
129
|
+
inputSchema: {
|
|
130
|
+
type: 'object',
|
|
131
|
+
properties: {
|
|
132
|
+
sessionId: { type: 'string' },
|
|
133
|
+
summary: { type: 'string' },
|
|
134
|
+
},
|
|
135
|
+
required: ['sessionId'],
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'goal_set',
|
|
140
|
+
description: 'Create goal node',
|
|
141
|
+
inputSchema: {
|
|
142
|
+
type: 'object',
|
|
143
|
+
properties: {
|
|
144
|
+
content: { type: 'string' },
|
|
145
|
+
scope: { type: 'string' },
|
|
146
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
147
|
+
},
|
|
148
|
+
required: ['content'],
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: 'goal_resolve',
|
|
153
|
+
description: 'Resolve a goal by ID',
|
|
154
|
+
inputSchema: {
|
|
155
|
+
type: 'object',
|
|
156
|
+
properties: { goalId: { type: 'string' } },
|
|
157
|
+
required: ['goalId'],
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: 'goal_list',
|
|
162
|
+
description: 'List goals',
|
|
163
|
+
inputSchema: {
|
|
164
|
+
type: 'object',
|
|
165
|
+
properties: {
|
|
166
|
+
scope: { type: 'string' },
|
|
167
|
+
includeResolved: { type: 'boolean' },
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: 'health',
|
|
173
|
+
description: 'Get health report',
|
|
174
|
+
inputSchema: { type: 'object', properties: {} },
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: 'vitals',
|
|
178
|
+
description: 'Get health vitals summary',
|
|
179
|
+
inputSchema: { type: 'object', properties: {} },
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: 'consolidate',
|
|
183
|
+
description: 'Trigger memory consolidation',
|
|
184
|
+
inputSchema: {
|
|
185
|
+
type: 'object',
|
|
186
|
+
properties: { nremOnly: { type: 'boolean' } },
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
export async function runStdioServer(db: Surreal, config: SuemoConfig): Promise<void> {
|
|
192
|
+
log.info('MCP stdio transport started')
|
|
193
|
+
|
|
194
|
+
const rl = createInterface({ input: process.stdin, terminal: false })
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
for await (const line of rl) {
|
|
198
|
+
if (!line.trim()) continue
|
|
199
|
+
|
|
200
|
+
let request: JsonRpcRequest
|
|
201
|
+
try {
|
|
202
|
+
request = JSON.parse(line) as JsonRpcRequest
|
|
203
|
+
} catch {
|
|
204
|
+
writeJsonRpc({
|
|
205
|
+
jsonrpc: '2.0',
|
|
206
|
+
id: null,
|
|
207
|
+
error: { code: -32700, message: 'Parse error' },
|
|
208
|
+
})
|
|
209
|
+
continue
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const id = request.id ?? null
|
|
213
|
+
const method = typeof request.method === 'string' ? request.method : null
|
|
214
|
+
const params = request.params && typeof request.params === 'object' && !Array.isArray(request.params)
|
|
215
|
+
? request.params as Record<string, unknown>
|
|
216
|
+
: {}
|
|
217
|
+
|
|
218
|
+
if (!method) {
|
|
219
|
+
writeJsonRpc({
|
|
220
|
+
jsonrpc: '2.0',
|
|
221
|
+
id,
|
|
222
|
+
error: { code: -32600, message: 'Invalid Request: method must be a string' },
|
|
223
|
+
})
|
|
224
|
+
continue
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
log.debug('Received stdio MCP request', { method })
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const result = await handleRpcMethod(db, config, method, params)
|
|
231
|
+
if (id === null) continue
|
|
232
|
+
writeJsonRpc({ jsonrpc: '2.0', id, result })
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (id === null) continue
|
|
235
|
+
writeJsonRpc({
|
|
236
|
+
jsonrpc: '2.0',
|
|
237
|
+
id,
|
|
238
|
+
error: { code: -32603, message: String(error) },
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
} finally {
|
|
243
|
+
rl.close()
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
log.info('stdin closed — stdio server exiting')
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function handleRpcMethod(
|
|
250
|
+
db: Surreal,
|
|
251
|
+
config: SuemoConfig,
|
|
252
|
+
method: string,
|
|
253
|
+
params: Record<string, unknown>,
|
|
254
|
+
): Promise<unknown> {
|
|
255
|
+
if (method === 'initialize' || method === 'initiaize') {
|
|
256
|
+
const requestedVersion = typeof params.protocolVersion === 'string'
|
|
257
|
+
? params.protocolVersion
|
|
258
|
+
: DEFAULT_PROTOCOL_VERSION
|
|
259
|
+
const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.has(requestedVersion)
|
|
260
|
+
? requestedVersion
|
|
261
|
+
: DEFAULT_PROTOCOL_VERSION
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
protocolVersion,
|
|
265
|
+
capabilities: {
|
|
266
|
+
tools: { listChanged: false },
|
|
267
|
+
},
|
|
268
|
+
serverInfo: {
|
|
269
|
+
name: 'suemo',
|
|
270
|
+
version: '0.0.1',
|
|
271
|
+
},
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (method === 'notifications/initialized') {
|
|
276
|
+
return {}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (method === 'ping') {
|
|
280
|
+
return {}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (method === 'tools/list') {
|
|
284
|
+
return { tools: MCP_TOOLS }
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (method === 'tools/call') {
|
|
288
|
+
const name = typeof params.name === 'string' ? params.name : null
|
|
289
|
+
if (!name) {
|
|
290
|
+
throw new Error('Invalid tools/call params: missing string field `name`')
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const toolArgs = params.arguments && typeof params.arguments === 'object' && !Array.isArray(params.arguments)
|
|
294
|
+
? params.arguments as Record<string, unknown>
|
|
295
|
+
: {}
|
|
296
|
+
|
|
297
|
+
const toolResult = await handleToolCall(db, config, name, toolArgs)
|
|
298
|
+
return {
|
|
299
|
+
content: [{ type: 'text', text: JSON.stringify(toolResult, null, 2) }],
|
|
300
|
+
structuredContent: toolResult,
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Backward-compatible direct tool method invocation.
|
|
305
|
+
if (MCP_TOOLS.some((t) => t.name === method)) {
|
|
306
|
+
return handleToolCall(db, config, method, params)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
throw new Error(`Unknown MCP method: ${method}`)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function writeJsonRpc(payload: unknown): void {
|
|
313
|
+
process.stdout.write(`${JSON.stringify(payload)}\n`)
|
|
314
|
+
}
|
package/src/mcp/tools.ts
CHANGED
|
@@ -1,100 +1,19 @@
|
|
|
1
1
|
// src/mcp/tools.ts
|
|
2
|
-
//
|
|
3
|
-
// No business logic here. SSoT lives in domain modules.
|
|
2
|
+
// HTTP transport adapter for MCP calls.
|
|
4
3
|
|
|
5
4
|
import type { Elysia } from 'elysia'
|
|
6
5
|
import type { Surreal } from 'surrealdb'
|
|
7
|
-
import { z } from 'zod'
|
|
8
|
-
import { consolidate } from '../cognitive/consolidate.ts'
|
|
9
|
-
import { healthReport, vitals } from '../cognitive/health.ts'
|
|
10
6
|
import type { SuemoConfig } from '../config.ts'
|
|
11
|
-
import {
|
|
12
|
-
import { episodeEnd, episodeStart } from '../memory/episode.ts'
|
|
13
|
-
import { query, recall, timeline, wander } from '../memory/read.ts'
|
|
14
|
-
import { believe, invalidate, observe } from '../memory/write.ts'
|
|
15
|
-
import { ObserveInputSchema, QueryInputSchema } from '../types.ts'
|
|
7
|
+
import { handleToolCall } from './dispatch.ts'
|
|
16
8
|
|
|
17
9
|
export function buildMcpRouter(db: Surreal, config: SuemoConfig) {
|
|
18
|
-
//
|
|
19
|
-
// Each route maps 1:1 to a domain function.
|
|
10
|
+
// Elysia plugin: route-level transport, shared dispatch handles behavior.
|
|
20
11
|
return (app: Elysia) =>
|
|
21
12
|
app
|
|
22
|
-
.post('/mcp
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return
|
|
27
|
-
})
|
|
28
|
-
.post('/mcp/invalidate', async ({ body }) => {
|
|
29
|
-
const p = z.object({ nodeId: z.string(), reason: z.string().optional() }).parse(body)
|
|
30
|
-
await invalidate(db, p.nodeId, p.reason)
|
|
31
|
-
return { ok: true }
|
|
32
|
-
})
|
|
33
|
-
.post('/mcp/query', async ({ body }) => query(db, QueryInputSchema.parse(body)))
|
|
34
|
-
.post('/mcp/recall', async ({ body }) => {
|
|
35
|
-
const p = z.object({ nodeId: z.string() }).parse(body)
|
|
36
|
-
return recall(db, p.nodeId)
|
|
37
|
-
})
|
|
38
|
-
.post('/mcp/wander', async ({ body }) => {
|
|
39
|
-
const p = z.object({ anchor: z.string().optional(), hops: z.number().optional(), scope: z.string().optional() })
|
|
40
|
-
.parse(body)
|
|
41
|
-
return wander(db, {
|
|
42
|
-
...(p.anchor ? { anchor: p.anchor } : {}),
|
|
43
|
-
...(p.hops ? { hops: p.hops } : {}),
|
|
44
|
-
...(p.scope ? { scope: p.scope } : {}),
|
|
45
|
-
})
|
|
46
|
-
})
|
|
47
|
-
.post('/mcp/timeline', async ({ body }) => {
|
|
48
|
-
const p = z.object({
|
|
49
|
-
scope: z.string().optional(),
|
|
50
|
-
from: z.string().optional(),
|
|
51
|
-
until: z.string().optional(),
|
|
52
|
-
limit: z.number().optional(),
|
|
53
|
-
}).parse(body)
|
|
54
|
-
return timeline(db, {
|
|
55
|
-
...(p.scope ? { scope: p.scope } : {}),
|
|
56
|
-
...(p.from ? { from: p.from } : {}),
|
|
57
|
-
...(p.until ? { until: p.until } : {}),
|
|
58
|
-
...(p.limit ? { limit: p.limit } : {}),
|
|
59
|
-
})
|
|
60
|
-
})
|
|
61
|
-
.post('/mcp/episode_start', async ({ body }) => {
|
|
62
|
-
const p = z.object({ sessionId: z.string() }).parse(body)
|
|
63
|
-
return episodeStart(db, p.sessionId)
|
|
64
|
-
})
|
|
65
|
-
.post('/mcp/episode_end', async ({ body }) => {
|
|
66
|
-
const p = z.object({ sessionId: z.string(), summary: z.string().optional() }).parse(body)
|
|
67
|
-
return episodeEnd(db, p.sessionId, p.summary)
|
|
68
|
-
})
|
|
69
|
-
.post('/mcp/goal_set', async ({ body }) => {
|
|
70
|
-
const p = z.object({ content: z.string(), scope: z.string().optional(), tags: z.array(z.string()).optional() })
|
|
71
|
-
.parse(body)
|
|
72
|
-
return goalSet(db, p.content, {
|
|
73
|
-
...(p.scope ? { scope: p.scope } : {}),
|
|
74
|
-
tags: p.tags ?? [],
|
|
75
|
-
})
|
|
76
|
-
})
|
|
77
|
-
.post('/mcp/goal_resolve', async ({ body }) => {
|
|
78
|
-
const p = z.object({ goalId: z.string() }).parse(body)
|
|
79
|
-
await goalResolve(db, p.goalId)
|
|
80
|
-
return { ok: true }
|
|
81
|
-
})
|
|
82
|
-
.post('/mcp/goal_list', async ({ body }) => {
|
|
83
|
-
const p = z.object({ scope: z.string().optional(), includeResolved: z.boolean().optional() }).parse(body)
|
|
84
|
-
return goalList(db, {
|
|
85
|
-
...(p.scope ? { scope: p.scope } : {}),
|
|
86
|
-
includeResolved: p.includeResolved ?? false,
|
|
87
|
-
})
|
|
88
|
-
})
|
|
89
|
-
.get('/mcp/health', async () => healthReport(db))
|
|
90
|
-
.get('/mcp/vitals', async () => vitals(db))
|
|
91
|
-
.post('/mcp/consolidate', async ({ body }) => {
|
|
92
|
-
const p = z.object({ nremOnly: z.boolean().optional() }).parse(body ?? {})
|
|
93
|
-
return consolidate(db, {
|
|
94
|
-
nremOnly: p.nremOnly ?? false,
|
|
95
|
-
nremSimilarityThreshold: config.consolidation.nremSimilarityThreshold,
|
|
96
|
-
remRelationThreshold: config.consolidation.remRelationThreshold,
|
|
97
|
-
llm: config.consolidation.llm,
|
|
98
|
-
})
|
|
13
|
+
.post('/mcp/:tool', async ({ params, body }) => {
|
|
14
|
+
const payload = body && typeof body === 'object' && !Array.isArray(body)
|
|
15
|
+
? body as Record<string, unknown>
|
|
16
|
+
: {}
|
|
17
|
+
return handleToolCall(db, config, params.tool, payload)
|
|
99
18
|
})
|
|
100
19
|
}
|