tana-mcp-codemode 0.2.3 → 0.3.3

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/src/index.ts CHANGED
@@ -1,167 +1,10 @@
1
1
  #!/usr/bin/env bun
2
- /**
3
- * Tana MCP Server - Entry Point
4
- *
5
- * Codemode MCP server for Tana knowledge management.
6
- * AI writes TypeScript code that executes against the Tana Local API.
7
- */
2
+ // Backward-compat shim — use src/entry-bun.ts directly
3
+ import { initCompat } from "./compat";
4
+ import { bunCompat } from "./compat/bun";
5
+ import { main } from "./main";
8
6
 
9
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
11
- import { z } from "zod";
12
-
13
- import { createClient } from "./api/client";
14
- import { createTanaAPI } from "./api/tana";
15
- import type { TanaClient } from "./api/client";
16
- import type { Workspace } from "./api/types";
17
- import { executeSandbox } from "./sandbox/executor";
18
- import { cleanupOldRuns, initDb } from "./storage/history";
19
- import { TOOL_DESCRIPTION } from "./prompts";
20
-
21
- /**
22
- * Resolves the default workspace from MAIN_TANA_WORKSPACE env var.
23
- * Matches by ID first, then case-insensitive name.
24
- * Returns null if unset, unmatched, or API unreachable.
25
- */
26
- async function resolveWorkspace(client: TanaClient): Promise<Workspace | null> {
27
- const envValue = process.env.MAIN_TANA_WORKSPACE?.trim();
28
- if (!envValue) return null;
29
-
30
- let workspaces: Workspace[];
31
- try {
32
- workspaces = await client.get<Workspace[]>("/workspaces");
33
- } catch (err) {
34
- console.error(
35
- `[workspace] Failed to fetch workspaces: ${err instanceof Error ? err.message : err}`
36
- );
37
- return null;
38
- }
39
-
40
- const byId = workspaces.find((w) => w.id === envValue);
41
- if (byId) {
42
- console.error(`[workspace] Resolved "${envValue}" → ${byId.name} (${byId.id})`);
43
- return byId;
44
- }
45
-
46
- const lowerEnv = envValue.toLowerCase();
47
- const byName = workspaces.find((w) => w.name.toLowerCase() === lowerEnv);
48
- if (byName) {
49
- console.error(`[workspace] Resolved "${envValue}" → ${byName.name} (${byName.id})`);
50
- return byName;
51
- }
52
-
53
- const available = workspaces.map((w) => `${w.name} (${w.id})`).join(", ");
54
- console.error(
55
- `[workspace] No match for "${envValue}". Available: ${available || "none"}`
56
- );
57
- return null;
58
- }
59
-
60
- /**
61
- * Resolves TANA_SEARCH_WORKSPACES env var to an array of workspace IDs.
62
- * Each value can be an ID or case-insensitive name.
63
- * Unresolved values are logged and skipped.
64
- */
65
- async function resolveSearchWorkspaces(client: TanaClient): Promise<string[]> {
66
- const envValue = process.env.TANA_SEARCH_WORKSPACES?.trim();
67
- if (!envValue) return [];
68
-
69
- const values = envValue.split(",").map((v) => v.trim()).filter(Boolean);
70
- if (values.length === 0) return [];
71
-
72
- let workspaces: Workspace[];
73
- try {
74
- workspaces = await client.get<Workspace[]>("/workspaces");
75
- } catch (err) {
76
- console.error(
77
- `[search-workspaces] Failed to fetch workspaces: ${err instanceof Error ? err.message : err}`
78
- );
79
- return [];
80
- }
81
-
82
- const resolvedIds: string[] = [];
83
- for (const value of values) {
84
- const byId = workspaces.find((w) => w.id === value);
85
- if (byId) {
86
- resolvedIds.push(byId.id);
87
- console.error(`[search-workspaces] Resolved "${value}" → ${byId.name} (${byId.id})`);
88
- continue;
89
- }
90
- const lowerValue = value.toLowerCase();
91
- const byName = workspaces.find((w) => w.name.toLowerCase() === lowerValue);
92
- if (byName) {
93
- resolvedIds.push(byName.id);
94
- console.error(`[search-workspaces] Resolved "${value}" → ${byName.name} (${byName.id})`);
95
- continue;
96
- }
97
- const available = workspaces.map((w) => `${w.name} (${w.id})`).join(", ");
98
- console.error(
99
- `[search-workspaces] No match for "${value}". Available: ${available || "none"}`
100
- );
101
- }
102
-
103
- return resolvedIds;
104
- }
105
-
106
- async function main() {
107
- initDb();
108
- const cleaned = cleanupOldRuns(30);
109
- if (cleaned > 0) {
110
- console.error(`Cleaned up ${cleaned} old script runs`);
111
- }
112
-
113
- const client = createClient();
114
- const workspace = await resolveWorkspace(client);
115
- const searchWorkspaceIds = await resolveSearchWorkspaces(client);
116
- const tana = createTanaAPI(client, workspace, searchWorkspaceIds);
117
-
118
- const server = new McpServer({
119
- name: "tana-mcp-codemode",
120
- version: "0.1.0",
121
- });
122
-
123
- server.registerTool(
124
- "execute",
125
- {
126
- description: TOOL_DESCRIPTION,
127
- inputSchema: {
128
- code: z.string().describe("TypeScript code to execute"),
129
- sessionId: z
130
- .string()
131
- .optional()
132
- .describe("Optional session ID for grouping script runs"),
133
- },
134
- },
135
- async ({ code, sessionId }) => {
136
- const result = await executeSandbox(code, tana, sessionId);
137
-
138
- let responseText = "";
139
- if (result.output) {
140
- responseText += result.output;
141
- }
142
- if (result.error) {
143
- responseText += responseText ? "\n\n" : "";
144
- responseText += `Error: ${result.error}`;
145
- }
146
- responseText += `\n\n[Executed in ${result.durationMs}ms]`;
147
-
148
- return {
149
- content: [
150
- {
151
- type: "text" as const,
152
- text: responseText,
153
- },
154
- ],
155
- isError: !result.success,
156
- };
157
- }
158
- );
159
-
160
- const transport = new StdioServerTransport();
161
- await server.connect(transport);
162
-
163
- console.error("Tana MCP server started");
164
- }
7
+ initCompat(bunCompat);
165
8
 
166
9
  main().catch((error) => {
167
10
  console.error("Fatal error:", error);
package/src/main.ts ADDED
@@ -0,0 +1,146 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { z } from "zod";
4
+
5
+ import { createClient } from "./api/client";
6
+ import { createTanaAPI } from "./api/tana";
7
+ import type { TanaClient } from "./api/client";
8
+ import type { Workspace } from "./api/types";
9
+ import { executeSandbox } from "./sandbox/executor";
10
+ import { cleanupOldRuns, initDb } from "./storage/history";
11
+ import { TOOL_DESCRIPTION } from "./prompts";
12
+
13
+ async function resolveWorkspace(client: TanaClient): Promise<Workspace | null> {
14
+ const envValue = process.env.MAIN_TANA_WORKSPACE?.trim();
15
+ if (!envValue) return null;
16
+
17
+ let workspaces: Workspace[];
18
+ try {
19
+ workspaces = await client.get<Workspace[]>("/workspaces");
20
+ } catch (err) {
21
+ console.error(
22
+ `[workspace] Failed to fetch workspaces: ${err instanceof Error ? err.message : err}`
23
+ );
24
+ return null;
25
+ }
26
+
27
+ const byId = workspaces.find((w) => w.id === envValue);
28
+ if (byId) {
29
+ console.error(`[workspace] Resolved "${envValue}" → ${byId.name} (${byId.id})`);
30
+ return byId;
31
+ }
32
+
33
+ const lowerEnv = envValue.toLowerCase();
34
+ const byName = workspaces.find((w) => w.name.toLowerCase() === lowerEnv);
35
+ if (byName) {
36
+ console.error(`[workspace] Resolved "${envValue}" → ${byName.name} (${byName.id})`);
37
+ return byName;
38
+ }
39
+
40
+ const available = workspaces.map((w) => `${w.name} (${w.id})`).join(", ");
41
+ console.error(
42
+ `[workspace] No match for "${envValue}". Available: ${available || "none"}`
43
+ );
44
+ return null;
45
+ }
46
+
47
+ async function resolveSearchWorkspaces(client: TanaClient): Promise<string[]> {
48
+ const envValue = process.env.TANA_SEARCH_WORKSPACES?.trim();
49
+ if (!envValue) return [];
50
+
51
+ const values = envValue.split(",").map((v) => v.trim()).filter(Boolean);
52
+ if (values.length === 0) return [];
53
+
54
+ let workspaces: Workspace[];
55
+ try {
56
+ workspaces = await client.get<Workspace[]>("/workspaces");
57
+ } catch (err) {
58
+ console.error(
59
+ `[search-workspaces] Failed to fetch workspaces: ${err instanceof Error ? err.message : err}`
60
+ );
61
+ return [];
62
+ }
63
+
64
+ const resolvedIds: string[] = [];
65
+ for (const value of values) {
66
+ const byId = workspaces.find((w) => w.id === value);
67
+ if (byId) {
68
+ resolvedIds.push(byId.id);
69
+ console.error(`[search-workspaces] Resolved "${value}" → ${byId.name} (${byId.id})`);
70
+ continue;
71
+ }
72
+ const lowerValue = value.toLowerCase();
73
+ const byName = workspaces.find((w) => w.name.toLowerCase() === lowerValue);
74
+ if (byName) {
75
+ resolvedIds.push(byName.id);
76
+ console.error(`[search-workspaces] Resolved "${value}" → ${byName.name} (${byName.id})`);
77
+ continue;
78
+ }
79
+ const available = workspaces.map((w) => `${w.name} (${w.id})`).join(", ");
80
+ console.error(
81
+ `[search-workspaces] No match for "${value}". Available: ${available || "none"}`
82
+ );
83
+ }
84
+
85
+ return resolvedIds;
86
+ }
87
+
88
+ export async function main() {
89
+ initDb();
90
+ const cleaned = cleanupOldRuns(30);
91
+ if (cleaned > 0) {
92
+ console.error(`Cleaned up ${cleaned} old script runs`);
93
+ }
94
+
95
+ const client = createClient();
96
+ const workspace = await resolveWorkspace(client);
97
+ const searchWorkspaceIds = await resolveSearchWorkspaces(client);
98
+ const tana = createTanaAPI(client, workspace, searchWorkspaceIds);
99
+
100
+ const server = new McpServer({
101
+ name: "tana-mcp-codemode",
102
+ version: "0.1.0",
103
+ });
104
+
105
+ server.registerTool(
106
+ "execute",
107
+ {
108
+ description: TOOL_DESCRIPTION,
109
+ inputSchema: {
110
+ code: z.string().describe("TypeScript code to execute"),
111
+ sessionId: z
112
+ .string()
113
+ .optional()
114
+ .describe("Optional session ID for grouping script runs"),
115
+ },
116
+ },
117
+ async ({ code, sessionId }) => {
118
+ const result = await executeSandbox(code, tana, sessionId);
119
+
120
+ let responseText = "";
121
+ if (result.output) {
122
+ responseText += result.output;
123
+ }
124
+ if (result.error) {
125
+ responseText += responseText ? "\n\n" : "";
126
+ responseText += `Error: ${result.error}`;
127
+ }
128
+ responseText += `\n\n[Executed in ${result.durationMs}ms]`;
129
+
130
+ return {
131
+ content: [
132
+ {
133
+ type: "text" as const,
134
+ text: responseText,
135
+ },
136
+ ],
137
+ isError: !result.success,
138
+ };
139
+ }
140
+ );
141
+
142
+ const transport = new StdioServerTransport();
143
+ await server.connect(transport);
144
+
145
+ console.error("Tana MCP server started");
146
+ }
package/src/prompts.ts CHANGED
@@ -74,6 +74,7 @@ console.log() output becomes LLM context. Keep it compact:
74
74
 
75
75
  ## API Notes
76
76
 
77
+ - **WARNING ON BULK UPDATES**: NEVER use tana.nodes.trash() followed by tana.import() to bulk-edit existing nodes. This destroys user data. Always update existing nodes individually using tana.fields.setContent or tana.nodes.edit.
77
78
  - search: no offset/pagination — use narrower queries, not repeated calls
78
79
  - getChildren: only endpoint with pagination (limit + offset)
79
80
  - Timeout is 10s. On timeout, try a different approach, not the same call.
@@ -13,6 +13,7 @@ import type { SandboxResult } from "../types";
13
13
  import type { TanaAPI } from "../api/tana";
14
14
  import { saveScriptRun } from "../storage/history";
15
15
  import { createWorkflowHelper } from "./workflow";
16
+ import { transpileTS } from "../compat";
16
17
 
17
18
  const EXECUTION_TIMEOUT = 10_000; // 10 seconds
18
19
 
@@ -192,8 +193,7 @@ export async function executeSandbox(
192
193
  "exports",
193
194
  ];
194
195
 
195
- const transpiler = new Bun.Transpiler({ loader: "ts" });
196
- const jsCode = transpiler.transformSync(code);
196
+ const jsCode = transpileTS(code);
197
197
 
198
198
  const fn = new AsyncFunction(
199
199
  "tana",
@@ -11,7 +11,7 @@
11
11
  * workflow.complete("Done!");
12
12
  */
13
13
 
14
- import { Database } from "bun:sqlite";
14
+ import type { CompatDatabase } from "../compat";
15
15
  import { initDb } from "../storage/history";
16
16
 
17
17
  export type WorkflowEventType =
@@ -45,10 +45,10 @@ export interface WorkflowHelper {
45
45
 
46
46
  let workflowTableInitialized = false;
47
47
 
48
- function ensureWorkflowTable(db: Database): void {
48
+ function ensureWorkflowTable(db: CompatDatabase): void {
49
49
  if (workflowTableInitialized) return;
50
50
 
51
- db.run(`
51
+ db.exec(`
52
52
  CREATE TABLE IF NOT EXISTS workflow_events (
53
53
  id INTEGER PRIMARY KEY AUTOINCREMENT,
54
54
  session_id TEXT NOT NULL,
@@ -59,7 +59,7 @@ function ensureWorkflowTable(db: Database): void {
59
59
  )
60
60
  `);
61
61
 
62
- db.run(`
62
+ db.exec(`
63
63
  CREATE INDEX IF NOT EXISTS idx_workflow_session
64
64
  ON workflow_events(session_id, timestamp)
65
65
  `);
@@ -8,13 +8,13 @@
8
8
  * Configure database location via TANA_HISTORY_PATH env var.
9
9
  */
10
10
 
11
- import { Database } from "bun:sqlite";
11
+ import { createDatabase, type CompatDatabase } from "../compat";
12
12
  import { homedir } from "os";
13
13
  import { join, dirname } from "path";
14
14
  import { mkdirSync, existsSync } from "fs";
15
15
  import type { ScriptRun } from "../types";
16
16
 
17
- let db: Database | null = null;
17
+ let db: CompatDatabase | null = null;
18
18
 
19
19
  function getDbPath(): string {
20
20
  // Allow custom path via env var
@@ -46,7 +46,7 @@ function getDbPath(): string {
46
46
  return join(baseDir, "history.db");
47
47
  }
48
48
 
49
- function migrateDb(database: Database): void {
49
+ function migrateDb(database: CompatDatabase): void {
50
50
  // Get existing columns
51
51
  const columns = database
52
52
  .prepare("PRAGMA table_info(script_runs)")
@@ -63,18 +63,18 @@ function migrateDb(database: Database): void {
63
63
 
64
64
  for (const col of newColumns) {
65
65
  if (!columnNames.has(col.name)) {
66
- database.run(`ALTER TABLE script_runs ADD COLUMN ${col.name} ${col.type}`);
66
+ database.exec(`ALTER TABLE script_runs ADD COLUMN ${col.name} ${col.type}`);
67
67
  }
68
68
  }
69
69
  }
70
70
 
71
- export function initDb(): Database {
71
+ export function initDb(): CompatDatabase {
72
72
  if (db) return db;
73
73
 
74
74
  const dbPath = getDbPath();
75
- db = new Database(dbPath, { create: true });
75
+ db = createDatabase(dbPath);
76
76
 
77
- db.run(`
77
+ db.exec(`
78
78
  CREATE TABLE IF NOT EXISTS script_runs (
79
79
  id INTEGER PRIMARY KEY AUTOINCREMENT,
80
80
  timestamp INTEGER NOT NULL,
@@ -94,22 +94,22 @@ export function initDb(): Database {
94
94
  // Migrate existing databases
95
95
  migrateDb(db);
96
96
 
97
- db.run(`
97
+ db.exec(`
98
98
  CREATE INDEX IF NOT EXISTS idx_script_runs_timestamp
99
99
  ON script_runs(timestamp DESC)
100
100
  `);
101
101
 
102
- db.run(`
102
+ db.exec(`
103
103
  CREATE INDEX IF NOT EXISTS idx_script_runs_session
104
104
  ON script_runs(session_id)
105
105
  `);
106
106
 
107
- db.run(`
107
+ db.exec(`
108
108
  CREATE INDEX IF NOT EXISTS idx_script_runs_workspace
109
109
  ON script_runs(workspace_id)
110
110
  `);
111
111
 
112
- return db;
112
+ return db!;
113
113
  }
114
114
 
115
115
  export interface SaveScriptRunOptions {