tana-mcp-codemode 0.2.3 → 0.3.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,33 +1,15 @@
1
1
  {
2
2
  "name": "tana-mcp-codemode",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Codemode MCP server for Tana — AI writes TypeScript that executes against Tana Local API",
5
5
  "type": "module",
6
- "main": "src/index.ts",
6
+ "main": "dist/entry-node.js",
7
7
  "bin": {
8
- "tana-mcp-codemode": "src/index.ts"
9
- },
10
- "scripts": {
11
- "generate": "bun run scripts/generate.ts",
12
- "list-endpoints": "bun run scripts/list-endpoints.ts",
13
- "start": "bun run --env-file=.env src/index.ts",
14
- "dev": "bun run --env-file=.env --watch src/index.ts",
15
- "debug": "bun run --env-file=.env src/debug-server.ts",
16
- "test": "bun test",
17
- "typecheck": "bunx tsc --noEmit",
18
- "prepublishOnly": "bun run typecheck",
19
- "release:patch": "npm version patch && npm publish",
20
- "release:minor": "npm version minor && npm publish",
21
- "release:major": "npm version major && npm publish"
8
+ "tana-mcp-codemode": "dist/entry-node.js"
22
9
  },
23
10
  "files": [
24
- "src/index.ts",
25
- "src/prompts.ts",
26
- "src/types.ts",
27
- "src/api",
28
- "src/sandbox",
29
- "src/storage",
30
- "src/generated",
11
+ "dist/",
12
+ "src/",
31
13
  "README.md"
32
14
  ],
33
15
  "keywords": [
@@ -43,16 +25,36 @@
43
25
  "url": "git+https://github.com/fabiogaliano/tana-mcp-codemode.git"
44
26
  },
45
27
  "engines": {
46
- "bun": ">=1.0.0"
28
+ "bun": ">=1.0.0",
29
+ "node": ">=20"
47
30
  },
48
31
  "dependencies": {
49
32
  "@modelcontextprotocol/sdk": "^1.25.3",
50
33
  "zod": "^3"
51
34
  },
35
+ "optionalDependencies": {
36
+ "better-sqlite3": "^11",
37
+ "esbuild": "^0.24"
38
+ },
52
39
  "devDependencies": {
53
40
  "@types/bun": "^1.3.8",
54
41
  "openapi-typescript": "^7.10.1",
42
+ "tsup": "^8",
55
43
  "typescript": "^5.9.3"
56
44
  },
57
- "license": "MIT"
58
- }
45
+ "license": "MIT",
46
+ "scripts": {
47
+ "generate": "bun run scripts/generate.ts",
48
+ "list-endpoints": "bun run scripts/list-endpoints.ts",
49
+ "start": "bun run --env-file=.env src/entry-bun.ts",
50
+ "dev": "bun run --env-file=.env --watch src/entry-bun.ts",
51
+ "debug": "bun run --env-file=.env src/debug-server.ts",
52
+ "build": "tsup",
53
+ "build:binary": "bun build --compile src/entry-bun.ts --outfile dist/tana-mcp-codemode",
54
+ "test": "bun test",
55
+ "typecheck": "bunx tsc --noEmit",
56
+ "release:patch": "npm version patch && npm publish",
57
+ "release:minor": "npm version minor && npm publish",
58
+ "release:major": "npm version major && npm publish"
59
+ }
60
+ }
@@ -0,0 +1,39 @@
1
+ import { Database } from "bun:sqlite";
2
+ import type { CompatDatabase, CompatOps, CompatStatement } from "./types";
3
+
4
+ function wrapStatement(stmt: ReturnType<Database["prepare"]>): CompatStatement {
5
+ return {
6
+ run(...params: unknown[]) {
7
+ const result = stmt.run(...(params as Parameters<typeof stmt.run>));
8
+ return { changes: result.changes };
9
+ },
10
+ all(...params: unknown[]) {
11
+ return stmt.all(...(params as Parameters<typeof stmt.all>)) as unknown[];
12
+ },
13
+ };
14
+ }
15
+
16
+ function wrapDatabase(db: Database): CompatDatabase {
17
+ return {
18
+ exec(sql: string) {
19
+ db.run(sql);
20
+ },
21
+ prepare(sql: string) {
22
+ return wrapStatement(db.prepare(sql));
23
+ },
24
+ close() {
25
+ db.close();
26
+ },
27
+ };
28
+ }
29
+
30
+ export const bunCompat: CompatOps = {
31
+ createDatabase(path: string): CompatDatabase {
32
+ return wrapDatabase(new Database(path, { create: true }));
33
+ },
34
+
35
+ transpileTS(code: string): string {
36
+ const transpiler = new Bun.Transpiler({ loader: "ts" });
37
+ return transpiler.transformSync(code);
38
+ },
39
+ };
@@ -0,0 +1,29 @@
1
+ import type { CompatDatabase, CompatOps } from "./types";
2
+
3
+ export type { CompatDatabase, CompatStatement, CompatOps } from "./types";
4
+
5
+ let ops: CompatOps | null = null;
6
+
7
+ export function initCompat(compatOps: CompatOps): void {
8
+ if (ops) {
9
+ console.warn("initCompat called multiple times — overwriting previous compat ops");
10
+ }
11
+ ops = compatOps;
12
+ }
13
+
14
+ function getOps(): CompatOps {
15
+ if (!ops) {
16
+ throw new Error(
17
+ "compat not initialized — call initCompat() before using createDatabase or transpileTS"
18
+ );
19
+ }
20
+ return ops;
21
+ }
22
+
23
+ export function createDatabase(path: string): CompatDatabase {
24
+ return getOps().createDatabase(path);
25
+ }
26
+
27
+ export function transpileTS(code: string): string {
28
+ return getOps().transpileTS(code);
29
+ }
@@ -0,0 +1,62 @@
1
+ import { createRequire } from "node:module";
2
+ import type { CompatDatabase, CompatOps, CompatStatement } from "./types";
3
+
4
+ const require = createRequire(import.meta.url);
5
+
6
+ function getBetterSqlite3(): any {
7
+ try {
8
+ return require("better-sqlite3");
9
+ } catch {
10
+ throw new Error(
11
+ "better-sqlite3 is required for Node.js. Install it: npm install better-sqlite3"
12
+ );
13
+ }
14
+ }
15
+
16
+ function getEsbuild(): any {
17
+ try {
18
+ return require("esbuild");
19
+ } catch {
20
+ throw new Error(
21
+ "esbuild is required for Node.js. Install it: npm install esbuild"
22
+ );
23
+ }
24
+ }
25
+
26
+ function wrapStatement(stmt: any): CompatStatement {
27
+ return {
28
+ run(...params: unknown[]) {
29
+ const result = stmt.run(...params);
30
+ return { changes: result.changes };
31
+ },
32
+ all(...params: unknown[]) {
33
+ return stmt.all(...params) as unknown[];
34
+ },
35
+ };
36
+ }
37
+
38
+ function wrapDatabase(db: any): CompatDatabase {
39
+ return {
40
+ exec(sql: string) {
41
+ db.exec(sql);
42
+ },
43
+ prepare(sql: string) {
44
+ return wrapStatement(db.prepare(sql));
45
+ },
46
+ close() {
47
+ db.close();
48
+ },
49
+ };
50
+ }
51
+
52
+ export const nodeCompat: CompatOps = {
53
+ createDatabase(path: string): CompatDatabase {
54
+ const BetterSqlite3 = getBetterSqlite3();
55
+ return wrapDatabase(new BetterSqlite3(path));
56
+ },
57
+
58
+ transpileTS(code: string): string {
59
+ const esbuild = getEsbuild();
60
+ return esbuild.transformSync(code, { loader: "ts" }).code;
61
+ },
62
+ };
@@ -0,0 +1,15 @@
1
+ export interface CompatStatement {
2
+ run(...params: unknown[]): { changes: number };
3
+ all(...params: unknown[]): unknown[];
4
+ }
5
+
6
+ export interface CompatDatabase {
7
+ exec(sql: string): void;
8
+ prepare(sql: string): CompatStatement;
9
+ close(): void;
10
+ }
11
+
12
+ export interface CompatOps {
13
+ createDatabase(path: string): CompatDatabase;
14
+ transpileTS(code: string): string;
15
+ }
@@ -0,0 +1,232 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Debug Server - WebSocket UI for Testing
4
+ *
5
+ * Provides a web-based interface for testing Tana API scripts.
6
+ * Run with: bun run src/debug-server.ts
7
+ * Open: http://localhost:3333
8
+ *
9
+ * In dev mode, proxies to Vite for hot-reload and dynamic glob resolution.
10
+ */
11
+
12
+ import { initCompat } from "./compat";
13
+ import { bunCompat } from "./compat/bun";
14
+ import { createClient } from "./api/client";
15
+ import { createTanaAPI } from "./api/tana";
16
+ import { executeSandbox } from "./sandbox/executor";
17
+ import { getWorkflowEvents, getRecentWorkflows } from "./sandbox/workflow";
18
+ import { initDb } from "./storage/history";
19
+ import { spawn, type Subprocess } from "bun";
20
+
21
+ const PORT = parseInt(process.env.DEBUG_PORT || "3333", 10);
22
+ const VITE_PORT = 5188; // Use unique port to avoid conflicts
23
+ let viteProcess: Subprocess | null = null;
24
+
25
+ interface ExecuteRequest {
26
+ type: "execute";
27
+ code: string;
28
+ sessionId?: string;
29
+ input?: string;
30
+ }
31
+
32
+ interface GetWorkflowRequest {
33
+ type: "getWorkflow";
34
+ sessionId: string;
35
+ }
36
+
37
+ interface ListWorkflowsRequest {
38
+ type: "listWorkflows";
39
+ limit?: number;
40
+ }
41
+
42
+ type WebSocketMessage = ExecuteRequest | GetWorkflowRequest | ListWorkflowsRequest;
43
+
44
+ async function startVite(): Promise<void> {
45
+ console.log("Starting Vite dev server...");
46
+
47
+ viteProcess = spawn({
48
+ cmd: ["bunx", "vite", "--port", String(VITE_PORT), "--strictPort"],
49
+ cwd: "./ui",
50
+ stdout: "inherit",
51
+ stderr: "inherit",
52
+ });
53
+
54
+ // Wait for Vite to be ready
55
+ const maxAttempts = 30;
56
+ for (let i = 0; i < maxAttempts; i++) {
57
+ try {
58
+ const res = await fetch(`http://localhost:${VITE_PORT}`, { method: "HEAD" });
59
+ if (res.ok) {
60
+ console.log("Vite dev server ready");
61
+ return;
62
+ }
63
+ } catch {
64
+ // Not ready yet
65
+ }
66
+ await Bun.sleep(200);
67
+ }
68
+ console.warn("Vite may not be fully ready, continuing anyway...");
69
+ }
70
+
71
+ async function proxyToVite(req: Request): Promise<Response> {
72
+ const url = new URL(req.url);
73
+ const viteUrl = `http://localhost:${VITE_PORT}${url.pathname}${url.search}`;
74
+
75
+ try {
76
+ const proxyReq = new Request(viteUrl, {
77
+ method: req.method,
78
+ headers: req.headers,
79
+ body: req.body,
80
+ });
81
+ return await fetch(proxyReq);
82
+ } catch (e) {
83
+ return new Response(`Vite proxy error: ${e}`, { status: 502 });
84
+ }
85
+ }
86
+
87
+ async function main() {
88
+ initCompat(bunCompat);
89
+ initDb();
90
+
91
+ // Start Vite dev server for dynamic glob resolution
92
+ await startVite();
93
+
94
+ let tana: ReturnType<typeof createTanaAPI> | null = null;
95
+ let clientError: string | null = null;
96
+
97
+ try {
98
+ const client = createClient();
99
+ tana = createTanaAPI(client);
100
+ } catch (e) {
101
+ clientError = e instanceof Error ? e.message : String(e);
102
+ console.error("Warning: Could not create Tana client:", clientError);
103
+ }
104
+
105
+ const server = Bun.serve({
106
+ port: PORT,
107
+ async fetch(req, server) {
108
+ const url = new URL(req.url);
109
+
110
+ if (url.pathname === "/ws") {
111
+ const upgraded = server.upgrade(req);
112
+ if (!upgraded) {
113
+ return new Response("WebSocket upgrade failed", { status: 400 });
114
+ }
115
+ return undefined;
116
+ }
117
+
118
+ if (url.pathname === "/health") {
119
+ return Response.json({ status: "ok", tanaConnected: !!tana, clientError });
120
+ }
121
+
122
+ // Proxy everything else to Vite dev server
123
+ return proxyToVite(req);
124
+ },
125
+
126
+ websocket: {
127
+ async message(ws, message) {
128
+ try {
129
+ const data = JSON.parse(String(message)) as WebSocketMessage;
130
+
131
+ switch (data.type) {
132
+ case "execute": {
133
+ if (!tana) {
134
+ ws.send(JSON.stringify({
135
+ type: "error",
136
+ error: clientError || "Tana client not initialized",
137
+ }));
138
+ return;
139
+ }
140
+
141
+ const sessionId = data.sessionId || crypto.randomUUID();
142
+ ws.send(JSON.stringify({ type: "started", sessionId }));
143
+
144
+ const result = await executeSandbox(
145
+ data.code,
146
+ tana,
147
+ sessionId
148
+ );
149
+
150
+ const events = getWorkflowEvents(sessionId);
151
+
152
+ ws.send(JSON.stringify({
153
+ type: "result",
154
+ sessionId,
155
+ ...result,
156
+ workflowEvents: events,
157
+ }));
158
+ break;
159
+ }
160
+
161
+ case "getWorkflow": {
162
+ const events = getWorkflowEvents(data.sessionId);
163
+ ws.send(JSON.stringify({
164
+ type: "workflow",
165
+ sessionId: data.sessionId,
166
+ events,
167
+ }));
168
+ break;
169
+ }
170
+
171
+ case "listWorkflows": {
172
+ const workflows = getRecentWorkflows(data.limit ?? 20);
173
+ ws.send(JSON.stringify({
174
+ type: "workflows",
175
+ workflows,
176
+ }));
177
+ break;
178
+ }
179
+
180
+ default:
181
+ ws.send(JSON.stringify({
182
+ type: "error",
183
+ error: "Unknown message type",
184
+ }));
185
+ }
186
+ } catch (e) {
187
+ ws.send(JSON.stringify({
188
+ type: "error",
189
+ error: e instanceof Error ? e.message : String(e),
190
+ }));
191
+ }
192
+ },
193
+
194
+ open(ws) {
195
+ console.log("Client connected");
196
+ ws.send(JSON.stringify({
197
+ type: "connected",
198
+ tanaConnected: !!tana,
199
+ clientError,
200
+ }));
201
+ },
202
+
203
+ close() {
204
+ console.log("Client disconnected");
205
+ },
206
+ },
207
+ });
208
+
209
+ console.log(`Debug server running at http://localhost:${PORT}`);
210
+ console.log(`WebSocket endpoint: ws://localhost:${PORT}/ws`);
211
+ console.log(`Vite dev server: http://localhost:${VITE_PORT} (proxied)`);
212
+ if (clientError) {
213
+ console.log(`Warning: ${clientError}`);
214
+ }
215
+
216
+ // Cleanup on shutdown
217
+ const cleanup = () => {
218
+ if (viteProcess) {
219
+ console.log("Stopping Vite dev server...");
220
+ viteProcess.kill();
221
+ }
222
+ process.exit(0);
223
+ };
224
+
225
+ process.on("SIGINT", cleanup);
226
+ process.on("SIGTERM", cleanup);
227
+ }
228
+
229
+ main().catch((error) => {
230
+ console.error("Fatal error:", error);
231
+ process.exit(1);
232
+ });
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env bun
2
+ import { initCompat } from "./compat";
3
+ import { bunCompat } from "./compat/bun";
4
+ import { main } from "./main";
5
+
6
+ initCompat(bunCompat);
7
+
8
+ main().catch((error) => {
9
+ console.error("Fatal error:", error);
10
+ process.exit(1);
11
+ });
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import { initCompat } from "./compat/index.js";
3
+ import { nodeCompat } from "./compat/node.js";
4
+ import { main } from "./main.js";
5
+
6
+ initCompat(nodeCompat);
7
+
8
+ main().catch((error) => {
9
+ console.error("Fatal error:", error);
10
+ process.exit(1);
11
+ });
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);