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 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
- var DEFAULT_ACTOR_ID = process.env.TABULA_ACTOR_ID || "claude";
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}/api/notes`, {
31018
- method: "POST",
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}). ` + "Vault writes require the Tabula app to be running — all mutations go " + "through its audited write path.");
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.7.0"
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 targetPath = path.join(VAULT_DST, rel_path);
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
- const content = await fs.readFile(src_abs_path, "utf-8");
31120
- const processed = injectOrReplaceId(content, true);
31121
- await postNote({ path: dest_rel_path, mode: "overwrite", content: processed });
31122
- } else {
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
- if (!existsSync(src_abs_path))
31140
- return { content: [{ type: "text", text: `Error: Source not found.` }] };
31141
- const srcInVault = src_abs_path.startsWith(VAULT_DST + "/");
31142
- if (srcInVault) {
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.endsWith(".md")) {
31148
- const content = await fs.readFile(src_abs_path, "utf-8");
31149
- const processed = injectOrReplaceId(content, false);
31150
- await postNote({ path: dest_rel_path, mode: "overwrite", content: processed });
31151
- await fs.unlink(src_abs_path);
31152
- } else {
31153
- const dstPath = path.join(VAULT_DST, dest_rel_path);
31154
- if (!dstPath.startsWith(VAULT_DST))
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 targetPath = path.join(VAULT_DST, rel_path || ".");
31170
- if (!targetPath.startsWith(VAULT_DST))
31171
- return { content: [{ type: "text", text: `Error: Path traversal.` }] };
31172
- if (!existsSync(targetPath))
31173
- return { content: [{ type: "text", text: `Error: Directory not found.` }] };
31174
- const items = await fs.readdir(targetPath, { withFileTypes: true });
31175
- const list = items.map((i) => `${i.isDirectory() ? "[DIR]" : "[FILE]"} ${i.name}`).join(`
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 || "(empty directory)" }] };
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 files by filename in tabula_vault.", {
31183
- query: exports_external.string().describe("Filename or keyword to search for.")
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 { stdout } = await execAsync2(`find "${VAULT_DST}" -type f -iname "*${query}*" | head -n 20`);
31187
- if (!stdout.trim())
31201
+ const results = await search(query, 20);
31202
+ if (!results.length)
31188
31203
  return { content: [{ type: "text", text: `No files found matching '${query}'` }] };
31189
- const relativePaths = stdout.trim().split(`
31190
- `).map((p) => path.relative(VAULT_DST, p)).join(`
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: `Search failed or no matches found.` }] };
31207
+ return { content: [{ type: "text", text: `Failed: ${err.message}` }] };
31195
31208
  }
31196
31209
  });
31197
- server.tool("tabula_grep", "Search file contents using regular expressions in tabula_vault.", {
31198
- query: exports_external.string().describe("Text or regex to search inside files.")
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 { stdout } = await execAsync2(`grep -rnI "${query}" "${VAULT_DST}" | head -n 20`);
31202
- if (!stdout.trim())
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 relativePaths = stdout.trim().split(`
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: relativePaths }] };
31219
+ return { content: [{ type: "text", text: lines }] };
31214
31220
  } catch (err) {
31215
- return { content: [{ type: "text", text: `Search failed or no matches found.` }] };
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.0.0",
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
+ }