@graypirate/tabula-mcp 1.0.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 ADDED
@@ -0,0 +1,59 @@
1
+ # Tabula MCP
2
+
3
+ Local stdio MCP server for Tabula. This package is a transport adapter and uses Tabula exclusively through the public `@graypirate/tabula` package API.
4
+
5
+ ## Global Installation
6
+
7
+ After the package is published, this installs the `tabula-mcp` executable and a compatible Tabula Core library from the npm registry. [Install Tabula Core globally](../core/README.md#global-installation) as well when you want its standalone CLI.
8
+
9
+ ```bash
10
+ bun add --global @graypirate/tabula-mcp
11
+ ```
12
+
13
+ Verify:
14
+ ```bash
15
+ command -v tabula-mcp
16
+ ```
17
+
18
+ Upgrade or remove the MCP package with Bun:
19
+ ```bash
20
+ bun update --global @graypirate/tabula-mcp
21
+ bun remove --global @graypirate/tabula-mcp
22
+ ```
23
+
24
+ Tabula MCP uses stdio. The MCP client starts and owns the server process; do not run it as a background daemon.
25
+
26
+ ## Configure a Client
27
+
28
+ ```json
29
+ {
30
+ "mcpServers": {
31
+ "tabula": {
32
+ "command": "tabula-mcp",
33
+ "args": []
34
+ }
35
+ }
36
+ }
37
+ ```
38
+
39
+ The client launches the server over stdio. No daemon is required. For desktop
40
+ clients that do not inherit the shell `PATH`, use the absolute path returned by
41
+ `command -v tabula-mcp`.
42
+
43
+ ## Tools
44
+
45
+ - `tabula_initialize_workspace`
46
+ - `tabula_list_workspaces`
47
+ - `tabula_read_workspace`
48
+ - `tabula_list_workspace_entities`
49
+ - `tabula_read_entity`
50
+ - `tabula_list_entity_children`
51
+ - `tabula_search_entities`
52
+ - `tabula_create_object`
53
+ - `tabula_create_block`
54
+ - `tabula_write_entity`
55
+ - `tabula_delete_entity`
56
+ - `tabula_delete_workspace`
57
+
58
+ Each tool accepts one JSON object. The server does not accept CLI arguments,
59
+ stdin payloads, file payloads, SQLite paths, or hidden secondary inputs.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bun
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ export declare function createTabulaMCPServer(): McpServer;
package/dist/index.js ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env bun
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import packageJSON from "../package.json" with { type: "json" };
5
+ import { registerTabulaTools } from "./tools/tabula.js";
6
+ export function createTabulaMCPServer() {
7
+ const server = new McpServer({
8
+ name: "tabula-mcp-server",
9
+ version: packageJSON.version,
10
+ });
11
+ registerTabulaTools(server);
12
+ return server;
13
+ }
14
+ async function main() {
15
+ const server = createTabulaMCPServer();
16
+ const transport = new StdioServerTransport();
17
+ await server.connect(transport);
18
+ }
19
+ if (import.meta.main) {
20
+ await main().catch((error) => {
21
+ console.error("Fatal MCP server error:", error);
22
+ process.exit(1);
23
+ });
24
+ }
@@ -0,0 +1,33 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ export declare class MCPInputError extends Error {
4
+ readonly code: string;
5
+ readonly details?: unknown | undefined;
6
+ readonly name = "MCPInputError";
7
+ constructor(code: string, message: string, details?: unknown | undefined);
8
+ }
9
+ type ToolAnnotations = {
10
+ readOnlyHint: boolean;
11
+ destructiveHint: boolean;
12
+ idempotentHint: boolean;
13
+ openWorldHint: boolean;
14
+ };
15
+ type ToolResult = {
16
+ content: [{
17
+ type: "text";
18
+ text: string;
19
+ }];
20
+ structuredContent: Record<string, unknown>;
21
+ isError?: true;
22
+ };
23
+ type TabulaTool = {
24
+ name: string;
25
+ title: string;
26
+ description: string;
27
+ inputSchema: z.ZodObject<z.ZodRawShape>;
28
+ annotations: ToolAnnotations;
29
+ execute: (input: unknown) => ToolResult;
30
+ };
31
+ export declare const tabulaTools: TabulaTool[];
32
+ export declare function registerTabulaTools(server: McpServer): void;
33
+ export {};
@@ -0,0 +1,388 @@
1
+ import { z } from "zod";
2
+ import { create, deleteEntity, deleteWorkspace, initializeWorkspace, InvalidWorkspaceNameError, listEntity, listWorkspaceNames, listWorkspace, openWorkspace, readEntity, readWorkspace, search, validateWriteInput, validateWorkspaceName, writeEntity, TabulaInputError, } from "@graypirate/tabula";
3
+ export class MCPInputError extends Error {
4
+ code;
5
+ details;
6
+ name = "MCPInputError";
7
+ constructor(code, message, details) {
8
+ super(message);
9
+ this.code = code;
10
+ this.details = details;
11
+ }
12
+ }
13
+ const workspaceSchema = z.object({
14
+ workspace: z.string().min(1),
15
+ }).strict();
16
+ const emptySchema = z.object({}).strict();
17
+ const entityIDSchema = z.object({
18
+ workspace: z.string().min(1),
19
+ id: z.string().min(1),
20
+ }).strict();
21
+ const searchSchema = z.object({
22
+ workspace: z.string().min(1),
23
+ query: z.string(),
24
+ type: z.enum(["object", "block"]).optional(),
25
+ }).strict();
26
+ const propertiesSchema = z.record(z.unknown()).optional();
27
+ const createObjectSchema = z.object({
28
+ workspace: z.string().min(1),
29
+ name: z.string(),
30
+ properties: propertiesSchema,
31
+ parentID: z.string().min(1).optional(),
32
+ }).strict();
33
+ const createBlockSchema = z.object({
34
+ workspace: z.string().min(1),
35
+ content: z.string(),
36
+ properties: propertiesSchema,
37
+ parentID: z.string().min(1),
38
+ }).strict();
39
+ const writeEntitySchema = z.object({
40
+ workspace: z.string().min(1),
41
+ parentID: z.string().min(1).optional(),
42
+ entity: z.unknown(),
43
+ }).strict();
44
+ export const tabulaTools = [
45
+ {
46
+ name: "tabula_initialize_workspace",
47
+ title: "Initialize Tabula Workspace",
48
+ description: "Create or open a managed Tabula workspace by workspace name.",
49
+ inputSchema: workspaceSchema,
50
+ annotations: mutation({ idempotentHint: true }),
51
+ execute(input) {
52
+ const { workspace } = parseWorkspaceInput(input);
53
+ return result(withInitializedWorkspace(workspace, readWorkspace));
54
+ },
55
+ },
56
+ {
57
+ name: "tabula_list_workspaces",
58
+ title: "List Tabula Workspaces",
59
+ description: "List managed Tabula workspace names.",
60
+ inputSchema: emptySchema,
61
+ annotations: readOnly(),
62
+ execute(input) {
63
+ emptySchema.parse(input);
64
+ return result({ workspaces: listWorkspaceNames() });
65
+ },
66
+ },
67
+ {
68
+ name: "tabula_read_workspace",
69
+ title: "Read Tabula Workspace",
70
+ description: "Read workspace metadata by workspace name.",
71
+ inputSchema: workspaceSchema,
72
+ annotations: readOnly(),
73
+ execute(input) {
74
+ const { workspace } = parseWorkspaceInput(input);
75
+ return result(withWorkspace(workspace, readWorkspace));
76
+ },
77
+ },
78
+ {
79
+ name: "tabula_list_workspace_entities",
80
+ title: "List Workspace Root Entities",
81
+ description: "List ordered root object IDs for a workspace name.",
82
+ inputSchema: workspaceSchema,
83
+ annotations: readOnly(),
84
+ execute(input) {
85
+ const { workspace } = parseWorkspaceInput(input);
86
+ return result(withWorkspace(workspace, (db) => ({ objectIDs: listWorkspace(db) })));
87
+ },
88
+ },
89
+ {
90
+ name: "tabula_read_entity",
91
+ title: "Read Tabula Entity",
92
+ description: "Read one object or block as a parent-aware recursive entity tree.",
93
+ inputSchema: entityIDSchema,
94
+ annotations: readOnly(),
95
+ execute(input) {
96
+ const { workspace, id } = parseEntityIDInput(input);
97
+ rejectWorkspaceID(id);
98
+ return result(withWorkspace(workspace, (db) => readEntity(db, id)));
99
+ },
100
+ },
101
+ {
102
+ name: "tabula_list_entity_children",
103
+ title: "List Entity Children",
104
+ description: "List ordered direct child IDs for one object or block.",
105
+ inputSchema: entityIDSchema,
106
+ annotations: readOnly(),
107
+ execute(input) {
108
+ const { workspace, id } = parseEntityIDInput(input);
109
+ rejectWorkspaceID(id);
110
+ return result(withWorkspace(workspace, (db) => ({ childIDs: listEntity(db, id) })));
111
+ },
112
+ },
113
+ {
114
+ name: "tabula_search_entities",
115
+ title: "Search Tabula Entities",
116
+ description: "Search object names/properties and block content/properties in a workspace.",
117
+ inputSchema: searchSchema,
118
+ annotations: readOnly(),
119
+ execute(input) {
120
+ const { workspace, query, type } = parseSearchInput(input);
121
+ return result(withWorkspace(workspace, (db) => ({ results: search(db, query, type) })));
122
+ },
123
+ },
124
+ {
125
+ name: "tabula_create_object",
126
+ title: "Create Tabula Object",
127
+ description: "Create one named object with optional properties and optional parent.",
128
+ inputSchema: createObjectSchema,
129
+ annotations: mutation({ idempotentHint: false }),
130
+ execute(input) {
131
+ const { workspace, createInput, parentID } = parseCreateObjectInput(input);
132
+ return result(withWorkspace(workspace, (db) => create(db, createInput, parentOptions(parentID))));
133
+ },
134
+ },
135
+ {
136
+ name: "tabula_create_block",
137
+ title: "Create Tabula Block",
138
+ description: "Create one content block with optional properties under an object or block parent.",
139
+ inputSchema: createBlockSchema,
140
+ annotations: mutation({ idempotentHint: false }),
141
+ execute(input) {
142
+ const { workspace, createInput, parentID } = parseCreateBlockInput(input);
143
+ return result(withWorkspace(workspace, (db) => create(db, createInput, parentOptions(parentID))));
144
+ },
145
+ },
146
+ {
147
+ name: "tabula_write_entity",
148
+ title: "Write Tabula Entity Tree",
149
+ description: "Create or replace one recursive public object or block entity tree.",
150
+ inputSchema: writeEntitySchema,
151
+ annotations: mutation({ idempotentHint: false }),
152
+ execute(input) {
153
+ const { workspace, entity, parentID } = parseWriteEntityInput(input);
154
+ return result(withWorkspace(workspace, (db) => writeEntity(db, entity, parentOptions(parentID))));
155
+ },
156
+ },
157
+ {
158
+ name: "tabula_delete_entity",
159
+ title: "Delete Tabula Entity",
160
+ description: "Delete one object or block and its descendants.",
161
+ inputSchema: entityIDSchema,
162
+ annotations: destructive(),
163
+ execute(input) {
164
+ const { workspace, id } = parseEntityIDInput(input);
165
+ rejectWorkspaceID(id);
166
+ return result(withWorkspace(workspace, (db) => ({ deleted: deleteEntity(db, id) })));
167
+ },
168
+ },
169
+ {
170
+ name: "tabula_delete_workspace",
171
+ title: "Delete Tabula Workspace",
172
+ description: "Delete one managed workspace by workspace name.",
173
+ inputSchema: workspaceSchema,
174
+ annotations: destructive(),
175
+ execute(input) {
176
+ const { workspace } = parseWorkspaceInput(input);
177
+ return result({ deleted: deleteWorkspace(workspace) });
178
+ },
179
+ },
180
+ ];
181
+ export function registerTabulaTools(server) {
182
+ for (const tool of tabulaTools) {
183
+ server.registerTool(tool.name, {
184
+ title: tool.title,
185
+ description: tool.description,
186
+ inputSchema: tool.inputSchema.shape,
187
+ annotations: tool.annotations,
188
+ }, async (input) => {
189
+ try {
190
+ return tool.execute(input);
191
+ }
192
+ catch (error) {
193
+ return toolError(error);
194
+ }
195
+ });
196
+ }
197
+ }
198
+ function parseWorkspaceInput(input) {
199
+ const value = workspaceSchema.parse(input);
200
+ validateMCPWorkspaceName(value.workspace);
201
+ return value;
202
+ }
203
+ function parseEntityIDInput(input) {
204
+ const value = entityIDSchema.parse(input);
205
+ validateMCPWorkspaceName(value.workspace);
206
+ inferMCPIDType(value.id);
207
+ return value;
208
+ }
209
+ function parseSearchInput(input) {
210
+ const value = searchSchema.parse(input);
211
+ validateMCPWorkspaceName(value.workspace);
212
+ return value;
213
+ }
214
+ function parseCreateObjectInput(input) {
215
+ const value = createObjectSchema.parse(input);
216
+ validateMCPWorkspaceName(value.workspace);
217
+ if (value.parentID !== undefined) {
218
+ inferMCPIDType(value.parentID);
219
+ }
220
+ return {
221
+ workspace: value.workspace,
222
+ createInput: {
223
+ type: "object",
224
+ name: value.name,
225
+ properties: properties(value.properties),
226
+ },
227
+ ...(value.parentID === undefined ? {} : { parentID: value.parentID }),
228
+ };
229
+ }
230
+ function parseCreateBlockInput(input) {
231
+ const value = createBlockSchema.parse(input);
232
+ validateMCPWorkspaceName(value.workspace);
233
+ if (inferMCPIDType(value.parentID) === "workspace") {
234
+ throw new MCPInputError("INVALID_PARENT", `Workspace parents can only contain objects: ${value.parentID}`);
235
+ }
236
+ return {
237
+ workspace: value.workspace,
238
+ createInput: {
239
+ type: "block",
240
+ content: value.content,
241
+ properties: properties(value.properties),
242
+ },
243
+ parentID: value.parentID,
244
+ };
245
+ }
246
+ function parseWriteEntityInput(input) {
247
+ const value = writeEntitySchema.parse(input);
248
+ validateMCPWorkspaceName(value.workspace);
249
+ if (value.parentID !== undefined) {
250
+ inferMCPIDType(value.parentID);
251
+ }
252
+ return {
253
+ workspace: value.workspace,
254
+ entity: validateWriteInput(value.entity),
255
+ ...(value.parentID === undefined ? {} : { parentID: value.parentID }),
256
+ };
257
+ }
258
+ function withInitializedWorkspace(workspace, callback) {
259
+ const db = initializeWorkspace(workspace);
260
+ try {
261
+ return callback(db);
262
+ }
263
+ finally {
264
+ db.close();
265
+ }
266
+ }
267
+ function withWorkspace(workspace, callback) {
268
+ const db = openWorkspace(workspace);
269
+ try {
270
+ return callback(db);
271
+ }
272
+ finally {
273
+ db.close();
274
+ }
275
+ }
276
+ function validateMCPWorkspaceName(workspace) {
277
+ try {
278
+ validateWorkspaceName(workspace);
279
+ }
280
+ catch (error) {
281
+ if (error instanceof InvalidWorkspaceNameError) {
282
+ throw new MCPInputError("INVALID_WORKSPACE_NAME", error.message, error.details);
283
+ }
284
+ throw error;
285
+ }
286
+ }
287
+ function inferMCPIDType(id) {
288
+ switch (id.slice(0, 2)) {
289
+ case "d_":
290
+ return "workspace";
291
+ case "o_":
292
+ return "object";
293
+ case "b_":
294
+ return "block";
295
+ default:
296
+ throw new MCPInputError("INVALID_ID", `Unknown entity ID prefix: ${id}`);
297
+ }
298
+ }
299
+ function rejectWorkspaceID(id) {
300
+ if (inferMCPIDType(id) === "workspace") {
301
+ throw new MCPInputError("INVALID_ID", `Expected object or block ID: ${id}`);
302
+ }
303
+ }
304
+ function properties(value) {
305
+ if (value === undefined) {
306
+ return {};
307
+ }
308
+ if (!isJSONValue(value)) {
309
+ throw new MCPInputError("INVALID_FIELD", "Properties must contain only JSON values", {
310
+ field: "properties",
311
+ });
312
+ }
313
+ return value;
314
+ }
315
+ function isJSONValue(value) {
316
+ if (value === null
317
+ || typeof value === "string"
318
+ || typeof value === "number"
319
+ || typeof value === "boolean") {
320
+ return true;
321
+ }
322
+ if (Array.isArray(value)) {
323
+ return value.every(isJSONValue);
324
+ }
325
+ if (typeof value === "object") {
326
+ return Object.values(value).every(isJSONValue);
327
+ }
328
+ return false;
329
+ }
330
+ function parentOptions(parentID) {
331
+ return parentID === undefined ? {} : { parentID };
332
+ }
333
+ function result(output) {
334
+ const structuredContent = toRecord(output);
335
+ return {
336
+ content: [{ type: "text", text: JSON.stringify(structuredContent) }],
337
+ structuredContent,
338
+ };
339
+ }
340
+ function toolError(error) {
341
+ const output = {
342
+ error: {
343
+ code: error instanceof MCPInputError || error instanceof TabulaInputError
344
+ ? error.code
345
+ : "OPERATION_FAILED",
346
+ message: error instanceof Error ? error.message : String(error),
347
+ ...((error instanceof MCPInputError || error instanceof TabulaInputError)
348
+ && error.details !== undefined
349
+ ? { details: error.details }
350
+ : {}),
351
+ },
352
+ };
353
+ return {
354
+ content: [{ type: "text", text: JSON.stringify(output) }],
355
+ structuredContent: output,
356
+ isError: true,
357
+ };
358
+ }
359
+ function toRecord(output) {
360
+ if (output !== null && typeof output === "object" && !Array.isArray(output)) {
361
+ return output;
362
+ }
363
+ return { value: output };
364
+ }
365
+ function readOnly() {
366
+ return {
367
+ readOnlyHint: true,
368
+ destructiveHint: false,
369
+ idempotentHint: true,
370
+ openWorldHint: false,
371
+ };
372
+ }
373
+ function mutation(options) {
374
+ return {
375
+ readOnlyHint: false,
376
+ destructiveHint: false,
377
+ idempotentHint: options.idempotentHint,
378
+ openWorldHint: false,
379
+ };
380
+ }
381
+ function destructive() {
382
+ return {
383
+ readOnlyHint: false,
384
+ destructiveHint: true,
385
+ idempotentHint: false,
386
+ openWorldHint: false,
387
+ };
388
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@graypirate/tabula-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Local stdio MCP server for Tabula.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "bin": {
17
+ "tabula-mcp": "dist/index.js"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md"
22
+ ],
23
+ "scripts": {
24
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && chmod 755 dist/index.js",
25
+ "prepublishOnly": "bun run typecheck && bun test && bun run build",
26
+ "start": "bun src/index.ts",
27
+ "test": "bun test",
28
+ "typecheck": "bunx tsc --noEmit"
29
+ },
30
+ "dependencies": {
31
+ "@graypirate/tabula": "^1.0.0",
32
+ "@modelcontextprotocol/sdk": "^1.29.0",
33
+ "zod": "^3.25.76"
34
+ },
35
+ "engines": {
36
+ "bun": ">=1.3.0"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "devDependencies": {
42
+ "@types/bun": "latest",
43
+ "@types/node": "^25.9.1",
44
+ "typescript": "^5"
45
+ }
46
+ }