@synth-coder/memhub 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/.eslintrc.cjs +46 -0
  2. package/.github/workflows/ci.yml +74 -0
  3. package/.iflow/commands/opsx-apply.md +152 -0
  4. package/.iflow/commands/opsx-archive.md +157 -0
  5. package/.iflow/commands/opsx-explore.md +173 -0
  6. package/.iflow/commands/opsx-propose.md +106 -0
  7. package/.iflow/skills/openspec-apply-change/SKILL.md +156 -0
  8. package/.iflow/skills/openspec-archive-change/SKILL.md +114 -0
  9. package/.iflow/skills/openspec-explore/SKILL.md +288 -0
  10. package/.iflow/skills/openspec-propose/SKILL.md +110 -0
  11. package/.prettierrc +11 -0
  12. package/README.md +171 -0
  13. package/README.zh-CN.md +169 -0
  14. package/dist/src/contracts/index.d.ts +7 -0
  15. package/dist/src/contracts/index.d.ts.map +1 -0
  16. package/dist/src/contracts/index.js +10 -0
  17. package/dist/src/contracts/index.js.map +1 -0
  18. package/dist/src/contracts/mcp.d.ts +194 -0
  19. package/dist/src/contracts/mcp.d.ts.map +1 -0
  20. package/dist/src/contracts/mcp.js +112 -0
  21. package/dist/src/contracts/mcp.js.map +1 -0
  22. package/dist/src/contracts/schemas.d.ts +1153 -0
  23. package/dist/src/contracts/schemas.d.ts.map +1 -0
  24. package/dist/src/contracts/schemas.js +246 -0
  25. package/dist/src/contracts/schemas.js.map +1 -0
  26. package/dist/src/contracts/types.d.ts +328 -0
  27. package/dist/src/contracts/types.d.ts.map +1 -0
  28. package/dist/src/contracts/types.js +30 -0
  29. package/dist/src/contracts/types.js.map +1 -0
  30. package/dist/src/index.d.ts +8 -0
  31. package/dist/src/index.d.ts.map +1 -0
  32. package/dist/src/index.js +8 -0
  33. package/dist/src/index.js.map +1 -0
  34. package/dist/src/server/index.d.ts +5 -0
  35. package/dist/src/server/index.d.ts.map +1 -0
  36. package/dist/src/server/index.js +5 -0
  37. package/dist/src/server/index.js.map +1 -0
  38. package/dist/src/server/mcp-server.d.ts +80 -0
  39. package/dist/src/server/mcp-server.d.ts.map +1 -0
  40. package/dist/src/server/mcp-server.js +263 -0
  41. package/dist/src/server/mcp-server.js.map +1 -0
  42. package/dist/src/services/index.d.ts +5 -0
  43. package/dist/src/services/index.d.ts.map +1 -0
  44. package/dist/src/services/index.js +5 -0
  45. package/dist/src/services/index.js.map +1 -0
  46. package/dist/src/services/memory-service.d.ts +105 -0
  47. package/dist/src/services/memory-service.d.ts.map +1 -0
  48. package/dist/src/services/memory-service.js +447 -0
  49. package/dist/src/services/memory-service.js.map +1 -0
  50. package/dist/src/storage/frontmatter-parser.d.ts +69 -0
  51. package/dist/src/storage/frontmatter-parser.d.ts.map +1 -0
  52. package/dist/src/storage/frontmatter-parser.js +207 -0
  53. package/dist/src/storage/frontmatter-parser.js.map +1 -0
  54. package/dist/src/storage/index.d.ts +6 -0
  55. package/dist/src/storage/index.d.ts.map +1 -0
  56. package/dist/src/storage/index.js +6 -0
  57. package/dist/src/storage/index.js.map +1 -0
  58. package/dist/src/storage/markdown-storage.d.ts +76 -0
  59. package/dist/src/storage/markdown-storage.d.ts.map +1 -0
  60. package/dist/src/storage/markdown-storage.js +193 -0
  61. package/dist/src/storage/markdown-storage.js.map +1 -0
  62. package/dist/src/utils/index.d.ts +5 -0
  63. package/dist/src/utils/index.d.ts.map +1 -0
  64. package/dist/src/utils/index.js +5 -0
  65. package/dist/src/utils/index.js.map +1 -0
  66. package/dist/src/utils/slugify.d.ts +24 -0
  67. package/dist/src/utils/slugify.d.ts.map +1 -0
  68. package/dist/src/utils/slugify.js +56 -0
  69. package/dist/src/utils/slugify.js.map +1 -0
  70. package/docs/architecture.md +349 -0
  71. package/docs/contracts.md +119 -0
  72. package/docs/prompt-template.md +79 -0
  73. package/docs/proposal-close-gates.md +58 -0
  74. package/docs/tool-calling-policy.md +107 -0
  75. package/package.json +53 -0
  76. package/src/contracts/index.ts +12 -0
  77. package/src/contracts/mcp.ts +303 -0
  78. package/src/contracts/schemas.ts +311 -0
  79. package/src/contracts/types.ts +414 -0
  80. package/src/index.ts +8 -0
  81. package/src/server/index.ts +5 -0
  82. package/src/server/mcp-server.ts +352 -0
  83. package/src/services/index.ts +5 -0
  84. package/src/services/memory-service.ts +548 -0
  85. package/src/storage/frontmatter-parser.ts +243 -0
  86. package/src/storage/index.ts +6 -0
  87. package/src/storage/markdown-storage.ts +236 -0
  88. package/src/utils/index.ts +5 -0
  89. package/src/utils/slugify.ts +63 -0
  90. package/test/contracts/schemas.test.ts +313 -0
  91. package/test/contracts/types.test.ts +21 -0
  92. package/test/frontmatter-parser-more.test.ts +94 -0
  93. package/test/server/mcp-server-internals.test.ts +257 -0
  94. package/test/server/mcp-server.test.ts +97 -0
  95. package/test/services/memory-service-edge.test.ts +248 -0
  96. package/test/services/memory-service.test.ts +279 -0
  97. package/test/storage/frontmatter-parser.test.ts +223 -0
  98. package/test/storage/markdown-storage.test.ts +217 -0
  99. package/test/storage/storage-edge.test.ts +238 -0
  100. package/test/utils/slugify-edge.test.ts +94 -0
  101. package/test/utils/slugify.test.ts +68 -0
  102. package/tsconfig.json +26 -0
  103. package/tsconfig.test.json +8 -0
  104. package/vitest.config.ts +27 -0
@@ -0,0 +1,352 @@
1
+ /**
2
+ * MCP Server - Model Context Protocol server implementation
3
+ * Communicates via stdio
4
+ */
5
+
6
+ import { readFileSync } from 'fs';
7
+ import { join, dirname } from 'path';
8
+ import { fileURLToPath } from 'url';
9
+ import type {
10
+ JsonRpcRequest,
11
+ JsonRpcResponse,
12
+ JsonRpcError,
13
+ RequestId,
14
+ InitializeParams,
15
+ InitializeResult,
16
+ ToolCallRequest,
17
+ ToolCallResult,
18
+ TextContent,
19
+ } from '../contracts/mcp.js';
20
+ import {
21
+ MCP_PROTOCOL_VERSION,
22
+ SERVER_INFO,
23
+ TOOL_DEFINITIONS,
24
+ MCP_METHODS,
25
+ ERROR_CODES,
26
+ } from '../contracts/mcp.js';
27
+ import { ErrorCode } from '../contracts/types.js';
28
+ import { MemoryService, ServiceError } from '../services/memory-service.js';
29
+ import {
30
+ MemoryLoadInputSchema,
31
+ MemoryUpdateInputV2Schema,
32
+ } from '../contracts/schemas.js';
33
+
34
+ // Get package version
35
+ const __filename = fileURLToPath(import.meta.url);
36
+ const __dirname = dirname(__filename);
37
+ interface PackageJson {
38
+ version?: string;
39
+ }
40
+
41
+ const packageJson = JSON.parse(
42
+ readFileSync(join(__dirname, '../../package.json'), 'utf-8')
43
+ ) as PackageJson;
44
+
45
+ /**
46
+ * MCP Server implementation
47
+ */
48
+ export class McpServer {
49
+ private readonly memoryService: MemoryService;
50
+
51
+ constructor() {
52
+ const storagePath = process.env.MEMHUB_STORAGE_PATH || './memories';
53
+ this.memoryService = new MemoryService({ storagePath });
54
+ }
55
+
56
+ /**
57
+ * Starts the MCP server and begins listening for requests on stdin
58
+ */
59
+ start(): void {
60
+ this.log('info', 'MemHub MCP Server starting...');
61
+
62
+ process.stdin.setEncoding('utf-8');
63
+
64
+ let buffer = '';
65
+
66
+ process.stdin.on('data', (chunk: string) => {
67
+ buffer += chunk;
68
+
69
+ // Process complete lines (JSON-RPC messages)
70
+ const lines = buffer.split('\n');
71
+ buffer = lines.pop() || ''; // Keep incomplete line in buffer
72
+
73
+ for (const line of lines) {
74
+ if (line.trim()) {
75
+ void this.handleMessage(line.trim());
76
+ }
77
+ }
78
+ });
79
+
80
+ process.stdin.on('end', () => {
81
+ this.log('info', 'Stdin closed, shutting down...');
82
+ process.exit(0);
83
+ });
84
+
85
+ process.on('SIGINT', () => {
86
+ this.log('info', 'Received SIGINT, shutting down...');
87
+ process.exit(0);
88
+ });
89
+
90
+ process.on('SIGTERM', () => {
91
+ this.log('info', 'Received SIGTERM, shutting down...');
92
+ process.exit(0);
93
+ });
94
+ }
95
+
96
+ /**
97
+ * Handles an incoming JSON-RPC message
98
+ *
99
+ * @param message - The JSON-RPC message string
100
+ */
101
+ private async handleMessage(message: string): Promise<void> {
102
+ let request: JsonRpcRequest | null = null;
103
+
104
+ try {
105
+ request = JSON.parse(message) as JsonRpcRequest;
106
+ } catch {
107
+ this.sendError(null, ERROR_CODES.PARSE_ERROR, 'Parse error: Invalid JSON');
108
+ return;
109
+ }
110
+
111
+ // Validate JSON-RPC request
112
+ if (request.jsonrpc !== '2.0' || !request.method) {
113
+ this.sendError(
114
+ request.id ?? null,
115
+ ERROR_CODES.INVALID_REQUEST,
116
+ 'Invalid Request'
117
+ );
118
+ return;
119
+ }
120
+
121
+ try {
122
+ const result = await this.handleMethod(request.method, request.params);
123
+
124
+ // Send response (only for requests with id, not notifications)
125
+ if (request.id !== undefined) {
126
+ this.sendResponse(request.id, result);
127
+ }
128
+ } catch (error) {
129
+ this.handleError(request.id ?? null, error);
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Handles a specific method call
135
+ *
136
+ * @param method - The method name
137
+ * @param params - The method parameters
138
+ * @returns The method result
139
+ */
140
+ private async handleMethod(
141
+ method: string,
142
+ params: unknown
143
+ ): Promise<unknown> {
144
+ switch (method) {
145
+ case MCP_METHODS.INITIALIZE:
146
+ return this.handleInitialize(params as InitializeParams);
147
+
148
+ case MCP_METHODS.INITIALIZED:
149
+ // Notification, no response needed
150
+ this.log('info', 'Client initialized');
151
+ return null;
152
+
153
+ case MCP_METHODS.SHUTDOWN:
154
+ return null;
155
+
156
+ case MCP_METHODS.EXIT:
157
+ process.exit(0);
158
+ return null;
159
+
160
+ case MCP_METHODS.TOOLS_LIST:
161
+ return { tools: TOOL_DEFINITIONS };
162
+
163
+ case MCP_METHODS.TOOLS_CALL:
164
+ return this.handleToolCall(params as ToolCallRequest);
165
+
166
+ default:
167
+ throw new ServiceError(
168
+ `Method not found: ${method}`,
169
+ ErrorCode.METHOD_NOT_FOUND
170
+ );
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Handles the initialize method
176
+ *
177
+ * @param params - Initialize parameters
178
+ * @returns Initialize result
179
+ */
180
+ private handleInitialize(params: InitializeParams): InitializeResult {
181
+ this.log('info', `Client initializing: ${params.clientInfo.name} v${params.clientInfo.version}`);
182
+
183
+ return {
184
+ protocolVersion: MCP_PROTOCOL_VERSION,
185
+ capabilities: {
186
+ tools: { listChanged: false },
187
+ logging: {},
188
+ },
189
+ serverInfo: {
190
+ name: SERVER_INFO.name,
191
+ version: packageJson.version || SERVER_INFO.version,
192
+ },
193
+ };
194
+ }
195
+
196
+ /**
197
+ * Handles tool calls
198
+ *
199
+ * @param request - Tool call request
200
+ * @returns Tool call result
201
+ */
202
+ private async handleToolCall(request: ToolCallRequest): Promise<ToolCallResult> {
203
+ const { name, arguments: args } = request;
204
+
205
+ try {
206
+ let result: unknown;
207
+
208
+ switch (name) {
209
+ case 'memory_load': {
210
+ const input = MemoryLoadInputSchema.parse(args ?? {});
211
+ result = await this.memoryService.memoryLoad(input);
212
+ break;
213
+ }
214
+
215
+ case 'memory_update': {
216
+ const input = MemoryUpdateInputV2Schema.parse(args ?? {});
217
+ result = await this.memoryService.memoryUpdate(input);
218
+ break;
219
+ }
220
+
221
+ default:
222
+ throw new ServiceError(
223
+ `Unknown tool: ${name}`,
224
+ ErrorCode.METHOD_NOT_FOUND
225
+ );
226
+ }
227
+
228
+ return {
229
+ content: [
230
+ {
231
+ type: 'text',
232
+ text: JSON.stringify(result, null, 2),
233
+ } as TextContent,
234
+ ],
235
+ };
236
+ } catch (error) {
237
+ if (error instanceof ServiceError) {
238
+ return {
239
+ content: [
240
+ {
241
+ type: 'text',
242
+ text: JSON.stringify({ error: error.message }, null, 2),
243
+ } as TextContent,
244
+ ],
245
+ isError: true,
246
+ };
247
+ }
248
+
249
+ // Re-throw for generic error handling
250
+ throw error;
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Handles errors and sends appropriate error response
256
+ *
257
+ * @param id - Request ID
258
+ * @param error - The error that occurred
259
+ */
260
+ private handleError(id: RequestId | null, error: unknown): void {
261
+ if (error instanceof ServiceError) {
262
+ this.sendError(id, error.code, error.message, error.data);
263
+ } else if (error instanceof Error && error.name === 'ZodError') {
264
+ this.sendError(
265
+ id,
266
+ ERROR_CODES.INVALID_PARAMS,
267
+ `Invalid parameters: ${error.message}`
268
+ );
269
+ } else {
270
+ this.sendError(
271
+ id,
272
+ ERROR_CODES.INTERNAL_ERROR,
273
+ `Internal error: ${error instanceof Error ? error.message : 'Unknown error'}`
274
+ );
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Sends a JSON-RPC response
280
+ *
281
+ * @param id - Request ID
282
+ * @param result - Response result
283
+ */
284
+ private sendResponse(id: RequestId, result: unknown): void {
285
+ const response: JsonRpcResponse = {
286
+ jsonrpc: '2.0',
287
+ id,
288
+ result,
289
+ };
290
+ this.sendMessage(response);
291
+ }
292
+
293
+ /**
294
+ * Sends a JSON-RPC error
295
+ *
296
+ * @param id - Request ID
297
+ * @param code - Error code
298
+ * @param message - Error message
299
+ * @param data - Additional error data
300
+ */
301
+ private sendError(
302
+ id: RequestId | null,
303
+ code: number,
304
+ message: string,
305
+ data?: Record<string, unknown>
306
+ ): void {
307
+ const error: JsonRpcError = {
308
+ code,
309
+ message,
310
+ data,
311
+ };
312
+
313
+ const response: JsonRpcResponse = {
314
+ jsonrpc: '2.0',
315
+ id: id ?? null,
316
+ error,
317
+ };
318
+
319
+ this.sendMessage(response);
320
+ }
321
+
322
+ /**
323
+ * Sends a message to stdout
324
+ *
325
+ * @param message - The message to send
326
+ */
327
+ private sendMessage(message: JsonRpcResponse | JsonRpcRequest): void {
328
+ const json = JSON.stringify(message);
329
+ process.stdout.write(json + '\n');
330
+ }
331
+
332
+ /**
333
+ * Logs a message
334
+ *
335
+ * @param level - Log level
336
+ * @param message - Log message
337
+ */
338
+ private log(level: 'debug' | 'info' | 'warn' | 'error', message: string): void {
339
+ const logLevel = process.env.MEMHUB_LOG_LEVEL || 'info';
340
+ const levels = { debug: 0, info: 1, warn: 2, error: 3 };
341
+
342
+ if (levels[level] >= levels[logLevel as keyof typeof levels]) {
343
+ console.error(`[${level.toUpperCase()}] ${message}`);
344
+ }
345
+ }
346
+ }
347
+
348
+ // Start server if this file is run directly
349
+ if (import.meta.url === `file://${process.argv[1]}`) {
350
+ const server = new McpServer();
351
+ server.start();
352
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Services exports
3
+ */
4
+
5
+ export * from './memory-service.js';