tana-mcp-codemode 0.2.2 → 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.
@@ -0,0 +1,1364 @@
1
+ #!/usr/bin/env node
2
+ #!/usr/bin/env node
3
+
4
+ // src/compat/index.ts
5
+ var ops = null;
6
+ function initCompat(compatOps) {
7
+ if (ops) {
8
+ console.warn("initCompat called multiple times \u2014 overwriting previous compat ops");
9
+ }
10
+ ops = compatOps;
11
+ }
12
+ function getOps() {
13
+ if (!ops) {
14
+ throw new Error(
15
+ "compat not initialized \u2014 call initCompat() before using createDatabase or transpileTS"
16
+ );
17
+ }
18
+ return ops;
19
+ }
20
+ function createDatabase(path) {
21
+ return getOps().createDatabase(path);
22
+ }
23
+ function transpileTS(code) {
24
+ return getOps().transpileTS(code);
25
+ }
26
+
27
+ // src/compat/node.ts
28
+ import { createRequire } from "module";
29
+ var require2 = createRequire(import.meta.url);
30
+ function getBetterSqlite3() {
31
+ try {
32
+ return require2("better-sqlite3");
33
+ } catch {
34
+ throw new Error(
35
+ "better-sqlite3 is required for Node.js. Install it: npm install better-sqlite3"
36
+ );
37
+ }
38
+ }
39
+ function getEsbuild() {
40
+ try {
41
+ return require2("esbuild");
42
+ } catch {
43
+ throw new Error(
44
+ "esbuild is required for Node.js. Install it: npm install esbuild"
45
+ );
46
+ }
47
+ }
48
+ function wrapStatement(stmt) {
49
+ return {
50
+ run(...params) {
51
+ const result = stmt.run(...params);
52
+ return { changes: result.changes };
53
+ },
54
+ all(...params) {
55
+ return stmt.all(...params);
56
+ }
57
+ };
58
+ }
59
+ function wrapDatabase(db2) {
60
+ return {
61
+ exec(sql) {
62
+ db2.exec(sql);
63
+ },
64
+ prepare(sql) {
65
+ return wrapStatement(db2.prepare(sql));
66
+ },
67
+ close() {
68
+ db2.close();
69
+ }
70
+ };
71
+ }
72
+ var nodeCompat = {
73
+ createDatabase(path) {
74
+ const BetterSqlite3 = getBetterSqlite3();
75
+ return wrapDatabase(new BetterSqlite3(path));
76
+ },
77
+ transpileTS(code) {
78
+ const esbuild = getEsbuild();
79
+ return esbuild.transformSync(code, { loader: "ts" }).code;
80
+ }
81
+ };
82
+
83
+ // src/main.ts
84
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
85
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
86
+ import { z } from "zod";
87
+
88
+ // src/api/client.ts
89
+ var TanaAPIError = class extends Error {
90
+ constructor(message, statusCode, response, suggestion) {
91
+ super(message);
92
+ this.statusCode = statusCode;
93
+ this.response = response;
94
+ this.suggestion = suggestion;
95
+ this.name = "TanaAPIError";
96
+ }
97
+ };
98
+ var RETRY_CONFIG = {
99
+ maxRetries: 2,
100
+ baseDelayMs: 500,
101
+ retryableCodes: /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504])
102
+ };
103
+ function isRetryableError(error) {
104
+ if (error instanceof TanaAPIError && error.statusCode) {
105
+ return RETRY_CONFIG.retryableCodes.has(error.statusCode);
106
+ }
107
+ if (error instanceof Error) {
108
+ const msg = error.message.toLowerCase();
109
+ return msg.includes("econnrefused") || msg.includes("econnreset") || msg.includes("etimedout") || msg.includes("timeout") || msg.includes("network");
110
+ }
111
+ return false;
112
+ }
113
+ function getFriendlyError(error) {
114
+ if (error instanceof TanaAPIError) {
115
+ switch (error.statusCode) {
116
+ case 401:
117
+ return {
118
+ message: "Authentication failed",
119
+ suggestion: "Check TANA_API_TOKEN - it may have expired. Generate a new token in Tana: Settings > API > Generate Token"
120
+ };
121
+ case 403:
122
+ return {
123
+ message: "Access forbidden",
124
+ suggestion: "Your API token may lack permissions for this operation. Try generating a new token."
125
+ };
126
+ case 404:
127
+ return {
128
+ message: "Resource not found",
129
+ suggestion: "The node or workspace may have been deleted, or the ID is incorrect."
130
+ };
131
+ case 429:
132
+ return {
133
+ message: "Rate limited",
134
+ suggestion: "Too many requests. Wait a moment and try again."
135
+ };
136
+ case 500:
137
+ case 502:
138
+ case 503:
139
+ return {
140
+ message: "Tana server error",
141
+ suggestion: "Tana is experiencing issues. Try again in a few seconds."
142
+ };
143
+ }
144
+ }
145
+ if (error instanceof Error) {
146
+ const msg = error.message.toLowerCase();
147
+ if (msg.includes("econnrefused")) {
148
+ return {
149
+ message: "Cannot connect to Tana",
150
+ suggestion: "Make sure Tana Desktop is running with Local API enabled. Check the port in Tana settings."
151
+ };
152
+ }
153
+ if (msg.includes("timeout") || msg.includes("etimedout")) {
154
+ return {
155
+ message: "Request timed out",
156
+ suggestion: "Tana took too long to respond. Try again or increase TANA_TIMEOUT."
157
+ };
158
+ }
159
+ }
160
+ return {
161
+ message: error instanceof Error ? error.message : "Unknown error",
162
+ suggestion: "Check if Tana Desktop is running and your API token is valid."
163
+ };
164
+ }
165
+ function delay(ms) {
166
+ return new Promise((resolve) => setTimeout(resolve, ms));
167
+ }
168
+ var TanaClient = class {
169
+ baseUrl;
170
+ token;
171
+ timeout;
172
+ constructor(config) {
173
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
174
+ this.token = config.token;
175
+ this.timeout = config.timeout;
176
+ }
177
+ async request(method, path, body) {
178
+ let lastError;
179
+ for (let attempt = 1; attempt <= RETRY_CONFIG.maxRetries; attempt++) {
180
+ const controller = new AbortController();
181
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
182
+ try {
183
+ const url = `${this.baseUrl}${path}`;
184
+ const headers = {
185
+ Authorization: `Bearer ${this.token}`,
186
+ "Content-Type": "application/json"
187
+ };
188
+ const response = await fetch(url, {
189
+ method,
190
+ headers,
191
+ body: body ? JSON.stringify(body) : void 0,
192
+ signal: controller.signal
193
+ });
194
+ if (!response.ok) {
195
+ let errorBody;
196
+ try {
197
+ errorBody = await response.json();
198
+ } catch {
199
+ errorBody = await response.text();
200
+ }
201
+ const error = new TanaAPIError(
202
+ `Tana API error: ${response.status} ${response.statusText}`,
203
+ response.status,
204
+ errorBody
205
+ );
206
+ if (isRetryableError(error) && attempt < RETRY_CONFIG.maxRetries) {
207
+ lastError = error;
208
+ const delayMs = RETRY_CONFIG.baseDelayMs * Math.pow(2, attempt - 1);
209
+ console.error(`Retry ${attempt}/${RETRY_CONFIG.maxRetries} after ${delayMs}ms...`);
210
+ await delay(delayMs);
211
+ continue;
212
+ }
213
+ throw error;
214
+ }
215
+ const contentType = response.headers.get("content-type");
216
+ if (contentType?.includes("application/json")) {
217
+ return await response.json();
218
+ }
219
+ return await response.text();
220
+ } catch (error) {
221
+ clearTimeout(timeoutId);
222
+ if (error instanceof TanaAPIError && !isRetryableError(error)) {
223
+ const friendly3 = getFriendlyError(error);
224
+ throw new TanaAPIError(friendly3.message, error.statusCode, error.response, friendly3.suggestion);
225
+ }
226
+ if (error instanceof Error && error.name === "AbortError") {
227
+ const friendly3 = getFriendlyError(new Error("timeout"));
228
+ throw new TanaAPIError(friendly3.message, void 0, void 0, friendly3.suggestion);
229
+ }
230
+ if (isRetryableError(error) && attempt < RETRY_CONFIG.maxRetries) {
231
+ lastError = error;
232
+ const delayMs = RETRY_CONFIG.baseDelayMs * Math.pow(2, attempt - 1);
233
+ console.error(`Retry ${attempt}/${RETRY_CONFIG.maxRetries} after ${delayMs}ms...`);
234
+ await delay(delayMs);
235
+ continue;
236
+ }
237
+ const friendly2 = getFriendlyError(error);
238
+ throw new TanaAPIError(friendly2.message, void 0, void 0, friendly2.suggestion);
239
+ } finally {
240
+ clearTimeout(timeoutId);
241
+ }
242
+ }
243
+ const friendly = getFriendlyError(lastError);
244
+ throw new TanaAPIError(
245
+ `Failed after ${RETRY_CONFIG.maxRetries} attempts: ${friendly.message}`,
246
+ void 0,
247
+ void 0,
248
+ friendly.suggestion
249
+ );
250
+ }
251
+ async get(path) {
252
+ return this.request("GET", path);
253
+ }
254
+ async post(path, body) {
255
+ return this.request("POST", path, body);
256
+ }
257
+ async put(path, body) {
258
+ return this.request("PUT", path, body);
259
+ }
260
+ async patch(path, body) {
261
+ return this.request("PATCH", path, body);
262
+ }
263
+ async delete(path, body) {
264
+ return this.request("DELETE", path, body);
265
+ }
266
+ };
267
+ function createClient() {
268
+ const baseUrl = process.env.TANA_API_URL || "http://127.0.0.1:8262";
269
+ const token = process.env.TANA_API_TOKEN;
270
+ const timeout = parseInt(process.env.TANA_TIMEOUT || "3000", 10);
271
+ if (!token) {
272
+ throw new Error(
273
+ "TANA_API_TOKEN environment variable is required. Get your token from Tana Desktop: Settings > API > Generate Token"
274
+ );
275
+ }
276
+ return new TanaClient({ baseUrl, token, timeout });
277
+ }
278
+
279
+ // src/api/format.ts
280
+ function hasKey(obj, key) {
281
+ return typeof obj === "object" && obj !== null && key in obj;
282
+ }
283
+ function formatTags(tags) {
284
+ if (!tags || tags.length === 0) return "";
285
+ return " " + tags.map((t) => `#${t.name}`).join(" ");
286
+ }
287
+ function formatSearchResults(data) {
288
+ const lines = data.map((r) => {
289
+ const tags = formatTags(r.tags);
290
+ const breadcrumb = r.breadcrumb?.length ? ` \u2014 ${r.breadcrumb.join(" > ")}` : "";
291
+ return ` [${r.id}] ${r.name}${tags}${breadcrumb}`;
292
+ });
293
+ return `${data.length} results:
294
+ ${lines.join("\n")}`;
295
+ }
296
+ function formatChildren(data) {
297
+ const showing = data.children.length;
298
+ const header = `${data.total} children (showing ${showing}${data.hasMore ? ", has more" : ""}):`;
299
+ const lines = data.children.map((c) => {
300
+ const tags = formatTags(c.tags);
301
+ const kids = c.childCount > 0 ? ` (${c.childCount} children)` : "";
302
+ return ` [${c.id}] ${c.name}${tags}${kids}`;
303
+ });
304
+ return `${header}
305
+ ${lines.join("\n")}`;
306
+ }
307
+ function formatChildNodes(data) {
308
+ const lines = data.map((c) => {
309
+ const tags = formatTags(c.tags);
310
+ const kids = c.childCount > 0 ? ` (${c.childCount} children)` : "";
311
+ return ` [${c.id}] ${c.name}${tags}${kids}`;
312
+ });
313
+ return `${data.length} nodes:
314
+ ${lines.join("\n")}`;
315
+ }
316
+ function formatTags_(data) {
317
+ const lines = data.map((t) => ` [${t.id}] ${t.name}`);
318
+ return `${data.length} tags:
319
+ ${lines.join("\n")}`;
320
+ }
321
+ function formatWorkspaces(data) {
322
+ const lines = data.map((w) => ` [${w.id}] ${w.name} (home: ${w.homeNodeId})`);
323
+ return `${data.length} workspaces:
324
+ ${lines.join("\n")}`;
325
+ }
326
+ function formatImportResult(data) {
327
+ if (data.success) {
328
+ const ids = data.nodeIds?.length ? ` (${data.nodeIds.join(", ")})` : "";
329
+ return `Import success: ${data.nodeIds?.length ?? 0} nodes${ids}`;
330
+ }
331
+ return `Import failed: ${data.error ?? "unknown error"}`;
332
+ }
333
+ function format(data) {
334
+ if (typeof data === "string") return data;
335
+ if (data === null || data === void 0) return String(data);
336
+ if (hasKey(data, "success") && (hasKey(data, "nodeIds") || hasKey(data, "error"))) {
337
+ return formatImportResult(data);
338
+ }
339
+ if (hasKey(data, "children") && hasKey(data, "total") && hasKey(data, "hasMore")) {
340
+ return formatChildren(data);
341
+ }
342
+ if (Array.isArray(data)) {
343
+ if (data.length === 0) return "0 results";
344
+ const first = data[0];
345
+ if (hasKey(first, "breadcrumb")) {
346
+ return formatSearchResults(data);
347
+ }
348
+ if (hasKey(first, "childCount")) {
349
+ return formatChildNodes(data);
350
+ }
351
+ if (hasKey(first, "homeNodeId")) {
352
+ return formatWorkspaces(data);
353
+ }
354
+ if (hasKey(first, "id") && hasKey(first, "name") && !hasKey(first, "breadcrumb") && !hasKey(first, "childCount") && !hasKey(first, "homeNodeId")) {
355
+ return formatTags_(data);
356
+ }
357
+ }
358
+ try {
359
+ return JSON.stringify(data, null, 2);
360
+ } catch {
361
+ return String(data);
362
+ }
363
+ }
364
+
365
+ // src/api/tana.ts
366
+ function addQueryParams(params, obj, prefix) {
367
+ for (const [key, value] of Object.entries(obj)) {
368
+ const paramKey = prefix ? `${prefix}[${key}]` : key;
369
+ if (value === null || value === void 0) continue;
370
+ if (typeof value === "object" && !Array.isArray(value)) {
371
+ addQueryParams(params, value, paramKey);
372
+ } else if (Array.isArray(value)) {
373
+ value.forEach((item, i) => {
374
+ if (typeof item === "object") {
375
+ addQueryParams(params, item, `${paramKey}[${i}]`);
376
+ } else {
377
+ params.push(`${encodeURIComponent(`${paramKey}[${i}]`)}=${encodeURIComponent(String(item))}`);
378
+ }
379
+ });
380
+ } else {
381
+ params.push(`${encodeURIComponent(paramKey)}=${encodeURIComponent(String(value))}`);
382
+ }
383
+ }
384
+ }
385
+ function parseOptionsFromMarkdown(md) {
386
+ const results = [];
387
+ const lines = md.split("\n");
388
+ let inOptions = false;
389
+ for (const line of lines) {
390
+ if (line.includes("**Options**:")) {
391
+ inOptions = true;
392
+ continue;
393
+ }
394
+ if (inOptions) {
395
+ const match = line.match(/^\s+- (.+?)\s*<!--\s*node-id:\s*(\S+)\s*-->/);
396
+ if (match) {
397
+ results.push({ id: match[2], name: match[1] });
398
+ } else if (line.trim() && !line.startsWith(" ")) {
399
+ break;
400
+ }
401
+ }
402
+ }
403
+ return results;
404
+ }
405
+ function parseRefsFromMarkdown(md) {
406
+ const results = [];
407
+ const re = /\[([^\]]+)\]\(tana:([^)]+)\)/g;
408
+ let match;
409
+ while ((match = re.exec(md)) !== null) {
410
+ results.push({ id: match[2], name: match[1] });
411
+ }
412
+ return results;
413
+ }
414
+ function parseFieldValues(md, fieldName) {
415
+ const results = [];
416
+ const lines = md.split("\n");
417
+ let inField = false;
418
+ let fieldIndent = -1;
419
+ for (const line of lines) {
420
+ if (!inField && line.includes(`**${fieldName}**`)) {
421
+ inField = true;
422
+ fieldIndent = line.search(/\S/);
423
+ const inlineMatch = line.match(
424
+ new RegExp(`\\*\\*${fieldName}\\*\\*:\\s*(.+?)\\s*<!--\\s*node-id:\\s*(\\S+)\\s*-->`)
425
+ );
426
+ if (inlineMatch) {
427
+ results.push({ id: inlineMatch[2], name: inlineMatch[1].trim() });
428
+ }
429
+ const linkMatch = line.match(
430
+ new RegExp(`\\*\\*${fieldName}\\*\\*:\\s*\\[([^\\]]+)\\]\\(tana:([^)]+)\\)`)
431
+ );
432
+ if (linkMatch) {
433
+ results.push({ id: linkMatch[2], name: linkMatch[1] });
434
+ }
435
+ continue;
436
+ }
437
+ if (inField) {
438
+ const currentIndent = line.search(/\S/);
439
+ if (line.trim() && currentIndent <= fieldIndent) break;
440
+ const childMatch = line.match(/^\s+- (.+?)\s*<!--\s*node-id:\s*(\S+)\s*-->/);
441
+ if (childMatch) {
442
+ results.push({ id: childMatch[2], name: childMatch[1].trim() });
443
+ continue;
444
+ }
445
+ const childLinkMatch = line.match(/^\s+- \[([^\]]+)\]\(tana:([^)]+)\)/);
446
+ if (childLinkMatch) {
447
+ results.push({ id: childLinkMatch[2], name: childLinkMatch[1] });
448
+ }
449
+ }
450
+ }
451
+ return results;
452
+ }
453
+ function createTanaAPI(client, workspace, defaultSearchWorkspaceIds) {
454
+ return {
455
+ workspace: workspace ?? null,
456
+ async health() {
457
+ return client.get("/health");
458
+ },
459
+ workspaces: {
460
+ async list() {
461
+ return client.get("/workspaces");
462
+ }
463
+ },
464
+ nodes: {
465
+ async search(query, options) {
466
+ const params = [];
467
+ addQueryParams(params, query, "query");
468
+ if (options?.limit) params.push(`limit=${options.limit}`);
469
+ const effectiveWorkspaceIds = options?.workspaceIds ?? (defaultSearchWorkspaceIds?.length ? defaultSearchWorkspaceIds : void 0);
470
+ if (effectiveWorkspaceIds) {
471
+ effectiveWorkspaceIds.forEach((id, i) => {
472
+ params.push(`workspaceIds[${i}]=${encodeURIComponent(id)}`);
473
+ });
474
+ }
475
+ return client.get(`/nodes/search?${params.join("&")}`);
476
+ },
477
+ async read(nodeId, maxDepth = 1) {
478
+ const result = await client.get(
479
+ `/nodes/${nodeId}?maxDepth=${maxDepth}`
480
+ );
481
+ return result.markdown;
482
+ },
483
+ async getChildren(nodeId, options) {
484
+ const params = new URLSearchParams();
485
+ if (options?.limit) params.set("limit", String(options.limit));
486
+ if (options?.offset) params.set("offset", String(options.offset));
487
+ const query = params.toString();
488
+ return client.get(
489
+ `/nodes/${nodeId}/children${query ? `?${query}` : ""}`
490
+ );
491
+ },
492
+ async edit(options) {
493
+ const result = await client.post(
494
+ `/nodes/${options.nodeId}/update`,
495
+ {
496
+ name: options.name,
497
+ description: options.description
498
+ }
499
+ );
500
+ return { success: !!result.nodeId };
501
+ },
502
+ async move(options) {
503
+ const result = await client.post(
504
+ `/nodes/${options.nodeId}/move`,
505
+ {
506
+ targetNodeId: options.targetNodeId,
507
+ keepSourceReference: options.keepSourceReference,
508
+ position: options.position,
509
+ referenceNodeId: options.referenceNodeId,
510
+ sourceParentId: options.sourceParentId
511
+ }
512
+ );
513
+ return { success: !!result.nodeId };
514
+ },
515
+ async open(nodeId, openType = "current") {
516
+ const result = await client.post(
517
+ `/nodes/${nodeId}/open`,
518
+ { openType }
519
+ );
520
+ return { success: !!result.nodeId };
521
+ },
522
+ async trash(nodeId) {
523
+ const result = await client.post(
524
+ `/nodes/${nodeId}/trash`,
525
+ {}
526
+ );
527
+ return { success: !!result.nodeId };
528
+ },
529
+ async check(nodeId) {
530
+ const result = await client.post(
531
+ `/nodes/${nodeId}/done`,
532
+ { done: true }
533
+ );
534
+ return { success: result.done === true };
535
+ },
536
+ async uncheck(nodeId) {
537
+ const result = await client.post(
538
+ `/nodes/${nodeId}/done`,
539
+ { done: false }
540
+ );
541
+ return { success: result.done === false };
542
+ }
543
+ },
544
+ tags: {
545
+ async listAll(workspaceId) {
546
+ const schemaNodeId = `${workspaceId}_SCHEMA`;
547
+ const tags = [];
548
+ let offset = 0;
549
+ while (true) {
550
+ const page = await client.get(
551
+ `/nodes/${schemaNodeId}/children?limit=200&offset=${offset}`
552
+ );
553
+ for (const child of page.children) {
554
+ if (child.docType === "tagDef") {
555
+ tags.push({ id: child.id, name: child.name });
556
+ }
557
+ }
558
+ if (!page.hasMore) break;
559
+ offset += 200;
560
+ }
561
+ return tags;
562
+ },
563
+ async getSchema(tagId, includeEditInstructions = false, includeInheritedFields = true) {
564
+ const result = await client.get(
565
+ `/tags/${tagId}/schema?includeEditInstructions=${includeEditInstructions}&includeInheritedFields=${includeInheritedFields}`
566
+ );
567
+ return truncateOptionLists(result.markdown);
568
+ },
569
+ async modify(nodeId, action, tagIds) {
570
+ const result = await client.post(
571
+ `/nodes/${nodeId}/tags`,
572
+ { action, tagIds }
573
+ );
574
+ return { success: !!result.nodeId };
575
+ },
576
+ async create(options) {
577
+ return client.post(
578
+ `/workspaces/${options.workspaceId}/tags`,
579
+ {
580
+ name: options.name,
581
+ description: options.description,
582
+ extendsTagIds: options.extendsTagIds,
583
+ showCheckbox: options.showCheckbox
584
+ }
585
+ );
586
+ },
587
+ async addField(options) {
588
+ return client.post(
589
+ `/tags/${options.tagId}/fields`,
590
+ {
591
+ name: options.name,
592
+ dataType: options.dataType,
593
+ description: options.description,
594
+ sourceTagId: options.sourceTagId,
595
+ options: options.options,
596
+ defaultValue: options.defaultValue,
597
+ isMultiValue: options.isMultiValue
598
+ }
599
+ );
600
+ },
601
+ async setCheckbox(options) {
602
+ const result = await client.post(
603
+ `/tags/${options.tagId}/checkbox`,
604
+ {
605
+ showCheckbox: options.showCheckbox,
606
+ doneStateMapping: options.doneStateMapping
607
+ }
608
+ );
609
+ return { success: !!result.tagId };
610
+ }
611
+ },
612
+ fields: {
613
+ // Workaround: append mode is broken in Tana API (clears field instead of appending).
614
+ // For arrays, we set first value via API, then import remaining into the field tuple.
615
+ // See claudedocs/api-bugs.md
616
+ async setOption(nodeId, attributeId, optionId, _mode) {
617
+ const ids = Array.isArray(optionId) ? optionId : [optionId];
618
+ if (ids.length === 0) return { success: true };
619
+ await client.post(
620
+ `/nodes/${nodeId}/fields/${attributeId}/option`,
621
+ { optionId: ids[0], mode: "replace" }
622
+ );
623
+ if (ids.length === 1) return { success: true };
624
+ const { children } = await client.get(
625
+ `/nodes/${nodeId}/children?limit=100`
626
+ );
627
+ let tupleId = null;
628
+ for (const child of children) {
629
+ if (child.docType === "tuple") {
630
+ const sub = await client.get(
631
+ `/nodes/${child.id}/children?limit=20`
632
+ );
633
+ if (sub.children.some((s) => s.id === attributeId)) {
634
+ tupleId = child.id;
635
+ break;
636
+ }
637
+ }
638
+ }
639
+ if (!tupleId) return { success: false };
640
+ const refs = ids.slice(1).map((id) => `- [[^${id}]]`).join("\n");
641
+ await client.post(`/nodes/${tupleId}/import`, { content: refs });
642
+ return { success: true };
643
+ },
644
+ async setContent(nodeId, attributeId, content, mode) {
645
+ const result = await client.post(
646
+ `/nodes/${nodeId}/fields/${attributeId}/content`,
647
+ { content, mode }
648
+ );
649
+ return { success: !!result.nodeId };
650
+ },
651
+ async getFieldOptions(fieldId, options) {
652
+ const result = await client.get(
653
+ `/nodes/${fieldId}?maxDepth=2`
654
+ );
655
+ const md = result.markdown;
656
+ if (md.includes("**Options**:")) {
657
+ return parseOptionsFromMarkdown(md);
658
+ }
659
+ if (md.includes("Options from supertag")) {
660
+ const sourceTagMatch = md.match(/\(tana:([^)]+)\)/);
661
+ if (sourceTagMatch) {
662
+ const params2 = [];
663
+ addQueryParams(params2, { hasType: sourceTagMatch[1] }, "query");
664
+ if (options?.workspaceId) {
665
+ params2.push(`workspaceIds[0]=${encodeURIComponent(options.workspaceId)}`);
666
+ }
667
+ if (options?.limit) params2.push(`limit=${options.limit}`);
668
+ const results = await client.get(`/nodes/search?${params2.join("&")}`);
669
+ return results.map((r) => ({ id: r.id, name: r.name }));
670
+ }
671
+ }
672
+ const searchNodeMatch = md.match(/\*\*Options List\*\*:.*?\(tana:([^)]+)\)/);
673
+ if (searchNodeMatch) {
674
+ const searchResult = await client.get(
675
+ `/nodes/${searchNodeMatch[1]}?maxDepth=1`
676
+ );
677
+ return parseRefsFromMarkdown(searchResult.markdown);
678
+ }
679
+ const fieldNameMatch = md.match(/^- (.+?) #field-definition/m);
680
+ if (!fieldNameMatch) return [];
681
+ const fieldName = fieldNameMatch[1];
682
+ const limit = options?.limit ?? 10;
683
+ const params = [];
684
+ const query = options?.tagId ? { and: [{ hasType: options.tagId }, { field: { fieldId, state: "set" } }] } : { field: { fieldId, state: "set" } };
685
+ addQueryParams(params, query, "query");
686
+ if (options?.workspaceId) {
687
+ params.push(`workspaceIds[0]=${encodeURIComponent(options.workspaceId)}`);
688
+ }
689
+ params.push(`limit=${limit}`);
690
+ const searchResults = await client.get(`/nodes/search?${params.join("&")}`);
691
+ const seen = /* @__PURE__ */ new Map();
692
+ for (const node of searchResults) {
693
+ const nodeMd = await client.get(
694
+ `/nodes/${node.id}?maxDepth=1`
695
+ );
696
+ for (const { id, name } of parseFieldValues(nodeMd.markdown, fieldName)) {
697
+ if (!seen.has(id)) seen.set(id, name);
698
+ }
699
+ }
700
+ return Array.from(seen, ([id, name]) => ({ id, name }));
701
+ }
702
+ },
703
+ calendar: {
704
+ async getOrCreate(workspaceId, granularity, date) {
705
+ const params = new URLSearchParams();
706
+ params.set("granularity", granularity);
707
+ if (date) params.set("date", date);
708
+ return client.get(
709
+ `/workspaces/${workspaceId}/calendar/node?${params.toString()}`
710
+ );
711
+ }
712
+ },
713
+ async import(parentNodeId, content) {
714
+ const raw = await client.post(`/nodes/${parentNodeId}/import`, { content });
715
+ return {
716
+ success: true,
717
+ nodeIds: raw.createdNodes.map((n) => n.id)
718
+ };
719
+ },
720
+ format(data) {
721
+ return format(data);
722
+ }
723
+ };
724
+ }
725
+ var MAX_OPTIONS_SHOWN = 5;
726
+ var OPTION_LINE_RE = /^ - .+ \(id:/;
727
+ function truncateOptionLists(markdown) {
728
+ const lines = markdown.split("\n");
729
+ const result = [];
730
+ let optionCount = 0;
731
+ let inOptions = false;
732
+ for (const line of lines) {
733
+ if (OPTION_LINE_RE.test(line)) {
734
+ if (!inOptions) {
735
+ inOptions = true;
736
+ optionCount = 0;
737
+ }
738
+ optionCount++;
739
+ if (optionCount <= MAX_OPTIONS_SHOWN) {
740
+ result.push(line);
741
+ }
742
+ } else {
743
+ if (inOptions && optionCount > MAX_OPTIONS_SHOWN) {
744
+ result.push(` - ... (${optionCount - MAX_OPTIONS_SHOWN} more, ${optionCount} total)`);
745
+ }
746
+ inOptions = false;
747
+ optionCount = 0;
748
+ result.push(line);
749
+ }
750
+ }
751
+ if (inOptions && optionCount > MAX_OPTIONS_SHOWN) {
752
+ result.push(` - ... (${optionCount - MAX_OPTIONS_SHOWN} more, ${optionCount} total)`);
753
+ }
754
+ return result.join("\n");
755
+ }
756
+
757
+ // src/storage/history.ts
758
+ import { homedir } from "os";
759
+ import { join, dirname } from "path";
760
+ import { mkdirSync, existsSync } from "fs";
761
+ var db = null;
762
+ function getDbPath() {
763
+ if (process.env.TANA_HISTORY_PATH) {
764
+ const customPath = process.env.TANA_HISTORY_PATH;
765
+ const dir = dirname(customPath);
766
+ if (!existsSync(dir)) {
767
+ mkdirSync(dir, { recursive: true });
768
+ }
769
+ return customPath;
770
+ }
771
+ const platform = process.platform;
772
+ let baseDir;
773
+ if (platform === "darwin") {
774
+ baseDir = join(homedir(), "Library", "Application Support", "tana-mcp");
775
+ } else if (platform === "win32") {
776
+ baseDir = join(process.env.APPDATA || homedir(), "tana-mcp");
777
+ } else {
778
+ baseDir = join(homedir(), ".local", "share", "tana-mcp");
779
+ }
780
+ if (!existsSync(baseDir)) {
781
+ mkdirSync(baseDir, { recursive: true });
782
+ }
783
+ return join(baseDir, "history.db");
784
+ }
785
+ function migrateDb(database) {
786
+ const columns = database.prepare("PRAGMA table_info(script_runs)").all();
787
+ const columnNames = new Set(columns.map((c) => c.name));
788
+ const newColumns = [
789
+ { name: "input", type: "TEXT" },
790
+ { name: "api_calls", type: "TEXT" },
791
+ { name: "node_ids_affected", type: "TEXT" },
792
+ { name: "workspace_id", type: "TEXT" }
793
+ ];
794
+ for (const col of newColumns) {
795
+ if (!columnNames.has(col.name)) {
796
+ database.exec(`ALTER TABLE script_runs ADD COLUMN ${col.name} ${col.type}`);
797
+ }
798
+ }
799
+ }
800
+ function initDb() {
801
+ if (db) return db;
802
+ const dbPath = getDbPath();
803
+ db = createDatabase(dbPath);
804
+ db.exec(`
805
+ CREATE TABLE IF NOT EXISTS script_runs (
806
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
807
+ timestamp INTEGER NOT NULL,
808
+ script TEXT NOT NULL,
809
+ success INTEGER NOT NULL,
810
+ output TEXT NOT NULL,
811
+ error TEXT,
812
+ duration_ms INTEGER NOT NULL,
813
+ session_id TEXT,
814
+ input TEXT,
815
+ api_calls TEXT,
816
+ node_ids_affected TEXT,
817
+ workspace_id TEXT
818
+ )
819
+ `);
820
+ migrateDb(db);
821
+ db.exec(`
822
+ CREATE INDEX IF NOT EXISTS idx_script_runs_timestamp
823
+ ON script_runs(timestamp DESC)
824
+ `);
825
+ db.exec(`
826
+ CREATE INDEX IF NOT EXISTS idx_script_runs_session
827
+ ON script_runs(session_id)
828
+ `);
829
+ db.exec(`
830
+ CREATE INDEX IF NOT EXISTS idx_script_runs_workspace
831
+ ON script_runs(workspace_id)
832
+ `);
833
+ return db;
834
+ }
835
+ function saveScriptRun(options) {
836
+ try {
837
+ const database = initDb();
838
+ const stmt = database.prepare(`
839
+ INSERT INTO script_runs (
840
+ timestamp, script, success, output, error, duration_ms, session_id,
841
+ input, api_calls, node_ids_affected, workspace_id
842
+ )
843
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
844
+ `);
845
+ stmt.run(
846
+ Date.now(),
847
+ options.script,
848
+ options.success ? 1 : 0,
849
+ options.output,
850
+ options.error,
851
+ options.durationMs,
852
+ options.sessionId,
853
+ options.input ?? null,
854
+ options.apiCalls ? JSON.stringify(options.apiCalls) : null,
855
+ options.nodeIdsAffected ? JSON.stringify(options.nodeIdsAffected) : null,
856
+ options.workspaceId ?? null
857
+ );
858
+ } catch (e) {
859
+ console.error("Failed to save script run to history:", e);
860
+ }
861
+ }
862
+ function cleanupOldRuns(daysOld = 30) {
863
+ const database = initDb();
864
+ const cutoff = Date.now() - daysOld * 24 * 60 * 60 * 1e3;
865
+ const stmt = database.prepare(`
866
+ DELETE FROM script_runs WHERE timestamp < ?
867
+ `);
868
+ const result = stmt.run(cutoff);
869
+ return result.changes;
870
+ }
871
+
872
+ // src/sandbox/workflow.ts
873
+ var workflowTableInitialized = false;
874
+ function ensureWorkflowTable(db2) {
875
+ if (workflowTableInitialized) return;
876
+ db2.exec(`
877
+ CREATE TABLE IF NOT EXISTS workflow_events (
878
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
879
+ session_id TEXT NOT NULL,
880
+ timestamp INTEGER NOT NULL,
881
+ event_type TEXT NOT NULL,
882
+ message TEXT NOT NULL,
883
+ metadata TEXT
884
+ )
885
+ `);
886
+ db2.exec(`
887
+ CREATE INDEX IF NOT EXISTS idx_workflow_session
888
+ ON workflow_events(session_id, timestamp)
889
+ `);
890
+ workflowTableInitialized = true;
891
+ }
892
+ function saveWorkflowEvent(sessionId, eventType, message, metadata) {
893
+ try {
894
+ const db2 = initDb();
895
+ ensureWorkflowTable(db2);
896
+ const stmt = db2.prepare(`
897
+ INSERT INTO workflow_events (session_id, timestamp, event_type, message, metadata)
898
+ VALUES (?, ?, ?, ?, ?)
899
+ `);
900
+ stmt.run(
901
+ sessionId,
902
+ Date.now(),
903
+ eventType,
904
+ message,
905
+ metadata ? JSON.stringify(metadata) : null
906
+ );
907
+ } catch (e) {
908
+ console.error("Failed to save workflow event:", e);
909
+ }
910
+ }
911
+ function createWorkflowHelper(sessionId) {
912
+ return {
913
+ start(message) {
914
+ saveWorkflowEvent(sessionId, "start", message);
915
+ },
916
+ step(message) {
917
+ saveWorkflowEvent(sessionId, "step", message);
918
+ },
919
+ progress(current, total, message) {
920
+ const progressMsg = message ? `${message} (${current}/${total})` : `${current}/${total}`;
921
+ saveWorkflowEvent(sessionId, "progress", progressMsg, { current, total });
922
+ },
923
+ complete(message) {
924
+ saveWorkflowEvent(sessionId, "complete", message ?? "Completed");
925
+ },
926
+ abort(reason) {
927
+ saveWorkflowEvent(sessionId, "abort", reason);
928
+ }
929
+ };
930
+ }
931
+
932
+ // src/sandbox/executor.ts
933
+ var EXECUTION_TIMEOUT = 1e4;
934
+ function createTrackedTanaAPI(tana, tracker) {
935
+ const track = (method, fn) => {
936
+ tracker.calls.push(method);
937
+ return fn();
938
+ };
939
+ const trackNodeId = (id) => tracker.nodeIds.add(id);
940
+ return {
941
+ workspace: tana.workspace,
942
+ health: () => track("health", () => tana.health()),
943
+ workspaces: {
944
+ list: () => track("workspaces.list", () => tana.workspaces.list())
945
+ },
946
+ nodes: {
947
+ search: (query, options) => {
948
+ if (options?.workspaceIds?.[0]) {
949
+ tracker.workspaceId = options.workspaceIds[0];
950
+ }
951
+ return track("nodes.search", () => tana.nodes.search(query, options));
952
+ },
953
+ read: (nodeId, maxDepth) => {
954
+ trackNodeId(nodeId);
955
+ return track("nodes.read", () => tana.nodes.read(nodeId, maxDepth));
956
+ },
957
+ getChildren: (nodeId, options) => {
958
+ trackNodeId(nodeId);
959
+ return track("nodes.getChildren", () => tana.nodes.getChildren(nodeId, options));
960
+ },
961
+ edit: (options) => {
962
+ trackNodeId(options.nodeId);
963
+ return track("nodes.edit", () => tana.nodes.edit(options));
964
+ },
965
+ move: (options) => {
966
+ trackNodeId(options.nodeId);
967
+ return track("nodes.move", () => tana.nodes.move(options));
968
+ },
969
+ open: (nodeId, openType) => {
970
+ trackNodeId(nodeId);
971
+ return track("nodes.open", () => tana.nodes.open(nodeId, openType));
972
+ },
973
+ trash: (nodeId) => {
974
+ trackNodeId(nodeId);
975
+ return track("nodes.trash", () => tana.nodes.trash(nodeId));
976
+ },
977
+ check: (nodeId) => {
978
+ trackNodeId(nodeId);
979
+ return track("nodes.check", () => tana.nodes.check(nodeId));
980
+ },
981
+ uncheck: (nodeId) => {
982
+ trackNodeId(nodeId);
983
+ return track("nodes.uncheck", () => tana.nodes.uncheck(nodeId));
984
+ }
985
+ },
986
+ tags: {
987
+ listAll: (workspaceId) => {
988
+ tracker.workspaceId = workspaceId;
989
+ return track("tags.listAll", () => tana.tags.listAll(workspaceId));
990
+ },
991
+ getSchema: (tagId, includeEditInstructions) => track("tags.getSchema", () => tana.tags.getSchema(tagId, includeEditInstructions)),
992
+ modify: (nodeId, action, tagIds) => {
993
+ trackNodeId(nodeId);
994
+ return track("tags.modify", () => tana.tags.modify(nodeId, action, tagIds));
995
+ },
996
+ create: (options) => {
997
+ tracker.workspaceId = options.workspaceId;
998
+ return track("tags.create", () => tana.tags.create(options));
999
+ },
1000
+ addField: (options) => track("tags.addField", () => tana.tags.addField(options)),
1001
+ setCheckbox: (options) => track("tags.setCheckbox", () => tana.tags.setCheckbox(options))
1002
+ },
1003
+ fields: {
1004
+ setOption: (nodeId, attributeId, optionId) => {
1005
+ trackNodeId(nodeId);
1006
+ return track("fields.setOption", () => tana.fields.setOption(nodeId, attributeId, optionId));
1007
+ },
1008
+ setContent: (nodeId, attributeId, content) => {
1009
+ trackNodeId(nodeId);
1010
+ return track("fields.setContent", () => tana.fields.setContent(nodeId, attributeId, content));
1011
+ },
1012
+ getFieldOptions: (...args) => track("fields.getFieldOptions", () => tana.fields.getFieldOptions(...args))
1013
+ },
1014
+ calendar: {
1015
+ getOrCreate: (workspaceId, granularity, date) => {
1016
+ tracker.workspaceId = workspaceId;
1017
+ return track(
1018
+ "calendar.getOrCreate",
1019
+ () => tana.calendar.getOrCreate(workspaceId, granularity, date)
1020
+ );
1021
+ }
1022
+ },
1023
+ import: (parentNodeId, content) => {
1024
+ trackNodeId(parentNodeId);
1025
+ return track("import", () => tana.import(parentNodeId, content));
1026
+ },
1027
+ format: (data) => tana.format(data)
1028
+ };
1029
+ }
1030
+ async function executeSandbox(code, tana, sessionId) {
1031
+ const startTime = performance.now();
1032
+ const logs = [];
1033
+ const tracker = {
1034
+ calls: [],
1035
+ nodeIds: /* @__PURE__ */ new Set(),
1036
+ workspaceId: tana.workspace?.id ?? null
1037
+ };
1038
+ const sandboxConsole = {
1039
+ log: (...args) => {
1040
+ logs.push(args.map(formatArg).join(" "));
1041
+ },
1042
+ error: (...args) => {
1043
+ logs.push(`[ERROR] ${args.map(formatArg).join(" ")}`);
1044
+ },
1045
+ warn: (...args) => {
1046
+ logs.push(`[WARN] ${args.map(formatArg).join(" ")}`);
1047
+ },
1048
+ info: (...args) => {
1049
+ logs.push(args.map(formatArg).join(" "));
1050
+ }
1051
+ };
1052
+ const effectiveSessionId = sessionId ?? crypto.randomUUID();
1053
+ const workflow = createWorkflowHelper(effectiveSessionId);
1054
+ const trackedTana = createTrackedTanaAPI(tana, tracker);
1055
+ try {
1056
+ const AsyncFunction = Object.getPrototypeOf(
1057
+ async function() {
1058
+ }
1059
+ ).constructor;
1060
+ const shadowedGlobals = [
1061
+ "process",
1062
+ "require",
1063
+ "Bun",
1064
+ "Deno",
1065
+ "globalThis",
1066
+ "global",
1067
+ "eval",
1068
+ "Function",
1069
+ "__dirname",
1070
+ "__filename",
1071
+ "module",
1072
+ "exports"
1073
+ ];
1074
+ const jsCode = transpileTS(code);
1075
+ const fn = new AsyncFunction(
1076
+ "tana",
1077
+ "console",
1078
+ "workflow",
1079
+ ...shadowedGlobals,
1080
+ jsCode
1081
+ );
1082
+ const timeoutPromise = new Promise((_, reject) => {
1083
+ setTimeout(() => {
1084
+ reject(new Error(`Execution timed out after ${EXECUTION_TIMEOUT}ms`));
1085
+ }, EXECUTION_TIMEOUT);
1086
+ });
1087
+ const undefinedArgs = shadowedGlobals.map(() => void 0);
1088
+ await Promise.race([
1089
+ fn(trackedTana, sandboxConsole, workflow, ...undefinedArgs),
1090
+ timeoutPromise
1091
+ ]);
1092
+ const durationMs = Math.round(performance.now() - startTime);
1093
+ const output = logs.join("\n");
1094
+ saveScriptRun({
1095
+ script: code,
1096
+ success: true,
1097
+ output,
1098
+ error: null,
1099
+ durationMs,
1100
+ sessionId: effectiveSessionId,
1101
+ apiCalls: tracker.calls.length > 0 ? tracker.calls : null,
1102
+ nodeIdsAffected: tracker.nodeIds.size > 0 ? Array.from(tracker.nodeIds) : null,
1103
+ workspaceId: tracker.workspaceId
1104
+ });
1105
+ return {
1106
+ success: true,
1107
+ output,
1108
+ durationMs
1109
+ };
1110
+ } catch (error) {
1111
+ const durationMs = Math.round(performance.now() - startTime);
1112
+ const errorMessage = error instanceof Error ? error.message : String(error);
1113
+ const output = logs.join("\n");
1114
+ saveScriptRun({
1115
+ script: code,
1116
+ success: false,
1117
+ output,
1118
+ error: errorMessage,
1119
+ durationMs,
1120
+ sessionId: effectiveSessionId,
1121
+ apiCalls: tracker.calls.length > 0 ? tracker.calls : null,
1122
+ nodeIdsAffected: tracker.nodeIds.size > 0 ? Array.from(tracker.nodeIds) : null,
1123
+ workspaceId: tracker.workspaceId
1124
+ });
1125
+ return {
1126
+ success: false,
1127
+ output,
1128
+ error: errorMessage,
1129
+ durationMs
1130
+ };
1131
+ }
1132
+ }
1133
+ function formatArg(arg) {
1134
+ if (arg === null) return "null";
1135
+ if (arg === void 0) return "undefined";
1136
+ if (typeof arg === "string") return arg;
1137
+ if (typeof arg === "number" || typeof arg === "boolean") return String(arg);
1138
+ try {
1139
+ return JSON.stringify(arg, null, 2);
1140
+ } catch {
1141
+ return String(arg);
1142
+ }
1143
+ }
1144
+
1145
+ // src/prompts.ts
1146
+ var TOOL_DESCRIPTION = `Execute TypeScript code to interact with Tana.
1147
+
1148
+ ## APIs
1149
+
1150
+ tana.workspace \u2192 Workspace | null (pre-resolved default workspace)
1151
+ tana.workspaces.list() \u2192 Workspace[] { id, name, homeNodeId }
1152
+
1153
+ ### Nodes
1154
+ tana.nodes.search(query, options?) \u2192 SearchResult[]
1155
+ tana.nodes.read(nodeId, maxDepth?) \u2192 string (markdown)
1156
+ tana.nodes.getChildren(nodeId, { limit?, offset? }) \u2192 { children, total, hasMore }
1157
+ tana.nodes.edit({ nodeId, name?, description? }) \u2192 { success }
1158
+ tana.nodes.move({ nodeId, targetNodeId, keepSourceReference?, position?, referenceNodeId?, sourceParentId? }) \u2192 { success }
1159
+ tana.nodes.open(nodeId, openType?) \u2192 { success } // openType: "current" | "panel" | "tab" (default: "current")
1160
+ tana.nodes.trash(nodeId) \u2192 { success }
1161
+ tana.nodes.check(nodeId) / uncheck(nodeId) \u2192 { success }
1162
+
1163
+ ### Tags
1164
+ tana.tags.listAll(workspaceId) \u2192 Tag[] (workspace supertags only \u2014 for schema analysis)
1165
+ tana.tags.getSchema(tagId) \u2192 string
1166
+ tana.tags.modify(nodeId, "add"|"remove", tagIds[])
1167
+ tana.tags.create({ workspaceId, name, description?, extendsTagIds?, showCheckbox? })
1168
+ tana.tags.addField({ tagId, name, dataType: "plain"|"number"|"date"|"url"|"email"|"checkbox"|"user"|"instance"|"options", ... })
1169
+ tana.tags.setCheckbox({ tagId, showCheckbox, doneStateMapping? })
1170
+
1171
+ ### Fields
1172
+ tana.fields.setOption(nodeId, attributeId, optionId) // optionId: string | string[] (array for multi-value)
1173
+ tana.fields.setContent(nodeId, attributeId, content, mode?) // content: string | null (null clears field), mode: "replace" | "append"
1174
+ tana.fields.getFieldOptions(fieldId, options?) \u2192 { id, name }[] // discover available options for a field
1175
+
1176
+ ### Calendar
1177
+ tana.calendar.getOrCreate(workspaceId, "day"|"week"|"month"|"year", date?)
1178
+
1179
+ ### Import
1180
+ tana.import(parentNodeId, tanaPasteContent) \u2192 { success, nodeIds? }
1181
+
1182
+ ### Formatting
1183
+ tana.format(data) \u2192 string (compact display of any API response)
1184
+
1185
+ Entry points: \`\${workspaceId}_CAPTURE_INBOX\` (inbox), \`\${workspaceId}_STASH\` (library)
1186
+
1187
+ ## Edit (search-and-replace)
1188
+
1189
+ tana.nodes.edit({ nodeId, name: { old_string, new_string }, description: { old_string, new_string } })
1190
+ Empty old_string matches absent field.
1191
+
1192
+ ## Search Query Operators
1193
+
1194
+ { textContains: string } | { textMatches: "/regex/i" }
1195
+ { hasType: tagId } | { field: { fieldId, stringValue?, numberValue?, nodeId?, state? } }
1196
+ { compare: { fieldId, operator: "gt"|"lt"|"eq", value, type } }
1197
+ { linksTo: nodeIds[] }
1198
+ { is: "done"|"todo"|"template"|"field"|"published"|"entity"|"calendarNode"|"onDayNode"|"chat"|"search"|"command"|"inLibrary" }
1199
+ { has: "tag"|"field"|"media"|"audio"|"video"|"image" }
1200
+ { created: { last: N } } | { edited: { last?, by?, since? } } | { done: { last: N } }
1201
+ { onDate: "YYYY-MM-DD" | { date, fieldId?, overlaps? } }
1202
+ { overdue: true } | { inLibrary: true }
1203
+ { and: [...] } | { or: [...] } | { not: {...} }
1204
+
1205
+ ## Default Workspace
1206
+
1207
+ tana.workspace is pre-resolved if MAIN_TANA_WORKSPACE is set. Prefer over workspaces.list(): \`const ws = tana.workspace ?? (await tana.workspaces.list())[0];\`
1208
+
1209
+ ## Output
1210
+
1211
+ console.log() output becomes LLM context. Keep it compact:
1212
+ - Use tana.format(data) for any API response, or .map() for task-specific fields
1213
+ - search returns metadata only (name, id, tags); use .read() for content and field values
1214
+ - Never JSON.stringify API responses
1215
+
1216
+ ## API Notes
1217
+
1218
+ - search: no offset/pagination \u2014 use narrower queries, not repeated calls
1219
+ - getChildren: only endpoint with pagination (limit + offset)
1220
+ - Timeout is 10s. On timeout, try a different approach, not the same call.
1221
+ - childOf/ownedBy/inWorkspace operators broken. Scope by workspace: search(query, { workspaceIds: ["id"] })
1222
+ - Tag names are not unique. Find a tag by name: search({ and: [{ hasType: "SYS_T01" }, { textContains: "name" }] })
1223
+ - search results: { id, name, breadcrumb[], tags[{id,name}], tagIds[], workspaceId, docType, description, created, inTrash }
1224
+ - getSchema output: line 1 is \`# Tag definition: name (id:xxx)\`. Line 2 is \`Extends #parent (id:xxx)\` when tag has inheritance, or \`Extends #parent (base type) (id:xxx)\` for Tana built-in types. Parse "Extends" to find relationships.
1225
+
1226
+ ## Examples
1227
+
1228
+ Search: await tana.nodes.search({ and: [{ hasType: "tagId" }, { is: "todo" }] })
1229
+
1230
+ Import:
1231
+ \`\`\`
1232
+ await tana.import(parentNodeId, \`
1233
+ - Node #[[^tagId]]
1234
+ - [[^fieldId]]:: value
1235
+ - [ ] Todo item
1236
+ \`)
1237
+ \`\`\`
1238
+
1239
+ `;
1240
+
1241
+ // src/main.ts
1242
+ async function resolveWorkspace(client) {
1243
+ const envValue = process.env.MAIN_TANA_WORKSPACE?.trim();
1244
+ if (!envValue) return null;
1245
+ let workspaces;
1246
+ try {
1247
+ workspaces = await client.get("/workspaces");
1248
+ } catch (err) {
1249
+ console.error(
1250
+ `[workspace] Failed to fetch workspaces: ${err instanceof Error ? err.message : err}`
1251
+ );
1252
+ return null;
1253
+ }
1254
+ const byId = workspaces.find((w) => w.id === envValue);
1255
+ if (byId) {
1256
+ console.error(`[workspace] Resolved "${envValue}" \u2192 ${byId.name} (${byId.id})`);
1257
+ return byId;
1258
+ }
1259
+ const lowerEnv = envValue.toLowerCase();
1260
+ const byName = workspaces.find((w) => w.name.toLowerCase() === lowerEnv);
1261
+ if (byName) {
1262
+ console.error(`[workspace] Resolved "${envValue}" \u2192 ${byName.name} (${byName.id})`);
1263
+ return byName;
1264
+ }
1265
+ const available = workspaces.map((w) => `${w.name} (${w.id})`).join(", ");
1266
+ console.error(
1267
+ `[workspace] No match for "${envValue}". Available: ${available || "none"}`
1268
+ );
1269
+ return null;
1270
+ }
1271
+ async function resolveSearchWorkspaces(client) {
1272
+ const envValue = process.env.TANA_SEARCH_WORKSPACES?.trim();
1273
+ if (!envValue) return [];
1274
+ const values = envValue.split(",").map((v) => v.trim()).filter(Boolean);
1275
+ if (values.length === 0) return [];
1276
+ let workspaces;
1277
+ try {
1278
+ workspaces = await client.get("/workspaces");
1279
+ } catch (err) {
1280
+ console.error(
1281
+ `[search-workspaces] Failed to fetch workspaces: ${err instanceof Error ? err.message : err}`
1282
+ );
1283
+ return [];
1284
+ }
1285
+ const resolvedIds = [];
1286
+ for (const value of values) {
1287
+ const byId = workspaces.find((w) => w.id === value);
1288
+ if (byId) {
1289
+ resolvedIds.push(byId.id);
1290
+ console.error(`[search-workspaces] Resolved "${value}" \u2192 ${byId.name} (${byId.id})`);
1291
+ continue;
1292
+ }
1293
+ const lowerValue = value.toLowerCase();
1294
+ const byName = workspaces.find((w) => w.name.toLowerCase() === lowerValue);
1295
+ if (byName) {
1296
+ resolvedIds.push(byName.id);
1297
+ console.error(`[search-workspaces] Resolved "${value}" \u2192 ${byName.name} (${byName.id})`);
1298
+ continue;
1299
+ }
1300
+ const available = workspaces.map((w) => `${w.name} (${w.id})`).join(", ");
1301
+ console.error(
1302
+ `[search-workspaces] No match for "${value}". Available: ${available || "none"}`
1303
+ );
1304
+ }
1305
+ return resolvedIds;
1306
+ }
1307
+ async function main() {
1308
+ initDb();
1309
+ const cleaned = cleanupOldRuns(30);
1310
+ if (cleaned > 0) {
1311
+ console.error(`Cleaned up ${cleaned} old script runs`);
1312
+ }
1313
+ const client = createClient();
1314
+ const workspace = await resolveWorkspace(client);
1315
+ const searchWorkspaceIds = await resolveSearchWorkspaces(client);
1316
+ const tana = createTanaAPI(client, workspace, searchWorkspaceIds);
1317
+ const server = new McpServer({
1318
+ name: "tana-mcp-codemode",
1319
+ version: "0.1.0"
1320
+ });
1321
+ server.registerTool(
1322
+ "execute",
1323
+ {
1324
+ description: TOOL_DESCRIPTION,
1325
+ inputSchema: {
1326
+ code: z.string().describe("TypeScript code to execute"),
1327
+ sessionId: z.string().optional().describe("Optional session ID for grouping script runs")
1328
+ }
1329
+ },
1330
+ async ({ code, sessionId }) => {
1331
+ const result = await executeSandbox(code, tana, sessionId);
1332
+ let responseText = "";
1333
+ if (result.output) {
1334
+ responseText += result.output;
1335
+ }
1336
+ if (result.error) {
1337
+ responseText += responseText ? "\n\n" : "";
1338
+ responseText += `Error: ${result.error}`;
1339
+ }
1340
+ responseText += `
1341
+
1342
+ [Executed in ${result.durationMs}ms]`;
1343
+ return {
1344
+ content: [
1345
+ {
1346
+ type: "text",
1347
+ text: responseText
1348
+ }
1349
+ ],
1350
+ isError: !result.success
1351
+ };
1352
+ }
1353
+ );
1354
+ const transport = new StdioServerTransport();
1355
+ await server.connect(transport);
1356
+ console.error("Tana MCP server started");
1357
+ }
1358
+
1359
+ // src/entry-node.ts
1360
+ initCompat(nodeCompat);
1361
+ main().catch((error) => {
1362
+ console.error("Fatal error:", error);
1363
+ process.exit(1);
1364
+ });