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/README.md +71 -259
- package/dist/entry-node.js +1364 -0
- package/package.json +28 -26
- 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/sandbox/executor.ts +2 -2
- package/src/sandbox/workflow.ts +4 -4
- package/src/storage/history.ts +11 -11
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/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
|
|
|
@@ -192,8 +193,7 @@ export async function executeSandbox(
|
|
|
192
193
|
"exports",
|
|
193
194
|
];
|
|
194
195
|
|
|
195
|
-
const
|
|
196
|
-
const jsCode = transpiler.transformSync(code);
|
|
196
|
+
const jsCode = transpileTS(code);
|
|
197
197
|
|
|
198
198
|
const fn = new AsyncFunction(
|
|
199
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
|
`);
|
package/src/storage/history.ts
CHANGED
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
* Configure database location via TANA_HISTORY_PATH env var.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
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:
|
|
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:
|
|
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.
|
|
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():
|
|
71
|
+
export function initDb(): CompatDatabase {
|
|
72
72
|
if (db) return db;
|
|
73
73
|
|
|
74
74
|
const dbPath = getDbPath();
|
|
75
|
-
db =
|
|
75
|
+
db = createDatabase(dbPath);
|
|
76
76
|
|
|
77
|
-
db.
|
|
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.
|
|
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.
|
|
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.
|
|
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 {
|