@synthaer/resonance 0.1.0
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 +19 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +40 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +24 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +44 -0
- package/dist/config.js.map +1 -0
- package/dist/contracts.d.ts +4 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +2 -0
- package/dist/contracts.js.map +1 -0
- package/dist/db/local.d.ts +21 -0
- package/dist/db/local.d.ts.map +1 -0
- package/dist/db/local.js +158 -0
- package/dist/db/local.js.map +1 -0
- package/dist/db-types.d.ts +14 -0
- package/dist/db-types.d.ts.map +1 -0
- package/dist/db-types.js +2 -0
- package/dist/db-types.js.map +1 -0
- package/dist/dikw-pipeline.d.ts +31 -0
- package/dist/dikw-pipeline.d.ts.map +1 -0
- package/dist/dikw-pipeline.js +130 -0
- package/dist/dikw-pipeline.js.map +1 -0
- package/dist/embedding-provider.d.ts +25 -0
- package/dist/embedding-provider.d.ts.map +1 -0
- package/dist/embedding-provider.js +62 -0
- package/dist/embedding-provider.js.map +1 -0
- package/dist/embeddings/local.d.ts +31 -0
- package/dist/embeddings/local.d.ts.map +1 -0
- package/dist/embeddings/local.js +72 -0
- package/dist/embeddings/local.js.map +1 -0
- package/dist/encryption.d.ts +22 -0
- package/dist/encryption.d.ts.map +1 -0
- package/dist/encryption.js +39 -0
- package/dist/encryption.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-server.d.ts +362 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +722 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/pki.d.ts +160 -0
- package/dist/pki.d.ts.map +1 -0
- package/dist/pki.js +502 -0
- package/dist/pki.js.map +1 -0
- package/dist/ratification.d.ts +125 -0
- package/dist/ratification.d.ts.map +1 -0
- package/dist/ratification.js +315 -0
- package/dist/ratification.js.map +1 -0
- package/dist/schema.d.ts +4 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +119 -0
- package/dist/schema.js.map +1 -0
- package/dist/search.d.ts +33 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +132 -0
- package/dist/search.js.map +1 -0
- package/dist/server.d.ts +11 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +297 -0
- package/dist/server.js.map +1 -0
- package/dist/store.d.ts +77 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +572 -0
- package/dist/store.js.map +1 -0
- package/dist/tools/cloud-gate.d.ts +72 -0
- package/dist/tools/cloud-gate.d.ts.map +1 -0
- package/dist/tools/cloud-gate.js +79 -0
- package/dist/tools/cloud-gate.js.map +1 -0
- package/dist/tools/experiences.d.ts +20 -0
- package/dist/tools/experiences.d.ts.map +1 -0
- package/dist/tools/experiences.js +135 -0
- package/dist/tools/experiences.js.map +1 -0
- package/dist/tools/knowledge.d.ts +37 -0
- package/dist/tools/knowledge.d.ts.map +1 -0
- package/dist/tools/knowledge.js +213 -0
- package/dist/tools/knowledge.js.map +1 -0
- package/dist/tools/sessions.d.ts +11 -0
- package/dist/tools/sessions.d.ts.map +1 -0
- package/dist/tools/sessions.js +66 -0
- package/dist/tools/sessions.js.map +1 -0
- package/dist/tools/trust.d.ts +18 -0
- package/dist/tools/trust.d.ts.map +1 -0
- package/dist/tools/trust.js +42 -0
- package/dist/tools/trust.js.map +1 -0
- package/dist/types.d.ts +75 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/package.json +50 -0
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { ExperienceKind, KnowledgeKind } from './types.js';
|
|
3
|
+
import { initializePKI, signContent, requestCertificateSigning } from './pki.js';
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// PKI state — initialized once on server start, shared across requests
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
let pkiIdentity = null;
|
|
8
|
+
let pkiInitPromise = null;
|
|
9
|
+
let pkiConfig = null;
|
|
10
|
+
export function getPkiIdentity() {
|
|
11
|
+
return pkiIdentity;
|
|
12
|
+
}
|
|
13
|
+
export function isPkiReady() {
|
|
14
|
+
return pkiIdentity !== null && pkiIdentity.certificate !== null;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Initialize PKI. Called once on server start. If a signed certificate is
|
|
18
|
+
* not yet available, the identity is created but writes that require signing
|
|
19
|
+
* will be blocked until the certificate is obtained.
|
|
20
|
+
*/
|
|
21
|
+
export async function initializeServerPKI(config) {
|
|
22
|
+
pkiConfig = config;
|
|
23
|
+
pkiIdentity = await initializePKI(config);
|
|
24
|
+
return pkiIdentity;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Request certificate signing from cloud PKI and update the local identity.
|
|
28
|
+
*/
|
|
29
|
+
export async function requestServerCertSigning(authToken) {
|
|
30
|
+
if (!pkiIdentity || !pkiConfig) {
|
|
31
|
+
throw new Error('PKI not initialized. Call initializeServerPKI first.');
|
|
32
|
+
}
|
|
33
|
+
await requestCertificateSigning(pkiIdentity.csr, authToken, pkiConfig.cloudPkiUrl, pkiConfig.dataDir);
|
|
34
|
+
// Re-initialize to pick up the new cert
|
|
35
|
+
pkiIdentity = await initializePKI(pkiConfig);
|
|
36
|
+
return pkiIdentity;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Sign content using the local identity for ratification.
|
|
40
|
+
*/
|
|
41
|
+
export function signContentWithLocalIdentity(content) {
|
|
42
|
+
if (!pkiIdentity || !pkiIdentity.fingerprint) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return signContent(content, pkiIdentity.privateKey, pkiIdentity.fingerprint);
|
|
46
|
+
}
|
|
47
|
+
const DIKW_STAGES = ['nascent', 'emerging', 'established', 'canonical'];
|
|
48
|
+
const MCP_PROTOCOL_VERSION = '2024-11-05';
|
|
49
|
+
const DEFAULT_LIMITS = {
|
|
50
|
+
edition: 'local',
|
|
51
|
+
maxProjects: 0,
|
|
52
|
+
maxConcurrentFleet: 0,
|
|
53
|
+
recallWindowSize: 0,
|
|
54
|
+
allowedDikwStages: DIKW_STAGES,
|
|
55
|
+
allowMassImport: true,
|
|
56
|
+
allowFleetKnowledge: true,
|
|
57
|
+
allowFullPromotion: true,
|
|
58
|
+
};
|
|
59
|
+
class ToolParamError extends Error {
|
|
60
|
+
code = -32602;
|
|
61
|
+
data;
|
|
62
|
+
constructor(message, data) {
|
|
63
|
+
super(message);
|
|
64
|
+
this.name = 'ToolParamError';
|
|
65
|
+
this.data = data;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const MCP_TOOLS = [
|
|
69
|
+
{
|
|
70
|
+
name: 'store_knowledge',
|
|
71
|
+
description: 'Store a knowledge item in local Resonance memory.',
|
|
72
|
+
inputSchema: {
|
|
73
|
+
type: 'object',
|
|
74
|
+
properties: {
|
|
75
|
+
content: { type: 'string' },
|
|
76
|
+
kind: { type: ['number', 'string'] },
|
|
77
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
78
|
+
projectId: { type: 'string' },
|
|
79
|
+
dikwStage: { type: 'string', enum: DIKW_STAGES },
|
|
80
|
+
},
|
|
81
|
+
required: ['content'],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'recall_knowledge',
|
|
86
|
+
description: 'Keyword search across stored knowledge.',
|
|
87
|
+
inputSchema: {
|
|
88
|
+
type: 'object',
|
|
89
|
+
properties: {
|
|
90
|
+
query: { type: 'string' },
|
|
91
|
+
limit: { type: 'number' },
|
|
92
|
+
minStrength: { type: 'number' },
|
|
93
|
+
projectId: { type: 'string' },
|
|
94
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
95
|
+
},
|
|
96
|
+
required: ['query'],
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'get_knowledge',
|
|
101
|
+
description: 'Fetch a single knowledge item by id.',
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: 'object',
|
|
104
|
+
properties: { id: { type: 'string' } },
|
|
105
|
+
required: ['id'],
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'promote_knowledge',
|
|
110
|
+
description: 'Promote a knowledge item to a higher DIKW stage.',
|
|
111
|
+
inputSchema: {
|
|
112
|
+
type: 'object',
|
|
113
|
+
properties: {
|
|
114
|
+
id: { type: 'string' },
|
|
115
|
+
toStage: { type: 'string', enum: DIKW_STAGES },
|
|
116
|
+
reason: { type: 'string' },
|
|
117
|
+
},
|
|
118
|
+
required: ['id', 'toStage', 'reason'],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'store_experience',
|
|
123
|
+
description: 'Store an agent experience record.',
|
|
124
|
+
inputSchema: {
|
|
125
|
+
type: 'object',
|
|
126
|
+
properties: {
|
|
127
|
+
content: { type: 'string' },
|
|
128
|
+
kind: { type: ['number', 'string'] },
|
|
129
|
+
agentId: { type: 'string' },
|
|
130
|
+
sessionId: { type: 'string' },
|
|
131
|
+
projectId: { type: 'string' },
|
|
132
|
+
relatedKnowledgeIds: { type: 'array', items: { type: 'string' } },
|
|
133
|
+
},
|
|
134
|
+
required: ['content'],
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'recall_experiences',
|
|
139
|
+
description: 'Keyword search across stored experiences.',
|
|
140
|
+
inputSchema: {
|
|
141
|
+
type: 'object',
|
|
142
|
+
properties: {
|
|
143
|
+
query: { type: 'string' },
|
|
144
|
+
limit: { type: 'number' },
|
|
145
|
+
projectId: { type: 'string' },
|
|
146
|
+
},
|
|
147
|
+
required: ['query'],
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: 'semantic_search',
|
|
152
|
+
description: 'Hybrid semantic search across knowledge + experiences.',
|
|
153
|
+
inputSchema: {
|
|
154
|
+
type: 'object',
|
|
155
|
+
properties: {
|
|
156
|
+
query: { type: 'string' },
|
|
157
|
+
limit: { type: 'number' },
|
|
158
|
+
minStrength: { type: 'number' },
|
|
159
|
+
projectId: { type: 'string' },
|
|
160
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
161
|
+
},
|
|
162
|
+
required: ['query'],
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: 'session_resume',
|
|
167
|
+
description: 'Resume or create session memory for agent/project.',
|
|
168
|
+
inputSchema: {
|
|
169
|
+
type: 'object',
|
|
170
|
+
properties: {
|
|
171
|
+
agentId: { type: 'string' },
|
|
172
|
+
projectId: { type: 'string' },
|
|
173
|
+
},
|
|
174
|
+
required: ['agentId', 'projectId'],
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: 'memory_append',
|
|
179
|
+
description: 'Append a memory item to an active session.',
|
|
180
|
+
inputSchema: {
|
|
181
|
+
type: 'object',
|
|
182
|
+
properties: {
|
|
183
|
+
sessionId: { type: 'string' },
|
|
184
|
+
role: { type: 'string', enum: ['system', 'user', 'assistant'] },
|
|
185
|
+
content: { type: 'string' },
|
|
186
|
+
timestamp: { type: 'string' },
|
|
187
|
+
},
|
|
188
|
+
required: ['sessionId', 'role', 'content'],
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: 'memory_checkpoint',
|
|
193
|
+
description: 'Persist a checkpoint for a session memory thread.',
|
|
194
|
+
inputSchema: {
|
|
195
|
+
type: 'object',
|
|
196
|
+
properties: { sessionId: { type: 'string' } },
|
|
197
|
+
required: ['sessionId'],
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: 'update_experience',
|
|
202
|
+
description: 'Update an existing experience record.',
|
|
203
|
+
inputSchema: {
|
|
204
|
+
type: 'object',
|
|
205
|
+
properties: {
|
|
206
|
+
id: { type: 'string' },
|
|
207
|
+
content: { type: 'string' },
|
|
208
|
+
kind: { type: ['number', 'string'] },
|
|
209
|
+
agentId: { type: 'string' },
|
|
210
|
+
sessionId: { type: 'string' },
|
|
211
|
+
projectId: { type: 'string' },
|
|
212
|
+
relatedKnowledgeIds: { type: 'array', items: { type: 'string' } },
|
|
213
|
+
},
|
|
214
|
+
required: ['id'],
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: 'delete_experience',
|
|
219
|
+
description: 'Delete an experience record by id.',
|
|
220
|
+
inputSchema: {
|
|
221
|
+
type: 'object',
|
|
222
|
+
properties: { id: { type: 'string' } },
|
|
223
|
+
required: ['id'],
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: 'get_experience',
|
|
228
|
+
description: 'Fetch a single experience record by id.',
|
|
229
|
+
inputSchema: {
|
|
230
|
+
type: 'object',
|
|
231
|
+
properties: { id: { type: 'string' } },
|
|
232
|
+
required: ['id'],
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: 'update_knowledge',
|
|
237
|
+
description: 'Update an existing knowledge item.',
|
|
238
|
+
inputSchema: {
|
|
239
|
+
type: 'object',
|
|
240
|
+
properties: {
|
|
241
|
+
id: { type: 'string' },
|
|
242
|
+
content: { type: 'string' },
|
|
243
|
+
kind: { type: ['number', 'string'] },
|
|
244
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
245
|
+
projectId: { type: 'string' },
|
|
246
|
+
dikwStage: { type: 'string', enum: DIKW_STAGES },
|
|
247
|
+
},
|
|
248
|
+
required: ['id'],
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
name: 'delete_knowledge',
|
|
253
|
+
description: 'Delete a knowledge item by id.',
|
|
254
|
+
inputSchema: {
|
|
255
|
+
type: 'object',
|
|
256
|
+
properties: { id: { type: 'string' } },
|
|
257
|
+
required: ['id'],
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
name: 'health',
|
|
262
|
+
description: 'Return local MCP health and knowledge counts.',
|
|
263
|
+
inputSchema: {
|
|
264
|
+
type: 'object',
|
|
265
|
+
properties: {},
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
];
|
|
269
|
+
function resolveLimits(limits) {
|
|
270
|
+
return {
|
|
271
|
+
...DEFAULT_LIMITS,
|
|
272
|
+
...limits,
|
|
273
|
+
allowedDikwStages: limits?.allowedDikwStages ?? DEFAULT_LIMITS.allowedDikwStages,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function clampDikwStage(stage, limits) {
|
|
277
|
+
if (limits.allowedDikwStages.includes(stage)) {
|
|
278
|
+
return stage;
|
|
279
|
+
}
|
|
280
|
+
return limits.allowedDikwStages.at(-1) ?? 'nascent';
|
|
281
|
+
}
|
|
282
|
+
function asRecord(value) {
|
|
283
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
284
|
+
return value;
|
|
285
|
+
}
|
|
286
|
+
return {};
|
|
287
|
+
}
|
|
288
|
+
function asString(value, field) {
|
|
289
|
+
if (typeof value === 'string' && value.trim().length > 0)
|
|
290
|
+
return value.trim();
|
|
291
|
+
throw new ToolParamError(`'${field}' must be a non-empty string`);
|
|
292
|
+
}
|
|
293
|
+
function asOptionalString(value) {
|
|
294
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
|
295
|
+
}
|
|
296
|
+
function asOptionalNumber(value) {
|
|
297
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
298
|
+
}
|
|
299
|
+
function asStringArray(value) {
|
|
300
|
+
if (!Array.isArray(value))
|
|
301
|
+
return [];
|
|
302
|
+
return value
|
|
303
|
+
.filter((entry) => typeof entry === 'string' && entry.trim().length > 0)
|
|
304
|
+
.map((entry) => entry.trim());
|
|
305
|
+
}
|
|
306
|
+
function parseKnowledgeKind(value) {
|
|
307
|
+
if (typeof value === 'number' && value >= 1 && value <= 5)
|
|
308
|
+
return value;
|
|
309
|
+
if (typeof value === 'string') {
|
|
310
|
+
const normalized = value.trim().toLowerCase();
|
|
311
|
+
if (normalized === 'fact')
|
|
312
|
+
return KnowledgeKind.Fact;
|
|
313
|
+
if (normalized === 'pattern')
|
|
314
|
+
return KnowledgeKind.Pattern;
|
|
315
|
+
if (normalized === 'error')
|
|
316
|
+
return KnowledgeKind.Error;
|
|
317
|
+
if (normalized === 'decision')
|
|
318
|
+
return KnowledgeKind.Decision;
|
|
319
|
+
if (normalized === 'principle')
|
|
320
|
+
return KnowledgeKind.Principle;
|
|
321
|
+
}
|
|
322
|
+
return KnowledgeKind.Fact;
|
|
323
|
+
}
|
|
324
|
+
function parseExperienceKind(value) {
|
|
325
|
+
if (typeof value === 'number' && value >= 1 && value <= 4)
|
|
326
|
+
return value;
|
|
327
|
+
if (typeof value === 'string') {
|
|
328
|
+
const normalized = value.trim().toLowerCase();
|
|
329
|
+
if (normalized === 'success')
|
|
330
|
+
return ExperienceKind.Success;
|
|
331
|
+
if (normalized === 'failure')
|
|
332
|
+
return ExperienceKind.Failure;
|
|
333
|
+
if (normalized === 'discovery')
|
|
334
|
+
return ExperienceKind.Discovery;
|
|
335
|
+
if (normalized === 'blocker')
|
|
336
|
+
return ExperienceKind.Blocker;
|
|
337
|
+
}
|
|
338
|
+
return ExperienceKind.Discovery;
|
|
339
|
+
}
|
|
340
|
+
function parseDikwStage(value) {
|
|
341
|
+
if (typeof value !== 'string')
|
|
342
|
+
return 'nascent';
|
|
343
|
+
const normalized = value.trim().toLowerCase();
|
|
344
|
+
if (DIKW_STAGES.includes(normalized))
|
|
345
|
+
return normalized;
|
|
346
|
+
return 'nascent';
|
|
347
|
+
}
|
|
348
|
+
function toMcpResult(payload) {
|
|
349
|
+
return {
|
|
350
|
+
content: [{ type: 'text', text: JSON.stringify(payload) }],
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
function jsonRpcError(id, code, message, data) {
|
|
354
|
+
return {
|
|
355
|
+
jsonrpc: '2.0',
|
|
356
|
+
id,
|
|
357
|
+
error: { code, message, data },
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
function jsonRpcSuccess(id, result) {
|
|
361
|
+
return {
|
|
362
|
+
jsonrpc: '2.0',
|
|
363
|
+
id,
|
|
364
|
+
result,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
function extractUserId(req) {
|
|
368
|
+
const authHeader = req.headers.authorization;
|
|
369
|
+
if (authHeader?.startsWith('Bearer ')) {
|
|
370
|
+
try {
|
|
371
|
+
const token = authHeader.slice(7);
|
|
372
|
+
const payloadB64 = token.split('.')[1];
|
|
373
|
+
if (payloadB64) {
|
|
374
|
+
const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString());
|
|
375
|
+
if (typeof payload.sub === 'string' && payload.sub.length > 0) {
|
|
376
|
+
return payload.sub;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
// Fall through to other methods.
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const headerUserId = req.headers['x-user-id'];
|
|
385
|
+
if (typeof headerUserId === 'string' && headerUserId.trim().length > 0) {
|
|
386
|
+
return headerUserId.trim();
|
|
387
|
+
}
|
|
388
|
+
const queryUserId = req.query['userId'];
|
|
389
|
+
if (typeof queryUserId === 'string' && queryUserId.trim().length > 0) {
|
|
390
|
+
return queryUserId.trim();
|
|
391
|
+
}
|
|
392
|
+
return 'system';
|
|
393
|
+
}
|
|
394
|
+
function buildToolHandlers(localResonance, limits) {
|
|
395
|
+
return {
|
|
396
|
+
async store_knowledge(args) {
|
|
397
|
+
if (!localResonance.knowledgeEnabled) {
|
|
398
|
+
throw new ToolParamError('Sign in to Synthaer to access your knowledge base. Local experiences are always available.');
|
|
399
|
+
}
|
|
400
|
+
const content = asString(args.content, 'content');
|
|
401
|
+
const requestedStage = parseDikwStage(args.dikwStage);
|
|
402
|
+
const clampedStage = clampDikwStage(requestedStage, limits);
|
|
403
|
+
const id = await localResonance.storeKnowledge({
|
|
404
|
+
content,
|
|
405
|
+
kind: parseKnowledgeKind(args.kind),
|
|
406
|
+
dikwStage: clampedStage,
|
|
407
|
+
strength: asOptionalNumber(args.strength) ?? 0.5,
|
|
408
|
+
tags: asStringArray(args.tags),
|
|
409
|
+
sourceCount: asOptionalNumber(args.sourceCount) ?? 1,
|
|
410
|
+
contextCount: asOptionalNumber(args.contextCount) ?? 1,
|
|
411
|
+
projectId: asOptionalString(args.projectId),
|
|
412
|
+
decayRate: asOptionalNumber(args.decayRate) ?? 0.01,
|
|
413
|
+
});
|
|
414
|
+
const item = await localResonance.getKnowledge(id);
|
|
415
|
+
return { id, item, edition: limits.edition, stageClamped: requestedStage !== clampedStage };
|
|
416
|
+
},
|
|
417
|
+
async recall_knowledge(args) {
|
|
418
|
+
if (!localResonance.knowledgeEnabled) {
|
|
419
|
+
throw new ToolParamError('Sign in to Synthaer to access your knowledge base. Local experiences are always available.');
|
|
420
|
+
}
|
|
421
|
+
const query = asString(args.query, 'query');
|
|
422
|
+
const requestedLimit = asOptionalNumber(args.limit);
|
|
423
|
+
const maxLimit = limits.recallWindowSize > 0
|
|
424
|
+
? Math.min(requestedLimit ?? 20, limits.recallWindowSize)
|
|
425
|
+
: requestedLimit;
|
|
426
|
+
const options = {
|
|
427
|
+
limit: maxLimit,
|
|
428
|
+
minStrength: asOptionalNumber(args.minStrength),
|
|
429
|
+
projectId: asOptionalString(args.projectId),
|
|
430
|
+
tags: asStringArray(args.tags),
|
|
431
|
+
};
|
|
432
|
+
const items = await localResonance.recallKnowledge(query, options);
|
|
433
|
+
return { total: items.length, items, recallWindowSize: limits.recallWindowSize };
|
|
434
|
+
},
|
|
435
|
+
async get_knowledge(args) {
|
|
436
|
+
if (!localResonance.knowledgeEnabled) {
|
|
437
|
+
throw new ToolParamError('Sign in to Synthaer to access your knowledge base. Local experiences are always available.');
|
|
438
|
+
}
|
|
439
|
+
const id = asString(args.id, 'id');
|
|
440
|
+
const item = await localResonance.getKnowledge(id);
|
|
441
|
+
return { found: Boolean(item), item };
|
|
442
|
+
},
|
|
443
|
+
async promote_knowledge(args) {
|
|
444
|
+
if (!localResonance.knowledgeEnabled) {
|
|
445
|
+
throw new ToolParamError('Sign in to Synthaer to access your knowledge base. Local experiences are always available.');
|
|
446
|
+
}
|
|
447
|
+
const id = asString(args.id, 'id');
|
|
448
|
+
const requestedStage = parseDikwStage(args.toStage);
|
|
449
|
+
const clampedStage = clampDikwStage(requestedStage, limits);
|
|
450
|
+
if (!limits.allowFullPromotion && requestedStage !== clampedStage) {
|
|
451
|
+
throw new ToolParamError(`Stage '${requestedStage}' is not available in ${limits.edition} edition. ` +
|
|
452
|
+
`Allowed stages: ${limits.allowedDikwStages.join(', ')}. Upgrade to unlock full DIKW.`);
|
|
453
|
+
}
|
|
454
|
+
const reason = asString(args.reason, 'reason');
|
|
455
|
+
await localResonance.promoteKnowledge(id, clampedStage, reason);
|
|
456
|
+
const item = await localResonance.getKnowledge(id);
|
|
457
|
+
return { id, item };
|
|
458
|
+
},
|
|
459
|
+
async store_experience(args) {
|
|
460
|
+
const content = asString(args.content, 'content');
|
|
461
|
+
const id = await localResonance.storeExperience({
|
|
462
|
+
content,
|
|
463
|
+
kind: parseExperienceKind(args.kind),
|
|
464
|
+
agentId: asOptionalString(args.agentId),
|
|
465
|
+
sessionId: asOptionalString(args.sessionId),
|
|
466
|
+
projectId: asOptionalString(args.projectId),
|
|
467
|
+
relatedKnowledgeIds: asStringArray(args.relatedKnowledgeIds),
|
|
468
|
+
});
|
|
469
|
+
const item = await localResonance.store.getExperience(id);
|
|
470
|
+
return { id, item };
|
|
471
|
+
},
|
|
472
|
+
async recall_experiences(args) {
|
|
473
|
+
const query = asString(args.query, 'query');
|
|
474
|
+
const options = {
|
|
475
|
+
limit: asOptionalNumber(args.limit),
|
|
476
|
+
projectId: asOptionalString(args.projectId),
|
|
477
|
+
};
|
|
478
|
+
const items = await localResonance.recallExperiences(query, options);
|
|
479
|
+
return { total: items.length, items };
|
|
480
|
+
},
|
|
481
|
+
async semantic_search(args) {
|
|
482
|
+
const query = asString(args.query, 'query');
|
|
483
|
+
const options = {
|
|
484
|
+
limit: asOptionalNumber(args.limit),
|
|
485
|
+
minStrength: asOptionalNumber(args.minStrength),
|
|
486
|
+
projectId: asOptionalString(args.projectId),
|
|
487
|
+
tags: asStringArray(args.tags),
|
|
488
|
+
};
|
|
489
|
+
const results = await localResonance.semanticSearch(query, options);
|
|
490
|
+
return { total: results.length, results };
|
|
491
|
+
},
|
|
492
|
+
async session_resume(args) {
|
|
493
|
+
const agentId = asString(args.agentId, 'agentId');
|
|
494
|
+
const projectId = asString(args.projectId, 'projectId');
|
|
495
|
+
const session = await localResonance.sessionResume(agentId, projectId);
|
|
496
|
+
return { session };
|
|
497
|
+
},
|
|
498
|
+
async memory_append(args) {
|
|
499
|
+
const sessionId = asString(args.sessionId, 'sessionId');
|
|
500
|
+
const roleValue = asString(args.role, 'role');
|
|
501
|
+
if (roleValue !== 'system' && roleValue !== 'user' && roleValue !== 'assistant') {
|
|
502
|
+
throw new ToolParamError("'role' must be one of: system, user, assistant");
|
|
503
|
+
}
|
|
504
|
+
const memoryItem = {
|
|
505
|
+
role: roleValue,
|
|
506
|
+
content: asString(args.content, 'content'),
|
|
507
|
+
timestamp: asOptionalString(args.timestamp) ?? new Date().toISOString(),
|
|
508
|
+
};
|
|
509
|
+
await localResonance.memoryAppend(sessionId, memoryItem);
|
|
510
|
+
return { ok: true };
|
|
511
|
+
},
|
|
512
|
+
async memory_checkpoint(args) {
|
|
513
|
+
const sessionId = asString(args.sessionId, 'sessionId');
|
|
514
|
+
await localResonance.memoryCheckpoint(sessionId);
|
|
515
|
+
return { ok: true };
|
|
516
|
+
},
|
|
517
|
+
async update_experience(args) {
|
|
518
|
+
const id = asString(args.id, 'id');
|
|
519
|
+
const updates = {};
|
|
520
|
+
if (args.content !== undefined)
|
|
521
|
+
updates.content = asString(args.content, 'content');
|
|
522
|
+
if (args.kind !== undefined)
|
|
523
|
+
updates.kind = parseExperienceKind(args.kind);
|
|
524
|
+
if (args.agentId !== undefined)
|
|
525
|
+
updates.agentId = asOptionalString(args.agentId);
|
|
526
|
+
if (args.sessionId !== undefined)
|
|
527
|
+
updates.sessionId = asOptionalString(args.sessionId);
|
|
528
|
+
if (args.projectId !== undefined)
|
|
529
|
+
updates.projectId = asOptionalString(args.projectId);
|
|
530
|
+
if (args.relatedKnowledgeIds !== undefined)
|
|
531
|
+
updates.relatedKnowledgeIds = asStringArray(args.relatedKnowledgeIds);
|
|
532
|
+
await localResonance.updateExperience(id, updates);
|
|
533
|
+
const item = await localResonance.store.getExperience(id);
|
|
534
|
+
return { id, item };
|
|
535
|
+
},
|
|
536
|
+
async delete_experience(args) {
|
|
537
|
+
const id = asString(args.id, 'id');
|
|
538
|
+
await localResonance.deleteExperience(id);
|
|
539
|
+
return { id, deleted: true };
|
|
540
|
+
},
|
|
541
|
+
async get_experience(args) {
|
|
542
|
+
const id = asString(args.id, 'id');
|
|
543
|
+
const item = await localResonance.store.getExperience(id);
|
|
544
|
+
return { found: Boolean(item), item };
|
|
545
|
+
},
|
|
546
|
+
async update_knowledge(args) {
|
|
547
|
+
if (!localResonance.knowledgeEnabled) {
|
|
548
|
+
throw new ToolParamError('Sign in to Synthaer to access your knowledge base. Local experiences are always available.');
|
|
549
|
+
}
|
|
550
|
+
const id = asString(args.id, 'id');
|
|
551
|
+
const updates = {};
|
|
552
|
+
if (args.content !== undefined)
|
|
553
|
+
updates.content = asString(args.content, 'content');
|
|
554
|
+
if (args.kind !== undefined)
|
|
555
|
+
updates.kind = parseKnowledgeKind(args.kind);
|
|
556
|
+
if (args.tags !== undefined)
|
|
557
|
+
updates.tags = asStringArray(args.tags);
|
|
558
|
+
if (args.projectId !== undefined)
|
|
559
|
+
updates.projectId = asOptionalString(args.projectId);
|
|
560
|
+
if (args.dikwStage !== undefined) {
|
|
561
|
+
const requestedStage = parseDikwStage(args.dikwStage);
|
|
562
|
+
updates.dikwStage = clampDikwStage(requestedStage, limits);
|
|
563
|
+
}
|
|
564
|
+
await localResonance.store.updateKnowledge(id, updates);
|
|
565
|
+
const item = await localResonance.getKnowledge(id);
|
|
566
|
+
return { id, item };
|
|
567
|
+
},
|
|
568
|
+
async delete_knowledge(args) {
|
|
569
|
+
if (!localResonance.knowledgeEnabled) {
|
|
570
|
+
throw new ToolParamError('Sign in to Synthaer to access your knowledge base. Local experiences are always available.');
|
|
571
|
+
}
|
|
572
|
+
const id = asString(args.id, 'id');
|
|
573
|
+
await localResonance.store.deleteKnowledge(id);
|
|
574
|
+
return { id, deleted: true };
|
|
575
|
+
},
|
|
576
|
+
async health() {
|
|
577
|
+
let knowledgeCount = 0;
|
|
578
|
+
let experienceCount = 0;
|
|
579
|
+
try {
|
|
580
|
+
const knowledge = await localResonance.store.getAllKnowledge();
|
|
581
|
+
knowledgeCount = knowledge.length;
|
|
582
|
+
}
|
|
583
|
+
catch {
|
|
584
|
+
// Knowledge may be unavailable without auth
|
|
585
|
+
}
|
|
586
|
+
try {
|
|
587
|
+
const experiences = await localResonance.store.getAllExperiences();
|
|
588
|
+
experienceCount = experiences.length;
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
// Shouldn't fail, but be safe
|
|
592
|
+
}
|
|
593
|
+
return {
|
|
594
|
+
status: 'ok',
|
|
595
|
+
edition: limits.edition,
|
|
596
|
+
tools: MCP_TOOLS.length,
|
|
597
|
+
knowledgeEnabled: localResonance.knowledgeEnabled,
|
|
598
|
+
counts: {
|
|
599
|
+
knowledge: knowledgeCount,
|
|
600
|
+
experiences: experienceCount,
|
|
601
|
+
knowledgeLimit: 200,
|
|
602
|
+
},
|
|
603
|
+
limits: {
|
|
604
|
+
maxProjects: limits.maxProjects,
|
|
605
|
+
maxConcurrentFleet: limits.maxConcurrentFleet,
|
|
606
|
+
recallWindowSize: limits.recallWindowSize,
|
|
607
|
+
allowedDikwStages: limits.allowedDikwStages,
|
|
608
|
+
allowMassImport: limits.allowMassImport,
|
|
609
|
+
allowFleetKnowledge: limits.allowFleetKnowledge,
|
|
610
|
+
},
|
|
611
|
+
pki: {
|
|
612
|
+
initialized: pkiIdentity !== null,
|
|
613
|
+
hasCertificate: isPkiReady(),
|
|
614
|
+
machineId: pkiIdentity?.machineId ?? null,
|
|
615
|
+
fingerprint: pkiIdentity?.fingerprint ?? null,
|
|
616
|
+
expiresAt: pkiIdentity?.expiresAt?.toISOString() ?? null,
|
|
617
|
+
},
|
|
618
|
+
};
|
|
619
|
+
},
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
export function createLocalMcpRuntime(localResonance, limits) {
|
|
623
|
+
const resolvedLimits = resolveLimits(limits);
|
|
624
|
+
const handlers = buildToolHandlers(localResonance, resolvedLimits);
|
|
625
|
+
return {
|
|
626
|
+
listTools: () => MCP_TOOLS,
|
|
627
|
+
async callTool(name, args = {}) {
|
|
628
|
+
const handler = handlers[name];
|
|
629
|
+
if (!handler) {
|
|
630
|
+
throw new ToolParamError(`Tool not found: ${name}`);
|
|
631
|
+
}
|
|
632
|
+
return handler(args);
|
|
633
|
+
},
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
export function createResonanceMcpRouter(localResonance, options) {
|
|
637
|
+
const router = Router();
|
|
638
|
+
const limits = resolveLimits(options?.limits);
|
|
639
|
+
const serverName = options?.serverName ?? 'resonance-local-mcp';
|
|
640
|
+
const runtime = createLocalMcpRuntime(localResonance, limits);
|
|
641
|
+
// Initialize PKI in the background if config is provided
|
|
642
|
+
if (options?.pkiConfig && !pkiInitPromise) {
|
|
643
|
+
pkiInitPromise = initializeServerPKI(options.pkiConfig).catch((err) => {
|
|
644
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
645
|
+
console.error(`[resonance-local] PKI initialization failed: ${message}`);
|
|
646
|
+
return null;
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
router.get('/', (_req, res) => {
|
|
650
|
+
res.json({
|
|
651
|
+
name: serverName,
|
|
652
|
+
edition: limits.edition,
|
|
653
|
+
transport: 'json-rpc-http',
|
|
654
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
655
|
+
endpoint: '/mcp',
|
|
656
|
+
tools: MCP_TOOLS.length,
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
router.get('/tools', (_req, res) => {
|
|
660
|
+
res.json({ tools: MCP_TOOLS });
|
|
661
|
+
});
|
|
662
|
+
router.post('/', async (req, res) => {
|
|
663
|
+
const body = req.body;
|
|
664
|
+
const id = body?.id ?? null;
|
|
665
|
+
const userId = extractUserId(req);
|
|
666
|
+
res.setHeader('x-resonance-user-id', userId);
|
|
667
|
+
if (!body || typeof body !== 'object') {
|
|
668
|
+
res.status(400).json(jsonRpcError(id, -32600, 'Invalid JSON-RPC request body'));
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
if (body.jsonrpc !== '2.0' || typeof body.method !== 'string') {
|
|
672
|
+
res.status(400).json(jsonRpcError(id, -32600, 'Invalid JSON-RPC envelope'));
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
try {
|
|
676
|
+
if (body.method === 'initialize') {
|
|
677
|
+
res.json(jsonRpcSuccess(id, {
|
|
678
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
679
|
+
serverInfo: {
|
|
680
|
+
name: serverName,
|
|
681
|
+
version: '0.1.0',
|
|
682
|
+
},
|
|
683
|
+
capabilities: {
|
|
684
|
+
tools: {
|
|
685
|
+
listChanged: false,
|
|
686
|
+
},
|
|
687
|
+
},
|
|
688
|
+
}));
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
if (body.method === 'ping') {
|
|
692
|
+
res.json(jsonRpcSuccess(id, { ok: true }));
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if (body.method === 'tools/list') {
|
|
696
|
+
res.json(jsonRpcSuccess(id, { tools: runtime.listTools() }));
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
if (body.method !== 'tools/call') {
|
|
700
|
+
res.status(404).json(jsonRpcError(id, -32601, `Method not found: ${body.method}`));
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
const scopedResonance = localResonance.forUser(userId);
|
|
704
|
+
const scopedRuntime = createLocalMcpRuntime(scopedResonance, limits);
|
|
705
|
+
const params = asRecord(body.params);
|
|
706
|
+
const name = asString(params.name, 'name');
|
|
707
|
+
const args = asRecord(params.arguments);
|
|
708
|
+
const payload = await scopedRuntime.callTool(name, args);
|
|
709
|
+
res.json(jsonRpcSuccess(id, toMcpResult(payload)));
|
|
710
|
+
}
|
|
711
|
+
catch (err) {
|
|
712
|
+
if (err instanceof ToolParamError) {
|
|
713
|
+
res.status(422).json(jsonRpcError(id, err.code, err.message, err.data));
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
717
|
+
res.status(500).json(jsonRpcError(id, -32000, message));
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
return router;
|
|
721
|
+
}
|
|
722
|
+
//# sourceMappingURL=mcp-server.js.map
|