suemo 0.0.2 → 0.0.3
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 +90 -17
- package/package.json +1 -1
- package/src/cli/commands/export-import.ts +26 -1
- package/src/cli/commands/health.ts +18 -1
- package/src/cli/commands/observe.ts +1 -0
- package/src/cli/commands/serve.ts +11 -1
- package/src/cli/commands/sync.ts +31 -6
- package/src/cognitive/health.ts +61 -1
- package/src/config.template.ts +22 -0
- package/src/config.ts +83 -2
- package/src/db/schema.surql +19 -1
- package/src/index.ts +5 -1
- package/src/mcp/dispatch.ts +105 -7
- package/src/mcp/server.ts +126 -3
- package/src/mcp/stdio.ts +75 -4
- package/src/mcp/tools.ts +6 -2
- package/src/memory/episode.ts +92 -0
- package/src/memory/read.ts +2 -0
- package/src/memory/write.ts +199 -2
- package/src/sync.ts +310 -66
- package/src/types.ts +30 -5
package/src/mcp/dispatch.ts
CHANGED
|
@@ -1,28 +1,54 @@
|
|
|
1
1
|
import type { Surreal } from 'surrealdb'
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import { consolidate } from '../cognitive/consolidate.ts'
|
|
4
|
-
import { healthReport, vitals } from '../cognitive/health.ts'
|
|
4
|
+
import { healthReport, suemoStats, vitals } from '../cognitive/health.ts'
|
|
5
5
|
import type { SuemoConfig } from '../config.ts'
|
|
6
6
|
import { goalList, goalResolve, goalSet } from '../goal.ts'
|
|
7
7
|
import { getLogger } from '../logger.ts'
|
|
8
|
-
import { episodeEnd, episodeStart } from '../memory/episode.ts'
|
|
8
|
+
import { episodeEnd, episodeStart, getSessionContext, setSessionContext } from '../memory/episode.ts'
|
|
9
9
|
import { query, recall, timeline, wander } from '../memory/read.ts'
|
|
10
|
-
import { believe, invalidate, observe } from '../memory/write.ts'
|
|
10
|
+
import { believe, capturePrompt, invalidate, observe, upsertByKey } from '../memory/write.ts'
|
|
11
11
|
import { ObserveInputSchema, QueryInputSchema } from '../types.ts'
|
|
12
12
|
|
|
13
13
|
const log = getLogger(['suemo', 'mcp', 'dispatch'])
|
|
14
14
|
|
|
15
|
+
const MUTATING_TOOLS = new Set([
|
|
16
|
+
'observe',
|
|
17
|
+
'believe',
|
|
18
|
+
'invalidate',
|
|
19
|
+
'goal_set',
|
|
20
|
+
'goal_resolve',
|
|
21
|
+
'upsert_by_key',
|
|
22
|
+
'capture_prompt',
|
|
23
|
+
'session_context_set',
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
interface DispatchOptions {
|
|
27
|
+
onMutation?: (tool: string) => Promise<void>
|
|
28
|
+
}
|
|
29
|
+
|
|
15
30
|
export async function handleToolCall(
|
|
16
31
|
db: Surreal,
|
|
17
32
|
config: SuemoConfig,
|
|
18
33
|
method: string,
|
|
19
34
|
params: Record<string, unknown>,
|
|
35
|
+
opts: DispatchOptions = {},
|
|
20
36
|
): Promise<unknown> {
|
|
21
37
|
log.debug('Dispatching MCP tool call', { method, paramKeys: Object.keys(params) })
|
|
22
38
|
|
|
39
|
+
const maybeTriggerMutationSync = (): void => {
|
|
40
|
+
if (!MUTATING_TOOLS.has(method) || !opts.onMutation) return
|
|
41
|
+
void opts.onMutation(method).catch((error) => {
|
|
42
|
+
log.warning('Post-mutation hook failed', { method, error: String(error) })
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
23
46
|
switch (method) {
|
|
24
|
-
case 'observe':
|
|
25
|
-
|
|
47
|
+
case 'observe': {
|
|
48
|
+
const result = await observe(db, ObserveInputSchema.parse(params), config)
|
|
49
|
+
maybeTriggerMutationSync()
|
|
50
|
+
return result
|
|
51
|
+
}
|
|
26
52
|
|
|
27
53
|
case 'believe': {
|
|
28
54
|
const parsed = z
|
|
@@ -32,12 +58,15 @@ export async function handleToolCall(
|
|
|
32
58
|
confidence: z.number().optional(),
|
|
33
59
|
})
|
|
34
60
|
.parse(params)
|
|
35
|
-
|
|
61
|
+
const result = await believe(db, parsed, config)
|
|
62
|
+
maybeTriggerMutationSync()
|
|
63
|
+
return result
|
|
36
64
|
}
|
|
37
65
|
|
|
38
66
|
case 'invalidate': {
|
|
39
67
|
const parsed = z.object({ nodeId: z.string(), reason: z.string().optional() }).parse(params)
|
|
40
68
|
await invalidate(db, parsed.nodeId, parsed.reason)
|
|
69
|
+
maybeTriggerMutationSync()
|
|
41
70
|
return { ok: true }
|
|
42
71
|
}
|
|
43
72
|
|
|
@@ -91,15 +120,18 @@ export async function handleToolCall(
|
|
|
91
120
|
const parsed = z
|
|
92
121
|
.object({ content: z.string(), scope: z.string().optional(), tags: z.array(z.string()).optional() })
|
|
93
122
|
.parse(params)
|
|
94
|
-
|
|
123
|
+
const result = await goalSet(db, parsed.content, config, {
|
|
95
124
|
...(parsed.scope ? { scope: parsed.scope } : {}),
|
|
96
125
|
tags: parsed.tags ?? [],
|
|
97
126
|
})
|
|
127
|
+
maybeTriggerMutationSync()
|
|
128
|
+
return result
|
|
98
129
|
}
|
|
99
130
|
|
|
100
131
|
case 'goal_resolve': {
|
|
101
132
|
const parsed = z.object({ goalId: z.string() }).parse(params)
|
|
102
133
|
await goalResolve(db, parsed.goalId)
|
|
134
|
+
maybeTriggerMutationSync()
|
|
103
135
|
return { ok: true }
|
|
104
136
|
}
|
|
105
137
|
|
|
@@ -114,6 +146,9 @@ export async function handleToolCall(
|
|
|
114
146
|
case 'health':
|
|
115
147
|
return healthReport(db)
|
|
116
148
|
|
|
149
|
+
case 'stats':
|
|
150
|
+
return suemoStats(db)
|
|
151
|
+
|
|
117
152
|
case 'vitals':
|
|
118
153
|
return vitals(db)
|
|
119
154
|
|
|
@@ -128,6 +163,69 @@ export async function handleToolCall(
|
|
|
128
163
|
})
|
|
129
164
|
}
|
|
130
165
|
|
|
166
|
+
case 'upsert_by_key': {
|
|
167
|
+
const parsed = z
|
|
168
|
+
.object({
|
|
169
|
+
topicKey: z.string(),
|
|
170
|
+
content: z.string(),
|
|
171
|
+
scope: z.string().optional(),
|
|
172
|
+
tags: z.array(z.string()).optional(),
|
|
173
|
+
confidence: z.number().optional(),
|
|
174
|
+
source: z.string().optional(),
|
|
175
|
+
sessionId: z.string().optional(),
|
|
176
|
+
kind: z.enum(['observation', 'belief', 'question', 'hypothesis', 'goal']).optional(),
|
|
177
|
+
})
|
|
178
|
+
.parse(params)
|
|
179
|
+
const result = await upsertByKey(db, config, parsed.topicKey, parsed.content, {
|
|
180
|
+
...(parsed.scope ? { scope: parsed.scope } : {}),
|
|
181
|
+
tags: parsed.tags ?? [],
|
|
182
|
+
...(parsed.source ? { source: parsed.source } : {}),
|
|
183
|
+
...(parsed.sessionId ? { sessionId: parsed.sessionId } : {}),
|
|
184
|
+
confidence: parsed.confidence ?? 1.0,
|
|
185
|
+
...(parsed.kind ? { kind: parsed.kind } : {}),
|
|
186
|
+
})
|
|
187
|
+
maybeTriggerMutationSync()
|
|
188
|
+
return result
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
case 'capture_prompt': {
|
|
192
|
+
const parsed = z
|
|
193
|
+
.object({
|
|
194
|
+
prompt: z.string(),
|
|
195
|
+
derivedIds: z.array(z.string()).optional(),
|
|
196
|
+
scope: z.string().optional(),
|
|
197
|
+
sessionId: z.string().optional(),
|
|
198
|
+
})
|
|
199
|
+
.parse(params)
|
|
200
|
+
const result = await capturePrompt(db, config, parsed.prompt, parsed.derivedIds ?? [], {
|
|
201
|
+
...(parsed.scope ? { scope: parsed.scope } : {}),
|
|
202
|
+
...(parsed.sessionId ? { sessionId: parsed.sessionId } : {}),
|
|
203
|
+
})
|
|
204
|
+
maybeTriggerMutationSync()
|
|
205
|
+
return result
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
case 'session_context_get': {
|
|
209
|
+
const parsed = z.object({ sessionId: z.string() }).parse(params)
|
|
210
|
+
return getSessionContext(db, parsed.sessionId)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
case 'session_context_set': {
|
|
214
|
+
const parsed = z
|
|
215
|
+
.object({
|
|
216
|
+
sessionId: z.string(),
|
|
217
|
+
summary: z.string().optional(),
|
|
218
|
+
context: z.record(z.string(), z.unknown()).optional(),
|
|
219
|
+
})
|
|
220
|
+
.parse(params)
|
|
221
|
+
await setSessionContext(db, parsed.sessionId, {
|
|
222
|
+
...(parsed.summary !== undefined ? { summary: parsed.summary } : {}),
|
|
223
|
+
...(parsed.context !== undefined ? { context: parsed.context } : {}),
|
|
224
|
+
})
|
|
225
|
+
maybeTriggerMutationSync()
|
|
226
|
+
return { ok: true }
|
|
227
|
+
}
|
|
228
|
+
|
|
131
229
|
default:
|
|
132
230
|
throw new Error(`Unknown MCP tool: ${method}`)
|
|
133
231
|
}
|
package/src/mcp/server.ts
CHANGED
|
@@ -1,22 +1,142 @@
|
|
|
1
1
|
// src/mcp/server.ts
|
|
2
2
|
import { Elysia } from 'elysia'
|
|
3
|
-
import type
|
|
3
|
+
import { resolveSyncConfig, type SuemoConfig } from '../config.ts'
|
|
4
4
|
import { connect, disconnect } from '../db/client.ts'
|
|
5
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 { syncTo } from '../sync.ts'
|
|
8
9
|
import { runStdioServer } from './stdio.ts'
|
|
9
10
|
import { buildMcpRouter } from './tools.ts'
|
|
10
11
|
|
|
11
12
|
const log = getLogger(['suemo', 'mcp'])
|
|
12
13
|
|
|
14
|
+
interface AutoSyncOptions {
|
|
15
|
+
reason: 'timer' | 'write'
|
|
16
|
+
force?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createAutoSyncRunner(
|
|
20
|
+
db: ReturnType<typeof connect> extends Promise<infer T> ? T : never,
|
|
21
|
+
config: SuemoConfig,
|
|
22
|
+
): {
|
|
23
|
+
start: () => void
|
|
24
|
+
onWrite: (tool: string) => Promise<void>
|
|
25
|
+
stop: () => void
|
|
26
|
+
} {
|
|
27
|
+
const resolvedSync = resolveSyncConfig(config)
|
|
28
|
+
if (!resolvedSync) {
|
|
29
|
+
return {
|
|
30
|
+
start: () => {},
|
|
31
|
+
onWrite: async () => {},
|
|
32
|
+
stop: () => {},
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let timer: ReturnType<typeof setInterval> | null = null
|
|
37
|
+
let running = false
|
|
38
|
+
let queued = false
|
|
39
|
+
let lastRunAt = 0
|
|
40
|
+
|
|
41
|
+
const runAutoSync = async ({ reason, force = false }: AutoSyncOptions): Promise<void> => {
|
|
42
|
+
if (!resolvedSync.auto.enabled) return
|
|
43
|
+
if (reason === 'write' && !resolvedSync.auto.onWrite) return
|
|
44
|
+
|
|
45
|
+
const now = Date.now()
|
|
46
|
+
const minIntervalMs = resolvedSync.auto.minWriteIntervalSeconds * 1000
|
|
47
|
+
if (!force && reason === 'write' && lastRunAt > 0 && now - lastRunAt < minIntervalMs) {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (running) {
|
|
52
|
+
queued = true
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const target = resolvedSync.remotes[resolvedSync.auto.remote]
|
|
57
|
+
if (!target) {
|
|
58
|
+
log.warning('Auto-sync skipped: remote missing', { remote: resolvedSync.auto.remote, reason })
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
running = true
|
|
63
|
+
try {
|
|
64
|
+
const result = await syncTo(db, target, {
|
|
65
|
+
direction: resolvedSync.auto.direction,
|
|
66
|
+
})
|
|
67
|
+
lastRunAt = Date.now()
|
|
68
|
+
log.info('Auto-sync completed', {
|
|
69
|
+
reason,
|
|
70
|
+
remote: resolvedSync.auto.remote,
|
|
71
|
+
direction: resolvedSync.auto.direction,
|
|
72
|
+
pushed: result.pushed,
|
|
73
|
+
errors: result.errors,
|
|
74
|
+
})
|
|
75
|
+
} catch (error) {
|
|
76
|
+
log.warning('Auto-sync failed', { reason, error: String(error) })
|
|
77
|
+
} finally {
|
|
78
|
+
running = false
|
|
79
|
+
if (queued) {
|
|
80
|
+
queued = false
|
|
81
|
+
queueMicrotask(() => {
|
|
82
|
+
void runAutoSync({ reason: 'write', force: true })
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const start = (): void => {
|
|
89
|
+
if (!resolvedSync.auto.enabled) return
|
|
90
|
+
if (timer) return
|
|
91
|
+
const intervalMs = resolvedSync.auto.intervalSeconds * 1000
|
|
92
|
+
timer = setInterval(() => {
|
|
93
|
+
void runAutoSync({ reason: 'timer' })
|
|
94
|
+
}, intervalMs)
|
|
95
|
+
log.info('Auto-sync timer started', {
|
|
96
|
+
intervalSeconds: resolvedSync.auto.intervalSeconds,
|
|
97
|
+
remote: resolvedSync.auto.remote,
|
|
98
|
+
direction: resolvedSync.auto.direction,
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const stop = (): void => {
|
|
103
|
+
if (!timer) return
|
|
104
|
+
clearInterval(timer)
|
|
105
|
+
timer = null
|
|
106
|
+
log.info('Auto-sync timer stopped')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const onWrite = async (tool: string): Promise<void> => {
|
|
110
|
+
if (!resolvedSync.auto.enabled || !resolvedSync.auto.onWrite) return
|
|
111
|
+
log.debug('Auto-sync write trigger', { tool })
|
|
112
|
+
await runAutoSync({ reason: 'write' })
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { start, onWrite, stop }
|
|
116
|
+
}
|
|
117
|
+
|
|
13
118
|
export async function startMcpServer(config: SuemoConfig): Promise<void> {
|
|
14
119
|
const db = await connect(config.surreal)
|
|
15
120
|
await requireCompatibility(db)
|
|
16
121
|
await runSchema(db)
|
|
122
|
+
const autoSync = createAutoSyncRunner(db, config)
|
|
123
|
+
autoSync.start()
|
|
124
|
+
|
|
125
|
+
const shutdown = async (): Promise<void> => {
|
|
126
|
+
autoSync.stop()
|
|
127
|
+
await disconnect()
|
|
128
|
+
process.exit(0)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
process.once('SIGINT', () => {
|
|
132
|
+
void shutdown()
|
|
133
|
+
})
|
|
134
|
+
process.once('SIGTERM', () => {
|
|
135
|
+
void shutdown()
|
|
136
|
+
})
|
|
17
137
|
|
|
18
138
|
new Elysia()
|
|
19
|
-
.use(buildMcpRouter(db, config))
|
|
139
|
+
.use(buildMcpRouter(db, config, { onMutation: autoSync.onWrite }))
|
|
20
140
|
.get('/health', () => ({ status: 'ok' }))
|
|
21
141
|
.listen({ port: config.mcp.port, hostname: config.mcp.host })
|
|
22
142
|
|
|
@@ -25,6 +145,8 @@ export async function startMcpServer(config: SuemoConfig): Promise<void> {
|
|
|
25
145
|
|
|
26
146
|
export async function startMcpStdioServer(config: SuemoConfig): Promise<void> {
|
|
27
147
|
const db = await connect(config.surreal)
|
|
148
|
+
const autoSync = createAutoSyncRunner(db, config)
|
|
149
|
+
autoSync.start()
|
|
28
150
|
try {
|
|
29
151
|
const compat = await checkCompatibility(db, {
|
|
30
152
|
requireEmbedding: config.embedding.provider === 'surreal',
|
|
@@ -39,8 +161,9 @@ export async function startMcpStdioServer(config: SuemoConfig): Promise<void> {
|
|
|
39
161
|
process.exit(1)
|
|
40
162
|
}
|
|
41
163
|
await runSchema(db)
|
|
42
|
-
await runStdioServer(db, config)
|
|
164
|
+
await runStdioServer(db, config, { onMutation: autoSync.onWrite })
|
|
43
165
|
} finally {
|
|
166
|
+
autoSync.stop()
|
|
44
167
|
await disconnect()
|
|
45
168
|
}
|
|
46
169
|
}
|
package/src/mcp/stdio.ts
CHANGED
|
@@ -22,6 +22,10 @@ interface McpToolDefinition {
|
|
|
22
22
|
const SUPPORTED_PROTOCOL_VERSIONS = new Set(['2024-11-05', '2025-03-26'])
|
|
23
23
|
const DEFAULT_PROTOCOL_VERSION = '2024-11-05'
|
|
24
24
|
|
|
25
|
+
interface StdioServerOptions {
|
|
26
|
+
onMutation?: (tool: string) => Promise<void>
|
|
27
|
+
}
|
|
28
|
+
|
|
25
29
|
const MCP_TOOLS: McpToolDefinition[] = [
|
|
26
30
|
{
|
|
27
31
|
name: 'observe',
|
|
@@ -35,6 +39,7 @@ const MCP_TOOLS: McpToolDefinition[] = [
|
|
|
35
39
|
scope: { type: 'string' },
|
|
36
40
|
source: { type: 'string' },
|
|
37
41
|
confidence: { type: 'number' },
|
|
42
|
+
sessionId: { type: 'string' },
|
|
38
43
|
},
|
|
39
44
|
required: ['content'],
|
|
40
45
|
},
|
|
@@ -178,6 +183,11 @@ const MCP_TOOLS: McpToolDefinition[] = [
|
|
|
178
183
|
description: 'Get health vitals summary',
|
|
179
184
|
inputSchema: { type: 'object', properties: {} },
|
|
180
185
|
},
|
|
186
|
+
{
|
|
187
|
+
name: 'stats',
|
|
188
|
+
description: 'Get lightweight usage stats summary',
|
|
189
|
+
inputSchema: { type: 'object', properties: {} },
|
|
190
|
+
},
|
|
181
191
|
{
|
|
182
192
|
name: 'consolidate',
|
|
183
193
|
description: 'Trigger memory consolidation',
|
|
@@ -186,9 +196,69 @@ const MCP_TOOLS: McpToolDefinition[] = [
|
|
|
186
196
|
properties: { nremOnly: { type: 'boolean' } },
|
|
187
197
|
},
|
|
188
198
|
},
|
|
199
|
+
{
|
|
200
|
+
name: 'upsert_by_key',
|
|
201
|
+
description: 'Upsert a memory node by topic key',
|
|
202
|
+
inputSchema: {
|
|
203
|
+
type: 'object',
|
|
204
|
+
properties: {
|
|
205
|
+
topicKey: { type: 'string' },
|
|
206
|
+
content: { type: 'string' },
|
|
207
|
+
scope: { type: 'string' },
|
|
208
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
209
|
+
source: { type: 'string' },
|
|
210
|
+
confidence: { type: 'number' },
|
|
211
|
+
sessionId: { type: 'string' },
|
|
212
|
+
kind: { type: 'string', enum: ['observation', 'belief', 'question', 'hypothesis', 'goal'] },
|
|
213
|
+
},
|
|
214
|
+
required: ['topicKey', 'content'],
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: 'capture_prompt',
|
|
219
|
+
description: 'Capture a raw prompt and link derived observations',
|
|
220
|
+
inputSchema: {
|
|
221
|
+
type: 'object',
|
|
222
|
+
properties: {
|
|
223
|
+
prompt: { type: 'string' },
|
|
224
|
+
derivedIds: { type: 'array', items: { type: 'string' } },
|
|
225
|
+
scope: { type: 'string' },
|
|
226
|
+
sessionId: { type: 'string' },
|
|
227
|
+
},
|
|
228
|
+
required: ['prompt'],
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
name: 'session_context_get',
|
|
233
|
+
description: 'Get open session context by session ID',
|
|
234
|
+
inputSchema: {
|
|
235
|
+
type: 'object',
|
|
236
|
+
properties: {
|
|
237
|
+
sessionId: { type: 'string' },
|
|
238
|
+
},
|
|
239
|
+
required: ['sessionId'],
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
name: 'session_context_set',
|
|
244
|
+
description: 'Update summary/context for open session by session ID',
|
|
245
|
+
inputSchema: {
|
|
246
|
+
type: 'object',
|
|
247
|
+
properties: {
|
|
248
|
+
sessionId: { type: 'string' },
|
|
249
|
+
summary: { type: 'string' },
|
|
250
|
+
context: { type: 'object' },
|
|
251
|
+
},
|
|
252
|
+
required: ['sessionId'],
|
|
253
|
+
},
|
|
254
|
+
},
|
|
189
255
|
]
|
|
190
256
|
|
|
191
|
-
export async function runStdioServer(
|
|
257
|
+
export async function runStdioServer(
|
|
258
|
+
db: Surreal,
|
|
259
|
+
config: SuemoConfig,
|
|
260
|
+
opts: StdioServerOptions = {},
|
|
261
|
+
): Promise<void> {
|
|
192
262
|
log.info('MCP stdio transport started')
|
|
193
263
|
|
|
194
264
|
const rl = createInterface({ input: process.stdin, terminal: false })
|
|
@@ -227,7 +297,7 @@ export async function runStdioServer(db: Surreal, config: SuemoConfig): Promise<
|
|
|
227
297
|
log.debug('Received stdio MCP request', { method })
|
|
228
298
|
|
|
229
299
|
try {
|
|
230
|
-
const result = await handleRpcMethod(db, config, method, params)
|
|
300
|
+
const result = await handleRpcMethod(db, config, method, params, opts)
|
|
231
301
|
if (id === null) continue
|
|
232
302
|
writeJsonRpc({ jsonrpc: '2.0', id, result })
|
|
233
303
|
} catch (error) {
|
|
@@ -251,6 +321,7 @@ async function handleRpcMethod(
|
|
|
251
321
|
config: SuemoConfig,
|
|
252
322
|
method: string,
|
|
253
323
|
params: Record<string, unknown>,
|
|
324
|
+
opts: StdioServerOptions,
|
|
254
325
|
): Promise<unknown> {
|
|
255
326
|
if (method === 'initialize' || method === 'initiaize') {
|
|
256
327
|
const requestedVersion = typeof params.protocolVersion === 'string'
|
|
@@ -294,7 +365,7 @@ async function handleRpcMethod(
|
|
|
294
365
|
? params.arguments as Record<string, unknown>
|
|
295
366
|
: {}
|
|
296
367
|
|
|
297
|
-
const toolResult = await handleToolCall(db, config, name, toolArgs)
|
|
368
|
+
const toolResult = await handleToolCall(db, config, name, toolArgs, opts)
|
|
298
369
|
return {
|
|
299
370
|
content: [{ type: 'text', text: JSON.stringify(toolResult, null, 2) }],
|
|
300
371
|
structuredContent: toolResult,
|
|
@@ -303,7 +374,7 @@ async function handleRpcMethod(
|
|
|
303
374
|
|
|
304
375
|
// Backward-compatible direct tool method invocation.
|
|
305
376
|
if (MCP_TOOLS.some((t) => t.name === method)) {
|
|
306
|
-
return handleToolCall(db, config, method, params)
|
|
377
|
+
return handleToolCall(db, config, method, params, opts)
|
|
307
378
|
}
|
|
308
379
|
|
|
309
380
|
throw new Error(`Unknown MCP method: ${method}`)
|
package/src/mcp/tools.ts
CHANGED
|
@@ -6,7 +6,11 @@ import type { Surreal } from 'surrealdb'
|
|
|
6
6
|
import type { SuemoConfig } from '../config.ts'
|
|
7
7
|
import { handleToolCall } from './dispatch.ts'
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
interface McpRouterOptions {
|
|
10
|
+
onMutation?: (tool: string) => Promise<void>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function buildMcpRouter(db: Surreal, config: SuemoConfig, opts: McpRouterOptions = {}) {
|
|
10
14
|
// Elysia plugin: route-level transport, shared dispatch handles behavior.
|
|
11
15
|
return (app: Elysia) =>
|
|
12
16
|
app
|
|
@@ -14,6 +18,6 @@ export function buildMcpRouter(db: Surreal, config: SuemoConfig) {
|
|
|
14
18
|
const payload = body && typeof body === 'object' && !Array.isArray(body)
|
|
15
19
|
? body as Record<string, unknown>
|
|
16
20
|
: {}
|
|
17
|
-
return handleToolCall(db, config, params.tool, payload)
|
|
21
|
+
return handleToolCall(db, config, params.tool, payload, opts)
|
|
18
22
|
})
|
|
19
23
|
}
|
package/src/memory/episode.ts
CHANGED
|
@@ -15,6 +15,7 @@ export async function episodeStart(
|
|
|
15
15
|
session_id: $sessionId,
|
|
16
16
|
ended_at: NONE,
|
|
17
17
|
summary: NONE,
|
|
18
|
+
context: NONE,
|
|
18
19
|
memory_ids: []
|
|
19
20
|
}
|
|
20
21
|
`,
|
|
@@ -64,3 +65,94 @@ export async function attachToEpisode(
|
|
|
64
65
|
{ sessionId, memoryId },
|
|
65
66
|
)
|
|
66
67
|
}
|
|
68
|
+
|
|
69
|
+
export interface SessionContext {
|
|
70
|
+
summary?: string
|
|
71
|
+
context?: Record<string, unknown>
|
|
72
|
+
memoryCount: number
|
|
73
|
+
startedAt: string
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function getSessionContext(
|
|
77
|
+
db: Surreal,
|
|
78
|
+
sessionId: string,
|
|
79
|
+
): Promise<SessionContext | null> {
|
|
80
|
+
const result = await db.query<[
|
|
81
|
+
{
|
|
82
|
+
session_id: string
|
|
83
|
+
summary: string | null
|
|
84
|
+
context: Record<string, unknown> | null
|
|
85
|
+
memory_ids: string[]
|
|
86
|
+
started_at: string
|
|
87
|
+
}[],
|
|
88
|
+
]>(
|
|
89
|
+
`
|
|
90
|
+
SELECT session_id, summary, context, memory_ids, started_at
|
|
91
|
+
FROM episode
|
|
92
|
+
WHERE session_id = $sessionId AND ended_at = NONE
|
|
93
|
+
LIMIT 1
|
|
94
|
+
`,
|
|
95
|
+
{ sessionId },
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
const ep = result[0]?.[0]
|
|
99
|
+
if (!ep) return null
|
|
100
|
+
|
|
101
|
+
const out: SessionContext = {
|
|
102
|
+
memoryCount: ep.memory_ids.length,
|
|
103
|
+
startedAt: ep.started_at,
|
|
104
|
+
}
|
|
105
|
+
if (ep.summary !== null) out.summary = ep.summary
|
|
106
|
+
if (ep.context !== null) out.context = ep.context
|
|
107
|
+
return out
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function setSessionContext(
|
|
111
|
+
db: Surreal,
|
|
112
|
+
sessionId: string,
|
|
113
|
+
patch: { summary?: string; context?: Record<string, unknown> },
|
|
114
|
+
): Promise<void> {
|
|
115
|
+
log.info('setSessionContext()', { sessionId })
|
|
116
|
+
|
|
117
|
+
if (patch.summary === undefined && patch.context === undefined) return
|
|
118
|
+
|
|
119
|
+
if (patch.summary !== undefined && patch.context !== undefined) {
|
|
120
|
+
const result = await db.query<[Episode[]]>(
|
|
121
|
+
`
|
|
122
|
+
UPDATE episode
|
|
123
|
+
SET summary = $summary,
|
|
124
|
+
context = $context
|
|
125
|
+
WHERE session_id = $sessionId AND ended_at = NONE
|
|
126
|
+
RETURN AFTER
|
|
127
|
+
`,
|
|
128
|
+
{ sessionId, summary: patch.summary, context: patch.context },
|
|
129
|
+
)
|
|
130
|
+
if (!result[0]?.[0]) throw new Error(`No open episode found for session: ${sessionId}`)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (patch.summary !== undefined) {
|
|
135
|
+
const result = await db.query<[Episode[]]>(
|
|
136
|
+
`
|
|
137
|
+
UPDATE episode
|
|
138
|
+
SET summary = $summary
|
|
139
|
+
WHERE session_id = $sessionId AND ended_at = NONE
|
|
140
|
+
RETURN AFTER
|
|
141
|
+
`,
|
|
142
|
+
{ sessionId, summary: patch.summary },
|
|
143
|
+
)
|
|
144
|
+
if (!result[0]?.[0]) throw new Error(`No open episode found for session: ${sessionId}`)
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const result = await db.query<[Episode[]]>(
|
|
149
|
+
`
|
|
150
|
+
UPDATE episode
|
|
151
|
+
SET context = $context
|
|
152
|
+
WHERE session_id = $sessionId AND ended_at = NONE
|
|
153
|
+
RETURN AFTER
|
|
154
|
+
`,
|
|
155
|
+
{ sessionId, context: patch.context },
|
|
156
|
+
)
|
|
157
|
+
if (!result[0]?.[0]) throw new Error(`No open episode found for session: ${sessionId}`)
|
|
158
|
+
}
|
package/src/memory/read.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Surreal } from 'surrealdb'
|
|
2
|
+
import { incrementQueryStats } from '../cognitive/health.ts'
|
|
2
3
|
import type { SuemoConfig } from '../config.ts'
|
|
3
4
|
import { getEmbedding } from '../embedding/index.ts'
|
|
4
5
|
import { getLogger } from '../logger.ts'
|
|
@@ -167,6 +168,7 @@ export async function query(
|
|
|
167
168
|
})
|
|
168
169
|
|
|
169
170
|
log.info('query() returned', { count: merged.length })
|
|
171
|
+
await incrementQueryStats(db)
|
|
170
172
|
return merged
|
|
171
173
|
}
|
|
172
174
|
|