@memoryrelay/mcp-server 0.1.6
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/LICENSE +21 -0
- package/README.md +583 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +804 -0
- package/dist/index.js.map +1 -0
- package/docs/SECURITY.md +449 -0
- package/package.json +69 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,804 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/config.ts
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
var configSchema = z.object({
|
|
6
|
+
apiKey: z.string().startsWith("mem_", { message: 'API key must start with "mem_"' }).min(20, { message: "API key appears to be invalid (too short)" }),
|
|
7
|
+
apiUrl: z.string().url({ message: "API URL must be a valid URL" }).default("https://api.memoryrelay.net"),
|
|
8
|
+
agentId: z.string().optional().describe("Agent identifier - auto-detected if not provided"),
|
|
9
|
+
timeout: z.number().positive({ message: "Timeout must be positive" }).default(3e4),
|
|
10
|
+
logLevel: z.enum(["debug", "info", "warn", "error"]).default("info")
|
|
11
|
+
});
|
|
12
|
+
function loadConfig() {
|
|
13
|
+
try {
|
|
14
|
+
const config = configSchema.parse({
|
|
15
|
+
apiKey: process.env.MEMORYRELAY_API_KEY,
|
|
16
|
+
apiUrl: process.env.MEMORYRELAY_API_URL,
|
|
17
|
+
agentId: process.env.MEMORYRELAY_AGENT_ID,
|
|
18
|
+
timeout: process.env.MEMORYRELAY_TIMEOUT ? parseInt(process.env.MEMORYRELAY_TIMEOUT, 10) : void 0,
|
|
19
|
+
logLevel: process.env.MEMORYRELAY_LOG_LEVEL
|
|
20
|
+
});
|
|
21
|
+
return config;
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (error instanceof z.ZodError) {
|
|
24
|
+
const issues = error.issues.map(
|
|
25
|
+
(issue) => ` - ${issue.path.join(".")}: ${issue.message}`
|
|
26
|
+
).join("\n");
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Configuration validation failed:
|
|
29
|
+
${issues}
|
|
30
|
+
|
|
31
|
+
Please check your environment variables:
|
|
32
|
+
- MEMORYRELAY_API_KEY (required, starts with "mem_")
|
|
33
|
+
- MEMORYRELAY_API_URL (optional, default: https://api.memoryrelay.net)
|
|
34
|
+
- MEMORYRELAY_AGENT_ID (optional, auto-detected)
|
|
35
|
+
- MEMORYRELAY_TIMEOUT (optional, default: 30000)
|
|
36
|
+
- MEMORYRELAY_LOG_LEVEL (optional, default: info)`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function getAgentId(config) {
|
|
43
|
+
if (config.agentId) {
|
|
44
|
+
return config.agentId;
|
|
45
|
+
}
|
|
46
|
+
const hostname = process.env.HOSTNAME || "unknown";
|
|
47
|
+
return `agent-${hostname.slice(0, 8)}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/logger.ts
|
|
51
|
+
var LOG_LEVELS = {
|
|
52
|
+
debug: 0,
|
|
53
|
+
info: 1,
|
|
54
|
+
warn: 2,
|
|
55
|
+
error: 3
|
|
56
|
+
};
|
|
57
|
+
var Logger = class {
|
|
58
|
+
minLevel;
|
|
59
|
+
constructor(level = "info") {
|
|
60
|
+
this.minLevel = LOG_LEVELS[level];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Mask sensitive data in log messages
|
|
64
|
+
* - API keys starting with "mem_" are masked
|
|
65
|
+
* - Internal paths are sanitized
|
|
66
|
+
*/
|
|
67
|
+
sanitize(message) {
|
|
68
|
+
let sanitized = message;
|
|
69
|
+
sanitized = sanitized.replace(/mem_[a-zA-Z0-9_-]+/g, "mem_****");
|
|
70
|
+
sanitized = sanitized.replace(/\/[a-zA-Z0-9_\-./]+\.(ts|js|json)/g, "<file>");
|
|
71
|
+
sanitized = sanitized.replace(/at\s+[^\s]+\s+\([^)]+\)/g, "at <location>");
|
|
72
|
+
return sanitized;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Format log message with timestamp and level
|
|
76
|
+
*/
|
|
77
|
+
format(level, message, data) {
|
|
78
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
79
|
+
const sanitizedMessage = this.sanitize(message);
|
|
80
|
+
let output = `[${timestamp}] [${level.toUpperCase()}] ${sanitizedMessage}`;
|
|
81
|
+
if (data !== void 0) {
|
|
82
|
+
const sanitizedData = this.sanitize(JSON.stringify(data, null, 2));
|
|
83
|
+
output += `
|
|
84
|
+
${sanitizedData}`;
|
|
85
|
+
}
|
|
86
|
+
return output;
|
|
87
|
+
}
|
|
88
|
+
debug(message, data) {
|
|
89
|
+
if (this.minLevel <= LOG_LEVELS.debug) {
|
|
90
|
+
console.error(this.format("debug", message, data));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
info(message, data) {
|
|
94
|
+
if (this.minLevel <= LOG_LEVELS.info) {
|
|
95
|
+
console.error(this.format("info", message, data));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
warn(message, data) {
|
|
99
|
+
if (this.minLevel <= LOG_LEVELS.warn) {
|
|
100
|
+
console.error(this.format("warn", message, data));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
error(message, data) {
|
|
104
|
+
if (this.minLevel <= LOG_LEVELS.error) {
|
|
105
|
+
console.error(this.format("error", message, data));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
var logger;
|
|
110
|
+
function initLogger(level = "info") {
|
|
111
|
+
logger = new Logger(level);
|
|
112
|
+
return logger;
|
|
113
|
+
}
|
|
114
|
+
function getLogger() {
|
|
115
|
+
if (!logger) {
|
|
116
|
+
logger = new Logger();
|
|
117
|
+
}
|
|
118
|
+
return logger;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/server.ts
|
|
122
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
123
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
124
|
+
import {
|
|
125
|
+
CallToolRequestSchema,
|
|
126
|
+
ListToolsRequestSchema
|
|
127
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
128
|
+
import { z as z2 } from "zod";
|
|
129
|
+
|
|
130
|
+
// src/client.ts
|
|
131
|
+
var MAX_RETRIES = 3;
|
|
132
|
+
var INITIAL_DELAY_MS = 1e3;
|
|
133
|
+
var MAX_CONTENT_SIZE = 50 * 1024;
|
|
134
|
+
async function withRetry(fn, retries = MAX_RETRIES) {
|
|
135
|
+
let lastError;
|
|
136
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
137
|
+
try {
|
|
138
|
+
return await fn();
|
|
139
|
+
} catch (error) {
|
|
140
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
141
|
+
if (lastError.message.includes("401") || lastError.message.includes("403") || lastError.message.includes("404") || lastError.message.includes("400")) {
|
|
142
|
+
throw lastError;
|
|
143
|
+
}
|
|
144
|
+
if (attempt === retries) {
|
|
145
|
+
throw lastError;
|
|
146
|
+
}
|
|
147
|
+
const delay = INITIAL_DELAY_MS * Math.pow(2, attempt);
|
|
148
|
+
const jitter = Math.random() * 0.3 * delay;
|
|
149
|
+
await new Promise((resolve) => setTimeout(resolve, delay + jitter));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
throw lastError || new Error("Retry failed");
|
|
153
|
+
}
|
|
154
|
+
function maskApiKey(message, apiKey) {
|
|
155
|
+
if (!apiKey) return message;
|
|
156
|
+
const maskedKey = apiKey.substring(0, 8) + "***";
|
|
157
|
+
return message.replace(new RegExp(apiKey, "g"), maskedKey);
|
|
158
|
+
}
|
|
159
|
+
var MemoryRelayClient = class {
|
|
160
|
+
config;
|
|
161
|
+
logger = getLogger();
|
|
162
|
+
constructor(config) {
|
|
163
|
+
this.config = config;
|
|
164
|
+
this.logger.info("MemoryRelay client initialized", {
|
|
165
|
+
apiUrl: config.apiUrl,
|
|
166
|
+
agentId: config.agentId
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Make authenticated HTTP request to MemoryRelay API with retry logic
|
|
171
|
+
*/
|
|
172
|
+
async request(method, path, body) {
|
|
173
|
+
return withRetry(async () => {
|
|
174
|
+
const url = `${this.config.apiUrl}${path}`;
|
|
175
|
+
this.logger.debug(`API request: ${method} ${path}`);
|
|
176
|
+
const controller = new AbortController();
|
|
177
|
+
const timeout = setTimeout(() => controller.abort(), this.config.timeout);
|
|
178
|
+
try {
|
|
179
|
+
const response = await fetch(url, {
|
|
180
|
+
method,
|
|
181
|
+
headers: {
|
|
182
|
+
"Content-Type": "application/json",
|
|
183
|
+
"Authorization": `Bearer ${this.config.apiKey}`,
|
|
184
|
+
"User-Agent": "@memoryrelay/mcp-server"
|
|
185
|
+
},
|
|
186
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
187
|
+
signal: controller.signal
|
|
188
|
+
});
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
if (response.status === 429) {
|
|
191
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
192
|
+
const waitMs = retryAfter ? parseInt(retryAfter) * 1e3 : 5e3;
|
|
193
|
+
this.logger.warn(`Rate limited, waiting ${waitMs}ms`);
|
|
194
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
195
|
+
throw new Error(`Rate limited: 429 - Retry after ${waitMs}ms`);
|
|
196
|
+
}
|
|
197
|
+
const errorData = await response.json().catch(() => ({}));
|
|
198
|
+
const errorMsg = `API request failed: ${response.status} ${response.statusText}` + (errorData.message ? ` - ${errorData.message}` : "");
|
|
199
|
+
throw new Error(maskApiKey(errorMsg, this.config.apiKey));
|
|
200
|
+
}
|
|
201
|
+
const data = await response.json();
|
|
202
|
+
this.logger.debug(`API response: ${method} ${path}`, { status: response.status });
|
|
203
|
+
return data;
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (error instanceof Error) {
|
|
206
|
+
if (error.name === "AbortError") {
|
|
207
|
+
throw new Error(`Request timeout after ${this.config.timeout}ms`);
|
|
208
|
+
}
|
|
209
|
+
error.message = maskApiKey(error.message, this.config.apiKey);
|
|
210
|
+
}
|
|
211
|
+
throw error;
|
|
212
|
+
} finally {
|
|
213
|
+
clearTimeout(timeout);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Validate content size
|
|
219
|
+
*/
|
|
220
|
+
validateContentSize(content) {
|
|
221
|
+
if (content.length > MAX_CONTENT_SIZE) {
|
|
222
|
+
throw new Error(`Content exceeds maximum size of ${MAX_CONTENT_SIZE} bytes`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Store a new memory
|
|
227
|
+
*/
|
|
228
|
+
async storeMemory(content, metadata) {
|
|
229
|
+
this.validateContentSize(content);
|
|
230
|
+
return this.request("POST", "/v1/memories/memories", {
|
|
231
|
+
content,
|
|
232
|
+
metadata,
|
|
233
|
+
agent_id: this.config.agentId
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Search memories using semantic search
|
|
238
|
+
*/
|
|
239
|
+
async searchMemories(query, limit = 10, threshold = 0.5) {
|
|
240
|
+
this.validateContentSize(query);
|
|
241
|
+
const response = await this.request(
|
|
242
|
+
"POST",
|
|
243
|
+
"/v1/memories/memories/search",
|
|
244
|
+
{ query, limit, threshold, agent_id: this.config.agentId }
|
|
245
|
+
);
|
|
246
|
+
return response.data;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* List recent memories with pagination
|
|
250
|
+
*/
|
|
251
|
+
async listMemories(limit = 20, offset = 0) {
|
|
252
|
+
return this.request(
|
|
253
|
+
"GET",
|
|
254
|
+
`/v1/memories/memories?limit=${limit}&offset=${offset}`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Get a specific memory by ID
|
|
259
|
+
*/
|
|
260
|
+
async getMemory(id) {
|
|
261
|
+
return this.request("GET", `/v1/memories/memories/${id}`);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Update an existing memory
|
|
265
|
+
*/
|
|
266
|
+
async updateMemory(id, content, metadata) {
|
|
267
|
+
this.validateContentSize(content);
|
|
268
|
+
return this.request("PATCH", `/v1/memories/memories/${id}`, {
|
|
269
|
+
content,
|
|
270
|
+
metadata
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Delete a memory
|
|
275
|
+
*/
|
|
276
|
+
async deleteMemory(id) {
|
|
277
|
+
await this.request("DELETE", `/v1/memories/memories/${id}`);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Create a named entity
|
|
281
|
+
*/
|
|
282
|
+
async createEntity(name, type, metadata) {
|
|
283
|
+
this.validateContentSize(name);
|
|
284
|
+
return this.request("POST", "/v1/entities", {
|
|
285
|
+
name,
|
|
286
|
+
type,
|
|
287
|
+
metadata
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Link an entity to a memory
|
|
292
|
+
*/
|
|
293
|
+
async linkEntity(entityId, memoryId, relationship = "mentioned_in") {
|
|
294
|
+
await this.request("POST", "/v1/entities/links", {
|
|
295
|
+
entity_id: entityId,
|
|
296
|
+
memory_id: memoryId,
|
|
297
|
+
relationship
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Get an entity by ID
|
|
302
|
+
*/
|
|
303
|
+
async getEntity(id) {
|
|
304
|
+
return this.request("GET", `/v1/entities/${id}`);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* List entities with pagination
|
|
308
|
+
*/
|
|
309
|
+
async listEntities(limit = 20, offset = 0) {
|
|
310
|
+
return this.request(
|
|
311
|
+
"GET",
|
|
312
|
+
`/v1/entities?limit=${limit}&offset=${offset}`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Delete an entity
|
|
317
|
+
*/
|
|
318
|
+
async deleteEntity(id) {
|
|
319
|
+
await this.request("DELETE", `/v1/entities/${id}`);
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Health check - verify API connectivity
|
|
323
|
+
*/
|
|
324
|
+
async healthCheck() {
|
|
325
|
+
try {
|
|
326
|
+
await this.request("GET", "/v1/health");
|
|
327
|
+
return {
|
|
328
|
+
status: "healthy",
|
|
329
|
+
message: "API connection successful"
|
|
330
|
+
};
|
|
331
|
+
} catch (error) {
|
|
332
|
+
const errorMsg = error instanceof Error ? error.message : "Unknown error";
|
|
333
|
+
return {
|
|
334
|
+
status: "unhealthy",
|
|
335
|
+
message: `API connection failed: ${errorMsg}`
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
// src/server.ts
|
|
342
|
+
function sanitizeHtml(str) {
|
|
343
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'").replace(/\//g, "/");
|
|
344
|
+
}
|
|
345
|
+
var uuidSchema = z2.string().uuid();
|
|
346
|
+
function validateUuid(id, fieldName = "id") {
|
|
347
|
+
const result = uuidSchema.safeParse(id);
|
|
348
|
+
if (!result.success) {
|
|
349
|
+
throw new Error(`Invalid ${fieldName}: must be a valid UUID`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
var MemoryRelayMCPServer = class {
|
|
353
|
+
server;
|
|
354
|
+
client;
|
|
355
|
+
logger = getLogger();
|
|
356
|
+
constructor(config) {
|
|
357
|
+
this.client = new MemoryRelayClient(config);
|
|
358
|
+
this.server = new Server(
|
|
359
|
+
{
|
|
360
|
+
name: "@memoryrelay/mcp-server",
|
|
361
|
+
version: "0.1.0"
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
capabilities: {
|
|
365
|
+
tools: {}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
);
|
|
369
|
+
this.setupHandlers();
|
|
370
|
+
this.logger.info("MCP server initialized");
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Setup MCP protocol handlers
|
|
374
|
+
*/
|
|
375
|
+
setupHandlers() {
|
|
376
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
377
|
+
tools: [
|
|
378
|
+
{
|
|
379
|
+
name: "memory_store",
|
|
380
|
+
description: "Store a new memory. Use this to save important information, facts, preferences, or context that should be remembered for future conversations.",
|
|
381
|
+
inputSchema: {
|
|
382
|
+
type: "object",
|
|
383
|
+
properties: {
|
|
384
|
+
content: {
|
|
385
|
+
type: "string",
|
|
386
|
+
description: "The memory content to store. Be specific and include relevant context."
|
|
387
|
+
},
|
|
388
|
+
metadata: {
|
|
389
|
+
type: "object",
|
|
390
|
+
description: "Optional key-value metadata to attach to the memory",
|
|
391
|
+
additionalProperties: { type: "string" }
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
required: ["content"]
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
name: "memory_search",
|
|
399
|
+
description: "Search memories using natural language. Returns the most relevant memories based on semantic similarity to the query.",
|
|
400
|
+
inputSchema: {
|
|
401
|
+
type: "object",
|
|
402
|
+
properties: {
|
|
403
|
+
query: {
|
|
404
|
+
type: "string",
|
|
405
|
+
description: "Natural language search query"
|
|
406
|
+
},
|
|
407
|
+
limit: {
|
|
408
|
+
type: "number",
|
|
409
|
+
description: "Maximum number of results to return (1-50)",
|
|
410
|
+
minimum: 1,
|
|
411
|
+
maximum: 50,
|
|
412
|
+
default: 10
|
|
413
|
+
},
|
|
414
|
+
threshold: {
|
|
415
|
+
type: "number",
|
|
416
|
+
description: "Minimum similarity threshold (0-1)",
|
|
417
|
+
minimum: 0,
|
|
418
|
+
maximum: 1,
|
|
419
|
+
default: 0.5
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
required: ["query"]
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
name: "memory_list",
|
|
427
|
+
description: "List recent memories chronologically. Use to review what has been remembered.",
|
|
428
|
+
inputSchema: {
|
|
429
|
+
type: "object",
|
|
430
|
+
properties: {
|
|
431
|
+
limit: {
|
|
432
|
+
type: "number",
|
|
433
|
+
description: "Number of memories to return (1-100)",
|
|
434
|
+
minimum: 1,
|
|
435
|
+
maximum: 100,
|
|
436
|
+
default: 20
|
|
437
|
+
},
|
|
438
|
+
offset: {
|
|
439
|
+
type: "number",
|
|
440
|
+
description: "Offset for pagination",
|
|
441
|
+
minimum: 0,
|
|
442
|
+
default: 0
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
name: "memory_get",
|
|
449
|
+
description: "Retrieve a specific memory by its ID.",
|
|
450
|
+
inputSchema: {
|
|
451
|
+
type: "object",
|
|
452
|
+
properties: {
|
|
453
|
+
id: {
|
|
454
|
+
type: "string",
|
|
455
|
+
description: "The memory ID (UUID) to retrieve"
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
required: ["id"]
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
name: "memory_update",
|
|
463
|
+
description: "Update the content of an existing memory. Use to correct or expand stored information.",
|
|
464
|
+
inputSchema: {
|
|
465
|
+
type: "object",
|
|
466
|
+
properties: {
|
|
467
|
+
id: {
|
|
468
|
+
type: "string",
|
|
469
|
+
description: "The memory ID (UUID) to update"
|
|
470
|
+
},
|
|
471
|
+
content: {
|
|
472
|
+
type: "string",
|
|
473
|
+
description: "The new content to replace the existing memory"
|
|
474
|
+
},
|
|
475
|
+
metadata: {
|
|
476
|
+
type: "object",
|
|
477
|
+
description: "Updated metadata (replaces existing)",
|
|
478
|
+
additionalProperties: { type: "string" }
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
required: ["id", "content"]
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
name: "memory_delete",
|
|
486
|
+
description: "Permanently delete a memory. Use sparingly - memories are valuable context.",
|
|
487
|
+
inputSchema: {
|
|
488
|
+
type: "object",
|
|
489
|
+
properties: {
|
|
490
|
+
id: {
|
|
491
|
+
type: "string",
|
|
492
|
+
description: "The memory ID (UUID) to delete"
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
required: ["id"]
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
name: "entity_create",
|
|
500
|
+
description: "Create a named entity (person, place, organization, project, concept) for the knowledge graph. Entities help organize and connect memories.",
|
|
501
|
+
inputSchema: {
|
|
502
|
+
type: "object",
|
|
503
|
+
properties: {
|
|
504
|
+
name: {
|
|
505
|
+
type: "string",
|
|
506
|
+
minLength: 1,
|
|
507
|
+
maxLength: 200,
|
|
508
|
+
description: "Entity name (1-200 characters)"
|
|
509
|
+
},
|
|
510
|
+
type: {
|
|
511
|
+
type: "string",
|
|
512
|
+
enum: ["person", "place", "organization", "project", "concept", "other"],
|
|
513
|
+
description: "Entity type classification"
|
|
514
|
+
},
|
|
515
|
+
metadata: {
|
|
516
|
+
type: "object",
|
|
517
|
+
description: "Optional key-value metadata",
|
|
518
|
+
additionalProperties: { type: "string" }
|
|
519
|
+
}
|
|
520
|
+
},
|
|
521
|
+
required: ["name", "type"]
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
name: "entity_link",
|
|
526
|
+
description: "Link an entity to a memory to establish relationships in the knowledge graph.",
|
|
527
|
+
inputSchema: {
|
|
528
|
+
type: "object",
|
|
529
|
+
properties: {
|
|
530
|
+
entity_id: {
|
|
531
|
+
type: "string",
|
|
532
|
+
description: "Entity UUID"
|
|
533
|
+
},
|
|
534
|
+
memory_id: {
|
|
535
|
+
type: "string",
|
|
536
|
+
description: "Memory UUID"
|
|
537
|
+
},
|
|
538
|
+
relationship: {
|
|
539
|
+
type: "string",
|
|
540
|
+
description: 'Relationship type (e.g., "mentioned_in", "created_by", "relates_to")',
|
|
541
|
+
default: "mentioned_in"
|
|
542
|
+
}
|
|
543
|
+
},
|
|
544
|
+
required: ["entity_id", "memory_id"]
|
|
545
|
+
}
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
name: "memory_health",
|
|
549
|
+
description: "Check API connectivity and health status.",
|
|
550
|
+
inputSchema: {
|
|
551
|
+
type: "object",
|
|
552
|
+
properties: {}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
]
|
|
556
|
+
}));
|
|
557
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
558
|
+
const { name, arguments: args } = request.params;
|
|
559
|
+
this.logger.debug(`Tool called: ${name}`, args);
|
|
560
|
+
try {
|
|
561
|
+
switch (name) {
|
|
562
|
+
case "memory_store": {
|
|
563
|
+
const memory = await this.client.storeMemory(
|
|
564
|
+
args.content,
|
|
565
|
+
args.metadata
|
|
566
|
+
);
|
|
567
|
+
return {
|
|
568
|
+
content: [
|
|
569
|
+
{
|
|
570
|
+
type: "text",
|
|
571
|
+
text: JSON.stringify(memory, null, 2)
|
|
572
|
+
}
|
|
573
|
+
]
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
case "memory_search": {
|
|
577
|
+
const results = await this.client.searchMemories(
|
|
578
|
+
args.query,
|
|
579
|
+
args.limit,
|
|
580
|
+
args.threshold
|
|
581
|
+
);
|
|
582
|
+
return {
|
|
583
|
+
content: [
|
|
584
|
+
{
|
|
585
|
+
type: "text",
|
|
586
|
+
text: JSON.stringify(
|
|
587
|
+
{ memories: results, total: results.length },
|
|
588
|
+
null,
|
|
589
|
+
2
|
|
590
|
+
)
|
|
591
|
+
}
|
|
592
|
+
]
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
case "memory_list": {
|
|
596
|
+
const response = await this.client.listMemories(
|
|
597
|
+
args.limit,
|
|
598
|
+
args.offset
|
|
599
|
+
);
|
|
600
|
+
return {
|
|
601
|
+
content: [
|
|
602
|
+
{
|
|
603
|
+
type: "text",
|
|
604
|
+
text: JSON.stringify(response, null, 2)
|
|
605
|
+
}
|
|
606
|
+
]
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
case "memory_get": {
|
|
610
|
+
const id = args.id;
|
|
611
|
+
validateUuid(id, "memory_id");
|
|
612
|
+
const memory = await this.client.getMemory(id);
|
|
613
|
+
return {
|
|
614
|
+
content: [
|
|
615
|
+
{
|
|
616
|
+
type: "text",
|
|
617
|
+
text: JSON.stringify(memory, null, 2)
|
|
618
|
+
}
|
|
619
|
+
]
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
case "memory_update": {
|
|
623
|
+
const id = args.id;
|
|
624
|
+
validateUuid(id, "memory_id");
|
|
625
|
+
const memory = await this.client.updateMemory(
|
|
626
|
+
id,
|
|
627
|
+
args.content,
|
|
628
|
+
args.metadata
|
|
629
|
+
);
|
|
630
|
+
return {
|
|
631
|
+
content: [
|
|
632
|
+
{
|
|
633
|
+
type: "text",
|
|
634
|
+
text: JSON.stringify(memory, null, 2)
|
|
635
|
+
}
|
|
636
|
+
]
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
case "memory_delete": {
|
|
640
|
+
const id = args.id;
|
|
641
|
+
validateUuid(id, "memory_id");
|
|
642
|
+
await this.client.deleteMemory(id);
|
|
643
|
+
return {
|
|
644
|
+
content: [
|
|
645
|
+
{
|
|
646
|
+
type: "text",
|
|
647
|
+
text: JSON.stringify(
|
|
648
|
+
{ success: true, message: "Memory deleted successfully" },
|
|
649
|
+
null,
|
|
650
|
+
2
|
|
651
|
+
)
|
|
652
|
+
}
|
|
653
|
+
]
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
case "entity_create": {
|
|
657
|
+
const entitySchema = z2.object({
|
|
658
|
+
name: z2.string().min(1).max(200),
|
|
659
|
+
type: z2.enum(["person", "place", "organization", "project", "concept", "other"]),
|
|
660
|
+
metadata: z2.record(z2.string()).optional()
|
|
661
|
+
});
|
|
662
|
+
const validatedInput = entitySchema.parse(args);
|
|
663
|
+
const sanitizedName = sanitizeHtml(validatedInput.name);
|
|
664
|
+
const entity = await this.client.createEntity(
|
|
665
|
+
sanitizedName,
|
|
666
|
+
validatedInput.type,
|
|
667
|
+
validatedInput.metadata
|
|
668
|
+
);
|
|
669
|
+
return {
|
|
670
|
+
content: [
|
|
671
|
+
{
|
|
672
|
+
type: "text",
|
|
673
|
+
text: JSON.stringify(entity, null, 2)
|
|
674
|
+
}
|
|
675
|
+
]
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
case "entity_link": {
|
|
679
|
+
const linkSchema = z2.object({
|
|
680
|
+
entity_id: z2.string().uuid(),
|
|
681
|
+
memory_id: z2.string().uuid(),
|
|
682
|
+
relationship: z2.string().default("mentioned_in")
|
|
683
|
+
});
|
|
684
|
+
const validatedInput = linkSchema.parse(args);
|
|
685
|
+
await this.client.linkEntity(
|
|
686
|
+
validatedInput.entity_id,
|
|
687
|
+
validatedInput.memory_id,
|
|
688
|
+
validatedInput.relationship
|
|
689
|
+
);
|
|
690
|
+
return {
|
|
691
|
+
content: [
|
|
692
|
+
{
|
|
693
|
+
type: "text",
|
|
694
|
+
text: JSON.stringify(
|
|
695
|
+
{
|
|
696
|
+
success: true,
|
|
697
|
+
message: "Entity linked to memory successfully",
|
|
698
|
+
entity_id: validatedInput.entity_id,
|
|
699
|
+
memory_id: validatedInput.memory_id,
|
|
700
|
+
relationship: validatedInput.relationship
|
|
701
|
+
},
|
|
702
|
+
null,
|
|
703
|
+
2
|
|
704
|
+
)
|
|
705
|
+
}
|
|
706
|
+
]
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
case "memory_health": {
|
|
710
|
+
const health = await this.client.healthCheck();
|
|
711
|
+
return {
|
|
712
|
+
content: [
|
|
713
|
+
{
|
|
714
|
+
type: "text",
|
|
715
|
+
text: JSON.stringify(health, null, 2)
|
|
716
|
+
}
|
|
717
|
+
]
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
default:
|
|
721
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
722
|
+
}
|
|
723
|
+
} catch (error) {
|
|
724
|
+
let errorMessage = "Unknown error";
|
|
725
|
+
let errorDetails = void 0;
|
|
726
|
+
if (error instanceof z2.ZodError) {
|
|
727
|
+
errorMessage = "Validation error";
|
|
728
|
+
errorDetails = error.errors;
|
|
729
|
+
} else if (error instanceof Error) {
|
|
730
|
+
errorMessage = error.message;
|
|
731
|
+
}
|
|
732
|
+
this.logger.error(`Tool execution failed: ${name}`, {
|
|
733
|
+
error: errorMessage,
|
|
734
|
+
details: errorDetails
|
|
735
|
+
});
|
|
736
|
+
return {
|
|
737
|
+
content: [
|
|
738
|
+
{
|
|
739
|
+
type: "text",
|
|
740
|
+
text: JSON.stringify(
|
|
741
|
+
{
|
|
742
|
+
error: "Tool execution failed",
|
|
743
|
+
message: errorMessage,
|
|
744
|
+
details: errorDetails
|
|
745
|
+
},
|
|
746
|
+
null,
|
|
747
|
+
2
|
|
748
|
+
)
|
|
749
|
+
}
|
|
750
|
+
],
|
|
751
|
+
isError: true
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Start the MCP server with STDIO transport
|
|
758
|
+
*/
|
|
759
|
+
async start() {
|
|
760
|
+
const transport = new StdioServerTransport();
|
|
761
|
+
await this.server.connect(transport);
|
|
762
|
+
this.logger.info("MCP server started on STDIO");
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
// src/index.ts
|
|
767
|
+
async function main() {
|
|
768
|
+
try {
|
|
769
|
+
const config = loadConfig();
|
|
770
|
+
initLogger(config.logLevel);
|
|
771
|
+
const logger2 = getLogger();
|
|
772
|
+
logger2.info("Starting MemoryRelay MCP server");
|
|
773
|
+
const agentId = getAgentId(config);
|
|
774
|
+
const server = new MemoryRelayMCPServer({
|
|
775
|
+
apiKey: config.apiKey,
|
|
776
|
+
apiUrl: config.apiUrl,
|
|
777
|
+
agentId,
|
|
778
|
+
timeout: config.timeout
|
|
779
|
+
});
|
|
780
|
+
await server.start();
|
|
781
|
+
process.on("SIGINT", () => {
|
|
782
|
+
logger2.info("Received SIGINT, shutting down gracefully");
|
|
783
|
+
process.exit(0);
|
|
784
|
+
});
|
|
785
|
+
process.on("SIGTERM", () => {
|
|
786
|
+
logger2.info("Received SIGTERM, shutting down gracefully");
|
|
787
|
+
process.exit(0);
|
|
788
|
+
});
|
|
789
|
+
} catch (error) {
|
|
790
|
+
const logger2 = getLogger();
|
|
791
|
+
if (error instanceof Error) {
|
|
792
|
+
logger2.error("Fatal error:", { message: error.message });
|
|
793
|
+
console.error("\n\u274C Failed to start MemoryRelay MCP server\n");
|
|
794
|
+
console.error(error.message);
|
|
795
|
+
console.error("\nFor help, see: https://github.com/Alteriom/ai-memory-service/tree/main/mcp\n");
|
|
796
|
+
} else {
|
|
797
|
+
logger2.error("Fatal error:", { error });
|
|
798
|
+
console.error("\n\u274C An unexpected error occurred\n");
|
|
799
|
+
}
|
|
800
|
+
process.exit(1);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
main();
|
|
804
|
+
//# sourceMappingURL=index.js.map
|