@cybermem/mcp 0.15.0 → 0.16.1

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @cybermem/mcp
2
2
 
3
+ ## 0.16.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Automated patch version bump.
8
+
9
+ ## 0.16.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [#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
14
+
3
15
  ## 0.15.0
4
16
 
5
17
  ### Minor Changes
package/dist/index.js CHANGED
@@ -89,12 +89,45 @@ async function initialize() {
89
89
  process.exit(1);
90
90
  }
91
91
  }
92
+ /**
93
+ * Derives the client name from the tool execution context.
94
+ *
95
+ * Priority:
96
+ * 1. STDIO: handshake clientInfo.name (via context.client.version.name)
97
+ * 2. HTTP with X-Client-Name header: the explicit header value
98
+ * 3. HTTP without header: handshake clientInfo.name as fallback
99
+ * 4. Last resort: "unknown"
100
+ */
101
+ function getClientName(context) {
102
+ const ctx = context;
103
+ const sessionName = ctx.session?.clientName;
104
+ // FastMCP exposes MCP handshake clientInfo at context.client.version
105
+ const handshakeName = ctx.client?.version?.name;
106
+ // STDIO: always use handshake name (the real client identity)
107
+ if (sessionName === "stdio") {
108
+ return sanitizeClientName(handshakeName || "unknown");
109
+ }
110
+ // HTTP: prefer explicit X-Client-Name header if it's meaningful
111
+ if (sessionName && sessionName !== "unknown") {
112
+ return sanitizeClientName(sessionName);
113
+ }
114
+ // HTTP without header: fall back to handshake name
115
+ return sanitizeClientName(handshakeName || sessionName || "unknown");
116
+ }
117
+ /** Strip control characters and truncate to prevent log injection. */
118
+ function sanitizeClientName(name) {
119
+ return name.replace(/[\x00-\x1f\x7f]/g, "").slice(0, 64);
120
+ }
92
121
  const server = new fastmcp_1.FastMCP({
93
122
  name: "cybermem",
94
123
  version: VALID_VERSION,
95
124
  instructions: CYBERMEM_INSTRUCTIONS,
96
125
  health: { enabled: true, path: "/health" },
97
126
  authenticate: async (req) => {
127
+ // STDIO transport doesn't provide an HTTP request object
128
+ if (!req?.headers) {
129
+ return { clientName: "stdio" };
130
+ }
98
131
  const clientName = (req.headers["x-client-name"] ||
99
132
  req.headers["X-Client-Name"] ||
100
133
  "unknown");
@@ -120,7 +153,8 @@ server.addTool({
120
153
  tags: zod_1.z.array(zod_1.z.string()).optional().describe("Category tags"),
121
154
  }),
122
155
  execute: async (args, context) => {
123
- return requestContext.run({ clientName: context.session?.clientName }, async () => {
156
+ const clientName = getClientName(context);
157
+ return requestContext.run({ clientName }, async () => {
124
158
  try {
125
159
  const res = await memory.add(args.content, { tags: args.tags });
126
160
  await logActivity("add_memory");
@@ -141,7 +175,8 @@ server.addTool({
141
175
  k: zod_1.z.number().default(5).describe("Number of results"),
142
176
  }),
143
177
  execute: async (args, context) => {
144
- return requestContext.run({ clientName: context.session?.clientName }, async () => {
178
+ const clientName = getClientName(context);
179
+ return requestContext.run({ clientName }, async () => {
145
180
  try {
146
181
  const res = await memory.search(args.query, { limit: args.k });
147
182
  await logActivity("query_memory");
@@ -168,7 +203,8 @@ server.addTool({
168
203
  path: ["content"],
169
204
  }),
170
205
  execute: async (args, context) => {
171
- return requestContext.run({ clientName: context.session?.clientName }, async () => {
206
+ const clientName = getClientName(context);
207
+ return requestContext.run({ clientName }, async () => {
172
208
  try {
173
209
  const res = await (0, hsg_js_1.update_memory)(args.id, args.content, args.tags);
174
210
  await logActivity("update_memory");
@@ -192,7 +228,8 @@ server.addTool({
192
228
  .describe("Relevance boost amount (0.0 to 1.0)"),
193
229
  }),
194
230
  execute: async (args, context) => {
195
- return requestContext.run({ clientName: context.session?.clientName }, async () => {
231
+ const clientName = getClientName(context);
232
+ return requestContext.run({ clientName }, async () => {
196
233
  try {
197
234
  await (0, hsg_js_1.reinforce_memory)(args.id, args.boost);
198
235
  await logActivity("reinforce_memory");
@@ -212,7 +249,8 @@ server.addTool({
212
249
  id: zod_1.z.string().describe("Memory ID"),
213
250
  }),
214
251
  execute: async (args, context) => {
215
- return requestContext.run({ clientName: context.session?.clientName }, async () => {
252
+ const clientName = getClientName(context);
253
+ return requestContext.run({ clientName }, async () => {
216
254
  try {
217
255
  await (0, db_js_1.run_async)("DELETE FROM memories WHERE id=?", [args.id]);
218
256
  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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cybermem/mcp",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
4
4
  "description": "CyberMem MCP Server - AI Memory with openmemory-js SDK",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/index.ts CHANGED
@@ -148,12 +148,62 @@ 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
+ *
166
+ * Priority:
167
+ * 1. STDIO: handshake clientInfo.name (via context.client.version.name)
168
+ * 2. HTTP with X-Client-Name header: the explicit header value
169
+ * 3. HTTP without header: handshake clientInfo.name as fallback
170
+ * 4. Last resort: "unknown"
171
+ */
172
+ function getClientName(context: any): string {
173
+ const ctx = context as ToolContext;
174
+ const sessionName = ctx.session?.clientName;
175
+ // FastMCP exposes MCP handshake clientInfo at context.client.version
176
+ const handshakeName = ctx.client?.version?.name;
177
+
178
+ // STDIO: always use handshake name (the real client identity)
179
+ if (sessionName === "stdio") {
180
+ return sanitizeClientName(handshakeName || "unknown");
181
+ }
182
+
183
+ // HTTP: prefer explicit X-Client-Name header if it's meaningful
184
+ if (sessionName && sessionName !== "unknown") {
185
+ return sanitizeClientName(sessionName);
186
+ }
187
+
188
+ // HTTP without header: fall back to handshake name
189
+ return sanitizeClientName(handshakeName || sessionName || "unknown");
190
+ }
191
+
192
+ /** Strip control characters and truncate to prevent log injection. */
193
+ function sanitizeClientName(name: string): string {
194
+ return name.replace(/[\x00-\x1f\x7f]/g, "").slice(0, 64);
195
+ }
196
+
151
197
  const server = new FastMCP<AuthContext>({
152
198
  name: "cybermem",
153
199
  version: VALID_VERSION,
154
200
  instructions: CYBERMEM_INSTRUCTIONS,
155
201
  health: { enabled: true, path: "/health" },
156
202
  authenticate: async (req) => {
203
+ // STDIO transport doesn't provide an HTTP request object
204
+ if (!req?.headers) {
205
+ return { clientName: "stdio" };
206
+ }
157
207
  const clientName = (req.headers["x-client-name"] ||
158
208
  req.headers["X-Client-Name"] ||
159
209
  "unknown") as string;
@@ -184,19 +234,18 @@ server.addTool({
184
234
  tags: z.array(z.string()).optional().describe("Category tags"),
185
235
  }),
186
236
  execute: async (args, context) => {
187
- return requestContext.run(
188
- { clientName: context.session?.clientName },
189
- async () => {
190
- try {
191
- const res = await memory.add(args.content, { tags: args.tags });
192
- await logActivity("add_memory");
193
- return JSON.stringify(res);
194
- } catch (err: any) {
195
- await logActivity("add_memory", 500);
196
- throw err;
197
- }
198
- },
199
- );
237
+ const clientName = getClientName(context);
238
+
239
+ return requestContext.run({ clientName }, async () => {
240
+ try {
241
+ const res = await memory.add(args.content, { tags: args.tags });
242
+ await logActivity("add_memory");
243
+ return JSON.stringify(res);
244
+ } catch (err: any) {
245
+ await logActivity("add_memory", 500);
246
+ throw err;
247
+ }
248
+ });
200
249
  },
201
250
  });
202
251
 
@@ -208,19 +257,18 @@ server.addTool({
208
257
  k: z.number().default(5).describe("Number of results"),
209
258
  }),
210
259
  execute: async (args, context) => {
211
- return requestContext.run(
212
- { clientName: context.session?.clientName },
213
- async () => {
214
- try {
215
- const res = await memory.search(args.query, { limit: args.k });
216
- await logActivity("query_memory");
217
- return JSON.stringify(res);
218
- } catch (err: any) {
219
- await logActivity("query_memory", 500);
220
- throw err;
221
- }
222
- },
223
- );
260
+ const clientName = getClientName(context);
261
+
262
+ return requestContext.run({ clientName }, async () => {
263
+ try {
264
+ const res = await memory.search(args.query, { limit: args.k });
265
+ await logActivity("query_memory");
266
+ return JSON.stringify(res);
267
+ } catch (err: any) {
268
+ await logActivity("query_memory", 500);
269
+ throw err;
270
+ }
271
+ });
224
272
  },
225
273
  });
226
274
 
@@ -239,19 +287,18 @@ server.addTool({
239
287
  path: ["content"],
240
288
  }),
241
289
  execute: async (args, context) => {
242
- return requestContext.run(
243
- { clientName: context.session?.clientName },
244
- async () => {
245
- try {
246
- const res = await update_memory(args.id, args.content, args.tags);
247
- await logActivity("update_memory");
248
- return JSON.stringify(res);
249
- } catch (err: any) {
250
- await logActivity("update_memory", 500);
251
- throw err;
252
- }
253
- },
254
- );
290
+ const clientName = getClientName(context);
291
+
292
+ return requestContext.run({ clientName }, async () => {
293
+ try {
294
+ const res = await update_memory(args.id, args.content, args.tags);
295
+ await logActivity("update_memory");
296
+ return JSON.stringify(res);
297
+ } catch (err: any) {
298
+ await logActivity("update_memory", 500);
299
+ throw err;
300
+ }
301
+ });
255
302
  },
256
303
  });
257
304
 
@@ -266,19 +313,18 @@ server.addTool({
266
313
  .describe("Relevance boost amount (0.0 to 1.0)"),
267
314
  }),
268
315
  execute: async (args, context) => {
269
- return requestContext.run(
270
- { clientName: context.session?.clientName },
271
- async () => {
272
- try {
273
- await reinforce_memory(args.id, args.boost);
274
- await logActivity("reinforce_memory");
275
- return `Memory reinforced: ${args.id}`;
276
- } catch (err: any) {
277
- await logActivity("reinforce_memory", 500);
278
- throw err;
279
- }
280
- },
281
- );
316
+ const clientName = getClientName(context);
317
+
318
+ return requestContext.run({ clientName }, async () => {
319
+ try {
320
+ await reinforce_memory(args.id, args.boost);
321
+ await logActivity("reinforce_memory");
322
+ return `Memory reinforced: ${args.id}`;
323
+ } catch (err: any) {
324
+ await logActivity("reinforce_memory", 500);
325
+ throw err;
326
+ }
327
+ });
282
328
  },
283
329
  });
284
330
 
@@ -289,20 +335,19 @@ server.addTool({
289
335
  id: z.string().describe("Memory ID"),
290
336
  }),
291
337
  execute: async (args, context) => {
292
- return requestContext.run(
293
- { clientName: context.session?.clientName },
294
- async () => {
295
- try {
296
- await run_async("DELETE FROM memories WHERE id=?", [args.id]);
297
- await run_async("DELETE FROM vectors WHERE id=?", [args.id]);
298
- await logActivity("delete_memory");
299
- return "Deleted";
300
- } catch (err: any) {
301
- await logActivity("delete_memory", 500);
302
- throw err;
303
- }
304
- },
305
- );
338
+ const clientName = getClientName(context);
339
+
340
+ return requestContext.run({ clientName }, async () => {
341
+ try {
342
+ await run_async("DELETE FROM memories WHERE id=?", [args.id]);
343
+ await run_async("DELETE FROM vectors WHERE id=?", [args.id]);
344
+ await logActivity("delete_memory");
345
+ return "Deleted";
346
+ } catch (err: any) {
347
+ await logActivity("delete_memory", 500);
348
+ throw err;
349
+ }
350
+ });
306
351
  },
307
352
  });
308
353