mcp-tabula-api 3.0.0 → 3.1.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/dist/index.js +91 -85
- package/package.json +2 -2
- package/dist/index.sdk.js +0 -28588
package/dist/index.js
CHANGED
|
@@ -30982,8 +30982,6 @@ var EMPTY_COMPLETION_RESULT = {
|
|
|
30982
30982
|
import * as path from "path";
|
|
30983
30983
|
import * as fs from "fs/promises";
|
|
30984
30984
|
import { existsSync } from "fs";
|
|
30985
|
-
import { exec as exec2 } from "child_process";
|
|
30986
|
-
import { promisify as promisify2 } from "util";
|
|
30987
30985
|
|
|
30988
30986
|
// src/utils/tabula-api.ts
|
|
30989
30987
|
import { exec } from "child_process";
|
|
@@ -31009,21 +31007,15 @@ async function apiKey() {
|
|
|
31009
31007
|
} catch {}
|
|
31010
31008
|
throw new Error("Tabula API key not found. Set TABULA_API_KEY in this server's MCP " + "registration (copy it from Tabula → Settings → Headless API).");
|
|
31011
31009
|
}
|
|
31012
|
-
|
|
31013
|
-
async function postNote(body) {
|
|
31014
|
-
const payload = { actor_kind: "agent", actor_id: DEFAULT_ACTOR_ID, ...body };
|
|
31010
|
+
async function apiFetch(pathAndQuery, init) {
|
|
31015
31011
|
let res;
|
|
31016
31012
|
try {
|
|
31017
|
-
res = await fetch(`${API_BASE}
|
|
31018
|
-
|
|
31019
|
-
headers: {
|
|
31020
|
-
"Content-Type": "application/json",
|
|
31021
|
-
"x-api-key": await apiKey()
|
|
31022
|
-
},
|
|
31023
|
-
body: JSON.stringify(payload)
|
|
31013
|
+
res = await fetch(`${API_BASE}${pathAndQuery}`, {
|
|
31014
|
+
...init,
|
|
31015
|
+
headers: { "x-api-key": await apiKey(), ...init?.headers || {} }
|
|
31024
31016
|
});
|
|
31025
31017
|
} catch (err) {
|
|
31026
|
-
throw new Error(`Cannot reach Tabula Headless API at ${API_BASE} (${err?.cause?.code || err.message}). ` + "
|
|
31018
|
+
throw new Error(`Cannot reach Tabula Headless API at ${API_BASE} (${err?.cause?.code || err.message}). ` + "The Tabula app must be running — this server talks to the vault only " + "through its headless API (reads and writes both).");
|
|
31027
31019
|
}
|
|
31028
31020
|
const text = await res.text();
|
|
31029
31021
|
if (!res.ok) {
|
|
@@ -31031,10 +31023,38 @@ async function postNote(body) {
|
|
|
31031
31023
|
}
|
|
31032
31024
|
return text;
|
|
31033
31025
|
}
|
|
31026
|
+
var DEFAULT_ACTOR_ID = process.env.TABULA_ACTOR_ID || "claude";
|
|
31027
|
+
async function postNote(body) {
|
|
31028
|
+
const payload = { actor_kind: "agent", actor_id: DEFAULT_ACTOR_ID, ...body };
|
|
31029
|
+
return apiFetch(`/api/notes`, {
|
|
31030
|
+
method: "POST",
|
|
31031
|
+
headers: { "Content-Type": "application/json" },
|
|
31032
|
+
body: JSON.stringify(payload)
|
|
31033
|
+
});
|
|
31034
|
+
}
|
|
31035
|
+
async function getNote(relPath) {
|
|
31036
|
+
const text = await apiFetch(`/api/notes?path=${encodeURIComponent(relPath)}`);
|
|
31037
|
+
const json2 = JSON.parse(text);
|
|
31038
|
+
return json2.content;
|
|
31039
|
+
}
|
|
31040
|
+
async function search(q, limit = 20) {
|
|
31041
|
+
const text = await apiFetch(`/api/search?q=${encodeURIComponent(q)}&limit=${limit}`);
|
|
31042
|
+
return JSON.parse(text);
|
|
31043
|
+
}
|
|
31044
|
+
var cachedVaultPath = null;
|
|
31045
|
+
async function vaultPath() {
|
|
31046
|
+
if (cachedVaultPath)
|
|
31047
|
+
return cachedVaultPath;
|
|
31048
|
+
const text = await apiFetch(`/api/status`);
|
|
31049
|
+
const json2 = JSON.parse(text);
|
|
31050
|
+
if (!json2.vault_path) {
|
|
31051
|
+
throw new Error("Tabula API /api/status returned no vault_path.");
|
|
31052
|
+
}
|
|
31053
|
+
cachedVaultPath = json2.vault_path;
|
|
31054
|
+
return cachedVaultPath;
|
|
31055
|
+
}
|
|
31034
31056
|
|
|
31035
31057
|
// src/mcp.ts
|
|
31036
|
-
var execAsync2 = promisify2(exec2);
|
|
31037
|
-
var VAULT_DST = "/home/takaki2/Documents/tabula_vault";
|
|
31038
31058
|
function injectOrReplaceId(content, regenerateId) {
|
|
31039
31059
|
const newId = crypto.randomUUID();
|
|
31040
31060
|
const eol = content.includes(`\r
|
|
@@ -31059,10 +31079,11 @@ function injectOrReplaceId(content, regenerateId) {
|
|
|
31059
31079
|
return fm + content;
|
|
31060
31080
|
}
|
|
31061
31081
|
}
|
|
31082
|
+
var TEXT_ONLY_NOTICE = "Error: Tabula's vault is text-only — only .md files can be imported through " + "the API. Media/binaries live outside the vault by design.";
|
|
31062
31083
|
function createMcpServer() {
|
|
31063
31084
|
const server = new McpServer({
|
|
31064
31085
|
name: "tabula-mcp-api",
|
|
31065
|
-
version: "1.
|
|
31086
|
+
version: "3.1.0"
|
|
31066
31087
|
}, {
|
|
31067
31088
|
instructions: [
|
|
31068
31089
|
"tabula_vault は「契約を持つ」vault です。ノートを書く/整理する前に、まず契約を読んでください(自動探索でなく明示ロード):",
|
|
@@ -31078,12 +31099,7 @@ function createMcpServer() {
|
|
|
31078
31099
|
rel_path: exports_external.string().describe("Relative path inside tabula_vault.")
|
|
31079
31100
|
}, async ({ rel_path }) => {
|
|
31080
31101
|
try {
|
|
31081
|
-
const
|
|
31082
|
-
if (!targetPath.startsWith(VAULT_DST))
|
|
31083
|
-
return { content: [{ type: "text", text: `Error: Path traversal.` }] };
|
|
31084
|
-
if (!existsSync(targetPath))
|
|
31085
|
-
return { content: [{ type: "text", text: `Error: File not found.` }] };
|
|
31086
|
-
const text = await fs.readFile(targetPath, "utf-8");
|
|
31102
|
+
const text = await getNote(rel_path);
|
|
31087
31103
|
return { content: [{ type: "text", text }] };
|
|
31088
31104
|
} catch (err) {
|
|
31089
31105
|
return { content: [{ type: "text", text: `Failed: ${err.message}` }] };
|
|
@@ -31109,110 +31125,100 @@ function createMcpServer() {
|
|
|
31109
31125
|
}
|
|
31110
31126
|
});
|
|
31111
31127
|
server.tool("tabula_copy", "Copy a markdown file into tabula_vault. It automatically generates a NEW UUID to prevent ID collision. Use this for importing or duplicating.", {
|
|
31112
|
-
src_abs_path: exports_external.string().describe("Absolute path of the source file (can be inside or outside tabula_vault)."),
|
|
31128
|
+
src_abs_path: exports_external.string().describe("Absolute path of the source .md file (can be inside or outside tabula_vault)."),
|
|
31113
31129
|
dest_rel_path: exports_external.string().describe("Destination relative path inside tabula_vault.")
|
|
31114
31130
|
}, async ({ src_abs_path, dest_rel_path }) => {
|
|
31115
31131
|
try {
|
|
31116
31132
|
if (!existsSync(src_abs_path))
|
|
31117
31133
|
return { content: [{ type: "text", text: `Error: Source not found.` }] };
|
|
31118
|
-
if (src_abs_path.endsWith(".md"))
|
|
31119
|
-
|
|
31120
|
-
|
|
31121
|
-
|
|
31122
|
-
|
|
31123
|
-
const dstPath = path.join(VAULT_DST, dest_rel_path);
|
|
31124
|
-
if (!dstPath.startsWith(VAULT_DST))
|
|
31125
|
-
return { content: [{ type: "text", text: `Error: Invalid destination.` }] };
|
|
31126
|
-
await fs.mkdir(path.dirname(dstPath), { recursive: true });
|
|
31127
|
-
await fs.copyFile(src_abs_path, dstPath);
|
|
31128
|
-
}
|
|
31134
|
+
if (!src_abs_path.endsWith(".md"))
|
|
31135
|
+
return { content: [{ type: "text", text: TEXT_ONLY_NOTICE }] };
|
|
31136
|
+
const content = await fs.readFile(src_abs_path, "utf-8");
|
|
31137
|
+
const processed = injectOrReplaceId(content, true);
|
|
31138
|
+
await postNote({ path: dest_rel_path, mode: "overwrite", content: processed });
|
|
31129
31139
|
return { content: [{ type: "text", text: `Success: Copied to '${dest_rel_path}' with regenerated UUID (via Tabula API).` }] };
|
|
31130
31140
|
} catch (err) {
|
|
31131
31141
|
return { content: [{ type: "text", text: `Failed: ${err.message}` }] };
|
|
31132
31142
|
}
|
|
31133
31143
|
});
|
|
31134
31144
|
server.tool("tabula_cut", "Move (cut) a file into tabula_vault. It preserves the existing UUID to keep links intact. Automatically injects one if missing.", {
|
|
31135
|
-
src_abs_path: exports_external.string().describe("Absolute path of the source file (can be inside or outside tabula_vault)."),
|
|
31145
|
+
src_abs_path: exports_external.string().describe("Absolute path of the source .md file (can be inside or outside tabula_vault)."),
|
|
31136
31146
|
dest_rel_path: exports_external.string().describe("Destination relative path inside tabula_vault.")
|
|
31137
31147
|
}, async ({ src_abs_path, dest_rel_path }) => {
|
|
31138
31148
|
try {
|
|
31139
|
-
|
|
31140
|
-
|
|
31141
|
-
|
|
31142
|
-
|
|
31143
|
-
const srcRel = path.relative(VAULT_DST, src_abs_path);
|
|
31149
|
+
const vault = await vaultPath();
|
|
31150
|
+
const vaultRoot = vault.replace(/\/+$/, "");
|
|
31151
|
+
if (src_abs_path === vaultRoot || src_abs_path.startsWith(vaultRoot + "/")) {
|
|
31152
|
+
const srcRel = path.relative(vaultRoot, src_abs_path);
|
|
31144
31153
|
await postNote({ path: srcRel, mode: "move", dest: dest_rel_path });
|
|
31145
31154
|
return { content: [{ type: "text", text: `Success: Moved to '${dest_rel_path}' while preserving UUID (via Tabula API).` }] };
|
|
31146
31155
|
}
|
|
31147
|
-
if (src_abs_path
|
|
31148
|
-
|
|
31149
|
-
|
|
31150
|
-
|
|
31151
|
-
|
|
31152
|
-
|
|
31153
|
-
|
|
31154
|
-
|
|
31155
|
-
return { content: [{ type: "text", text: `Error: Invalid destination.` }] };
|
|
31156
|
-
await fs.mkdir(path.dirname(dstPath), { recursive: true });
|
|
31157
|
-
await fs.copyFile(src_abs_path, dstPath);
|
|
31158
|
-
await fs.unlink(src_abs_path);
|
|
31159
|
-
}
|
|
31156
|
+
if (!existsSync(src_abs_path))
|
|
31157
|
+
return { content: [{ type: "text", text: `Error: Source not found.` }] };
|
|
31158
|
+
if (!src_abs_path.endsWith(".md"))
|
|
31159
|
+
return { content: [{ type: "text", text: TEXT_ONLY_NOTICE }] };
|
|
31160
|
+
const content = await fs.readFile(src_abs_path, "utf-8");
|
|
31161
|
+
const processed = injectOrReplaceId(content, false);
|
|
31162
|
+
await postNote({ path: dest_rel_path, mode: "overwrite", content: processed });
|
|
31163
|
+
await fs.unlink(src_abs_path);
|
|
31160
31164
|
return { content: [{ type: "text", text: `Success: Moved to '${dest_rel_path}' while preserving UUID (via Tabula API).` }] };
|
|
31161
31165
|
} catch (err) {
|
|
31162
31166
|
return { content: [{ type: "text", text: `Failed: ${err.message}` }] };
|
|
31163
31167
|
}
|
|
31164
31168
|
});
|
|
31165
|
-
server.tool("tabula_eye", "List directory contents in tabula_vault. Returns files and folders.", {
|
|
31169
|
+
server.tool("tabula_eye", "List directory contents in tabula_vault. Returns files and folders (immediate children of rel_path).", {
|
|
31166
31170
|
rel_path: exports_external.string().describe("Directory relative path inside tabula_vault (use '' for root).")
|
|
31167
31171
|
}, async ({ rel_path }) => {
|
|
31168
31172
|
try {
|
|
31169
|
-
const
|
|
31170
|
-
|
|
31171
|
-
|
|
31172
|
-
|
|
31173
|
-
|
|
31174
|
-
|
|
31175
|
-
|
|
31173
|
+
const all = await search("", 1e5);
|
|
31174
|
+
const prefix = rel_path ? rel_path.replace(/^\/+|\/+$/g, "") + "/" : "";
|
|
31175
|
+
const children = new Map;
|
|
31176
|
+
for (const r of all) {
|
|
31177
|
+
if (prefix && !r.name.startsWith(prefix))
|
|
31178
|
+
continue;
|
|
31179
|
+
const rest = r.name.slice(prefix.length);
|
|
31180
|
+
if (!rest)
|
|
31181
|
+
continue;
|
|
31182
|
+
const slash = rest.indexOf("/");
|
|
31183
|
+
if (slash === -1)
|
|
31184
|
+
children.set(rest, false);
|
|
31185
|
+
else
|
|
31186
|
+
children.set(rest.slice(0, slash), true);
|
|
31187
|
+
}
|
|
31188
|
+
if (children.size === 0)
|
|
31189
|
+
return { content: [{ type: "text", text: "(empty directory)" }] };
|
|
31190
|
+
const list = [...children.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([n, isDir]) => `${isDir ? "[DIR]" : "[FILE]"} ${n}`).join(`
|
|
31176
31191
|
`);
|
|
31177
|
-
return { content: [{ type: "text", text: list
|
|
31192
|
+
return { content: [{ type: "text", text: list }] };
|
|
31178
31193
|
} catch (err) {
|
|
31179
31194
|
return { content: [{ type: "text", text: `Failed: ${err.message}` }] };
|
|
31180
31195
|
}
|
|
31181
31196
|
});
|
|
31182
|
-
server.tool("tabula_find", "Find
|
|
31183
|
-
query: exports_external.string().describe("Filename or keyword to
|
|
31197
|
+
server.tool("tabula_find", "Find notes by filename in tabula_vault (fuzzy match on the path).", {
|
|
31198
|
+
query: exports_external.string().describe("Filename or keyword to fuzzy-match against note paths.")
|
|
31184
31199
|
}, async ({ query }) => {
|
|
31185
31200
|
try {
|
|
31186
|
-
const
|
|
31187
|
-
if (!
|
|
31201
|
+
const results = await search(query, 20);
|
|
31202
|
+
if (!results.length)
|
|
31188
31203
|
return { content: [{ type: "text", text: `No files found matching '${query}'` }] };
|
|
31189
|
-
|
|
31190
|
-
`)
|
|
31191
|
-
`);
|
|
31192
|
-
return { content: [{ type: "text", text: relativePaths }] };
|
|
31204
|
+
return { content: [{ type: "text", text: results.map((r) => r.name).join(`
|
|
31205
|
+
`) }] };
|
|
31193
31206
|
} catch (err) {
|
|
31194
|
-
return { content: [{ type: "text", text: `
|
|
31207
|
+
return { content: [{ type: "text", text: `Failed: ${err.message}` }] };
|
|
31195
31208
|
}
|
|
31196
31209
|
});
|
|
31197
|
-
server.tool("tabula_grep", "
|
|
31198
|
-
query: exports_external.string().describe("Text
|
|
31210
|
+
server.tool("tabula_grep", "Full-text search over note contents in tabula_vault. Returns matching notes with snippets.", {
|
|
31211
|
+
query: exports_external.string().describe("Text to search inside note contents.")
|
|
31199
31212
|
}, async ({ query }) => {
|
|
31200
31213
|
try {
|
|
31201
|
-
const
|
|
31202
|
-
if (!
|
|
31214
|
+
const results = await search("@" + query, 20);
|
|
31215
|
+
if (!results.length)
|
|
31203
31216
|
return { content: [{ type: "text", text: `No matches found for '${query}'` }] };
|
|
31204
|
-
const
|
|
31205
|
-
`).map((line) => {
|
|
31206
|
-
const absPath = line.substring(0, line.indexOf(":"));
|
|
31207
|
-
if (absPath.startsWith(VAULT_DST)) {
|
|
31208
|
-
return path.relative(VAULT_DST, absPath) + line.substring(line.indexOf(":"));
|
|
31209
|
-
}
|
|
31210
|
-
return line;
|
|
31211
|
-
}).join(`
|
|
31217
|
+
const lines = results.map((r) => r.snippet ? `${r.name}: ${r.snippet}` : r.name).join(`
|
|
31212
31218
|
`);
|
|
31213
|
-
return { content: [{ type: "text", text:
|
|
31219
|
+
return { content: [{ type: "text", text: lines }] };
|
|
31214
31220
|
} catch (err) {
|
|
31215
|
-
return { content: [{ type: "text", text: `
|
|
31221
|
+
return { content: [{ type: "text", text: `Failed: ${err.message}` }] };
|
|
31216
31222
|
}
|
|
31217
31223
|
});
|
|
31218
31224
|
return server;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-tabula-api",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "MCP bridge that lets a cloud AI (Claude, etc.) drive a local Tabula vault via its Headless API.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -31,4 +31,4 @@
|
|
|
31
31
|
"hono": "^4.0.0",
|
|
32
32
|
"zod": "^4.3.5"
|
|
33
33
|
}
|
|
34
|
-
}
|
|
34
|
+
}
|