@cybermem/mcp 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/README.md +49 -0
- package/dist/index.js +104 -0
- package/package.json +27 -0
- package/requirements.txt +2 -0
- package/server.py +347 -0
- package/src/index.ts +114 -0
- package/test_mcp.py +111 -0
- package/tsconfig.json +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# @cybermem/mcp
|
|
2
|
+
|
|
3
|
+
Official TypeScript MCP Server for CyberMem.
|
|
4
|
+
|
|
5
|
+
## Configuration
|
|
6
|
+
|
|
7
|
+
### Option A: Local (Standard)
|
|
8
|
+
If you are running CyberMem locally on your machine, use `npx` to spawn the MCP server. It will bridge the connection to your local Docker instance.
|
|
9
|
+
|
|
10
|
+
**Claude Desktop Config (`~/Library/Application Support/Claude/claude_desktop_config.json`):**
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"mcpServers": {
|
|
14
|
+
"cybermem": {
|
|
15
|
+
"command": "npx",
|
|
16
|
+
"args": [
|
|
17
|
+
"-y",
|
|
18
|
+
"@cybermem/mcp"
|
|
19
|
+
],
|
|
20
|
+
"env": {
|
|
21
|
+
"CYBERMEM_URL": "http://localhost:8080/memory",
|
|
22
|
+
"CYBERMEM_API_KEY": "your-api-key"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Option B: Remote (RPi / Cloud)
|
|
30
|
+
If you have deployed CyberMem to a Raspberry Pi or Cloud VPS, **do not use npx**. Instead, connect directly to the remote SSE endpoint.
|
|
31
|
+
|
|
32
|
+
**Claude Desktop Config:**
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"mcpServers": {
|
|
36
|
+
"cybermem-remote": {
|
|
37
|
+
"url": "http://<your-rpi-ip>:8080/mcp",
|
|
38
|
+
"transport": "sse",
|
|
39
|
+
"headers": {
|
|
40
|
+
"x-api-key": "your-api-key"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Environment Variables
|
|
48
|
+
- `CYBERMEM_URL`: URL to the OpenMemory API (default: `http://localhost:8080/memory`)
|
|
49
|
+
- `CYBERMEM_API_KEY`: Your API Key (found in `~/.cybermem/.env`)
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
7
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
8
|
+
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
9
|
+
const axios_1 = __importDefault(require("axios"));
|
|
10
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
11
|
+
dotenv_1.default.config();
|
|
12
|
+
const API_URL = process.env.CYBERMEM_URL || "http://localhost:8080/memory";
|
|
13
|
+
const API_KEY = process.env.CYBERMEM_API_KEY || "dev-secret-key";
|
|
14
|
+
const server = new index_js_1.Server({
|
|
15
|
+
name: "cybermem-mcp",
|
|
16
|
+
version: "0.1.0",
|
|
17
|
+
}, {
|
|
18
|
+
capabilities: {
|
|
19
|
+
tools: {},
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
const tools = [
|
|
23
|
+
{
|
|
24
|
+
name: "add_memory",
|
|
25
|
+
description: "Store a new memory in CyberMem",
|
|
26
|
+
inputSchema: {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
content: { type: "string" },
|
|
30
|
+
user_id: { type: "string" },
|
|
31
|
+
tags: { type: "array", items: { type: "string" } },
|
|
32
|
+
},
|
|
33
|
+
required: ["content"],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "query_memory",
|
|
38
|
+
description: "Search for relevant memories",
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: {
|
|
42
|
+
query: { type: "string" },
|
|
43
|
+
k: { type: "number", default: 5 },
|
|
44
|
+
},
|
|
45
|
+
required: ["query"],
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "list_memories",
|
|
50
|
+
description: "List recent memories",
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: {
|
|
54
|
+
limit: { type: "number", default: 10 },
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
];
|
|
59
|
+
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
60
|
+
tools,
|
|
61
|
+
}));
|
|
62
|
+
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
63
|
+
const { name, arguments: args } = request.params;
|
|
64
|
+
try {
|
|
65
|
+
switch (name) {
|
|
66
|
+
case "add_memory": {
|
|
67
|
+
const response = await axios_1.default.post(`${API_URL}/add`, args, {
|
|
68
|
+
headers: { "Authorization": `Bearer ${API_KEY}` }
|
|
69
|
+
});
|
|
70
|
+
return { content: [{ type: "text", text: JSON.stringify(response.data) }] };
|
|
71
|
+
}
|
|
72
|
+
case "query_memory": {
|
|
73
|
+
const response = await axios_1.default.post(`${API_URL}/query`, args, {
|
|
74
|
+
headers: { "Authorization": `Bearer ${API_KEY}` }
|
|
75
|
+
});
|
|
76
|
+
return { content: [{ type: "text", text: JSON.stringify(response.data) }] };
|
|
77
|
+
}
|
|
78
|
+
case "list_memories": {
|
|
79
|
+
const limit = args?.limit || 10;
|
|
80
|
+
const response = await axios_1.default.get(`${API_URL}/list?limit=${limit}`, {
|
|
81
|
+
headers: { "Authorization": `Bearer ${API_KEY}` }
|
|
82
|
+
});
|
|
83
|
+
return { content: [{ type: "text", text: JSON.stringify(response.data) }] };
|
|
84
|
+
}
|
|
85
|
+
default:
|
|
86
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
return {
|
|
91
|
+
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
92
|
+
isError: true,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
async function run() {
|
|
97
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
98
|
+
await server.connect(transport);
|
|
99
|
+
console.error("CyberMem MCP Server running on stdio");
|
|
100
|
+
}
|
|
101
|
+
run().catch((error) => {
|
|
102
|
+
console.error("Fatal error running server:", error);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cybermem/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CyberMem MCP Server (TypeScript)",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cybermem-mcp": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"start": "node dist/index.js",
|
|
12
|
+
"dev": "ts-node src/index.ts"
|
|
13
|
+
},
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
19
|
+
"axios": "^1.6.0",
|
|
20
|
+
"dotenv": "^16.0.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^18.0.0",
|
|
24
|
+
"ts-node": "^10.9.1",
|
|
25
|
+
"typescript": "^5.0.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/requirements.txt
ADDED
package/server.py
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
CyberMem MCP Server
|
|
4
|
+
|
|
5
|
+
MCP server that exposes shared memory functionality to several LLM.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import os
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
from mcp import types
|
|
14
|
+
from mcp.server import NotificationOptions, Server
|
|
15
|
+
from mcp.server.models import InitializationOptions
|
|
16
|
+
from mcp.server.stdio import stdio_server
|
|
17
|
+
|
|
18
|
+
# CyberMem configuration (OpenMemory backend)
|
|
19
|
+
OPENMEMORY_URL = os.getenv("OPENMEMORY_URL", "http://localhost:80")
|
|
20
|
+
CYBERMEM_API_KEY = os.getenv("CYBERMEM_API_KEY", "dev-secret-key")
|
|
21
|
+
|
|
22
|
+
# Create MCP server instance
|
|
23
|
+
server = Server("cybermem")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@server.list_tools()
|
|
27
|
+
async def handle_list_tools() -> list[types.Tool]:
|
|
28
|
+
"""List available memory management tools."""
|
|
29
|
+
return [
|
|
30
|
+
types.Tool(
|
|
31
|
+
name="add_memory",
|
|
32
|
+
description="Store information in long-term memory. Use this to remember important facts, decisions, or context for future conversations.",
|
|
33
|
+
inputSchema={
|
|
34
|
+
"type": "object",
|
|
35
|
+
"properties": {
|
|
36
|
+
"content": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"description": "The information to remember (text content)",
|
|
39
|
+
},
|
|
40
|
+
"metadata": {
|
|
41
|
+
"type": "object",
|
|
42
|
+
"description": "Optional metadata (tags, category, importance, etc.)",
|
|
43
|
+
"additionalProperties": True,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
"required": ["content"],
|
|
47
|
+
},
|
|
48
|
+
),
|
|
49
|
+
types.Tool(
|
|
50
|
+
name="search_memory",
|
|
51
|
+
description="Search through stored memories to find relevant information. Use this to recall past conversations, decisions, or facts.",
|
|
52
|
+
inputSchema={
|
|
53
|
+
"type": "object",
|
|
54
|
+
"properties": {
|
|
55
|
+
"query": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"description": "What to search for (semantic search query)",
|
|
58
|
+
},
|
|
59
|
+
"limit": {
|
|
60
|
+
"type": "number",
|
|
61
|
+
"description": "Maximum number of results (default: 5)",
|
|
62
|
+
"default": 5,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
"required": ["query"],
|
|
66
|
+
},
|
|
67
|
+
),
|
|
68
|
+
types.Tool(
|
|
69
|
+
name="list_memories",
|
|
70
|
+
description="List recent memories stored in the system. Useful for browsing what has been remembered.",
|
|
71
|
+
inputSchema={
|
|
72
|
+
"type": "object",
|
|
73
|
+
"properties": {
|
|
74
|
+
"limit": {
|
|
75
|
+
"type": "number",
|
|
76
|
+
"description": "Maximum number of memories to retrieve (default: 10)",
|
|
77
|
+
"default": 10,
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
),
|
|
82
|
+
types.Tool(
|
|
83
|
+
name="delete_memory",
|
|
84
|
+
description="Delete a specific memory by its ID. Use this to remove outdated or incorrect information.",
|
|
85
|
+
inputSchema={
|
|
86
|
+
"type": "object",
|
|
87
|
+
"properties": {
|
|
88
|
+
"memory_id": {
|
|
89
|
+
"type": "string",
|
|
90
|
+
"description": "The UUID of the memory to delete",
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"required": ["memory_id"],
|
|
94
|
+
},
|
|
95
|
+
),
|
|
96
|
+
types.Tool(
|
|
97
|
+
name="update_memory",
|
|
98
|
+
description="Update an existing memory's content, tags, or metadata.",
|
|
99
|
+
inputSchema={
|
|
100
|
+
"type": "object",
|
|
101
|
+
"properties": {
|
|
102
|
+
"memory_id": {
|
|
103
|
+
"type": "string",
|
|
104
|
+
"description": "The UUID of the memory to update",
|
|
105
|
+
},
|
|
106
|
+
"content": {
|
|
107
|
+
"type": "string",
|
|
108
|
+
"description": "New content for the memory (optional)",
|
|
109
|
+
},
|
|
110
|
+
"tags": {
|
|
111
|
+
"type": "array",
|
|
112
|
+
"items": {"type": "string"},
|
|
113
|
+
"description": "New tags for the memory (optional)",
|
|
114
|
+
},
|
|
115
|
+
"metadata": {
|
|
116
|
+
"type": "object",
|
|
117
|
+
"description": "New metadata for the memory (optional)",
|
|
118
|
+
"additionalProperties": True,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
"required": ["memory_id"],
|
|
122
|
+
},
|
|
123
|
+
),
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@server.call_tool()
|
|
128
|
+
async def handle_call_tool(
|
|
129
|
+
name: str, arguments: dict[str, Any]
|
|
130
|
+
) -> list[types.TextContent]:
|
|
131
|
+
"""Handle tool execution requests."""
|
|
132
|
+
|
|
133
|
+
# Get client info from MCP context
|
|
134
|
+
client_name, client_version = "unknown", "unknown"
|
|
135
|
+
try:
|
|
136
|
+
ctx = server.request_context
|
|
137
|
+
if hasattr(ctx, "session"):
|
|
138
|
+
session = ctx.session
|
|
139
|
+
if hasattr(session, "_client_params"):
|
|
140
|
+
client_params = session._client_params
|
|
141
|
+
if client_params and hasattr(client_params, "clientInfo"):
|
|
142
|
+
client_info = client_params.clientInfo
|
|
143
|
+
client_name = client_info.name or "unknown"
|
|
144
|
+
client_version = client_info.version or "unknown"
|
|
145
|
+
except Exception:
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
headers = {
|
|
149
|
+
"Authorization": f"Bearer {CYBERMEM_API_KEY}",
|
|
150
|
+
"Content-Type": "application/json",
|
|
151
|
+
"X-Client-Name": client_name,
|
|
152
|
+
"X-Client-Version": client_version,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
156
|
+
try:
|
|
157
|
+
if name == "add_memory":
|
|
158
|
+
# Store memory in OpenMemory
|
|
159
|
+
content = arguments.get("content")
|
|
160
|
+
metadata = arguments.get("metadata", {})
|
|
161
|
+
|
|
162
|
+
response = await client.post(
|
|
163
|
+
f"{OPENMEMORY_URL}/memory/add",
|
|
164
|
+
json={"content": content, "metadata": metadata},
|
|
165
|
+
headers=headers,
|
|
166
|
+
)
|
|
167
|
+
response.raise_for_status()
|
|
168
|
+
result = response.json()
|
|
169
|
+
|
|
170
|
+
return [
|
|
171
|
+
types.TextContent(
|
|
172
|
+
type="text",
|
|
173
|
+
text=f"ā
Memory stored successfully!\n\nID: {result.get('id')}\nChunks: {result.get('chunks')}\nSectors: {', '.join(result.get('sectors', []))}",
|
|
174
|
+
)
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
elif name == "search_memory":
|
|
178
|
+
# Search memories in OpenMemory
|
|
179
|
+
query = arguments.get("query")
|
|
180
|
+
limit = arguments.get("limit", 5)
|
|
181
|
+
|
|
182
|
+
response = await client.post(
|
|
183
|
+
f"{OPENMEMORY_URL}/memory/query",
|
|
184
|
+
json={"query": query, "k": limit},
|
|
185
|
+
headers=headers,
|
|
186
|
+
)
|
|
187
|
+
response.raise_for_status()
|
|
188
|
+
data = response.json()
|
|
189
|
+
matches = data.get("matches", [])
|
|
190
|
+
|
|
191
|
+
if not matches:
|
|
192
|
+
return [
|
|
193
|
+
types.TextContent(
|
|
194
|
+
type="text",
|
|
195
|
+
text="š No memories found matching your query.",
|
|
196
|
+
)
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
# Format search results
|
|
200
|
+
formatted_results = []
|
|
201
|
+
for i, memory in enumerate(matches, 1):
|
|
202
|
+
content = memory.get("content", "")
|
|
203
|
+
score = memory.get("score", 0)
|
|
204
|
+
sector = memory.get("primary_sector", "")
|
|
205
|
+
|
|
206
|
+
formatted_results.append(
|
|
207
|
+
f"**Result {i}** (score: {score:.2f}, sector: {sector})\n{content}\n"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
return [
|
|
211
|
+
types.TextContent(
|
|
212
|
+
type="text",
|
|
213
|
+
text=f"š Found {len(matches)} memories:\n\n"
|
|
214
|
+
+ "\n".join(formatted_results),
|
|
215
|
+
)
|
|
216
|
+
]
|
|
217
|
+
|
|
218
|
+
elif name == "list_memories":
|
|
219
|
+
# List recent memories
|
|
220
|
+
limit = arguments.get("limit", 10)
|
|
221
|
+
|
|
222
|
+
# Note: OpenMemory doesn't have a /list endpoint yet,
|
|
223
|
+
# so we'll do a broad query with generic term
|
|
224
|
+
response = await client.post(
|
|
225
|
+
f"{OPENMEMORY_URL}/memory/query",
|
|
226
|
+
json={"query": "memory context", "k": limit},
|
|
227
|
+
headers=headers,
|
|
228
|
+
)
|
|
229
|
+
response.raise_for_status()
|
|
230
|
+
data = response.json()
|
|
231
|
+
matches = data.get("matches", [])
|
|
232
|
+
|
|
233
|
+
if not matches:
|
|
234
|
+
return [
|
|
235
|
+
types.TextContent(
|
|
236
|
+
type="text", text="š No memories stored yet."
|
|
237
|
+
)
|
|
238
|
+
]
|
|
239
|
+
|
|
240
|
+
# Format list
|
|
241
|
+
formatted_list = []
|
|
242
|
+
for i, memory in enumerate(matches, 1):
|
|
243
|
+
content = memory.get("content", "")
|
|
244
|
+
sector = memory.get("primary_sector", "")
|
|
245
|
+
# Truncate long content
|
|
246
|
+
if len(content) > 100:
|
|
247
|
+
content = content[:97] + "..."
|
|
248
|
+
formatted_list.append(f"{i}. [{sector}] {content}")
|
|
249
|
+
|
|
250
|
+
return [
|
|
251
|
+
types.TextContent(
|
|
252
|
+
type="text",
|
|
253
|
+
text=f"š Recent memories ({len(matches)}):\n\n"
|
|
254
|
+
+ "\n".join(formatted_list),
|
|
255
|
+
)
|
|
256
|
+
]
|
|
257
|
+
|
|
258
|
+
elif name == "delete_memory":
|
|
259
|
+
# Delete memory by ID
|
|
260
|
+
memory_id = arguments.get("memory_id")
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
response = await client.delete(
|
|
264
|
+
f"{OPENMEMORY_URL}/memory/{memory_id}", headers=headers
|
|
265
|
+
)
|
|
266
|
+
response.raise_for_status()
|
|
267
|
+
result = response.json()
|
|
268
|
+
status = result.get("ok", "deleted")
|
|
269
|
+
except httpx.HTTPStatusError as e:
|
|
270
|
+
# Workaround: OpenMemory returns 500 on successful delete (bug in OpenMemory)
|
|
271
|
+
# We'll treat it as success since the memory is actually deleted
|
|
272
|
+
if e.response.status_code == 500:
|
|
273
|
+
status = "deleted (500 ignored - OpenMemory bug)"
|
|
274
|
+
else:
|
|
275
|
+
raise
|
|
276
|
+
|
|
277
|
+
return [
|
|
278
|
+
types.TextContent(
|
|
279
|
+
type="text",
|
|
280
|
+
text=f"šļø Memory deleted successfully!\n\nID: {memory_id}\nStatus: {status}",
|
|
281
|
+
)
|
|
282
|
+
]
|
|
283
|
+
|
|
284
|
+
elif name == "update_memory":
|
|
285
|
+
# Update memory by ID
|
|
286
|
+
memory_id = arguments.get("memory_id")
|
|
287
|
+
content = arguments.get("content")
|
|
288
|
+
tags = arguments.get("tags")
|
|
289
|
+
metadata = arguments.get("metadata")
|
|
290
|
+
|
|
291
|
+
payload = {}
|
|
292
|
+
if content:
|
|
293
|
+
payload["content"] = content
|
|
294
|
+
if tags:
|
|
295
|
+
payload["tags"] = tags
|
|
296
|
+
if metadata:
|
|
297
|
+
payload["metadata"] = metadata
|
|
298
|
+
|
|
299
|
+
response = await client.patch(
|
|
300
|
+
f"{OPENMEMORY_URL}/memory/{memory_id}",
|
|
301
|
+
json=payload,
|
|
302
|
+
headers=headers,
|
|
303
|
+
)
|
|
304
|
+
response.raise_for_status()
|
|
305
|
+
result = response.json()
|
|
306
|
+
|
|
307
|
+
return [
|
|
308
|
+
types.TextContent(
|
|
309
|
+
type="text",
|
|
310
|
+
text=f"āļø Memory updated successfully!\n\nID: {result.get('id', memory_id)}",
|
|
311
|
+
)
|
|
312
|
+
]
|
|
313
|
+
|
|
314
|
+
else:
|
|
315
|
+
raise ValueError(f"Unknown tool: {name}")
|
|
316
|
+
|
|
317
|
+
except httpx.HTTPStatusError as e:
|
|
318
|
+
return [
|
|
319
|
+
types.TextContent(
|
|
320
|
+
type="text",
|
|
321
|
+
text=f"ā API Error: {e.response.status_code}\n{e.response.text}",
|
|
322
|
+
)
|
|
323
|
+
]
|
|
324
|
+
except Exception as e:
|
|
325
|
+
return [types.TextContent(type="text", text=f"ā Error: {str(e)}")]
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
async def main():
|
|
329
|
+
"""Run the MCP server."""
|
|
330
|
+
|
|
331
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
332
|
+
await server.run(
|
|
333
|
+
read_stream,
|
|
334
|
+
write_stream,
|
|
335
|
+
InitializationOptions(
|
|
336
|
+
server_name="cybermem",
|
|
337
|
+
server_version="0.1.0",
|
|
338
|
+
capabilities=server.get_capabilities(
|
|
339
|
+
notification_options=NotificationOptions(),
|
|
340
|
+
experimental_capabilities={},
|
|
341
|
+
),
|
|
342
|
+
),
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
if __name__ == "__main__":
|
|
347
|
+
asyncio.run(main())
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import {
|
|
4
|
+
CallToolRequestSchema,
|
|
5
|
+
ListToolsRequestSchema,
|
|
6
|
+
Tool
|
|
7
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
import axios from "axios";
|
|
9
|
+
import dotenv from "dotenv";
|
|
10
|
+
|
|
11
|
+
dotenv.config();
|
|
12
|
+
|
|
13
|
+
const API_URL = process.env.CYBERMEM_URL || "http://localhost:8080/memory";
|
|
14
|
+
const API_KEY = process.env.CYBERMEM_API_KEY || "dev-secret-key";
|
|
15
|
+
|
|
16
|
+
const server = new Server(
|
|
17
|
+
{
|
|
18
|
+
name: "cybermem-mcp",
|
|
19
|
+
version: "0.1.0",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
capabilities: {
|
|
23
|
+
tools: {},
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const tools: Tool[] = [
|
|
29
|
+
{
|
|
30
|
+
name: "add_memory",
|
|
31
|
+
description: "Store a new memory in CyberMem",
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: "object",
|
|
34
|
+
properties: {
|
|
35
|
+
content: { type: "string" },
|
|
36
|
+
user_id: { type: "string" },
|
|
37
|
+
tags: { type: "array", items: { type: "string" } },
|
|
38
|
+
},
|
|
39
|
+
required: ["content"],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "query_memory",
|
|
44
|
+
description: "Search for relevant memories",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: {
|
|
48
|
+
query: { type: "string" },
|
|
49
|
+
k: { type: "number", default: 5 },
|
|
50
|
+
},
|
|
51
|
+
required: ["query"],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "list_memories",
|
|
56
|
+
description: "List recent memories",
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: "object",
|
|
59
|
+
properties: {
|
|
60
|
+
limit: { type: "number", default: 10 },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
67
|
+
tools,
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
71
|
+
const { name, arguments: args } = request.params;
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
switch (name) {
|
|
75
|
+
case "add_memory": {
|
|
76
|
+
const response = await axios.post(`${API_URL}/add`, args, {
|
|
77
|
+
headers: { "Authorization": `Bearer ${API_KEY}` }
|
|
78
|
+
});
|
|
79
|
+
return { content: [{ type: "text", text: JSON.stringify(response.data) }] };
|
|
80
|
+
}
|
|
81
|
+
case "query_memory": {
|
|
82
|
+
const response = await axios.post(`${API_URL}/query`, args, {
|
|
83
|
+
headers: { "Authorization": `Bearer ${API_KEY}` }
|
|
84
|
+
});
|
|
85
|
+
return { content: [{ type: "text", text: JSON.stringify(response.data) }] };
|
|
86
|
+
}
|
|
87
|
+
case "list_memories": {
|
|
88
|
+
const limit = args?.limit || 10;
|
|
89
|
+
const response = await axios.get(`${API_URL}/list?limit=${limit}`, {
|
|
90
|
+
headers: { "Authorization": `Bearer ${API_KEY}` }
|
|
91
|
+
});
|
|
92
|
+
return { content: [{ type: "text", text: JSON.stringify(response.data) }] };
|
|
93
|
+
}
|
|
94
|
+
default:
|
|
95
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
96
|
+
}
|
|
97
|
+
} catch (error: any) {
|
|
98
|
+
return {
|
|
99
|
+
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
100
|
+
isError: true,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
async function run() {
|
|
106
|
+
const transport = new StdioServerTransport();
|
|
107
|
+
await server.connect(transport);
|
|
108
|
+
console.error("CyberMem MCP Server running on stdio");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
run().catch((error) => {
|
|
112
|
+
console.error("Fatal error running server:", error);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
});
|
package/test_mcp.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Test script for OpenMemory MCP Server
|
|
4
|
+
|
|
5
|
+
Demonstrates MCP server functionality by simulating tool calls.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import httpx
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
OPENMEMORY_URL = "http://localhost/memory"
|
|
14
|
+
OPENMEMORY_API_KEY = "dev-secret-key"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def test_add_memory():
|
|
18
|
+
"""Test adding a memory."""
|
|
19
|
+
print("\nš§Ŗ Test 1: Adding memory...")
|
|
20
|
+
|
|
21
|
+
headers = {
|
|
22
|
+
"Authorization": f"Bearer {OPENMEMORY_API_KEY}",
|
|
23
|
+
"Content-Type": "application/json"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
27
|
+
response = await client.post(
|
|
28
|
+
f"{OPENMEMORY_URL}/add",
|
|
29
|
+
json={
|
|
30
|
+
"content": "MCP server test: Claude Code can now remember conversations and context",
|
|
31
|
+
"metadata": {
|
|
32
|
+
"source": "mcp_test",
|
|
33
|
+
"category": "integration_test",
|
|
34
|
+
"timestamp": "2025-11-30"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
headers=headers
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if response.status_code == 200:
|
|
41
|
+
result = response.json()
|
|
42
|
+
print(f"ā
Memory added successfully!")
|
|
43
|
+
print(f" ID: {result.get('id')}")
|
|
44
|
+
print(f" Chunks: {result.get('chunks')}")
|
|
45
|
+
print(f" Sectors: {', '.join(result.get('sectors', []))}")
|
|
46
|
+
return result.get('id')
|
|
47
|
+
else:
|
|
48
|
+
print(f"ā Failed: {response.status_code} - {response.text}")
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def test_search_memory():
|
|
53
|
+
"""Test searching memories."""
|
|
54
|
+
print("\nš§Ŗ Test 2: Searching memory...")
|
|
55
|
+
|
|
56
|
+
headers = {
|
|
57
|
+
"Authorization": f"Bearer {OPENMEMORY_API_KEY}",
|
|
58
|
+
"Content-Type": "application/json"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
62
|
+
response = await client.post(
|
|
63
|
+
f"{OPENMEMORY_URL}/query",
|
|
64
|
+
json={
|
|
65
|
+
"query": "Claude Code remember conversations",
|
|
66
|
+
"k": 3
|
|
67
|
+
},
|
|
68
|
+
headers=headers
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if response.status_code == 200:
|
|
72
|
+
data = response.json()
|
|
73
|
+
matches = data.get("matches", [])
|
|
74
|
+
print(f"ā
Found {len(matches)} memories:")
|
|
75
|
+
for i, memory in enumerate(matches, 1):
|
|
76
|
+
content = memory.get("content", "")
|
|
77
|
+
score = memory.get("score", 0)
|
|
78
|
+
sector = memory.get("primary_sector", "")
|
|
79
|
+
print(f"\n Result {i} (score: {score:.2f}, sector: {sector}):")
|
|
80
|
+
print(f" {content[:100]}...")
|
|
81
|
+
else:
|
|
82
|
+
print(f"ā Failed: {response.status_code} - {response.text}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def test_mcp_integration():
|
|
86
|
+
"""Run full MCP integration test."""
|
|
87
|
+
print("=" * 60)
|
|
88
|
+
print("OpenMemory MCP Server - Integration Test")
|
|
89
|
+
print("=" * 60)
|
|
90
|
+
|
|
91
|
+
# Test 1: Add memory
|
|
92
|
+
memory_id = await test_add_memory()
|
|
93
|
+
|
|
94
|
+
# Wait a bit for indexing
|
|
95
|
+
await asyncio.sleep(2)
|
|
96
|
+
|
|
97
|
+
# Test 2: Search memory
|
|
98
|
+
await test_search_memory()
|
|
99
|
+
|
|
100
|
+
print("\n" + "=" * 60)
|
|
101
|
+
print("ā
Integration test completed!")
|
|
102
|
+
print("=" * 60)
|
|
103
|
+
print("\nNext steps:")
|
|
104
|
+
print("1. Restart Claude Code to load the MCP server")
|
|
105
|
+
print("2. Try asking Claude Code to remember something")
|
|
106
|
+
print("3. Check Grafana dashboard for audit trail")
|
|
107
|
+
print(" ā http://localhost:3000/d/cybermem-memory")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
asyncio.run(test_mcp_integration())
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"]
|
|
14
|
+
}
|