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.
- package/README.md +71 -259
- package/dist/entry-node.js +1364 -0
- package/package.json +28 -26
- package/src/api/tana.ts +230 -34
- package/src/compat/bun.ts +39 -0
- package/src/compat/index.ts +29 -0
- package/src/compat/node.ts +62 -0
- package/src/compat/types.ts +15 -0
- package/src/debug-server.ts +232 -0
- package/src/entry-bun.ts +11 -0
- package/src/entry-node.ts +11 -0
- package/src/index.ts +5 -162
- package/src/main.ts +146 -0
- package/src/prompts.ts +2 -1
- package/src/sandbox/executor.ts +4 -2
- package/src/sandbox/workflow.ts +4 -4
- package/src/storage/history.ts +11 -11
|
@@ -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
|
+
});
|
package/src/entry-bun.ts
ADDED
|
@@ -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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
@@ -28,8 +28,9 @@ tana.tags.addField({ tagId, name, dataType: "plain"|"number"|"date"|"url"|"email
|
|
|
28
28
|
tana.tags.setCheckbox({ tagId, showCheckbox, doneStateMapping? })
|
|
29
29
|
|
|
30
30
|
### Fields
|
|
31
|
-
tana.fields.setOption(nodeId, attributeId, optionId
|
|
31
|
+
tana.fields.setOption(nodeId, attributeId, optionId) // optionId: string | string[] (array for multi-value)
|
|
32
32
|
tana.fields.setContent(nodeId, attributeId, content, mode?) // content: string | null (null clears field), mode: "replace" | "append"
|
|
33
|
+
tana.fields.getFieldOptions(fieldId, options?) → { id, name }[] // discover available options for a field
|
|
33
34
|
|
|
34
35
|
### Calendar
|
|
35
36
|
tana.calendar.getOrCreate(workspaceId, "day"|"week"|"month"|"year", date?)
|
package/src/sandbox/executor.ts
CHANGED
|
@@ -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
|
|
|
@@ -110,6 +111,8 @@ function createTrackedTanaAPI(
|
|
|
110
111
|
trackNodeId(nodeId);
|
|
111
112
|
return track("fields.setContent", () => tana.fields.setContent(nodeId, attributeId, content));
|
|
112
113
|
},
|
|
114
|
+
getFieldOptions: (...args) =>
|
|
115
|
+
track("fields.getFieldOptions", () => tana.fields.getFieldOptions(...args)),
|
|
113
116
|
},
|
|
114
117
|
|
|
115
118
|
calendar: {
|
|
@@ -190,8 +193,7 @@ export async function executeSandbox(
|
|
|
190
193
|
"exports",
|
|
191
194
|
];
|
|
192
195
|
|
|
193
|
-
const
|
|
194
|
-
const jsCode = transpiler.transformSync(code);
|
|
196
|
+
const jsCode = transpileTS(code);
|
|
195
197
|
|
|
196
198
|
const fn = new AsyncFunction(
|
|
197
199
|
"tana",
|
package/src/sandbox/workflow.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* workflow.complete("Done!");
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
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:
|
|
48
|
+
function ensureWorkflowTable(db: CompatDatabase): void {
|
|
49
49
|
if (workflowTableInitialized) return;
|
|
50
50
|
|
|
51
|
-
db.
|
|
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.
|
|
62
|
+
db.exec(`
|
|
63
63
|
CREATE INDEX IF NOT EXISTS idx_workflow_session
|
|
64
64
|
ON workflow_events(session_id, timestamp)
|
|
65
65
|
`);
|