@cybermem/mcp 0.15.0 → 0.16.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/CHANGELOG.md +6 -0
- package/dist/index.js +26 -5
- package/e2e/stdio_attribution.spec.ts +74 -0
- package/package.json +1 -1
- package/src/index.ts +92 -68
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @cybermem/mcp
|
|
2
2
|
|
|
3
|
+
## 0.16.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#140](https://github.com/mikhailkogan17/cybermem/pull/140) [`bcf40e2`](https://github.com/mikhailkogan17/cybermem/commit/bcf40e2173ad66214cfc6089d45481966255581d) Thanks [@mikhailkogan17-antigravity](https://github.com/mikhailkogan17-antigravity)! - Moved server tools usage to FastMCP
|
|
8
|
+
|
|
3
9
|
## 0.15.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/dist/index.js
CHANGED
|
@@ -89,12 +89,28 @@ async function initialize() {
|
|
|
89
89
|
process.exit(1);
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Derives the client name from the tool execution context.
|
|
94
|
+
* Falls back to handshake client name if session is 'stdio'.
|
|
95
|
+
*/
|
|
96
|
+
function getClientName(context) {
|
|
97
|
+
const ctx = context;
|
|
98
|
+
const sessionName = ctx.session?.clientName;
|
|
99
|
+
if (sessionName === "stdio" && ctx.client?.version?.name) {
|
|
100
|
+
return ctx.client.version.name;
|
|
101
|
+
}
|
|
102
|
+
return sessionName || "unknown";
|
|
103
|
+
}
|
|
92
104
|
const server = new fastmcp_1.FastMCP({
|
|
93
105
|
name: "cybermem",
|
|
94
106
|
version: VALID_VERSION,
|
|
95
107
|
instructions: CYBERMEM_INSTRUCTIONS,
|
|
96
108
|
health: { enabled: true, path: "/health" },
|
|
97
109
|
authenticate: async (req) => {
|
|
110
|
+
// STDIO transport doesn't provide an HTTP request object
|
|
111
|
+
if (!req?.headers) {
|
|
112
|
+
return { clientName: "stdio" };
|
|
113
|
+
}
|
|
98
114
|
const clientName = (req.headers["x-client-name"] ||
|
|
99
115
|
req.headers["X-Client-Name"] ||
|
|
100
116
|
"unknown");
|
|
@@ -120,7 +136,8 @@ server.addTool({
|
|
|
120
136
|
tags: zod_1.z.array(zod_1.z.string()).optional().describe("Category tags"),
|
|
121
137
|
}),
|
|
122
138
|
execute: async (args, context) => {
|
|
123
|
-
|
|
139
|
+
const clientName = getClientName(context);
|
|
140
|
+
return requestContext.run({ clientName }, async () => {
|
|
124
141
|
try {
|
|
125
142
|
const res = await memory.add(args.content, { tags: args.tags });
|
|
126
143
|
await logActivity("add_memory");
|
|
@@ -141,7 +158,8 @@ server.addTool({
|
|
|
141
158
|
k: zod_1.z.number().default(5).describe("Number of results"),
|
|
142
159
|
}),
|
|
143
160
|
execute: async (args, context) => {
|
|
144
|
-
|
|
161
|
+
const clientName = getClientName(context);
|
|
162
|
+
return requestContext.run({ clientName }, async () => {
|
|
145
163
|
try {
|
|
146
164
|
const res = await memory.search(args.query, { limit: args.k });
|
|
147
165
|
await logActivity("query_memory");
|
|
@@ -168,7 +186,8 @@ server.addTool({
|
|
|
168
186
|
path: ["content"],
|
|
169
187
|
}),
|
|
170
188
|
execute: async (args, context) => {
|
|
171
|
-
|
|
189
|
+
const clientName = getClientName(context);
|
|
190
|
+
return requestContext.run({ clientName }, async () => {
|
|
172
191
|
try {
|
|
173
192
|
const res = await (0, hsg_js_1.update_memory)(args.id, args.content, args.tags);
|
|
174
193
|
await logActivity("update_memory");
|
|
@@ -192,7 +211,8 @@ server.addTool({
|
|
|
192
211
|
.describe("Relevance boost amount (0.0 to 1.0)"),
|
|
193
212
|
}),
|
|
194
213
|
execute: async (args, context) => {
|
|
195
|
-
|
|
214
|
+
const clientName = getClientName(context);
|
|
215
|
+
return requestContext.run({ clientName }, async () => {
|
|
196
216
|
try {
|
|
197
217
|
await (0, hsg_js_1.reinforce_memory)(args.id, args.boost);
|
|
198
218
|
await logActivity("reinforce_memory");
|
|
@@ -212,7 +232,8 @@ server.addTool({
|
|
|
212
232
|
id: zod_1.z.string().describe("Memory ID"),
|
|
213
233
|
}),
|
|
214
234
|
execute: async (args, context) => {
|
|
215
|
-
|
|
235
|
+
const clientName = getClientName(context);
|
|
236
|
+
return requestContext.run({ clientName }, async () => {
|
|
216
237
|
try {
|
|
217
238
|
await (0, db_js_1.run_async)("DELETE FROM memories WHERE id=?", [args.id]);
|
|
218
239
|
await (0, db_js_1.run_async)("DELETE FROM vectors WHERE id=?", [args.id]);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
const TEST_CLIENT_NAME = "antigravity-e2e-stdio";
|
|
6
|
+
|
|
7
|
+
test("MCP:E2E (STDIO Attribution) — Verified handshake identity propagates to logs", async () => {
|
|
8
|
+
const serverPath = path.join(__dirname, "../dist/index.js");
|
|
9
|
+
|
|
10
|
+
// 1. Spawn the server in STDIO mode
|
|
11
|
+
const server = spawn("node", [serverPath], {
|
|
12
|
+
env: {
|
|
13
|
+
...process.env,
|
|
14
|
+
OM_DB_PATH: ":memory:",
|
|
15
|
+
CYBERMEM_INSTANCE: "stdio-e2e-test",
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
let logOutput = "";
|
|
20
|
+
// In STDIO mode, console.log is redirected to stderr by console-fix.ts
|
|
21
|
+
server.stderr?.on("data", (data) => {
|
|
22
|
+
logOutput += data.toString();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// 2. Send initialize handshake with custom clientInfo
|
|
26
|
+
server.stdin?.write(
|
|
27
|
+
JSON.stringify({
|
|
28
|
+
jsonrpc: "2.0",
|
|
29
|
+
id: 1,
|
|
30
|
+
method: "initialize",
|
|
31
|
+
params: {
|
|
32
|
+
protocolVersion: "2024-11-05",
|
|
33
|
+
capabilities: {},
|
|
34
|
+
clientInfo: {
|
|
35
|
+
name: TEST_CLIENT_NAME,
|
|
36
|
+
version: "1.0.0",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
}) + "\n",
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Wait for initialization to be processed
|
|
43
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
44
|
+
|
|
45
|
+
// 3. Send tool call
|
|
46
|
+
server.stdin?.write(
|
|
47
|
+
JSON.stringify({
|
|
48
|
+
jsonrpc: "2.0",
|
|
49
|
+
id: 2,
|
|
50
|
+
method: "tools/call",
|
|
51
|
+
params: {
|
|
52
|
+
name: "query_memory",
|
|
53
|
+
arguments: {
|
|
54
|
+
query: "test attribution",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
}) + "\n",
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Wait for tool execution and logging (console.log is async in some buffers)
|
|
61
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
62
|
+
|
|
63
|
+
// Cleanup
|
|
64
|
+
server.kill();
|
|
65
|
+
|
|
66
|
+
// 4. Assert that the log output contains the correctly attributed client name
|
|
67
|
+
const expectedLogLine = `[MCP-LOG] client=${TEST_CLIENT_NAME} tool=query_memory`;
|
|
68
|
+
|
|
69
|
+
if (!logOutput.includes(expectedLogLine)) {
|
|
70
|
+
console.log("Full stderr received:", logOutput);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
expect(logOutput).toContain(expectedLogLine);
|
|
74
|
+
});
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -12,8 +12,8 @@ import { z } from "zod";
|
|
|
12
12
|
import { all_async, run_async } from "openmemory-js/dist/core/db.js";
|
|
13
13
|
import { Memory } from "openmemory-js/dist/core/memory.js";
|
|
14
14
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
reinforce_memory,
|
|
16
|
+
update_memory,
|
|
17
17
|
} from "openmemory-js/dist/memory/hsg.js";
|
|
18
18
|
|
|
19
19
|
// --- TYPES ---
|
|
@@ -148,12 +148,41 @@ interface AuthContext {
|
|
|
148
148
|
[key: string]: unknown;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Extended context for MCP tools to handle STDIO client attribution.
|
|
153
|
+
*/
|
|
154
|
+
interface ToolContext {
|
|
155
|
+
session?: AuthContext;
|
|
156
|
+
client?: {
|
|
157
|
+
version: {
|
|
158
|
+
name: string;
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Derives the client name from the tool execution context.
|
|
165
|
+
* Falls back to handshake client name if session is 'stdio'.
|
|
166
|
+
*/
|
|
167
|
+
function getClientName(context: any): string {
|
|
168
|
+
const ctx = context as ToolContext;
|
|
169
|
+
const sessionName = ctx.session?.clientName;
|
|
170
|
+
if (sessionName === "stdio" && ctx.client?.version?.name) {
|
|
171
|
+
return ctx.client.version.name;
|
|
172
|
+
}
|
|
173
|
+
return sessionName || "unknown";
|
|
174
|
+
}
|
|
175
|
+
|
|
151
176
|
const server = new FastMCP<AuthContext>({
|
|
152
177
|
name: "cybermem",
|
|
153
178
|
version: VALID_VERSION,
|
|
154
179
|
instructions: CYBERMEM_INSTRUCTIONS,
|
|
155
180
|
health: { enabled: true, path: "/health" },
|
|
156
181
|
authenticate: async (req) => {
|
|
182
|
+
// STDIO transport doesn't provide an HTTP request object
|
|
183
|
+
if (!req?.headers) {
|
|
184
|
+
return { clientName: "stdio" };
|
|
185
|
+
}
|
|
157
186
|
const clientName = (req.headers["x-client-name"] ||
|
|
158
187
|
req.headers["X-Client-Name"] ||
|
|
159
188
|
"unknown") as string;
|
|
@@ -184,19 +213,18 @@ server.addTool({
|
|
|
184
213
|
tags: z.array(z.string()).optional().describe("Category tags"),
|
|
185
214
|
}),
|
|
186
215
|
execute: async (args, context) => {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
);
|
|
216
|
+
const clientName = getClientName(context);
|
|
217
|
+
|
|
218
|
+
return requestContext.run({ clientName }, async () => {
|
|
219
|
+
try {
|
|
220
|
+
const res = await memory.add(args.content, { tags: args.tags });
|
|
221
|
+
await logActivity("add_memory");
|
|
222
|
+
return JSON.stringify(res);
|
|
223
|
+
} catch (err: any) {
|
|
224
|
+
await logActivity("add_memory", 500);
|
|
225
|
+
throw err;
|
|
226
|
+
}
|
|
227
|
+
});
|
|
200
228
|
},
|
|
201
229
|
});
|
|
202
230
|
|
|
@@ -208,19 +236,18 @@ server.addTool({
|
|
|
208
236
|
k: z.number().default(5).describe("Number of results"),
|
|
209
237
|
}),
|
|
210
238
|
execute: async (args, context) => {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
);
|
|
239
|
+
const clientName = getClientName(context);
|
|
240
|
+
|
|
241
|
+
return requestContext.run({ clientName }, async () => {
|
|
242
|
+
try {
|
|
243
|
+
const res = await memory.search(args.query, { limit: args.k });
|
|
244
|
+
await logActivity("query_memory");
|
|
245
|
+
return JSON.stringify(res);
|
|
246
|
+
} catch (err: any) {
|
|
247
|
+
await logActivity("query_memory", 500);
|
|
248
|
+
throw err;
|
|
249
|
+
}
|
|
250
|
+
});
|
|
224
251
|
},
|
|
225
252
|
});
|
|
226
253
|
|
|
@@ -239,19 +266,18 @@ server.addTool({
|
|
|
239
266
|
path: ["content"],
|
|
240
267
|
}),
|
|
241
268
|
execute: async (args, context) => {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
);
|
|
269
|
+
const clientName = getClientName(context);
|
|
270
|
+
|
|
271
|
+
return requestContext.run({ clientName }, async () => {
|
|
272
|
+
try {
|
|
273
|
+
const res = await update_memory(args.id, args.content, args.tags);
|
|
274
|
+
await logActivity("update_memory");
|
|
275
|
+
return JSON.stringify(res);
|
|
276
|
+
} catch (err: any) {
|
|
277
|
+
await logActivity("update_memory", 500);
|
|
278
|
+
throw err;
|
|
279
|
+
}
|
|
280
|
+
});
|
|
255
281
|
},
|
|
256
282
|
});
|
|
257
283
|
|
|
@@ -266,19 +292,18 @@ server.addTool({
|
|
|
266
292
|
.describe("Relevance boost amount (0.0 to 1.0)"),
|
|
267
293
|
}),
|
|
268
294
|
execute: async (args, context) => {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
);
|
|
295
|
+
const clientName = getClientName(context);
|
|
296
|
+
|
|
297
|
+
return requestContext.run({ clientName }, async () => {
|
|
298
|
+
try {
|
|
299
|
+
await reinforce_memory(args.id, args.boost);
|
|
300
|
+
await logActivity("reinforce_memory");
|
|
301
|
+
return `Memory reinforced: ${args.id}`;
|
|
302
|
+
} catch (err: any) {
|
|
303
|
+
await logActivity("reinforce_memory", 500);
|
|
304
|
+
throw err;
|
|
305
|
+
}
|
|
306
|
+
});
|
|
282
307
|
},
|
|
283
308
|
});
|
|
284
309
|
|
|
@@ -289,20 +314,19 @@ server.addTool({
|
|
|
289
314
|
id: z.string().describe("Memory ID"),
|
|
290
315
|
}),
|
|
291
316
|
execute: async (args, context) => {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
);
|
|
317
|
+
const clientName = getClientName(context);
|
|
318
|
+
|
|
319
|
+
return requestContext.run({ clientName }, async () => {
|
|
320
|
+
try {
|
|
321
|
+
await run_async("DELETE FROM memories WHERE id=?", [args.id]);
|
|
322
|
+
await run_async("DELETE FROM vectors WHERE id=?", [args.id]);
|
|
323
|
+
await logActivity("delete_memory");
|
|
324
|
+
return "Deleted";
|
|
325
|
+
} catch (err: any) {
|
|
326
|
+
await logActivity("delete_memory", 500);
|
|
327
|
+
throw err;
|
|
328
|
+
}
|
|
329
|
+
});
|
|
306
330
|
},
|
|
307
331
|
});
|
|
308
332
|
|