@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 +12 -0
- package/dist/index.js +43 -5
- package/e2e/stdio_attribution.spec.ts +74 -0
- package/package.json +1 -1
- package/src/index.ts +111 -66
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
|