@memoryrelay/mcp-server 0.2.0 → 0.4.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/dist/index.js CHANGED
@@ -1,356 +1,25 @@
1
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
- if (process.env.OPENCLAW_AGENT_NAME) {
47
- return process.env.OPENCLAW_AGENT_NAME;
48
- }
49
- const hostname = process.env.HOSTNAME || "unknown";
50
- return `agent-${hostname.slice(0, 8)}`;
51
- }
52
-
53
- // src/logger.ts
54
- var LOG_LEVELS = {
55
- debug: 0,
56
- info: 1,
57
- warn: 2,
58
- error: 3
59
- };
60
- var Logger = class {
61
- minLevel;
62
- constructor(level = "info") {
63
- this.minLevel = LOG_LEVELS[level];
64
- }
65
- /**
66
- * Mask sensitive data in log messages
67
- * - API keys starting with "mem_" are masked
68
- * - Internal paths are sanitized
69
- */
70
- sanitize(message) {
71
- let sanitized = message;
72
- sanitized = sanitized.replace(/mem_[a-zA-Z0-9_-]+/g, "mem_****");
73
- sanitized = sanitized.replace(/\/[a-zA-Z0-9_\-./]+\.(ts|js|json)/g, "<file>");
74
- sanitized = sanitized.replace(/at\s+[^\s]+\s+\([^)]+\)/g, "at <location>");
75
- return sanitized;
76
- }
77
- /**
78
- * Format log message with timestamp and level
79
- */
80
- format(level, message, data) {
81
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
82
- const sanitizedMessage = this.sanitize(message);
83
- let output = `[${timestamp}] [${level.toUpperCase()}] ${sanitizedMessage}`;
84
- if (data !== void 0) {
85
- const sanitizedData = this.sanitize(JSON.stringify(data, null, 2));
86
- output += `
87
- ${sanitizedData}`;
88
- }
89
- return output;
90
- }
91
- debug(message, data) {
92
- if (this.minLevel <= LOG_LEVELS.debug) {
93
- console.error(this.format("debug", message, data));
94
- }
95
- }
96
- info(message, data) {
97
- if (this.minLevel <= LOG_LEVELS.info) {
98
- console.error(this.format("info", message, data));
99
- }
100
- }
101
- warn(message, data) {
102
- if (this.minLevel <= LOG_LEVELS.warn) {
103
- console.error(this.format("warn", message, data));
104
- }
105
- }
106
- error(message, data) {
107
- if (this.minLevel <= LOG_LEVELS.error) {
108
- console.error(this.format("error", message, data));
109
- }
110
- }
111
- };
112
- var logger;
113
- function initLogger(level = "info") {
114
- logger = new Logger(level);
115
- return logger;
116
- }
117
- function getLogger() {
118
- if (!logger) {
119
- logger = new Logger();
120
- }
121
- return logger;
122
- }
2
+ import {
3
+ MemoryRelayClient,
4
+ getAgentId,
5
+ getEnabledTools,
6
+ getLogger,
7
+ initLogger,
8
+ loadConfig
9
+ } from "./chunk-J5PEZEMN.js";
123
10
 
124
11
  // src/server.ts
125
12
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
126
13
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
127
14
  import {
128
15
  CallToolRequestSchema,
129
- ListToolsRequestSchema,
130
- ListResourcesRequestSchema,
131
- ListResourceTemplatesRequestSchema,
132
- ReadResourceRequestSchema,
133
- ListPromptsRequestSchema,
134
- GetPromptRequestSchema
16
+ ListToolsRequestSchema
135
17
  } from "@modelcontextprotocol/sdk/types.js";
136
- import { z as z2 } from "zod";
137
-
138
- // src/client.ts
139
- var MAX_RETRIES = 3;
140
- var INITIAL_DELAY_MS = 1e3;
141
- var MAX_CONTENT_SIZE = 50 * 1024;
142
- async function withRetry(fn, retries = MAX_RETRIES) {
143
- let lastError;
144
- for (let attempt = 0; attempt <= retries; attempt++) {
145
- try {
146
- return await fn();
147
- } catch (error) {
148
- lastError = error instanceof Error ? error : new Error(String(error));
149
- if (lastError.message.includes("401") || lastError.message.includes("403") || lastError.message.includes("404") || lastError.message.includes("400")) {
150
- throw lastError;
151
- }
152
- if (attempt === retries) {
153
- throw lastError;
154
- }
155
- const delay = INITIAL_DELAY_MS * Math.pow(2, attempt);
156
- const jitter = Math.random() * 0.3 * delay;
157
- await new Promise((resolve) => setTimeout(resolve, delay + jitter));
158
- }
159
- }
160
- throw lastError || new Error("Retry failed");
161
- }
162
- function maskApiKey(message, apiKey) {
163
- if (!apiKey) return message;
164
- const maskedKey = apiKey.substring(0, 8) + "***";
165
- return message.replace(new RegExp(apiKey, "g"), maskedKey);
166
- }
167
- var MemoryRelayClient = class {
168
- config;
169
- logger = getLogger();
170
- constructor(config) {
171
- this.config = config;
172
- this.logger.info("MemoryRelay client initialized", {
173
- apiUrl: config.apiUrl,
174
- agentId: config.agentId
175
- });
176
- }
177
- /**
178
- * Make authenticated HTTP request to MemoryRelay API with retry logic
179
- */
180
- async request(method, path, body) {
181
- return withRetry(async () => {
182
- const url = `${this.config.apiUrl}${path}`;
183
- this.logger.debug(`API request: ${method} ${path}`);
184
- const controller = new AbortController();
185
- const timeout = setTimeout(() => controller.abort(), this.config.timeout);
186
- try {
187
- const response = await fetch(url, {
188
- method,
189
- headers: {
190
- "Content-Type": "application/json",
191
- "Authorization": `Bearer ${this.config.apiKey}`,
192
- "User-Agent": "@memoryrelay/mcp-server"
193
- },
194
- body: body ? JSON.stringify(body) : void 0,
195
- signal: controller.signal
196
- });
197
- if (!response.ok) {
198
- if (response.status === 429) {
199
- const retryAfter = response.headers.get("Retry-After");
200
- const waitMs = retryAfter ? parseInt(retryAfter) * 1e3 : 5e3;
201
- this.logger.warn(`Rate limited, waiting ${waitMs}ms`);
202
- await new Promise((resolve) => setTimeout(resolve, waitMs));
203
- throw new Error(`Rate limited: 429 - Retry after ${waitMs}ms`);
204
- }
205
- const errorData = await response.json().catch(() => ({}));
206
- const errorMsg = `API request failed: ${response.status} ${response.statusText}` + (errorData.message ? ` - ${errorData.message}` : "");
207
- throw new Error(maskApiKey(errorMsg, this.config.apiKey));
208
- }
209
- const data = await response.json();
210
- this.logger.debug(`API response: ${method} ${path}`, { status: response.status });
211
- return data;
212
- } catch (error) {
213
- if (error instanceof Error) {
214
- if (error.name === "AbortError") {
215
- throw new Error(`Request timeout after ${this.config.timeout}ms`);
216
- }
217
- error.message = maskApiKey(error.message, this.config.apiKey);
218
- }
219
- throw error;
220
- } finally {
221
- clearTimeout(timeout);
222
- }
223
- });
224
- }
225
- /**
226
- * Validate content size
227
- */
228
- validateContentSize(content) {
229
- if (content.length > MAX_CONTENT_SIZE) {
230
- throw new Error(`Content exceeds maximum size of ${MAX_CONTENT_SIZE} bytes`);
231
- }
232
- }
233
- /**
234
- * Store a new memory
235
- */
236
- async storeMemory(content, metadata) {
237
- this.validateContentSize(content);
238
- return this.request("POST", "/v1/memories", {
239
- content,
240
- metadata,
241
- agent_id: this.config.agentId
242
- });
243
- }
244
- /**
245
- * Search memories using semantic search
246
- */
247
- async searchMemories(query, limit = 10, threshold = 0.5) {
248
- this.validateContentSize(query);
249
- const response = await this.request(
250
- "POST",
251
- "/v1/memories/search",
252
- { query, limit, threshold, agent_id: this.config.agentId }
253
- );
254
- return response.data;
255
- }
256
- /**
257
- * List recent memories with pagination
258
- */
259
- async listMemories(limit = 20, offset = 0) {
260
- return this.request(
261
- "GET",
262
- `/v1/memories?limit=${limit}&offset=${offset}`
263
- );
264
- }
265
- /**
266
- * Get a specific memory by ID
267
- */
268
- async getMemory(id) {
269
- return this.request("GET", `/v1/memories/${id}`);
270
- }
271
- /**
272
- * Update an existing memory
273
- */
274
- async updateMemory(id, content, metadata) {
275
- this.validateContentSize(content);
276
- return this.request("PATCH", `/v1/memories/${id}`, {
277
- content,
278
- metadata
279
- });
280
- }
281
- /**
282
- * Delete a memory
283
- */
284
- async deleteMemory(id) {
285
- await this.request("DELETE", `/v1/memories/${id}`);
286
- }
287
- /**
288
- * Create a named entity
289
- */
290
- async createEntity(name, type, metadata) {
291
- this.validateContentSize(name);
292
- return this.request("POST", "/v1/entities", {
293
- name,
294
- type,
295
- metadata
296
- });
297
- }
298
- /**
299
- * Link an entity to a memory
300
- */
301
- async linkEntity(entityId, memoryId, relationship = "mentioned_in") {
302
- await this.request("POST", "/v1/entities/links", {
303
- entity_id: entityId,
304
- memory_id: memoryId,
305
- relationship
306
- });
307
- }
308
- /**
309
- * Get an entity by ID
310
- */
311
- async getEntity(id) {
312
- return this.request("GET", `/v1/entities/${id}`);
313
- }
314
- /**
315
- * List entities with pagination
316
- */
317
- async listEntities(limit = 20, offset = 0) {
318
- return this.request(
319
- "GET",
320
- `/v1/entities?limit=${limit}&offset=${offset}`
321
- );
322
- }
323
- /**
324
- * Delete an entity
325
- */
326
- async deleteEntity(id) {
327
- await this.request("DELETE", `/v1/entities/${id}`);
328
- }
329
- /**
330
- * Health check - verify API connectivity
331
- */
332
- async healthCheck() {
333
- try {
334
- await this.request("GET", "/health");
335
- return {
336
- status: "healthy",
337
- message: "API connection successful"
338
- };
339
- } catch (error) {
340
- const errorMsg = error instanceof Error ? error.message : "Unknown error";
341
- return {
342
- status: "unhealthy",
343
- message: `API connection failed: ${errorMsg}`
344
- };
345
- }
346
- }
347
- };
348
-
349
- // src/server.ts
18
+ import { z } from "zod";
350
19
  function sanitizeHtml(str) {
351
20
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;").replace(/\//g, "&#x2F;");
352
21
  }
353
- var uuidSchema = z2.string().uuid();
22
+ var uuidSchema = z.string().uuid();
354
23
  function validateUuid(id, fieldName = "id") {
355
24
  const result = uuidSchema.safeParse(id);
356
25
  if (!result.success) {
@@ -361,35 +30,50 @@ var MemoryRelayMCPServer = class {
361
30
  server;
362
31
  client;
363
32
  logger = getLogger();
364
- constructor(config) {
365
- this.client = new MemoryRelayClient(config);
33
+ activeSessionId = null;
34
+ enabledTools = null;
35
+ constructor(config, client) {
36
+ this.client = client ?? new MemoryRelayClient(config);
37
+ this.enabledTools = getEnabledTools();
366
38
  this.server = new Server(
367
39
  {
368
40
  name: "@memoryrelay/mcp-server",
369
- version: "0.2.0"
41
+ version: "0.3.0"
370
42
  },
371
43
  {
372
44
  capabilities: {
373
- tools: {},
374
- resources: {},
375
- prompts: {}
376
- }
45
+ tools: {}
46
+ },
47
+ instructions: [
48
+ "Recommended workflow:",
49
+ "1. Call project_context(project) to load hot-tier memories for context",
50
+ "2. Call session_start(project, goal) to begin tracking your work",
51
+ "3. Call decision_check(project, topic) before making architectural choices",
52
+ "4. Call pattern_search(query) to find established conventions",
53
+ "5. Work on the task, using memory_store for important findings",
54
+ "6. Call session_end(session_id, summary) when done"
55
+ ].join("\n")
377
56
  }
378
57
  );
379
- this.setupToolHandlers();
380
- this.setupResourceHandlers();
381
- this.setupPromptHandlers();
58
+ this.setupHandlers();
382
59
  this.logger.info("MCP server initialized");
383
60
  }
384
61
  /**
385
- * Setup MCP tool handlers
62
+ * Check if a tool is enabled by the current tool group configuration.
63
+ */
64
+ isToolEnabled(name) {
65
+ return this.enabledTools === null || this.enabledTools.has(name);
66
+ }
67
+ /**
68
+ * Setup MCP protocol handlers
386
69
  */
387
- setupToolHandlers() {
388
- this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
389
- tools: [
70
+ setupHandlers() {
71
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => {
72
+ const sessionHint = this.activeSessionId ? ` Active session: ${this.activeSessionId}.` : " Tip: Call session_start first to track what you are working on.";
73
+ const allTools = [
390
74
  {
391
75
  name: "memory_store",
392
- description: "Store a new memory. Use this to save important information, facts, preferences, or context that should be remembered for future conversations.",
76
+ description: `Store a new memory. Use this to save important information, facts, preferences, or context that should be remembered for future conversations.${sessionHint}`,
393
77
  inputSchema: {
394
78
  type: "object",
395
79
  properties: {
@@ -401,6 +85,35 @@ var MemoryRelayMCPServer = class {
401
85
  type: "object",
402
86
  description: "Optional key-value metadata to attach to the memory",
403
87
  additionalProperties: { type: "string" }
88
+ },
89
+ deduplicate: {
90
+ type: "boolean",
91
+ description: "Check for duplicate/near-duplicate content before storing. Returns existing memory if match found.",
92
+ default: false
93
+ },
94
+ dedup_threshold: {
95
+ type: "number",
96
+ description: "Semantic similarity threshold for dedup (0.5-1.0). Only used when deduplicate=true.",
97
+ minimum: 0.5,
98
+ maximum: 1,
99
+ default: 0.95
100
+ },
101
+ project: {
102
+ type: "string",
103
+ description: 'Project slug to associate the memory with (e.g., "my-api")',
104
+ maxLength: 100
105
+ },
106
+ importance: {
107
+ type: "number",
108
+ description: "Memory importance (0.0-1.0). Defaults to 0.5. Values >= 0.8 promote to hot tier.",
109
+ minimum: 0,
110
+ maximum: 1,
111
+ default: 0.5
112
+ },
113
+ tier: {
114
+ type: "string",
115
+ description: 'Memory tier override: "hot" (always in context), "warm" (default), "cold" (archived). Auto-computed from importance if omitted.',
116
+ enum: ["hot", "warm", "cold"]
404
117
  }
405
118
  },
406
119
  required: ["content"]
@@ -408,7 +121,7 @@ var MemoryRelayMCPServer = class {
408
121
  },
409
122
  {
410
123
  name: "memory_search",
411
- description: "Search memories using natural language. Returns the most relevant memories based on semantic similarity to the query.",
124
+ description: "Search memories using natural language. Returns the most relevant memories based on semantic similarity to the query. Omit agent_id to search across all agents.",
412
125
  inputSchema: {
413
126
  type: "object",
414
127
  properties: {
@@ -416,6 +129,10 @@ var MemoryRelayMCPServer = class {
416
129
  type: "string",
417
130
  description: "Natural language search query"
418
131
  },
132
+ agent_id: {
133
+ type: "string",
134
+ description: "Optional agent ID to scope the search. If omitted, searches across all agents."
135
+ },
419
136
  limit: {
420
137
  type: "number",
421
138
  description: "Maximum number of results to return (1-50)",
@@ -429,6 +146,41 @@ var MemoryRelayMCPServer = class {
429
146
  minimum: 0,
430
147
  maximum: 1,
431
148
  default: 0.5
149
+ },
150
+ include_confidential: {
151
+ type: "boolean",
152
+ description: "Include confidential memories in results (default: false)",
153
+ default: false
154
+ },
155
+ include_archived: {
156
+ type: "boolean",
157
+ description: "Include archived memories in results (default: false)",
158
+ default: false
159
+ },
160
+ compress: {
161
+ type: "boolean",
162
+ description: "Enable context compression on results",
163
+ default: false
164
+ },
165
+ max_context_tokens: {
166
+ type: "number",
167
+ description: "Target total token budget for compressed results"
168
+ },
169
+ project: {
170
+ type: "string",
171
+ description: "Filter by project slug. Omit for cross-project search.",
172
+ maxLength: 100
173
+ },
174
+ tier: {
175
+ type: "string",
176
+ description: 'Filter by tier: "hot", "warm", or "cold". Omit to search all tiers.',
177
+ enum: ["hot", "warm", "cold"]
178
+ },
179
+ min_importance: {
180
+ type: "number",
181
+ description: "Minimum importance threshold (0.0-1.0). Only return memories above this.",
182
+ minimum: 0,
183
+ maximum: 1
432
184
  }
433
185
  },
434
186
  required: ["query"]
@@ -557,79 +309,962 @@ var MemoryRelayMCPServer = class {
557
309
  }
558
310
  },
559
311
  {
560
- name: "memory_health",
561
- description: "Check API connectivity and health status.",
312
+ name: "entity_list",
313
+ description: "List entities in the knowledge graph with pagination. Entities are people, places, organizations, projects, and concepts extracted from memories.",
562
314
  inputSchema: {
563
315
  type: "object",
564
- properties: {}
316
+ properties: {
317
+ limit: {
318
+ type: "number",
319
+ description: "Number of entities to return (1-100)",
320
+ minimum: 1,
321
+ maximum: 100,
322
+ default: 20
323
+ },
324
+ offset: {
325
+ type: "number",
326
+ description: "Offset for pagination",
327
+ minimum: 0,
328
+ default: 0
329
+ }
330
+ }
565
331
  }
566
- }
567
- ]
568
- }));
569
- this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
570
- const { name, arguments: args = {} } = request.params;
571
- this.logger.debug(`Tool called: ${name}`, args);
572
- try {
573
- switch (name) {
574
- case "memory_store": {
575
- const memory = await this.client.storeMemory(
576
- args.content,
577
- args.metadata
578
- );
579
- return {
580
- content: [
581
- {
582
- type: "text",
583
- text: JSON.stringify(memory, null, 2)
584
- }
585
- ]
586
- };
332
+ },
333
+ {
334
+ name: "agent_list",
335
+ description: "List all agents with their memory counts. Agents are namespaces that isolate memories.",
336
+ inputSchema: {
337
+ type: "object",
338
+ properties: {
339
+ limit: {
340
+ type: "number",
341
+ description: "Number of agents to return (1-100)",
342
+ minimum: 1,
343
+ maximum: 100,
344
+ default: 20
345
+ }
346
+ }
587
347
  }
588
- case "memory_search": {
589
- const results = await this.client.searchMemories(
590
- args.query,
591
- args.limit,
592
- args.threshold
593
- );
594
- return {
595
- content: [
596
- {
597
- type: "text",
598
- text: JSON.stringify(
599
- { memories: results, total: results.length },
600
- null,
601
- 2
602
- )
603
- }
604
- ]
605
- };
348
+ },
349
+ {
350
+ name: "agent_create",
351
+ description: "Create a named agent. Agents are namespaces that isolate memories for different use cases or AI assistants.",
352
+ inputSchema: {
353
+ type: "object",
354
+ properties: {
355
+ name: {
356
+ type: "string",
357
+ description: 'Agent name (descriptive, e.g. "iris", "friday", "code-assistant")'
358
+ },
359
+ description: {
360
+ type: "string",
361
+ description: "Optional description of the agent's purpose"
362
+ }
363
+ },
364
+ required: ["name"]
606
365
  }
607
- case "memory_list": {
608
- const response = await this.client.listMemories(
609
- args.limit,
610
- args.offset
611
- );
612
- return {
613
- content: [
614
- {
615
- type: "text",
616
- text: JSON.stringify(response, null, 2)
617
- }
618
- ]
619
- };
366
+ },
367
+ {
368
+ name: "agent_get",
369
+ description: "Get details of a specific agent by ID, including memory count.",
370
+ inputSchema: {
371
+ type: "object",
372
+ properties: {
373
+ id: {
374
+ type: "string",
375
+ description: "The agent ID (UUID) to retrieve"
376
+ }
377
+ },
378
+ required: ["id"]
620
379
  }
621
- case "memory_get": {
622
- const id = args.id;
623
- validateUuid(id, "memory_id");
624
- const memory = await this.client.getMemory(id);
625
- return {
626
- content: [
627
- {
628
- type: "text",
629
- text: JSON.stringify(memory, null, 2)
630
- }
631
- ]
632
- };
380
+ },
381
+ {
382
+ name: "entity_graph",
383
+ description: "Explore the knowledge graph around an entity. Returns neighboring entities and their relationships up to N hops away. Useful for understanding connections between people, projects, and concepts.",
384
+ inputSchema: {
385
+ type: "object",
386
+ properties: {
387
+ entity_id: {
388
+ type: "string",
389
+ description: "Entity UUID to explore from"
390
+ },
391
+ depth: {
392
+ type: "number",
393
+ description: "How many hops to traverse (1 or 2)",
394
+ minimum: 1,
395
+ maximum: 2,
396
+ default: 1
397
+ },
398
+ max_neighbors: {
399
+ type: "number",
400
+ description: "Maximum neighbor entities to return (1-100)",
401
+ minimum: 1,
402
+ maximum: 100,
403
+ default: 50
404
+ }
405
+ },
406
+ required: ["entity_id"]
407
+ }
408
+ },
409
+ {
410
+ name: "memory_batch_store",
411
+ description: "Store multiple memories in a single operation. More efficient than calling memory_store repeatedly when saving several related facts or observations.",
412
+ inputSchema: {
413
+ type: "object",
414
+ properties: {
415
+ memories: {
416
+ type: "array",
417
+ description: "Array of memories to store (1-100 items)",
418
+ items: {
419
+ type: "object",
420
+ properties: {
421
+ content: {
422
+ type: "string",
423
+ description: "The memory content to store"
424
+ },
425
+ metadata: {
426
+ type: "object",
427
+ description: "Optional key-value metadata",
428
+ additionalProperties: { type: "string" }
429
+ }
430
+ },
431
+ required: ["content"]
432
+ },
433
+ minItems: 1,
434
+ maxItems: 100
435
+ }
436
+ },
437
+ required: ["memories"]
438
+ }
439
+ },
440
+ {
441
+ name: "memory_context",
442
+ description: "Build a formatted context string from relevant memories for a given query. Returns ranked memories with similarity scores, ready for use in prompts. More convenient than memory_search when you need a single context block.",
443
+ inputSchema: {
444
+ type: "object",
445
+ properties: {
446
+ query: {
447
+ type: "string",
448
+ description: "Natural language query to find relevant memories"
449
+ },
450
+ limit: {
451
+ type: "number",
452
+ description: "Maximum number of memories to include (1-50)",
453
+ minimum: 1,
454
+ maximum: 50,
455
+ default: 10
456
+ },
457
+ threshold: {
458
+ type: "number",
459
+ description: "Minimum similarity threshold (0-1)",
460
+ minimum: 0,
461
+ maximum: 1,
462
+ default: 0.5
463
+ },
464
+ max_tokens: {
465
+ type: "number",
466
+ description: "Maximum token budget for the context string. Results are truncated to fit."
467
+ }
468
+ },
469
+ required: ["query"]
470
+ }
471
+ },
472
+ {
473
+ name: "project_register",
474
+ description: "Register a project (codebase/repository) to scope memories, sessions, and decisions. Projects have a unique slug for ergonomic referencing.",
475
+ inputSchema: {
476
+ type: "object",
477
+ properties: {
478
+ slug: {
479
+ type: "string",
480
+ description: 'URL-safe identifier (lowercase, hyphens allowed, e.g., "my-api")',
481
+ pattern: "^[a-z0-9][a-z0-9-]*$",
482
+ maxLength: 100
483
+ },
484
+ name: {
485
+ type: "string",
486
+ description: 'Human-readable project name (e.g., "My API Service")',
487
+ maxLength: 255
488
+ },
489
+ description: {
490
+ type: "string",
491
+ description: "Optional project description"
492
+ },
493
+ stack: {
494
+ type: "object",
495
+ description: 'Technical stack metadata (e.g., {"languages": ["python"], "frameworks": ["fastapi"]})'
496
+ },
497
+ repo_url: {
498
+ type: "string",
499
+ description: 'Repository URL (e.g., "https://github.com/org/repo")',
500
+ maxLength: 500
501
+ }
502
+ },
503
+ required: ["slug", "name"]
504
+ }
505
+ },
506
+ {
507
+ name: "project_list",
508
+ description: "List registered projects. Shows all projects with their memory counts.",
509
+ inputSchema: {
510
+ type: "object",
511
+ properties: {
512
+ limit: {
513
+ type: "number",
514
+ description: "Maximum number of projects to return (1-100)",
515
+ minimum: 1,
516
+ maximum: 100,
517
+ default: 20
518
+ }
519
+ }
520
+ }
521
+ },
522
+ {
523
+ name: "project_info",
524
+ description: "Get detailed information about a specific project by its slug, including memory count and stack metadata.",
525
+ inputSchema: {
526
+ type: "object",
527
+ properties: {
528
+ slug: {
529
+ type: "string",
530
+ description: "Project slug to look up"
531
+ }
532
+ },
533
+ required: ["slug"]
534
+ }
535
+ },
536
+ // ── Project Relationship Tools (Issue #186) ──
537
+ {
538
+ name: "project_add_relationship",
539
+ description: "Register a relationship between two projects (e.g., depends_on, api_consumer, shares_schema, shares_infra, pattern_source, forked_from).",
540
+ inputSchema: {
541
+ type: "object",
542
+ properties: {
543
+ from: {
544
+ type: "string",
545
+ description: "Source project slug"
546
+ },
547
+ to: {
548
+ type: "string",
549
+ description: "Target project slug"
550
+ },
551
+ type: {
552
+ type: "string",
553
+ description: "Relationship type (e.g., depends_on, api_consumer, shares_schema)"
554
+ },
555
+ details: {
556
+ type: "object",
557
+ description: "Optional relationship metadata",
558
+ additionalProperties: true
559
+ }
560
+ },
561
+ required: ["from", "to", "type"]
562
+ }
563
+ },
564
+ {
565
+ name: "project_dependencies",
566
+ description: "What does this project depend on? Returns outgoing depends_on and api_consumer relationships.",
567
+ inputSchema: {
568
+ type: "object",
569
+ properties: {
570
+ project: {
571
+ type: "string",
572
+ description: "Project slug"
573
+ }
574
+ },
575
+ required: ["project"]
576
+ }
577
+ },
578
+ {
579
+ name: "project_dependents",
580
+ description: "What depends on this project? Returns incoming depends_on and api_consumer relationships.",
581
+ inputSchema: {
582
+ type: "object",
583
+ properties: {
584
+ project: {
585
+ type: "string",
586
+ description: "Project slug"
587
+ }
588
+ },
589
+ required: ["project"]
590
+ }
591
+ },
592
+ {
593
+ name: "project_related",
594
+ description: "All related projects with relationship types and direction indicators.",
595
+ inputSchema: {
596
+ type: "object",
597
+ properties: {
598
+ project: {
599
+ type: "string",
600
+ description: "Project slug"
601
+ }
602
+ },
603
+ required: ["project"]
604
+ }
605
+ },
606
+ {
607
+ name: "project_impact",
608
+ description: "Impact analysis: given a change description, which dependent projects might be affected? Searches decisions and memories in downstream projects.",
609
+ inputSchema: {
610
+ type: "object",
611
+ properties: {
612
+ project: {
613
+ type: "string",
614
+ description: "Project slug being changed"
615
+ },
616
+ change_description: {
617
+ type: "string",
618
+ description: "Description of the change to analyze (max 5000 chars)",
619
+ maxLength: 5e3
620
+ }
621
+ },
622
+ required: ["project", "change_description"]
623
+ }
624
+ },
625
+ {
626
+ name: "project_shared_patterns",
627
+ description: "What do two projects have in common? Returns patterns adopted by both projects.",
628
+ inputSchema: {
629
+ type: "object",
630
+ properties: {
631
+ project_a: {
632
+ type: "string",
633
+ description: "First project slug"
634
+ },
635
+ project_b: {
636
+ type: "string",
637
+ description: "Second project slug"
638
+ }
639
+ },
640
+ required: ["project_a", "project_b"]
641
+ }
642
+ },
643
+ {
644
+ name: "memory_health",
645
+ description: "Check API connectivity and health status.",
646
+ inputSchema: {
647
+ type: "object",
648
+ properties: {}
649
+ }
650
+ },
651
+ {
652
+ name: "session_start",
653
+ description: "Start a new session to group related memories. Sessions track bounded interaction periods (e.g., a coding session, a conversation). Use session_recall to see what happened in previous sessions.",
654
+ inputSchema: {
655
+ type: "object",
656
+ properties: {
657
+ title: {
658
+ type: "string",
659
+ description: 'Session title or goal (e.g., "Fix email queue retry logic")',
660
+ maxLength: 500
661
+ },
662
+ project: {
663
+ type: "string",
664
+ description: 'Project label for filtering sessions (e.g., "northrelay")',
665
+ maxLength: 255
666
+ },
667
+ metadata: {
668
+ type: "object",
669
+ description: "Optional metadata",
670
+ additionalProperties: { type: "string" }
671
+ }
672
+ }
673
+ }
674
+ },
675
+ {
676
+ name: "session_end",
677
+ description: "End an active session with an optional summary of what was accomplished.",
678
+ inputSchema: {
679
+ type: "object",
680
+ properties: {
681
+ id: {
682
+ type: "string",
683
+ description: "Session ID (UUID) to end"
684
+ },
685
+ summary: {
686
+ type: "string",
687
+ description: "Summary of what was accomplished during the session",
688
+ maxLength: 5e4
689
+ }
690
+ },
691
+ required: ["id"]
692
+ }
693
+ },
694
+ {
695
+ name: "session_recall",
696
+ description: "Recall a session by ID, including all memories created during it. Use this to review what happened in a past session.",
697
+ inputSchema: {
698
+ type: "object",
699
+ properties: {
700
+ id: {
701
+ type: "string",
702
+ description: "Session ID (UUID) to recall"
703
+ }
704
+ },
705
+ required: ["id"]
706
+ }
707
+ },
708
+ {
709
+ name: "session_list",
710
+ description: "List recent sessions, optionally filtered by project or status. Shows session summaries to understand what happened in previous sessions.",
711
+ inputSchema: {
712
+ type: "object",
713
+ properties: {
714
+ limit: {
715
+ type: "number",
716
+ description: "Maximum number of sessions to return (1-100)",
717
+ minimum: 1,
718
+ maximum: 100,
719
+ default: 20
720
+ },
721
+ project: {
722
+ type: "string",
723
+ description: "Filter by project label"
724
+ },
725
+ status: {
726
+ type: "string",
727
+ enum: ["active", "ended"],
728
+ description: "Filter by session status"
729
+ }
730
+ }
731
+ }
732
+ },
733
+ {
734
+ name: "decision_record",
735
+ description: "Record an architectural decision with rationale. Use to log important technical choices, design decisions, or policy changes so they can be referenced later.",
736
+ inputSchema: {
737
+ type: "object",
738
+ properties: {
739
+ title: {
740
+ type: "string",
741
+ description: 'Decision title (e.g., "Use PostgreSQL instead of MongoDB")',
742
+ maxLength: 500
743
+ },
744
+ rationale: {
745
+ type: "string",
746
+ description: "Why this decision was made. Include context, constraints, and reasoning.",
747
+ maxLength: 5e4
748
+ },
749
+ alternatives: {
750
+ type: "string",
751
+ description: "Alternatives that were considered and why they were rejected"
752
+ },
753
+ project: {
754
+ type: "string",
755
+ description: "Project slug to scope the decision to",
756
+ maxLength: 100
757
+ },
758
+ tags: {
759
+ type: "array",
760
+ items: { type: "string" },
761
+ description: 'Tags for categorization (e.g., ["architecture", "database"])'
762
+ },
763
+ status: {
764
+ type: "string",
765
+ enum: ["active", "experimental"],
766
+ description: "Decision status (default: active)"
767
+ }
768
+ },
769
+ required: ["title", "rationale"]
770
+ }
771
+ },
772
+ {
773
+ name: "decision_list",
774
+ description: "List recorded decisions, optionally filtered by project, status, or tags.",
775
+ inputSchema: {
776
+ type: "object",
777
+ properties: {
778
+ limit: {
779
+ type: "number",
780
+ description: "Maximum results (1-100, default 20)",
781
+ minimum: 1,
782
+ maximum: 100,
783
+ default: 20
784
+ },
785
+ project: {
786
+ type: "string",
787
+ description: "Filter by project slug"
788
+ },
789
+ status: {
790
+ type: "string",
791
+ enum: ["active", "superseded", "reverted", "experimental"],
792
+ description: "Filter by decision status"
793
+ },
794
+ tags: {
795
+ type: "string",
796
+ description: "Comma-separated tags to filter by"
797
+ }
798
+ }
799
+ }
800
+ },
801
+ {
802
+ name: "decision_supersede",
803
+ description: "Supersede an existing decision with a new one. Marks the old decision as superseded and creates a new active decision.",
804
+ inputSchema: {
805
+ type: "object",
806
+ properties: {
807
+ id: {
808
+ type: "string",
809
+ description: "ID (UUID) of the decision to supersede"
810
+ },
811
+ title: {
812
+ type: "string",
813
+ description: "Title of the new decision"
814
+ },
815
+ rationale: {
816
+ type: "string",
817
+ description: "Rationale for the new decision"
818
+ },
819
+ alternatives: {
820
+ type: "string",
821
+ description: "Alternatives considered"
822
+ },
823
+ tags: {
824
+ type: "array",
825
+ items: { type: "string" },
826
+ description: "Tags for the new decision (inherits from old if omitted)"
827
+ }
828
+ },
829
+ required: ["id", "title", "rationale"]
830
+ }
831
+ },
832
+ {
833
+ name: "decision_check",
834
+ description: "Check if there are existing decisions about a topic using semantic search. Use this before making a new decision to see if the topic has already been addressed.",
835
+ inputSchema: {
836
+ type: "object",
837
+ properties: {
838
+ query: {
839
+ type: "string",
840
+ description: "Natural language query describing the decision topic to check"
841
+ },
842
+ project: {
843
+ type: "string",
844
+ description: "Filter by project slug"
845
+ },
846
+ limit: {
847
+ type: "number",
848
+ description: "Maximum results (1-20, default 5)",
849
+ minimum: 1,
850
+ maximum: 20,
851
+ default: 5
852
+ },
853
+ threshold: {
854
+ type: "number",
855
+ description: "Minimum similarity threshold (0-1, default 0.3)",
856
+ minimum: 0,
857
+ maximum: 1,
858
+ default: 0.3
859
+ },
860
+ include_superseded: {
861
+ type: "boolean",
862
+ description: "Include superseded decisions in results (default: false)",
863
+ default: false
864
+ }
865
+ },
866
+ required: ["query"]
867
+ }
868
+ },
869
+ {
870
+ name: "pattern_create",
871
+ description: 'Create a reusable development pattern or convention. Use to document patterns like "always use Zod for validation" or "error responses follow RFC 7807" so they can be discovered and adopted across projects.',
872
+ inputSchema: {
873
+ type: "object",
874
+ properties: {
875
+ title: {
876
+ type: "string",
877
+ description: 'Pattern title (e.g., "Zod validation at API boundaries")',
878
+ maxLength: 500
879
+ },
880
+ description: {
881
+ type: "string",
882
+ description: "Full description with context, rationale, and when to apply this pattern",
883
+ maxLength: 5e4
884
+ },
885
+ category: {
886
+ type: "string",
887
+ description: 'Category (e.g., "validation", "error-handling", "auth", "testing")',
888
+ maxLength: 100
889
+ },
890
+ example_code: {
891
+ type: "string",
892
+ description: "Code snippet demonstrating the pattern"
893
+ },
894
+ scope: {
895
+ type: "string",
896
+ enum: ["global", "project"],
897
+ description: "Scope: global (all projects) or project (specific projects). Default: global"
898
+ },
899
+ tags: {
900
+ type: "array",
901
+ items: { type: "string" },
902
+ description: "Tags for categorization"
903
+ },
904
+ source_project: {
905
+ type: "string",
906
+ description: "Slug of the project where this pattern was first established"
907
+ }
908
+ },
909
+ required: ["title", "description"]
910
+ }
911
+ },
912
+ {
913
+ name: "pattern_search",
914
+ description: "Search for development patterns using semantic similarity. When working on a project, shows both global patterns and patterns adopted by that project.",
915
+ inputSchema: {
916
+ type: "object",
917
+ properties: {
918
+ query: {
919
+ type: "string",
920
+ description: 'Natural language query describing the pattern to find (e.g., "validation", "error handling")'
921
+ },
922
+ category: {
923
+ type: "string",
924
+ description: "Filter by category"
925
+ },
926
+ project: {
927
+ type: "string",
928
+ description: "Project context \u2014 shows global patterns plus patterns adopted by this project"
929
+ },
930
+ limit: {
931
+ type: "number",
932
+ description: "Maximum results (1-50, default 10)",
933
+ minimum: 1,
934
+ maximum: 50,
935
+ default: 10
936
+ },
937
+ threshold: {
938
+ type: "number",
939
+ description: "Minimum similarity threshold (0-1, default 0.3)",
940
+ minimum: 0,
941
+ maximum: 1,
942
+ default: 0.3
943
+ }
944
+ },
945
+ required: ["query"]
946
+ }
947
+ },
948
+ {
949
+ name: "pattern_adopt",
950
+ description: "Mark a pattern as adopted by a project. This links the pattern to the project so it shows up in project-scoped searches.",
951
+ inputSchema: {
952
+ type: "object",
953
+ properties: {
954
+ id: {
955
+ type: "string",
956
+ description: "Pattern ID (UUID) to adopt"
957
+ },
958
+ project: {
959
+ type: "string",
960
+ description: "Project slug to adopt this pattern for"
961
+ }
962
+ },
963
+ required: ["id", "project"]
964
+ }
965
+ },
966
+ {
967
+ name: "pattern_suggest",
968
+ description: "Suggest relevant patterns for a project based on what similar projects have adopted. Returns patterns not yet adopted by the target project, ranked by popularity.",
969
+ inputSchema: {
970
+ type: "object",
971
+ properties: {
972
+ project: {
973
+ type: "string",
974
+ description: "Project slug to get suggestions for"
975
+ },
976
+ limit: {
977
+ type: "number",
978
+ description: "Maximum suggestions (1-50, default 10)",
979
+ minimum: 1,
980
+ maximum: 50,
981
+ default: 10
982
+ }
983
+ },
984
+ required: ["project"]
985
+ }
986
+ },
987
+ {
988
+ name: "project_context",
989
+ description: "Get full project context for LLM consumption. Returns hot-tier memories, active decisions, adopted patterns, and a pre-formatted markdown context string.",
990
+ inputSchema: {
991
+ type: "object",
992
+ properties: {
993
+ project: {
994
+ type: "string",
995
+ description: "Project slug to get context for"
996
+ }
997
+ },
998
+ required: ["project"]
999
+ }
1000
+ },
1001
+ {
1002
+ name: "memory_promote",
1003
+ description: "Update a memory's importance and tier. Use to promote critical memories to hot tier (always in context) or demote them to cold tier (archived).",
1004
+ inputSchema: {
1005
+ type: "object",
1006
+ properties: {
1007
+ memory_id: {
1008
+ type: "string",
1009
+ description: "Memory ID (UUID) to promote/demote"
1010
+ },
1011
+ importance: {
1012
+ type: "number",
1013
+ description: "New importance value (0.0-1.0). Values >= 0.8 promote to hot tier.",
1014
+ minimum: 0,
1015
+ maximum: 1
1016
+ },
1017
+ tier: {
1018
+ type: "string",
1019
+ description: 'Optional tier override: "hot", "warm", or "cold". Auto-computed from importance if omitted.',
1020
+ enum: ["hot", "warm", "cold"]
1021
+ }
1022
+ },
1023
+ required: ["memory_id", "importance"]
1024
+ }
1025
+ },
1026
+ // ── V2 Async API Tools (60-600x faster) ──
1027
+ {
1028
+ name: "memory_store_async",
1029
+ description: "Store a memory asynchronously using V2 API. Returns immediately (<50ms) with 202 Accepted and a job ID. Background workers generate the embedding. Use memory_status to poll for completion. Prefer this over memory_store for high-throughput or latency-sensitive applications.",
1030
+ inputSchema: {
1031
+ type: "object",
1032
+ properties: {
1033
+ content: {
1034
+ type: "string",
1035
+ description: "The memory content to store. Be specific and include relevant context."
1036
+ },
1037
+ metadata: {
1038
+ type: "object",
1039
+ description: "Optional key-value metadata to attach to the memory",
1040
+ additionalProperties: { type: "string" }
1041
+ },
1042
+ project: {
1043
+ type: "string",
1044
+ description: "Project slug to associate the memory with",
1045
+ maxLength: 100
1046
+ },
1047
+ importance: {
1048
+ type: "number",
1049
+ description: "Memory importance (0.0-1.0). Values >= 0.8 promote to hot tier.",
1050
+ minimum: 0,
1051
+ maximum: 1
1052
+ },
1053
+ tier: {
1054
+ type: "string",
1055
+ description: 'Memory tier override: "hot", "warm", or "cold"',
1056
+ enum: ["hot", "warm", "cold"]
1057
+ },
1058
+ webhook_url: {
1059
+ type: "string",
1060
+ description: "Optional webhook URL to receive completion notification"
1061
+ }
1062
+ },
1063
+ required: ["content"]
1064
+ }
1065
+ },
1066
+ {
1067
+ name: "memory_status",
1068
+ description: "Check the processing status of a memory created via memory_store_async. Poll this endpoint to determine when embedding generation is complete. Status values: pending (waiting for worker), processing (generating embedding), ready (searchable), failed (error occurred).",
1069
+ inputSchema: {
1070
+ type: "object",
1071
+ properties: {
1072
+ memory_id: {
1073
+ type: "string",
1074
+ description: "Memory ID (UUID) to check status for"
1075
+ }
1076
+ },
1077
+ required: ["memory_id"]
1078
+ }
1079
+ },
1080
+ {
1081
+ name: "context_build",
1082
+ description: "Build a ranked context bundle from memories using V2 API with optional AI summarization. Searches for relevant memories, ranks them by composite score, and optionally generates an AI summary. Useful for building token-efficient context windows for LLM prompts. Supports custom LLM endpoints (Ollama, llama.cpp, vLLM, etc.).",
1083
+ inputSchema: {
1084
+ type: "object",
1085
+ properties: {
1086
+ query: {
1087
+ type: "string",
1088
+ description: "Context query describing what information is needed"
1089
+ },
1090
+ max_memories: {
1091
+ type: "number",
1092
+ description: "Maximum number of memories to include (1-100, default 20)",
1093
+ minimum: 1,
1094
+ maximum: 100,
1095
+ default: 20
1096
+ },
1097
+ max_tokens: {
1098
+ type: "number",
1099
+ description: "Token budget for context window (enforces truncation)",
1100
+ minimum: 100,
1101
+ maximum: 128e3
1102
+ },
1103
+ ai_enhanced: {
1104
+ type: "boolean",
1105
+ description: "Enable AI summarization of context (uses LLM)",
1106
+ default: false
1107
+ },
1108
+ search_mode: {
1109
+ type: "string",
1110
+ description: 'Search mode: "semantic", "hybrid", or "keyword"',
1111
+ enum: ["semantic", "hybrid", "keyword"],
1112
+ default: "hybrid"
1113
+ },
1114
+ llm_api_url: {
1115
+ type: "string",
1116
+ description: "Custom LLM API URL for summarization (OpenAI-compatible). Use for local LLMs: Ollama (http://localhost:11434/v1), llama.cpp, vLLM, LM Studio, etc."
1117
+ },
1118
+ llm_model: {
1119
+ type: "string",
1120
+ description: 'Model name for custom LLM (e.g., "mistral", "llama3", "gemma")'
1121
+ },
1122
+ exclude_memory_ids: {
1123
+ type: "array",
1124
+ items: { type: "string" },
1125
+ description: "Memory IDs to exclude (for multi-turn context building)"
1126
+ }
1127
+ },
1128
+ required: ["query"]
1129
+ }
1130
+ },
1131
+ // ── Tool Name Aliases (backward compatibility with OpenClaw plugin) ──
1132
+ {
1133
+ name: "memory_forget",
1134
+ description: "Delete a memory by ID, or search by query to find candidates. Alias for memory_delete. Provide memoryId for direct deletion, or query to search first.",
1135
+ inputSchema: {
1136
+ type: "object",
1137
+ properties: {
1138
+ memoryId: {
1139
+ type: "string",
1140
+ description: "Memory ID to delete"
1141
+ },
1142
+ query: {
1143
+ type: "string",
1144
+ description: "Search query to find memory"
1145
+ }
1146
+ }
1147
+ }
1148
+ },
1149
+ {
1150
+ name: "memory_recall",
1151
+ description: "Search memories using natural language. Alias for memory_search with default project scoping. Returns the most relevant memories based on semantic similarity.",
1152
+ inputSchema: {
1153
+ type: "object",
1154
+ properties: {
1155
+ query: {
1156
+ type: "string",
1157
+ description: "Natural language search query"
1158
+ },
1159
+ limit: {
1160
+ type: "number",
1161
+ description: "Maximum results (1-50). Default 5.",
1162
+ minimum: 1,
1163
+ maximum: 50,
1164
+ default: 5
1165
+ },
1166
+ threshold: {
1167
+ type: "number",
1168
+ description: "Minimum similarity threshold (0-1). Default 0.3.",
1169
+ minimum: 0,
1170
+ maximum: 1,
1171
+ default: 0.3
1172
+ },
1173
+ project: {
1174
+ type: "string",
1175
+ description: "Filter by project slug"
1176
+ },
1177
+ max_tokens: {
1178
+ type: "number",
1179
+ description: "Token budget for context window"
1180
+ }
1181
+ },
1182
+ required: ["query"]
1183
+ }
1184
+ }
1185
+ ];
1186
+ return {
1187
+ tools: allTools.filter((t) => this.isToolEnabled(t.name))
1188
+ };
1189
+ });
1190
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
1191
+ const { name, arguments: args = {} } = request.params;
1192
+ this.logger.debug(`Tool called: ${name}`, args);
1193
+ try {
1194
+ switch (name) {
1195
+ case "memory_store": {
1196
+ const memory = await this.client.storeMemory(
1197
+ args.content,
1198
+ args.metadata,
1199
+ args.deduplicate,
1200
+ args.dedup_threshold,
1201
+ args.project,
1202
+ args.importance,
1203
+ args.tier
1204
+ );
1205
+ return {
1206
+ content: [
1207
+ {
1208
+ type: "text",
1209
+ text: JSON.stringify(memory, null, 2)
1210
+ }
1211
+ ]
1212
+ };
1213
+ }
1214
+ case "memory_search": {
1215
+ const searchAgentId = args.agent_id != null ? args.agent_id : null;
1216
+ const results = await this.client.searchMemories(
1217
+ args.query,
1218
+ args.limit,
1219
+ args.threshold,
1220
+ searchAgentId,
1221
+ args.include_confidential ?? false,
1222
+ args.include_archived ?? false,
1223
+ args.compress ?? false,
1224
+ args.max_context_tokens,
1225
+ args.project,
1226
+ args.tier,
1227
+ args.min_importance
1228
+ );
1229
+ return {
1230
+ content: [
1231
+ {
1232
+ type: "text",
1233
+ text: JSON.stringify(
1234
+ { memories: results, total: results.length },
1235
+ null,
1236
+ 2
1237
+ )
1238
+ }
1239
+ ]
1240
+ };
1241
+ }
1242
+ case "memory_list": {
1243
+ const response = await this.client.listMemories(
1244
+ args.limit,
1245
+ args.offset
1246
+ );
1247
+ return {
1248
+ content: [
1249
+ {
1250
+ type: "text",
1251
+ text: JSON.stringify(response, null, 2)
1252
+ }
1253
+ ]
1254
+ };
1255
+ }
1256
+ case "memory_get": {
1257
+ const id = args.id;
1258
+ validateUuid(id, "memory_id");
1259
+ const memory = await this.client.getMemory(id);
1260
+ return {
1261
+ content: [
1262
+ {
1263
+ type: "text",
1264
+ text: JSON.stringify(memory, null, 2)
1265
+ }
1266
+ ]
1267
+ };
633
1268
  }
634
1269
  case "memory_update": {
635
1270
  const id = args.id;
@@ -643,99 +1278,601 @@ var MemoryRelayMCPServer = class {
643
1278
  content: [
644
1279
  {
645
1280
  type: "text",
646
- text: JSON.stringify(memory, null, 2)
1281
+ text: JSON.stringify(memory, null, 2)
1282
+ }
1283
+ ]
1284
+ };
1285
+ }
1286
+ case "memory_delete": {
1287
+ const id = args.id;
1288
+ validateUuid(id, "memory_id");
1289
+ await this.client.deleteMemory(id);
1290
+ return {
1291
+ content: [
1292
+ {
1293
+ type: "text",
1294
+ text: JSON.stringify(
1295
+ { success: true, message: "Memory deleted successfully" },
1296
+ null,
1297
+ 2
1298
+ )
1299
+ }
1300
+ ]
1301
+ };
1302
+ }
1303
+ case "entity_create": {
1304
+ const entitySchema = z.object({
1305
+ name: z.string().min(1).max(200),
1306
+ type: z.enum(["person", "place", "organization", "project", "concept", "other"]),
1307
+ metadata: z.record(z.string()).optional()
1308
+ });
1309
+ const validatedInput = entitySchema.parse(args);
1310
+ const sanitizedName = sanitizeHtml(validatedInput.name);
1311
+ const entity = await this.client.createEntity(
1312
+ sanitizedName,
1313
+ validatedInput.type,
1314
+ validatedInput.metadata
1315
+ );
1316
+ return {
1317
+ content: [
1318
+ {
1319
+ type: "text",
1320
+ text: JSON.stringify(entity, null, 2)
1321
+ }
1322
+ ]
1323
+ };
1324
+ }
1325
+ case "entity_link": {
1326
+ const linkSchema = z.object({
1327
+ entity_id: z.string().uuid(),
1328
+ memory_id: z.string().uuid(),
1329
+ relationship: z.string().default("mentioned_in")
1330
+ });
1331
+ const validatedInput = linkSchema.parse(args);
1332
+ await this.client.linkEntity(
1333
+ validatedInput.entity_id,
1334
+ validatedInput.memory_id,
1335
+ validatedInput.relationship
1336
+ );
1337
+ return {
1338
+ content: [
1339
+ {
1340
+ type: "text",
1341
+ text: JSON.stringify(
1342
+ {
1343
+ success: true,
1344
+ message: "Entity linked to memory successfully",
1345
+ entity_id: validatedInput.entity_id,
1346
+ memory_id: validatedInput.memory_id,
1347
+ relationship: validatedInput.relationship
1348
+ },
1349
+ null,
1350
+ 2
1351
+ )
1352
+ }
1353
+ ]
1354
+ };
1355
+ }
1356
+ case "entity_list": {
1357
+ const response = await this.client.listEntities(
1358
+ args.limit,
1359
+ args.offset
1360
+ );
1361
+ return {
1362
+ content: [
1363
+ {
1364
+ type: "text",
1365
+ text: JSON.stringify(response, null, 2)
1366
+ }
1367
+ ]
1368
+ };
1369
+ }
1370
+ case "agent_list": {
1371
+ const response = await this.client.listAgents(
1372
+ args.limit
1373
+ );
1374
+ return {
1375
+ content: [
1376
+ {
1377
+ type: "text",
1378
+ text: JSON.stringify(response, null, 2)
1379
+ }
1380
+ ]
1381
+ };
1382
+ }
1383
+ case "agent_create": {
1384
+ const createSchema = z.object({
1385
+ name: z.string().min(1).max(255),
1386
+ description: z.string().max(1e3).optional()
1387
+ });
1388
+ const validatedInput = createSchema.parse(args);
1389
+ const sanitizedName = sanitizeHtml(validatedInput.name);
1390
+ const agent = await this.client.createAgent(
1391
+ sanitizedName,
1392
+ validatedInput.description
1393
+ );
1394
+ return {
1395
+ content: [
1396
+ {
1397
+ type: "text",
1398
+ text: JSON.stringify(agent, null, 2)
1399
+ }
1400
+ ]
1401
+ };
1402
+ }
1403
+ case "agent_get": {
1404
+ const id = args.id;
1405
+ validateUuid(id, "agent_id");
1406
+ const agent = await this.client.getAgent(id);
1407
+ return {
1408
+ content: [
1409
+ {
1410
+ type: "text",
1411
+ text: JSON.stringify(agent, null, 2)
1412
+ }
1413
+ ]
1414
+ };
1415
+ }
1416
+ case "entity_graph": {
1417
+ const graphSchema = z.object({
1418
+ entity_id: z.string().uuid(),
1419
+ depth: z.number().min(1).max(2).default(1),
1420
+ max_neighbors: z.number().min(1).max(100).default(50)
1421
+ });
1422
+ const validatedInput = graphSchema.parse(args);
1423
+ const result = await this.client.getEntityNeighborhood(
1424
+ validatedInput.entity_id,
1425
+ validatedInput.depth,
1426
+ validatedInput.max_neighbors
1427
+ );
1428
+ return {
1429
+ content: [
1430
+ {
1431
+ type: "text",
1432
+ text: JSON.stringify(result, null, 2)
647
1433
  }
648
1434
  ]
649
1435
  };
650
1436
  }
651
- case "memory_delete": {
652
- const id = args.id;
653
- validateUuid(id, "memory_id");
654
- await this.client.deleteMemory(id);
1437
+ case "memory_batch_store": {
1438
+ const batchSchema = z.object({
1439
+ memories: z.array(z.object({
1440
+ content: z.string().min(1),
1441
+ metadata: z.record(z.string()).optional()
1442
+ })).min(1).max(100)
1443
+ });
1444
+ const validatedInput = batchSchema.parse(args);
1445
+ const result = await this.client.batchStoreMemories(validatedInput.memories);
655
1446
  return {
656
1447
  content: [
657
1448
  {
658
1449
  type: "text",
659
- text: JSON.stringify(
660
- { success: true, message: "Memory deleted successfully" },
661
- null,
662
- 2
663
- )
1450
+ text: JSON.stringify(result, null, 2)
664
1451
  }
665
1452
  ]
666
1453
  };
667
1454
  }
668
- case "entity_create": {
669
- const entitySchema = z2.object({
670
- name: z2.string().min(1).max(200),
671
- type: z2.enum(["person", "place", "organization", "project", "concept", "other"]),
672
- metadata: z2.record(z2.string()).optional()
1455
+ case "memory_context": {
1456
+ const contextSchema = z.object({
1457
+ query: z.string().min(1),
1458
+ limit: z.number().min(1).max(50).default(10),
1459
+ threshold: z.number().min(0).max(1).default(0.5),
1460
+ max_tokens: z.number().positive().optional()
673
1461
  });
674
- const validatedInput = entitySchema.parse(args);
675
- const sanitizedName = sanitizeHtml(validatedInput.name);
676
- const entity = await this.client.createEntity(
677
- sanitizedName,
678
- validatedInput.type,
679
- validatedInput.metadata
1462
+ const validatedInput = contextSchema.parse(args);
1463
+ const result = await this.client.buildContext(
1464
+ validatedInput.query,
1465
+ validatedInput.limit,
1466
+ validatedInput.threshold,
1467
+ validatedInput.max_tokens
680
1468
  );
681
1469
  return {
682
1470
  content: [
683
1471
  {
684
1472
  type: "text",
685
- text: JSON.stringify(entity, null, 2)
1473
+ text: JSON.stringify(result, null, 2)
686
1474
  }
687
1475
  ]
688
1476
  };
689
1477
  }
690
- case "entity_link": {
691
- const linkSchema = z2.object({
692
- entity_id: z2.string().uuid(),
693
- memory_id: z2.string().uuid(),
694
- relationship: z2.string().default("mentioned_in")
1478
+ case "project_register": {
1479
+ const projectSchema = z.object({
1480
+ slug: z.string().min(1).max(100).regex(/^[a-z0-9][a-z0-9-]*$/),
1481
+ name: z.string().min(1).max(255),
1482
+ description: z.string().optional(),
1483
+ stack: z.record(z.unknown()).optional(),
1484
+ repo_url: z.string().max(500).optional()
695
1485
  });
696
- const validatedInput = linkSchema.parse(args);
697
- await this.client.linkEntity(
698
- validatedInput.entity_id,
699
- validatedInput.memory_id,
700
- validatedInput.relationship
1486
+ const validatedInput = projectSchema.parse(args);
1487
+ const project = await this.client.createProject(
1488
+ validatedInput.slug,
1489
+ validatedInput.name,
1490
+ validatedInput.description,
1491
+ validatedInput.stack,
1492
+ validatedInput.repo_url
1493
+ );
1494
+ return {
1495
+ content: [{ type: "text", text: JSON.stringify(project, null, 2) }]
1496
+ };
1497
+ }
1498
+ case "project_list": {
1499
+ const response = await this.client.listProjects(
1500
+ args.limit
1501
+ );
1502
+ return {
1503
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
1504
+ };
1505
+ }
1506
+ case "project_info": {
1507
+ const slug = args.slug;
1508
+ if (!slug) {
1509
+ throw new Error("Project slug is required");
1510
+ }
1511
+ const project = await this.client.getProject(slug);
1512
+ return {
1513
+ content: [{ type: "text", text: JSON.stringify(project, null, 2) }]
1514
+ };
1515
+ }
1516
+ // ── Project Relationship Handlers (Issue #186) ──
1517
+ case "project_add_relationship": {
1518
+ const relSchema = z.object({
1519
+ from: z.string().min(1),
1520
+ to: z.string().min(1),
1521
+ type: z.string().min(1).max(100),
1522
+ details: z.record(z.unknown()).optional()
1523
+ });
1524
+ const relArgs = relSchema.parse(args);
1525
+ const relationship = await this.client.addProjectRelationship(
1526
+ relArgs.from,
1527
+ relArgs.to,
1528
+ relArgs.type,
1529
+ relArgs.details
1530
+ );
1531
+ return {
1532
+ content: [{ type: "text", text: JSON.stringify(relationship, null, 2) }]
1533
+ };
1534
+ }
1535
+ case "project_dependencies": {
1536
+ const depSlug = args.project;
1537
+ if (!depSlug) throw new Error("Project slug is required");
1538
+ const deps = await this.client.getProjectDependencies(depSlug);
1539
+ return {
1540
+ content: [{ type: "text", text: JSON.stringify(deps, null, 2) }]
1541
+ };
1542
+ }
1543
+ case "project_dependents": {
1544
+ const deptSlug = args.project;
1545
+ if (!deptSlug) throw new Error("Project slug is required");
1546
+ const dependents = await this.client.getProjectDependents(deptSlug);
1547
+ return {
1548
+ content: [{ type: "text", text: JSON.stringify(dependents, null, 2) }]
1549
+ };
1550
+ }
1551
+ case "project_related": {
1552
+ const relatedSlug = args.project;
1553
+ if (!relatedSlug) throw new Error("Project slug is required");
1554
+ const related = await this.client.getProjectRelated(relatedSlug);
1555
+ return {
1556
+ content: [{ type: "text", text: JSON.stringify(related, null, 2) }]
1557
+ };
1558
+ }
1559
+ case "project_impact": {
1560
+ const impactSchema = z.object({
1561
+ project: z.string().min(1),
1562
+ change_description: z.string().min(1).max(5e3)
1563
+ });
1564
+ const impactArgs = impactSchema.parse(args);
1565
+ const impact = await this.client.projectImpactAnalysis(
1566
+ impactArgs.project,
1567
+ impactArgs.change_description
1568
+ );
1569
+ return {
1570
+ content: [{ type: "text", text: JSON.stringify(impact, null, 2) }]
1571
+ };
1572
+ }
1573
+ case "project_shared_patterns": {
1574
+ const spSchema = z.object({
1575
+ project_a: z.string().min(1),
1576
+ project_b: z.string().min(1)
1577
+ });
1578
+ const spArgs = spSchema.parse(args);
1579
+ const shared = await this.client.getSharedPatterns(
1580
+ spArgs.project_a,
1581
+ spArgs.project_b
1582
+ );
1583
+ return {
1584
+ content: [{ type: "text", text: JSON.stringify(shared, null, 2) }]
1585
+ };
1586
+ }
1587
+ case "memory_health": {
1588
+ const health = await this.client.healthCheck();
1589
+ return {
1590
+ content: [
1591
+ {
1592
+ type: "text",
1593
+ text: JSON.stringify(health, null, 2)
1594
+ }
1595
+ ]
1596
+ };
1597
+ }
1598
+ case "session_start": {
1599
+ const session = await this.client.startSession(
1600
+ args.title,
1601
+ args.project,
1602
+ args.metadata
701
1603
  );
1604
+ this.activeSessionId = session.id ?? null;
702
1605
  return {
703
1606
  content: [
704
1607
  {
705
1608
  type: "text",
706
- text: JSON.stringify(
707
- {
708
- success: true,
709
- message: "Entity linked to memory successfully",
710
- entity_id: validatedInput.entity_id,
711
- memory_id: validatedInput.memory_id,
712
- relationship: validatedInput.relationship
713
- },
714
- null,
715
- 2
716
- )
1609
+ text: JSON.stringify(session, null, 2)
717
1610
  }
718
1611
  ]
719
1612
  };
720
1613
  }
721
- case "memory_health": {
722
- const health = await this.client.healthCheck();
1614
+ case "session_end": {
1615
+ const id = args.id;
1616
+ validateUuid(id, "session_id");
1617
+ const session = await this.client.endSession(
1618
+ id,
1619
+ args.summary
1620
+ );
1621
+ if (this.activeSessionId === id) {
1622
+ this.activeSessionId = null;
1623
+ }
723
1624
  return {
724
1625
  content: [
725
1626
  {
726
1627
  type: "text",
727
- text: JSON.stringify(health, null, 2)
1628
+ text: JSON.stringify(session, null, 2)
1629
+ }
1630
+ ]
1631
+ };
1632
+ }
1633
+ case "session_recall": {
1634
+ const id = args.id;
1635
+ validateUuid(id, "session_id");
1636
+ const session = await this.client.getSession(id);
1637
+ return {
1638
+ content: [
1639
+ {
1640
+ type: "text",
1641
+ text: JSON.stringify(session, null, 2)
1642
+ }
1643
+ ]
1644
+ };
1645
+ }
1646
+ case "session_list": {
1647
+ const response = await this.client.listSessions(
1648
+ args.limit,
1649
+ void 0,
1650
+ args.project,
1651
+ args.status
1652
+ );
1653
+ return {
1654
+ content: [
1655
+ {
1656
+ type: "text",
1657
+ text: JSON.stringify(response, null, 2)
728
1658
  }
729
1659
  ]
730
1660
  };
731
1661
  }
1662
+ case "decision_record": {
1663
+ const decision = await this.client.recordDecision(
1664
+ args.title,
1665
+ args.rationale,
1666
+ args.alternatives,
1667
+ args.project,
1668
+ args.tags,
1669
+ args.status
1670
+ );
1671
+ return {
1672
+ content: [{ type: "text", text: JSON.stringify(decision, null, 2) }]
1673
+ };
1674
+ }
1675
+ case "decision_list": {
1676
+ const response = await this.client.listDecisions(
1677
+ args.limit,
1678
+ args.project,
1679
+ args.status,
1680
+ args.tags
1681
+ );
1682
+ return {
1683
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
1684
+ };
1685
+ }
1686
+ case "decision_supersede": {
1687
+ const id = args.id;
1688
+ validateUuid(id, "decision_id");
1689
+ const decision = await this.client.supersedeDecision(
1690
+ id,
1691
+ args.title,
1692
+ args.rationale,
1693
+ args.alternatives,
1694
+ args.tags
1695
+ );
1696
+ return {
1697
+ content: [{ type: "text", text: JSON.stringify(decision, null, 2) }]
1698
+ };
1699
+ }
1700
+ case "decision_check": {
1701
+ const response = await this.client.checkDecisions(
1702
+ args.query,
1703
+ args.project,
1704
+ args.limit,
1705
+ args.threshold,
1706
+ args.include_superseded ?? false
1707
+ );
1708
+ return {
1709
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
1710
+ };
1711
+ }
1712
+ case "pattern_create": {
1713
+ const patternSchema = z.object({
1714
+ title: z.string().min(1).max(500),
1715
+ description: z.string().min(1).max(5e4),
1716
+ category: z.string().max(100).optional(),
1717
+ example_code: z.string().optional(),
1718
+ scope: z.enum(["global", "project"]).optional(),
1719
+ tags: z.array(z.string()).max(20).optional(),
1720
+ source_project: z.string().max(100).optional()
1721
+ });
1722
+ const validatedInput = patternSchema.parse(args);
1723
+ const pattern = await this.client.createPattern(
1724
+ validatedInput.title,
1725
+ validatedInput.description,
1726
+ validatedInput.category,
1727
+ validatedInput.example_code,
1728
+ validatedInput.scope,
1729
+ validatedInput.tags,
1730
+ validatedInput.source_project
1731
+ );
1732
+ return {
1733
+ content: [{ type: "text", text: JSON.stringify(pattern, null, 2) }]
1734
+ };
1735
+ }
1736
+ case "pattern_search": {
1737
+ const response = await this.client.searchPatterns(
1738
+ args.query,
1739
+ args.category,
1740
+ args.project,
1741
+ args.limit,
1742
+ args.threshold
1743
+ );
1744
+ return {
1745
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
1746
+ };
1747
+ }
1748
+ case "pattern_adopt": {
1749
+ const id = args.id;
1750
+ validateUuid(id, "pattern_id");
1751
+ const pattern = await this.client.adoptPattern(
1752
+ id,
1753
+ args.project
1754
+ );
1755
+ return {
1756
+ content: [{ type: "text", text: JSON.stringify(pattern, null, 2) }]
1757
+ };
1758
+ }
1759
+ case "pattern_suggest": {
1760
+ const response = await this.client.suggestPatterns(
1761
+ args.project,
1762
+ args.limit
1763
+ );
1764
+ return {
1765
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
1766
+ };
1767
+ }
1768
+ case "project_context": {
1769
+ const context = await this.client.getProjectContext(
1770
+ args.project
1771
+ );
1772
+ return {
1773
+ content: [{ type: "text", text: JSON.stringify(context, null, 2) }]
1774
+ };
1775
+ }
1776
+ case "memory_promote": {
1777
+ const id = args.memory_id;
1778
+ validateUuid(id, "memory_id");
1779
+ const updated = await this.client.promoteMemory(
1780
+ id,
1781
+ args.importance,
1782
+ args.tier
1783
+ );
1784
+ return {
1785
+ content: [{ type: "text", text: JSON.stringify(updated, null, 2) }]
1786
+ };
1787
+ }
1788
+ // ── V2 Async API Tool Handlers ──
1789
+ case "memory_store_async": {
1790
+ const response = await this.client.storeMemoryAsync(
1791
+ args.content,
1792
+ args.metadata,
1793
+ args.project,
1794
+ args.importance,
1795
+ args.tier,
1796
+ args.webhook_url
1797
+ );
1798
+ return {
1799
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
1800
+ };
1801
+ }
1802
+ case "memory_status": {
1803
+ const id = args.memory_id;
1804
+ validateUuid(id, "memory_id");
1805
+ const status = await this.client.getMemoryStatus(id);
1806
+ return {
1807
+ content: [{ type: "text", text: JSON.stringify(status, null, 2) }]
1808
+ };
1809
+ }
1810
+ case "context_build": {
1811
+ const result = await this.client.buildContextV2(
1812
+ args.query,
1813
+ {
1814
+ agentId: args.agent_id,
1815
+ maxMemories: args.max_memories,
1816
+ maxTokens: args.max_tokens,
1817
+ aiEnhanced: args.ai_enhanced,
1818
+ rankingVersion: args.ranking_version,
1819
+ searchMode: args.search_mode,
1820
+ llmApiUrl: args.llm_api_url,
1821
+ llmModel: args.llm_model,
1822
+ excludeMemoryIds: args.exclude_memory_ids
1823
+ }
1824
+ );
1825
+ return {
1826
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1827
+ };
1828
+ }
1829
+ // ── Tool Name Aliases (backward compatibility) ──
1830
+ case "memory_forget": {
1831
+ const id = args.memoryId;
1832
+ const query = args.query;
1833
+ if (id) {
1834
+ await this.client.deleteMemory(id);
1835
+ return {
1836
+ content: [{ type: "text", text: `Memory ${id.slice(0, 8)}... deleted.` }]
1837
+ };
1838
+ } else if (query) {
1839
+ const results = await this.client.searchMemories(query, 1, 0.9);
1840
+ if (results.length === 1) {
1841
+ await this.client.deleteMemory(results[0].memory.id);
1842
+ return {
1843
+ content: [{ type: "text", text: `Memory ${results[0].memory.id.slice(0, 8)}... deleted (matched query).` }]
1844
+ };
1845
+ }
1846
+ return {
1847
+ content: [{ type: "text", text: `Found ${results.length} matches. Provide memoryId to delete.` }]
1848
+ };
1849
+ }
1850
+ throw new Error("memoryId or query is required");
1851
+ }
1852
+ case "memory_recall": {
1853
+ const results = await this.client.searchMemories(
1854
+ args.query,
1855
+ args.limit ?? 5,
1856
+ args.threshold ?? 0.3,
1857
+ void 0,
1858
+ // use default agent
1859
+ false,
1860
+ false,
1861
+ false,
1862
+ args.max_tokens,
1863
+ args.project
1864
+ );
1865
+ return {
1866
+ content: [{ type: "text", text: JSON.stringify({ memories: results, total: results.length }, null, 2) }]
1867
+ };
1868
+ }
732
1869
  default:
733
1870
  throw new Error(`Unknown tool: ${name}`);
734
1871
  }
735
1872
  } catch (error) {
736
1873
  let errorMessage = "Unknown error";
737
1874
  let errorDetails = void 0;
738
- if (error instanceof z2.ZodError) {
1875
+ if (error instanceof z.ZodError) {
739
1876
  errorMessage = "Validation error";
740
1877
  errorDetails = error.errors;
741
1878
  } else if (error instanceof Error) {
@@ -766,206 +1903,99 @@ var MemoryRelayMCPServer = class {
766
1903
  });
767
1904
  }
768
1905
  /**
769
- * Setup MCP resource handlers
1906
+ * Start the MCP server with STDIO transport
770
1907
  */
771
- setupResourceHandlers() {
772
- this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
773
- resources: [
774
- {
775
- uri: "memory:///recent",
776
- name: "Recent Memories",
777
- description: "The 20 most recent memories stored in MemoryRelay",
778
- mimeType: "application/json"
779
- }
780
- ]
781
- }));
782
- this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
783
- resourceTemplates: [
784
- {
785
- uriTemplate: "memory:///{id}",
786
- name: "Memory by ID",
787
- description: "Retrieve a specific memory by its UUID",
788
- mimeType: "application/json"
789
- }
790
- ]
791
- }));
792
- this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
793
- const { uri } = request.params;
794
- try {
795
- if (uri === "memory:///recent") {
796
- const response = await this.client.listMemories(20, 0);
797
- return {
798
- contents: [
799
- {
800
- uri,
801
- mimeType: "application/json",
802
- text: JSON.stringify(response, null, 2)
803
- }
804
- ]
805
- };
806
- }
807
- const match = uri.match(/^memory:\/\/\/([0-9a-f-]{36})$/);
808
- if (match) {
809
- const id = match[1];
810
- validateUuid(id, "memory_id");
811
- const memory = await this.client.getMemory(id);
812
- return {
813
- contents: [
814
- {
815
- uri,
816
- mimeType: "application/json",
817
- text: JSON.stringify(memory, null, 2)
818
- }
819
- ]
820
- };
821
- }
822
- throw new Error(`Unknown resource: ${uri}`);
823
- } catch (error) {
824
- const message = error instanceof Error ? error.message : "Unknown error";
825
- this.logger.error(`Resource read failed: ${uri}`, { error: message });
826
- throw error;
827
- }
828
- });
1908
+ async start() {
1909
+ const transport = new StdioServerTransport();
1910
+ await this.server.connect(transport);
1911
+ this.logger.info("MCP server started on STDIO");
829
1912
  }
830
1913
  /**
831
- * Setup MCP prompt handlers
1914
+ * Connect the MCP server to a custom transport (for testing)
832
1915
  */
833
- setupPromptHandlers() {
834
- this.server.setRequestHandler(ListPromptsRequestSchema, async () => ({
835
- prompts: [
836
- {
837
- name: "store_memory",
838
- description: "Store information as a persistent memory with appropriate metadata",
839
- arguments: [
840
- {
841
- name: "information",
842
- description: "The information to remember",
843
- required: true
844
- },
845
- {
846
- name: "category",
847
- description: "Category for the memory (e.g., preference, fact, instruction, context)",
848
- required: false
849
- }
850
- ]
851
- },
852
- {
853
- name: "recall_memories",
854
- description: "Search for and recall relevant memories about a topic",
855
- arguments: [
856
- {
857
- name: "topic",
858
- description: "The topic or question to search memories for",
859
- required: true
860
- }
861
- ]
862
- },
863
- {
864
- name: "summarize_memories",
865
- description: "List and summarize all recent memories for context",
866
- arguments: []
867
- }
868
- ]
869
- }));
870
- this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
871
- const { name, arguments: args } = request.params;
872
- switch (name) {
873
- case "store_memory": {
874
- const information = args?.information ?? "";
875
- const category = args?.category ?? "";
876
- const metadataInstruction = category ? ` Attach metadata with category "${category}".` : " Attach appropriate metadata with a category tag.";
877
- return {
878
- messages: [
879
- {
880
- role: "user",
881
- content: {
882
- type: "text",
883
- text: `Please store the following information as a persistent memory using the memory_store tool.${metadataInstruction}
884
-
885
- Information to remember:
886
- ${information}`
887
- }
888
- }
889
- ]
890
- };
891
- }
892
- case "recall_memories": {
893
- const topic = args?.topic ?? "";
894
- return {
895
- messages: [
896
- {
897
- role: "user",
898
- content: {
899
- type: "text",
900
- text: `Search my memories for information related to: "${topic}"
901
-
902
- Use the memory_search tool to find relevant memories, then summarize what you found. If nothing relevant is found, let me know.`
903
- }
904
- }
905
- ]
906
- };
907
- }
908
- case "summarize_memories": {
909
- return {
910
- messages: [
911
- {
912
- role: "user",
913
- content: {
914
- type: "text",
915
- text: "List my recent memories using the memory_list tool and provide a brief summary of what has been remembered. Group them by topic or category if possible."
916
- }
917
- }
918
- ]
919
- };
920
- }
921
- default:
922
- throw new Error(`Unknown prompt: ${name}`);
923
- }
924
- });
1916
+ async connectTransport(transport) {
1917
+ await this.server.connect(transport);
925
1918
  }
926
1919
  /**
927
- * Start the MCP server with STDIO transport
1920
+ * Close the MCP server connection
928
1921
  */
929
- async start() {
930
- const transport = new StdioServerTransport();
931
- await this.server.connect(transport);
932
- this.logger.info("MCP server started on STDIO");
1922
+ async close() {
1923
+ await this.server.close();
933
1924
  }
934
1925
  };
935
1926
 
936
1927
  // src/index.ts
1928
+ async function startServer() {
1929
+ const config = loadConfig();
1930
+ initLogger(config.logLevel);
1931
+ const logger = getLogger();
1932
+ logger.info("Starting MemoryRelay MCP server");
1933
+ const agentId = getAgentId(config);
1934
+ const server = new MemoryRelayMCPServer({
1935
+ apiKey: config.apiKey,
1936
+ apiUrl: config.apiUrl,
1937
+ agentId,
1938
+ timeout: config.timeout
1939
+ });
1940
+ await server.start();
1941
+ process.on("SIGINT", () => {
1942
+ logger.info("Received SIGINT, shutting down gracefully");
1943
+ process.exit(0);
1944
+ });
1945
+ process.on("SIGTERM", () => {
1946
+ logger.info("Received SIGTERM, shutting down gracefully");
1947
+ process.exit(0);
1948
+ });
1949
+ }
937
1950
  async function main() {
1951
+ const command = process.argv[2];
938
1952
  try {
939
- const config = loadConfig();
940
- initLogger(config.logLevel);
941
- const logger2 = getLogger();
942
- logger2.info("Starting MemoryRelay MCP server");
943
- const agentId = getAgentId(config);
944
- const server = new MemoryRelayMCPServer({
945
- apiKey: config.apiKey,
946
- apiUrl: config.apiUrl,
947
- agentId,
948
- timeout: config.timeout
949
- });
950
- await server.start();
951
- process.on("SIGINT", () => {
952
- logger2.info("Received SIGINT, shutting down gracefully");
953
- process.exit(0);
954
- });
955
- process.on("SIGTERM", () => {
956
- logger2.info("Received SIGTERM, shutting down gracefully");
957
- process.exit(0);
958
- });
1953
+ switch (command) {
1954
+ case "setup": {
1955
+ const { runSetup } = await import("./cli/setup.js");
1956
+ await runSetup();
1957
+ break;
1958
+ }
1959
+ case "test": {
1960
+ const { runTest } = await import("./cli/test.js");
1961
+ await runTest();
1962
+ break;
1963
+ }
1964
+ case "--help":
1965
+ case "-h": {
1966
+ console.error(`
1967
+ memoryrelay-mcp - Persistent memory for AI agents
1968
+
1969
+ Usage:
1970
+ npx memoryrelay-mcp Start MCP server (stdio transport)
1971
+ npx memoryrelay-mcp setup Interactive setup wizard
1972
+ npx memoryrelay-mcp test Test API connectivity
1973
+ npx memoryrelay-mcp --help Show this help message
1974
+
1975
+ Environment variables:
1976
+ MEMORYRELAY_API_KEY API key (required, starts with "mem_")
1977
+ MEMORYRELAY_API_URL API URL (default: https://api.memoryrelay.net)
1978
+ MEMORYRELAY_AGENT_ID Agent ID (optional, auto-detected)
1979
+ MEMORYRELAY_TIMEOUT Request timeout in ms (default: 30000)
1980
+ MEMORYRELAY_LOG_LEVEL Log level: debug|info|warn|error (default: info)
1981
+
1982
+ Documentation: https://github.com/memoryrelay/mcp-server
1983
+ `);
1984
+ break;
1985
+ }
1986
+ default:
1987
+ await startServer();
1988
+ }
959
1989
  } catch (error) {
960
- const logger2 = getLogger();
1990
+ const logger = getLogger();
961
1991
  if (error instanceof Error) {
962
- logger2.error("Fatal error:", { message: error.message });
963
- console.error("\n\u274C Failed to start MemoryRelay MCP server\n");
1992
+ logger.error("Fatal error:", { message: error.message });
1993
+ console.error("\nFailed to start MemoryRelay MCP server\n");
964
1994
  console.error(error.message);
965
- console.error("\nFor help, see: https://github.com/memoryrelay/mcp-server#troubleshooting\n");
1995
+ console.error("\nFor help, run: npx memoryrelay-mcp --help\n");
966
1996
  } else {
967
- logger2.error("Fatal error:", { error });
968
- console.error("\n\u274C An unexpected error occurred\n");
1997
+ logger.error("Fatal error:", { error });
1998
+ console.error("\nAn unexpected error occurred\n");
969
1999
  }
970
2000
  process.exit(1);
971
2001
  }