engram-notion-mcp 0.1.0 → 1.0.0-rc.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/package.json CHANGED
@@ -1,19 +1,47 @@
1
1
  {
2
2
  "name": "engram-notion-mcp",
3
- "version": "0.1.0",
4
- "description": "Node.js implementation of the Engram Notion MCP server",
5
- "main": "src/index.js",
3
+ "version": "1.0.0-rc.0",
4
+ "description": "Bun implementation of the Engram Notion MCP server",
5
+ "main": "src/index.ts",
6
6
  "type": "module",
7
+ "keywords": [
8
+ "mcp",
9
+ "notion",
10
+ "ai",
11
+ "memory",
12
+ "agent",
13
+ "engram"
14
+ ],
15
+ "author": "Shubham Omar",
16
+ "license": "MIT",
17
+ "config": {
18
+ "release": "patch",
19
+ "preid": "rc",
20
+ "tagVersionPrefix": "REL-"
21
+ },
7
22
  "scripts": {
8
- "start": "node src/index.js"
23
+ "start": "bun run src/index.ts",
24
+ "get-version": "echo $npm_package_version",
25
+ "get-name": "echo ${npm_package_name}",
26
+ "prebuild": "rm -rf ./dist",
27
+ "build": "bun build ./src/index.ts --outfile ./dist/index.js --target node --external better-sqlite3",
28
+ "preremove-dependencies": "rm -f ./bun.lock ./package-lock.json",
29
+ "remove-dependencies": "rm -rf ./node_modules",
30
+ "postremove-dependencies": "bun install",
31
+ "release": "bun run scripts/release.ts"
9
32
  },
10
33
  "dependencies": {
11
34
  "@modelcontextprotocol/sdk": "^1.0.1",
12
35
  "@notionhq/client": "^2.2.14",
13
- "sqlite3": "^5.1.7",
36
+ "better-sqlite3": "^11.5.0",
14
37
  "dotenv": "^16.4.5"
15
38
  },
39
+ "devDependencies": {
40
+ "@types/better-sqlite3": "^7.6.11",
41
+ "bun-types": "^1.1.0",
42
+ "typescript": "^5.0.0"
43
+ },
16
44
  "bin": {
17
- "engram-mcp": "src/index.js"
45
+ "engram-notion-mcp": "dist/index.js"
18
46
  }
19
- }
47
+ }
@@ -0,0 +1,76 @@
1
+ import { $ } from "bun";
2
+ import { join } from "path";
3
+
4
+ // 1. Load configuration: Env > package.json > Defaults
5
+ const pkgPath = join(import.meta.dir, "../package.json");
6
+ const pyPath = join(import.meta.dir, "../../python/pyproject.toml");
7
+
8
+ const pkg = await Bun.file(pkgPath).json();
9
+
10
+ const npmrcPath = join(import.meta.dir, "../.npmrc");
11
+ const npmrc = await (async () => {
12
+ try {
13
+ const content = await Bun.file(npmrcPath).text();
14
+ const config: Record<string, string> = {};
15
+ content.split('\n').filter(Boolean).forEach(line => {
16
+ const [key, value] = line.split('=');
17
+ if(key && value) config[key.trim()] = value.trim();
18
+ });
19
+ return config;
20
+ } catch {
21
+ return {};
22
+ }
23
+ })();
24
+
25
+ // Helper to get config value with fallback: Env -> .npmrc -> package.json -> Default
26
+ const getConfig = (key: string, envKey: string, defaultVal: string) => {
27
+ return process.env[envKey] || npmrc[key] || pkg.config?.[key] || defaultVal;
28
+ };
29
+
30
+ const release = getConfig("release", "npm_config_release", "prerelease");
31
+ const preid = getConfig("preid", "npm_config_preid", "");
32
+ const tagPrefix = getConfig("tagVersionPrefix", "npm_config_tag_version_prefix", "v");
33
+
34
+ console.log(`🚀 Starting ${release} release (ID: ${preid || 'none'}) with prefix "${tagPrefix}"`);
35
+
36
+ try {
37
+ // 2. Bump version in package.json (no git tag yet)
38
+ // We use --no-git-tag-version so we can sync python first, then commit all together
39
+ const preidFlag = preid ? `--preid=${preid}` : "";
40
+ await $`bun pm version ${release} ${preidFlag} --no-git-tag-version`;
41
+
42
+ // 3. Read the NEW version from updated package.json
43
+ const newPkg = await Bun.file(pkgPath).json();
44
+ const newVersion = newPkg.version;
45
+ console.log(`📦 New Version: ${newVersion}`);
46
+
47
+ // 4. Sync to Python (pyproject.toml)
48
+ console.log(`Syncing version to ${pyPath}...`);
49
+ let pyConfig = await Bun.file(pyPath).text();
50
+ const versionRegex = /^version\s*=\s*".*"/m;
51
+
52
+ if(versionRegex.test(pyConfig)) {
53
+ pyConfig = pyConfig.replace(versionRegex, `version = "${newVersion}"`);
54
+ await Bun.write(pyPath, pyConfig);
55
+ console.log("✅ Updated python/pyproject.toml");
56
+ } else {
57
+ throw new Error("Could not find version string in pyproject.toml");
58
+ }
59
+
60
+ // 5. Commit and Tag
61
+ const tagName = `${tagPrefix}${newVersion}`;
62
+ const message = `[skip ci] [npm-auto-versioning] new released version v${newVersion}`;
63
+
64
+ console.log("📝 Committing and Tagging...");
65
+ await $`git add ${pkgPath} ${pyPath}`;
66
+ await $`git commit -m ${message}`;
67
+ await $`git tag -a ${tagName} -m ${message}`;
68
+
69
+ // 6. Push (Optional, or leave to workflow)
70
+ // The workflow handles pushing, but for local runs:
71
+ console.log(`✅ Release ${tagName} ready! Don't forget to push: git push --follow-tags`);
72
+
73
+ } catch(err: any) {
74
+ console.error("❌ Release failed:", err.message);
75
+ process.exit(1);
76
+ }
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env bun
2
2
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import {
@@ -6,26 +6,24 @@ import {
6
6
  ListToolsRequestSchema,
7
7
  } from "@modelcontextprotocol/sdk/types.js";
8
8
  import { Client } from "@notionhq/client";
9
- import sqlite3 from "sqlite3";
9
+
10
10
  import dotenv from "dotenv";
11
11
  import path from "path";
12
- import { fileURLToPath } from "url";
13
12
  import fs from "fs";
14
13
  import os from "os";
15
- import https from "https";
16
14
 
17
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
- // Load .env from parent directory (monorepo structure implication or consistent with original script location expectation)
19
- dotenv.config({ path: path.join(__dirname, "../.env") });
15
+ // Load .env from parent directory
16
+ dotenv.config({ path: path.join(import.meta.dir, "../.env") });
20
17
 
21
18
  // Initialize Notion Client
22
- const notion = new Client({ auth: process.env.NOTION_API_KEY });
19
+ const notionApiKey = process.env.NOTION_API_KEY;
20
+ const notion = new Client({ auth: notionApiKey });
23
21
 
24
22
  // Database Initialization
25
- const get_default_db_path = () => {
23
+ const get_default_db_path = (): string => {
26
24
  const system = os.platform();
27
25
  const home = os.homedir();
28
- let basePath;
26
+ let basePath: string;
29
27
 
30
28
  if(system === "win32") {
31
29
  basePath = path.join(home, ".engram", "data");
@@ -38,7 +36,14 @@ const get_default_db_path = () => {
38
36
  return path.join(basePath, "agent_memory.db");
39
37
  };
40
38
 
41
- let DB_PATH = process.env.AGENT_MEMORY_PATH || get_default_db_path();
39
+ // Handle optional env var and path expansion
40
+ // Node/Bun usually handles ~ only if shell expands it, but here we can support explicit ~
41
+ let envDbPath = process.env.AGENT_MEMORY_PATH;
42
+ if(envDbPath && envDbPath.startsWith("~")) {
43
+ envDbPath = path.join(os.homedir(), envDbPath.slice(1));
44
+ }
45
+
46
+ let DB_PATH = envDbPath || get_default_db_path();
42
47
 
43
48
  // Ensure directory exists
44
49
  try {
@@ -48,49 +53,86 @@ try {
48
53
  DB_PATH = "agent_memory.db";
49
54
  }
50
55
 
51
- const db = new sqlite3.Database(DB_PATH);
56
+ // Database Interface
57
+ interface DBAdapter {
58
+ query(sql: string): any; // Abstracted query preparation relative to implementation
59
+ run(params: any): void; // Execution abstraction
60
+ }
52
61
 
53
- const init_db = () => {
54
- db.serialize(() => {
55
- db.run(`CREATE VIRTUAL TABLE IF NOT EXISTS memory_index USING fts5(content, metadata, tokenize='porter')`, (err) => {
56
- if(err) {
57
- // Fallback
58
- db.run(`CREATE TABLE IF NOT EXISTS memory_index (content TEXT, metadata TEXT)`);
59
- }
60
- });
61
- });
62
- };
62
+ const get_db_adapter = (dbPath: string): DBAdapter => {
63
+ const isBun = typeof Bun !== "undefined";
63
64
 
64
- init_db();
65
+ if(isBun) {
66
+ // runtime: Bun
67
+ // @ts-ignore
68
+ const { Database } = require("bun:sqlite");
69
+ const db = new Database(dbPath, { create: true });
65
70
 
66
- const _save_to_db = (content, metadata = null) => {
67
- return new Promise((resolve, reject) => {
71
+ // Init FTS5 for Bun
68
72
  try {
69
- // Create a fresh connection or use the existing one? Python code creates a new connection every time.
70
- // SQLite in Node usually handles concurrency better with a single connection in WAL mode, but standard is single connection object.
71
- // We will use the global `db` object here, assuming single-threaded event loop access.
72
- const meta_str = metadata ? JSON.stringify(metadata) : "{}";
73
- const stmt = db.prepare("INSERT INTO memory_index (content, metadata) VALUES (?, ?)");
74
- stmt.run(content, meta_str, function(err) {
75
- if(err) {
76
- console.error(`Error saving to DB: ${err}`);
77
- reject(err);
78
- } else {
79
- resolve();
80
- }
81
- });
82
- stmt.finalize();
73
+ db.run(`CREATE VIRTUAL TABLE IF NOT EXISTS memory_index USING fts5(content, metadata, tokenize='porter')`);
83
74
  } catch(e) {
84
- console.error(`Error saving to DB: ${e}`);
85
- reject(e);
75
+ console.warn("FTS5 creation failed in Bun, falling back to standard table", e);
76
+ db.run(`CREATE TABLE IF NOT EXISTS memory_index (content TEXT, metadata TEXT)`);
86
77
  }
87
- });
78
+
79
+ return {
80
+ query: (sql: string) => {
81
+ const stmt = db.query(sql);
82
+ return {
83
+ run: (params: any) => stmt.run(params)
84
+ };
85
+ },
86
+ run: () => {} // Not needed for Bun's prepared stmt flow
87
+ };
88
+ } else {
89
+ // runtime: Node.js (via better-sqlite3)
90
+ console.log("\x1b[33m%s\x1b[0m", "ℹ️ Tip: This MCP server runs 3x faster with Bun! Try: bunx engram-notion-mcp");
91
+
92
+ // @ts-ignore
93
+ const Database = require("better-sqlite3");
94
+ const db = new Database(dbPath);
95
+
96
+ // Init FTS5 for Node (better-sqlite3 usually bundles it)
97
+ try {
98
+ db.prepare(`CREATE VIRTUAL TABLE IF NOT EXISTS memory_index USING fts5(content, metadata, tokenize='porter')`).run();
99
+ } catch(e) {
100
+ console.warn("FTS5 creation failed in Node, falling back to standard table", e);
101
+ db.prepare(`CREATE TABLE IF NOT EXISTS memory_index (content TEXT, metadata TEXT)`).run();
102
+ }
103
+
104
+ return {
105
+ query: (sql: string) => {
106
+ const stmt = db.prepare(sql);
107
+ return {
108
+ run: (params: any) => stmt.run(params)
109
+ }
110
+ },
111
+ run: () => {}
112
+ };
113
+ }
114
+ };
115
+
116
+ const dbAdapter = get_db_adapter(DB_PATH);
117
+
118
+ const _save_to_db = (content: string, metadata: any = null) => {
119
+ try {
120
+ const meta_str = metadata ? JSON.stringify(metadata) : "{}";
121
+ const query = dbAdapter.query("INSERT INTO memory_index (content, metadata) VALUES ($content, $metadata)");
122
+ query.run({ $content: content, $metadata: meta_str });
123
+ } catch(e) {
124
+ console.error(`Error saving to DB: ${e}`);
125
+ }
88
126
  };
89
127
 
128
+ interface ToolArgs {
129
+ [key: string]: any;
130
+ }
131
+
90
132
  // Tools implementation
91
- const tools = {
133
+ const tools: Record<string, (args: ToolArgs) => Promise<string | string[]>> = {
92
134
  remember_fact: async ({ fact }) => {
93
- await _save_to_db(fact, { type: "manual_fact", timestamp: new Date().toISOString() });
135
+ _save_to_db(fact, { type: "manual_fact", timestamp: new Date().toISOString() });
94
136
  return `Remembered: ${fact}`;
95
137
  },
96
138
 
@@ -101,18 +143,18 @@ const tools = {
101
143
  }
102
144
 
103
145
  try {
104
- const children = [];
146
+ const children: any[] = [];
105
147
  if(content) {
106
148
  children.push({
107
149
  object: "block",
108
150
  type: "paragraph",
109
151
  paragraph: {
110
- rich_text: [ { type: "text", text: { content: content } } ]
152
+ rich_text: [{ type: "text", text: { content: content } }]
111
153
  }
112
154
  });
113
155
  }
114
156
 
115
- const response = await notion.pages.create({
157
+ const response: any = await notion.pages.create({
116
158
  parent: { page_id: target_parent },
117
159
  properties: {
118
160
  title: [
@@ -128,7 +170,6 @@ const tools = {
128
170
 
129
171
  const page_url = response.url || "URL not found";
130
172
 
131
- // Spy Logging
132
173
  const log_content = `Created Page: ${title}. Content snippet: ${content.substring(0, 100)}`;
133
174
  const meta = {
134
175
  type: "create_page",
@@ -136,10 +177,10 @@ const tools = {
136
177
  url: page_url,
137
178
  timestamp: new Date().toISOString()
138
179
  };
139
- await _save_to_db(log_content, meta);
180
+ _save_to_db(log_content, meta);
140
181
 
141
182
  return `Successfully created page '${title}'. URL: ${page_url}`;
142
- } catch(e) {
183
+ } catch(e: any) {
143
184
  return `Error creating page: ${e.message}`;
144
185
  }
145
186
  },
@@ -153,19 +194,19 @@ const tools = {
153
194
  section_title: title,
154
195
  timestamp: new Date().toISOString()
155
196
  };
156
- await _save_to_db(log_content, meta);
197
+ _save_to_db(log_content, meta);
157
198
 
158
- const validTypes = [ "paragraph", "bulleted_list_item", "code", "table" ];
199
+ const validTypes = ["paragraph", "bulleted_list_item", "code", "table"];
159
200
  if(!validTypes.includes(type)) {
160
201
  return `Error: Invalid type '${type}'. Must be 'paragraph', 'bulleted_list_item', 'code', or 'table'.`;
161
202
  }
162
203
 
163
- const children = [
204
+ const children: any[] = [
164
205
  {
165
206
  object: "block",
166
207
  type: "heading_2",
167
208
  heading_2: {
168
- rich_text: [ { type: "text", text: { content: title } } ]
209
+ rich_text: [{ type: "text", text: { content: title } }]
169
210
  }
170
211
  }
171
212
  ];
@@ -176,23 +217,23 @@ const tools = {
176
217
  object: "block",
177
218
  type: "code",
178
219
  code: {
179
- rich_text: [ { type: "text", text: { content: cleaned_content } } ],
220
+ rich_text: [{ type: "text", text: { content: cleaned_content } }],
180
221
  language: language
181
222
  }
182
223
  });
183
224
  } else if(type === "table") {
184
- const rows = [];
225
+ const rows: string[][] = [];
185
226
  const lines = content.trim().split('\n');
186
227
  let has_header = false;
187
228
 
188
229
  for(let i = 0; i < lines.length; i++) {
189
- const line = lines[ i ];
230
+ const line = lines[i];
190
231
  if(/^\s*\|?[\s\-:|]+\|?\s*$/.test(line)) {
191
232
  if(i === 1) has_header = true;
192
233
  continue;
193
234
  }
194
235
 
195
- let cells = line.split('|').map(c => c.trim());
236
+ let cells = line.split('|').map((c: string) => c.trim());
196
237
  if(line.trim().startsWith('|') && cells.length > 0) cells.shift();
197
238
  if(line.trim().endsWith('|') && cells.length > 0) cells.pop();
198
239
 
@@ -203,14 +244,14 @@ const tools = {
203
244
 
204
245
  if(rows.length === 0) return "Error: Could not parse table content.";
205
246
 
206
- const table_width = rows[ 0 ].length;
247
+ const table_width = rows[0].length;
207
248
  const table_children = rows.map(row => {
208
249
  while(row.length < table_width) row.push("");
209
250
  return {
210
251
  object: "block",
211
252
  type: "table_row",
212
253
  table_row: {
213
- cells: row.map(cell => [ { type: "text", text: { content: cell } } ])
254
+ cells: row.map(cell => [{ type: "text", text: { content: cell } }])
214
255
  }
215
256
  };
216
257
  });
@@ -230,8 +271,8 @@ const tools = {
230
271
  children.push({
231
272
  object: "block",
232
273
  type: type,
233
- [ type ]: {
234
- rich_text: [ { type: "text", text: { content: content } } ]
274
+ [type]: {
275
+ rich_text: [{ type: "text", text: { content: content } }]
235
276
  }
236
277
  });
237
278
  }
@@ -239,7 +280,7 @@ const tools = {
239
280
  try {
240
281
  await notion.blocks.children.append({ block_id: page_id, children: children });
241
282
  return `Successfully updated page ${page_id}: ${title}`;
242
- } catch(e) {
283
+ } catch(e: any) {
243
284
  return `Error updating page: ${e.message}`;
244
285
  }
245
286
  },
@@ -249,7 +290,6 @@ const tools = {
249
290
  if(!target_page) {
250
291
  return "Error: No page_id provided and NOTION_PAGE_ID not set.";
251
292
  }
252
- // Re-use update_page logic
253
293
  return tools.update_page({ page_id: target_page, title, content, type, language });
254
294
  },
255
295
 
@@ -262,8 +302,8 @@ const tools = {
262
302
 
263
303
  try {
264
304
  const response = await notion.blocks.children.list({ block_id: target_id });
265
- const pages = [];
266
- for(const block of response.results) {
305
+ const pages: string[] = [];
306
+ for(const block of response.results as any[]) {
267
307
  if(block.type === "child_page") {
268
308
  pages.push(`- ${block.child_page.title} (ID: ${block.id})`);
269
309
  }
@@ -271,7 +311,7 @@ const tools = {
271
311
 
272
312
  if(pages.length === 0) return "No sub-pages found.";
273
313
  return pages.join("\n");
274
- } catch(e) {
314
+ } catch(e: any) {
275
315
  return `Error listing sub-pages: ${e.message}`;
276
316
  }
277
317
  },
@@ -285,8 +325,6 @@ const tools = {
285
325
  }
286
326
 
287
327
  try {
288
- // Using built-in https request for zero dependency http, or we could use fetch if node 18+
289
- // Using fetch is cleaner.
290
328
  const response = await fetch(`https://api.telegram.org/bot${bot_token}/sendMessage`, {
291
329
  method: 'POST',
292
330
  headers: {
@@ -299,7 +337,7 @@ const tools = {
299
337
  throw new Error(`HTTP error! status: ${response.status}`);
300
338
  }
301
339
  return "Alert sent successfully.";
302
- } catch(e) {
340
+ } catch(e: any) {
303
341
  return `Failed to send alert: ${e.message}`;
304
342
  }
305
343
  }
@@ -307,8 +345,8 @@ const tools = {
307
345
 
308
346
  const server = new Server(
309
347
  {
310
- name: "engram-mcp",
311
- version: "0.1.0",
348
+ name: "engram-notion-mcp",
349
+ version: "0.2.0-rc.1",
312
350
  },
313
351
  {
314
352
  capabilities: {
@@ -328,7 +366,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
328
366
  properties: {
329
367
  fact: { type: "string" },
330
368
  },
331
- required: [ "fact" ],
369
+ required: ["fact"],
332
370
  },
333
371
  },
334
372
  {
@@ -341,7 +379,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
341
379
  content: { type: "string" },
342
380
  parent_id: { type: "string" }
343
381
  },
344
- required: [ "title" ],
382
+ required: ["title"],
345
383
  },
346
384
  },
347
385
  {
@@ -353,10 +391,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
353
391
  page_id: { type: "string" },
354
392
  title: { type: "string" },
355
393
  content: { type: "string" },
356
- type: { type: "string", enum: [ "paragraph", "bulleted_list_item", "code", "table" ], default: "paragraph" },
394
+ type: { type: "string", enum: ["paragraph", "bulleted_list_item", "code", "table"], default: "paragraph" },
357
395
  language: { type: "string", default: "plain text" }
358
396
  },
359
- required: [ "page_id", "title", "content" ],
397
+ required: ["page_id", "title", "content"],
360
398
  },
361
399
  },
362
400
  {
@@ -367,11 +405,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
367
405
  properties: {
368
406
  title: { type: "string" },
369
407
  content: { type: "string" },
370
- type: { type: "string", enum: [ "paragraph", "bulleted_list_item", "code", "table" ], default: "paragraph" },
408
+ type: { type: "string", enum: ["paragraph", "bulleted_list_item", "code", "table"], default: "paragraph" },
371
409
  language: { type: "string", default: "plain text" },
372
410
  page_id: { type: "string" }
373
411
  },
374
- required: [ "title", "content" ],
412
+ required: ["title", "content"],
375
413
  },
376
414
  },
377
415
  {
@@ -392,7 +430,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
392
430
  properties: {
393
431
  message: { type: "string" }
394
432
  },
395
- required: [ "message" ],
433
+ required: ["message"],
396
434
  },
397
435
  }
398
436
  ],
@@ -402,12 +440,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
402
440
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
403
441
  const { name, arguments: args } = request.params;
404
442
 
405
- if(tools[ name ]) {
443
+ if(tools[name]) {
444
+ const result = await tools[name](args as ToolArgs);
445
+ // Handle array result (from multiple blocks?) or string
446
+ const textContent = Array.isArray(result) ? result.join("\n") : result;
406
447
  return {
407
448
  content: [
408
449
  {
409
450
  type: "text",
410
- text: await tools[ name ](args)
451
+ text: textContent
411
452
  }
412
453
  ]
413
454
  };
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "esnext",
4
+ "module": "esnext",
5
+ "moduleResolution": "bundler",
6
+ "types": [
7
+ "bun-types"
8
+ ],
9
+ "strict": true,
10
+ "skipLibCheck": true,
11
+ "esModuleInterop": true,
12
+ "allowSyntheticDefaultImports": true
13
+ }
14
+ }