openbrain-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.
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Multi-provider embedding abstraction.
3
+ * Supports Gemini (free tier, default) and OpenAI.
4
+ */
5
+ export interface EmbeddingProvider {
6
+ name: string;
7
+ dimensions: number;
8
+ embed(text: string): Promise<number[]>;
9
+ }
10
+ export declare class GeminiProvider implements EmbeddingProvider {
11
+ private apiKey;
12
+ name: string;
13
+ dimensions: number;
14
+ constructor(apiKey: string);
15
+ embed(text: string): Promise<number[]>;
16
+ }
17
+ export declare class OpenAIProvider implements EmbeddingProvider {
18
+ private apiKey;
19
+ name: string;
20
+ dimensions: number;
21
+ constructor(apiKey: string);
22
+ embed(text: string): Promise<number[]>;
23
+ }
24
+ export declare function createProvider(provider: string, config: Record<string, string>): EmbeddingProvider;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Multi-provider embedding abstraction.
3
+ * Supports Gemini (free tier, default) and OpenAI.
4
+ */
5
+ export class GeminiProvider {
6
+ apiKey;
7
+ name = "gemini";
8
+ dimensions = 1536;
9
+ constructor(apiKey) {
10
+ this.apiKey = apiKey;
11
+ if (!apiKey)
12
+ throw new Error("Gemini API key required. Run 'npx openbrain-ai setup' to configure.");
13
+ }
14
+ async embed(text) {
15
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:embedContent?key=${this.apiKey}`;
16
+ const resp = await fetch(url, {
17
+ method: "POST",
18
+ headers: { "Content-Type": "application/json" },
19
+ body: JSON.stringify({
20
+ content: { parts: [{ text }] },
21
+ outputDimensionality: this.dimensions,
22
+ }),
23
+ });
24
+ const data = await resp.json();
25
+ if (data.error)
26
+ throw new Error(`Gemini embedding error: ${data.error.message}`);
27
+ return data.embedding.values;
28
+ }
29
+ }
30
+ export class OpenAIProvider {
31
+ apiKey;
32
+ name = "openai";
33
+ dimensions = 1536;
34
+ constructor(apiKey) {
35
+ this.apiKey = apiKey;
36
+ if (!apiKey)
37
+ throw new Error("OpenAI API key required. Run 'npx openbrain-ai setup' to configure.");
38
+ }
39
+ async embed(text) {
40
+ const resp = await fetch("https://api.openai.com/v1/embeddings", {
41
+ method: "POST",
42
+ headers: {
43
+ "Content-Type": "application/json",
44
+ Authorization: `Bearer ${this.apiKey}`,
45
+ },
46
+ body: JSON.stringify({
47
+ model: "text-embedding-3-small",
48
+ input: text,
49
+ dimensions: this.dimensions,
50
+ }),
51
+ });
52
+ const data = await resp.json();
53
+ if (data.error)
54
+ throw new Error(`OpenAI embedding error: ${data.error.message}`);
55
+ return data.data[0].embedding;
56
+ }
57
+ }
58
+ export function createProvider(provider, config) {
59
+ switch (provider) {
60
+ case "gemini":
61
+ return new GeminiProvider(config.geminiApiKey || config.GEMINI_API_KEY || "");
62
+ case "openai":
63
+ return new OpenAIProvider(config.openaiApiKey || config.OPENAI_API_KEY || "");
64
+ default:
65
+ throw new Error(`Unknown embedding provider: ${provider}. Use "gemini" or "openai".`);
66
+ }
67
+ }
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * OpenBrain MCP Server
4
+ * Gives any MCP-compatible AI tool access to your shared memory brain.
5
+ *
6
+ * Tools: store_memory, search_memories, list_memories, delete_memory, brain_stats
7
+ */
8
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,348 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * OpenBrain MCP Server
4
+ * Gives any MCP-compatible AI tool access to your shared memory brain.
5
+ *
6
+ * Tools: store_memory, search_memories, list_memories, delete_memory, brain_stats
7
+ */
8
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
9
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
11
+ import { readFileSync } from "fs";
12
+ import { resolve, dirname } from "path";
13
+ import { homedir } from "os";
14
+ import { fileURLToPath } from "url";
15
+ import { SupabaseClient } from "./supabase.js";
16
+ import { createProvider } from "./embeddings.js";
17
+ function loadConfig() {
18
+ const configPath = resolve(homedir(), ".openbrain", "config.json");
19
+ try {
20
+ const raw = readFileSync(configPath, "utf-8");
21
+ return JSON.parse(raw);
22
+ }
23
+ catch {
24
+ // Fall back to environment variables
25
+ return {
26
+ supabaseUrl: process.env.OPENBRAIN_SUPABASE_URL || "",
27
+ supabaseKey: process.env.OPENBRAIN_SUPABASE_KEY || "",
28
+ embeddingProvider: process.env.OPENBRAIN_EMBEDDING_PROVIDER || "gemini",
29
+ geminiApiKey: process.env.GEMINI_API_KEY || "",
30
+ openaiApiKey: process.env.OPENAI_API_KEY || "",
31
+ };
32
+ }
33
+ }
34
+ const config = loadConfig();
35
+ if (!config.supabaseUrl || !config.supabaseKey) {
36
+ console.error("OpenBrain: Missing config. Run 'npx openbrain-ai setup' or set environment variables.");
37
+ console.error(" Config file: ~/.openbrain/config.json");
38
+ console.error(" Or env vars: OPENBRAIN_SUPABASE_URL, OPENBRAIN_SUPABASE_KEY");
39
+ process.exit(1);
40
+ }
41
+ const db = new SupabaseClient(config.supabaseUrl, config.supabaseKey);
42
+ const embedder = createProvider(config.embeddingProvider || "gemini", config);
43
+ // --- Valid categories ---
44
+ const VALID_CATEGORIES = [
45
+ "company",
46
+ "contact",
47
+ "interaction",
48
+ "decision",
49
+ "insight",
50
+ "task",
51
+ "preference",
52
+ "project",
53
+ ];
54
+ // --- Tool Implementations ---
55
+ async function storeMemory(args) {
56
+ const embedding = await embedder.embed(args.content);
57
+ const record = {
58
+ content: args.content,
59
+ source: args.source || "mcp",
60
+ category: args.category || "insight",
61
+ tags: args.tags || [],
62
+ embedding,
63
+ summary: args.summary || null,
64
+ metadata: {},
65
+ };
66
+ return db.request("POST", "memories", record);
67
+ }
68
+ async function searchMemories(args) {
69
+ const embedding = await embedder.embed(args.query);
70
+ const params = {
71
+ query_embedding: embedding,
72
+ match_threshold: args.threshold ?? 0.5,
73
+ match_count: args.limit ?? 5,
74
+ };
75
+ if (args.source)
76
+ params.filter_source = args.source;
77
+ if (args.category)
78
+ params.filter_category = args.category;
79
+ return db.rpc("search_memories", params);
80
+ }
81
+ async function listMemories(args) {
82
+ const limit = args.limit ?? 20;
83
+ let path = `memories?order=created_at.desc&limit=${limit}&select=id,content,summary,source,category,tags,created_at`;
84
+ if (args.source)
85
+ path += `&source=eq.${args.source}`;
86
+ return db.request("GET", path);
87
+ }
88
+ async function deleteMemory(args) {
89
+ await db.request("DELETE", `memories?id=eq.${args.id}`);
90
+ }
91
+ async function updateMemory(args) {
92
+ const record = {};
93
+ if (args.content) {
94
+ record.content = args.content;
95
+ record.embedding = await embedder.embed(args.content);
96
+ }
97
+ if (args.category)
98
+ record.category = args.category;
99
+ if (args.tags)
100
+ record.tags = args.tags;
101
+ if (args.summary)
102
+ record.summary = args.summary;
103
+ return db.request("PATCH", `memories?id=eq.${args.id}`, record);
104
+ }
105
+ async function getStats() {
106
+ return db.rpc("brain_stats", {});
107
+ }
108
+ // --- MCP Server ---
109
+ const __dirname = dirname(fileURLToPath(import.meta.url));
110
+ const packageJson = JSON.parse(readFileSync(resolve(__dirname, "..", "package.json"), "utf-8"));
111
+ const server = new Server({ name: "openbrain", version: packageJson.version }, { capabilities: { tools: {} } });
112
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
113
+ tools: [
114
+ {
115
+ name: "store_memory",
116
+ description: "Store a memory in the shared brain. Use this to save important information, decisions, preferences, or insights that should be accessible across all AI tools.",
117
+ inputSchema: {
118
+ type: "object",
119
+ properties: {
120
+ content: {
121
+ type: "string",
122
+ description: "The memory content to store",
123
+ },
124
+ source: {
125
+ type: "string",
126
+ description: "Which AI tool is storing this (e.g. claude, chatgpt, cursor)",
127
+ default: "mcp",
128
+ },
129
+ category: {
130
+ type: "string",
131
+ enum: [...VALID_CATEGORIES],
132
+ description: "Category of memory",
133
+ default: "insight",
134
+ },
135
+ tags: {
136
+ type: "array",
137
+ items: { type: "string" },
138
+ description: "Tags for filtering",
139
+ default: [],
140
+ },
141
+ summary: {
142
+ type: "string",
143
+ description: "Optional short summary",
144
+ },
145
+ },
146
+ required: ["content"],
147
+ },
148
+ },
149
+ {
150
+ name: "search_memories",
151
+ description: "Search the shared brain for memories by meaning. Returns semantically similar results even if exact words don't match.",
152
+ inputSchema: {
153
+ type: "object",
154
+ properties: {
155
+ query: {
156
+ type: "string",
157
+ description: "What to search for (natural language)",
158
+ },
159
+ limit: {
160
+ type: "number",
161
+ description: "Max results",
162
+ default: 5,
163
+ },
164
+ source: {
165
+ type: "string",
166
+ description: "Filter by source",
167
+ },
168
+ category: {
169
+ type: "string",
170
+ description: "Filter by category",
171
+ },
172
+ threshold: {
173
+ type: "number",
174
+ description: "Similarity threshold 0-1",
175
+ default: 0.5,
176
+ },
177
+ },
178
+ required: ["query"],
179
+ },
180
+ },
181
+ {
182
+ name: "list_memories",
183
+ description: "List recent memories from the shared brain.",
184
+ inputSchema: {
185
+ type: "object",
186
+ properties: {
187
+ limit: {
188
+ type: "number",
189
+ description: "Max results",
190
+ default: 20,
191
+ },
192
+ source: {
193
+ type: "string",
194
+ description: "Filter by source",
195
+ },
196
+ },
197
+ },
198
+ },
199
+ {
200
+ name: "delete_memory",
201
+ description: "Delete a memory by its ID.",
202
+ inputSchema: {
203
+ type: "object",
204
+ properties: {
205
+ id: {
206
+ type: "string",
207
+ description: "UUID of the memory to delete",
208
+ },
209
+ },
210
+ required: ["id"],
211
+ },
212
+ },
213
+ {
214
+ name: "brain_stats",
215
+ description: "Show statistics about the shared brain: total memories, breakdown by source and category.",
216
+ inputSchema: {
217
+ type: "object",
218
+ properties: {},
219
+ },
220
+ },
221
+ {
222
+ name: "update_memory",
223
+ description: "Update an existing memory by ID. Re-generates embedding if content is changed.",
224
+ inputSchema: {
225
+ type: "object",
226
+ properties: {
227
+ id: {
228
+ type: "string",
229
+ description: "UUID of the memory to update",
230
+ },
231
+ content: {
232
+ type: "string",
233
+ description: "New content (triggers embedding regeneration)",
234
+ },
235
+ category: {
236
+ type: "string",
237
+ enum: [...VALID_CATEGORIES],
238
+ description: "New category",
239
+ },
240
+ tags: {
241
+ type: "array",
242
+ items: { type: "string" },
243
+ description: "New tags",
244
+ },
245
+ summary: {
246
+ type: "string",
247
+ description: "New summary",
248
+ },
249
+ },
250
+ required: ["id"],
251
+ },
252
+ },
253
+ ],
254
+ }));
255
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
256
+ const { name, arguments: args } = request.params;
257
+ try {
258
+ switch (name) {
259
+ case "store_memory": {
260
+ const result = await storeMemory(args);
261
+ return {
262
+ content: [
263
+ {
264
+ type: "text",
265
+ text: `Memory stored. ID: ${result[0]?.id || "unknown"}`,
266
+ },
267
+ ],
268
+ };
269
+ }
270
+ case "search_memories": {
271
+ const results = await searchMemories(args);
272
+ if (!results.length) {
273
+ return {
274
+ content: [{ type: "text", text: "No matching memories found." }],
275
+ };
276
+ }
277
+ const output = results
278
+ .map((r) => {
279
+ const sim = ((r.similarity || 0) * 100).toFixed(1);
280
+ const date = r.created_at?.slice(0, 10) || "?";
281
+ const tags = r.tags?.join(", ") || "";
282
+ return `[${sim}%] [${date}] [${r.source}/${r.category}] ${tags}\n${r.content}`;
283
+ })
284
+ .join("\n\n---\n\n");
285
+ return { content: [{ type: "text", text: output }] };
286
+ }
287
+ case "list_memories": {
288
+ const results = await listMemories(args);
289
+ const output = results
290
+ .map((r) => {
291
+ const date = r.created_at?.slice(0, 10) || "?";
292
+ const summary = r.summary || r.content?.slice(0, 100);
293
+ return `[${date}] [${r.source}/${r.category}] ${summary}`;
294
+ })
295
+ .join("\n");
296
+ return {
297
+ content: [{ type: "text", text: output || "No memories found." }],
298
+ };
299
+ }
300
+ case "delete_memory": {
301
+ await deleteMemory(args);
302
+ return {
303
+ content: [{ type: "text", text: `Memory deleted: ${args.id}` }],
304
+ };
305
+ }
306
+ case "update_memory": {
307
+ const updateArgs = args;
308
+ if (!updateArgs.content && !updateArgs.category && !updateArgs.tags && !updateArgs.summary) {
309
+ return {
310
+ content: [{ type: "text", text: "Nothing to update. Provide at least one of: content, category, tags, summary." }],
311
+ isError: true,
312
+ };
313
+ }
314
+ await updateMemory(updateArgs);
315
+ return {
316
+ content: [{ type: "text", text: `Memory updated: ${updateArgs.id}` }],
317
+ };
318
+ }
319
+ case "brain_stats": {
320
+ const stats = await getStats();
321
+ let text = `Total memories: ${stats.total}\n\nBy source:\n`;
322
+ for (const [s, c] of Object.entries(stats.by_source).sort((a, b) => b[1] - a[1])) {
323
+ text += ` ${s}: ${c}\n`;
324
+ }
325
+ text += `\nBy category:\n`;
326
+ for (const [s, c] of Object.entries(stats.by_category).sort((a, b) => b[1] - a[1])) {
327
+ text += ` ${s}: ${c}\n`;
328
+ }
329
+ return { content: [{ type: "text", text }] };
330
+ }
331
+ default:
332
+ return {
333
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
334
+ isError: true,
335
+ };
336
+ }
337
+ }
338
+ catch (error) {
339
+ const message = error instanceof Error ? error.message : String(error);
340
+ return {
341
+ content: [{ type: "text", text: `Error: ${message}` }],
342
+ isError: true,
343
+ };
344
+ }
345
+ });
346
+ // Start server
347
+ const transport = new StdioServerTransport();
348
+ await server.connect(transport);
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Supabase client wrapper for OpenBrain.
3
+ * Handles REST API calls and RPC function invocations.
4
+ */
5
+ export declare class SupabaseClient {
6
+ private url;
7
+ private key;
8
+ constructor(url: string, key: string);
9
+ private get headers();
10
+ request<T = unknown>(method: string, path: string, body?: unknown): Promise<T>;
11
+ rpc<T = unknown>(fn: string, params: Record<string, unknown>): Promise<T>;
12
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Supabase client wrapper for OpenBrain.
3
+ * Handles REST API calls and RPC function invocations.
4
+ */
5
+ export class SupabaseClient {
6
+ url;
7
+ key;
8
+ constructor(url, key) {
9
+ this.url = url;
10
+ this.key = key;
11
+ if (!url)
12
+ throw new Error("Supabase URL required");
13
+ if (!key)
14
+ throw new Error("Supabase key required");
15
+ }
16
+ get headers() {
17
+ return {
18
+ apikey: this.key,
19
+ Authorization: `Bearer ${this.key}`,
20
+ "Content-Type": "application/json",
21
+ Prefer: "return=representation",
22
+ };
23
+ }
24
+ async request(method, path, body) {
25
+ const url = `${this.url}/rest/v1/${path}`;
26
+ const opts = { method, headers: this.headers };
27
+ if (body)
28
+ opts.body = JSON.stringify(body);
29
+ const resp = await fetch(url, opts);
30
+ if (!resp.ok) {
31
+ const error = await resp.text();
32
+ throw new Error(`Supabase ${method} ${path} failed (${resp.status}): ${error}`);
33
+ }
34
+ return resp.json();
35
+ }
36
+ async rpc(fn, params) {
37
+ const url = `${this.url}/rest/v1/rpc/${fn}`;
38
+ const resp = await fetch(url, {
39
+ method: "POST",
40
+ headers: this.headers,
41
+ body: JSON.stringify(params),
42
+ });
43
+ if (!resp.ok) {
44
+ const error = await resp.text();
45
+ throw new Error(`Supabase RPC ${fn} failed (${resp.status}): ${error}`);
46
+ }
47
+ return resp.json();
48
+ }
49
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "openbrain-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for OpenBrain - cross-AI memory system",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "openbrain-mcp": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "start": "node dist/index.js"
13
+ },
14
+ "dependencies": {
15
+ "@modelcontextprotocol/sdk": "^1.0.0"
16
+ },
17
+ "devDependencies": {
18
+ "typescript": "^5.4.0",
19
+ "@types/node": "^20.0.0"
20
+ },
21
+ "files": [
22
+ "dist/"
23
+ ],
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/matpcoelho/openbrain"
30
+ },
31
+ "license": "MIT",
32
+ "keywords": [
33
+ "mcp",
34
+ "ai",
35
+ "memory",
36
+ "embeddings",
37
+ "supabase",
38
+ "claude",
39
+ "chatgpt"
40
+ ]
41
+ }