@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.
- package/.eslintrc.cjs +46 -0
- package/.github/workflows/ci.yml +74 -0
- package/.iflow/commands/opsx-apply.md +152 -0
- package/.iflow/commands/opsx-archive.md +157 -0
- package/.iflow/commands/opsx-explore.md +173 -0
- package/.iflow/commands/opsx-propose.md +106 -0
- package/.iflow/skills/openspec-apply-change/SKILL.md +156 -0
- package/.iflow/skills/openspec-archive-change/SKILL.md +114 -0
- package/.iflow/skills/openspec-explore/SKILL.md +288 -0
- package/.iflow/skills/openspec-propose/SKILL.md +110 -0
- package/.prettierrc +11 -0
- package/README.md +171 -0
- package/README.zh-CN.md +169 -0
- package/dist/src/contracts/index.d.ts +7 -0
- package/dist/src/contracts/index.d.ts.map +1 -0
- package/dist/src/contracts/index.js +10 -0
- package/dist/src/contracts/index.js.map +1 -0
- package/dist/src/contracts/mcp.d.ts +194 -0
- package/dist/src/contracts/mcp.d.ts.map +1 -0
- package/dist/src/contracts/mcp.js +112 -0
- package/dist/src/contracts/mcp.js.map +1 -0
- package/dist/src/contracts/schemas.d.ts +1153 -0
- package/dist/src/contracts/schemas.d.ts.map +1 -0
- package/dist/src/contracts/schemas.js +246 -0
- package/dist/src/contracts/schemas.js.map +1 -0
- package/dist/src/contracts/types.d.ts +328 -0
- package/dist/src/contracts/types.d.ts.map +1 -0
- package/dist/src/contracts/types.js +30 -0
- package/dist/src/contracts/types.js.map +1 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +8 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/server/index.d.ts +5 -0
- package/dist/src/server/index.d.ts.map +1 -0
- package/dist/src/server/index.js +5 -0
- package/dist/src/server/index.js.map +1 -0
- package/dist/src/server/mcp-server.d.ts +80 -0
- package/dist/src/server/mcp-server.d.ts.map +1 -0
- package/dist/src/server/mcp-server.js +263 -0
- package/dist/src/server/mcp-server.js.map +1 -0
- package/dist/src/services/index.d.ts +5 -0
- package/dist/src/services/index.d.ts.map +1 -0
- package/dist/src/services/index.js +5 -0
- package/dist/src/services/index.js.map +1 -0
- package/dist/src/services/memory-service.d.ts +105 -0
- package/dist/src/services/memory-service.d.ts.map +1 -0
- package/dist/src/services/memory-service.js +447 -0
- package/dist/src/services/memory-service.js.map +1 -0
- package/dist/src/storage/frontmatter-parser.d.ts +69 -0
- package/dist/src/storage/frontmatter-parser.d.ts.map +1 -0
- package/dist/src/storage/frontmatter-parser.js +207 -0
- package/dist/src/storage/frontmatter-parser.js.map +1 -0
- package/dist/src/storage/index.d.ts +6 -0
- package/dist/src/storage/index.d.ts.map +1 -0
- package/dist/src/storage/index.js +6 -0
- package/dist/src/storage/index.js.map +1 -0
- package/dist/src/storage/markdown-storage.d.ts +76 -0
- package/dist/src/storage/markdown-storage.d.ts.map +1 -0
- package/dist/src/storage/markdown-storage.js +193 -0
- package/dist/src/storage/markdown-storage.js.map +1 -0
- package/dist/src/utils/index.d.ts +5 -0
- package/dist/src/utils/index.d.ts.map +1 -0
- package/dist/src/utils/index.js +5 -0
- package/dist/src/utils/index.js.map +1 -0
- package/dist/src/utils/slugify.d.ts +24 -0
- package/dist/src/utils/slugify.d.ts.map +1 -0
- package/dist/src/utils/slugify.js +56 -0
- package/dist/src/utils/slugify.js.map +1 -0
- package/docs/architecture.md +349 -0
- package/docs/contracts.md +119 -0
- package/docs/prompt-template.md +79 -0
- package/docs/proposal-close-gates.md +58 -0
- package/docs/tool-calling-policy.md +107 -0
- package/package.json +53 -0
- package/src/contracts/index.ts +12 -0
- package/src/contracts/mcp.ts +303 -0
- package/src/contracts/schemas.ts +311 -0
- package/src/contracts/types.ts +414 -0
- package/src/index.ts +8 -0
- package/src/server/index.ts +5 -0
- package/src/server/mcp-server.ts +352 -0
- package/src/services/index.ts +5 -0
- package/src/services/memory-service.ts +548 -0
- package/src/storage/frontmatter-parser.ts +243 -0
- package/src/storage/index.ts +6 -0
- package/src/storage/markdown-storage.ts +236 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/slugify.ts +63 -0
- package/test/contracts/schemas.test.ts +313 -0
- package/test/contracts/types.test.ts +21 -0
- package/test/frontmatter-parser-more.test.ts +94 -0
- package/test/server/mcp-server-internals.test.ts +257 -0
- package/test/server/mcp-server.test.ts +97 -0
- package/test/services/memory-service-edge.test.ts +248 -0
- package/test/services/memory-service.test.ts +279 -0
- package/test/storage/frontmatter-parser.test.ts +223 -0
- package/test/storage/markdown-storage.test.ts +217 -0
- package/test/storage/storage-edge.test.ts +238 -0
- package/test/utils/slugify-edge.test.ts +94 -0
- package/test/utils/slugify.test.ts +68 -0
- package/tsconfig.json +26 -0
- package/tsconfig.test.json +8 -0
- 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
|
+
}
|