stackby-mcp-server 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/dist/index.js CHANGED
@@ -1,554 +1,11 @@
1
1
  /**
2
- * Stackby MCP Server — entry point.
3
- * list_stacks, list_tables call real Stackby API.
2
+ * Stackby MCP Server — stdio entry point (Cursor, Claude Desktop).
3
+ * For hosted HTTP (ChatGPT), use server-http.js.
4
4
  */
5
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { createStackbyMcpServer } from "./mcp-server.js";
6
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
- import { z } from "zod";
8
- import { hasApiKey, getApiBaseUrl, getWorkspaces, getAllStacks, getTables, getTableViewList, describeTable, getRowList, searchRecords, getRecord, createRow, updateRows, deleteRows, createTable, createColumn, } from "./stackby-api.js";
9
- const mcpServer = new McpServer({
10
- name: "stackby-mcp-server",
11
- version: "0.1.0",
12
- });
13
- mcpServer.registerTool("list_workspaces", {
14
- description: "List Stackby workspaces the user can access. Requires STACKBY_API_KEY (or PAT) in MCP config.",
15
- inputSchema: {},
16
- }, async () => {
17
- if (!hasApiKey()) {
18
- return {
19
- content: [
20
- {
21
- type: "text",
22
- text: "STACKBY_API_KEY is not set. Add it to your MCP config (e.g. in Cursor: .cursor/mcp.json → env.STACKBY_API_KEY) with your Stackby API key or Personal Access Token (PAT).",
23
- },
24
- ],
25
- };
26
- }
27
- try {
28
- const workspaces = await getWorkspaces();
29
- const lines = workspaces.length === 0
30
- ? ["No workspaces found."]
31
- : workspaces.map((w) => `- ${w.name} (id: ${w.id})`);
32
- return {
33
- content: [
34
- {
35
- type: "text",
36
- text: `Workspaces (${workspaces.length}):\n${lines.join("\n")}`,
37
- },
38
- ],
39
- };
40
- }
41
- catch (err) {
42
- const message = err instanceof Error ? err.message : String(err);
43
- return {
44
- content: [
45
- {
46
- type: "text",
47
- text: `Failed to list workspaces: ${message}. STACKBY_API_KEY and STACKBY_API_URL in use: ${getApiBaseUrl()}.`,
48
- },
49
- ],
50
- isError: true,
51
- };
52
- }
53
- });
54
- mcpServer.registerTool("list_stacks", {
55
- description: "List Stackby stacks (bases) the user can access. Requires STACKBY_API_KEY (or PAT) in MCP config.",
56
- inputSchema: {},
57
- }, async () => {
58
- if (!hasApiKey()) {
59
- return {
60
- content: [
61
- {
62
- type: "text",
63
- text: "STACKBY_API_KEY is not set. Add it to your MCP config (e.g. in Cursor: .cursor/mcp.json → env.STACKBY_API_KEY) with your Stackby API key or Personal Access Token (PAT).",
64
- },
65
- ],
66
- };
67
- }
68
- try {
69
- const stacks = await getAllStacks();
70
- const lines = stacks.length === 0
71
- ? ["No stacks found."]
72
- : stacks.map((s) => `- ${s.stackName} (id: ${s.stackId}, workspace: ${s.workspaceName ?? s.workspaceId})`);
73
- return {
74
- content: [
75
- {
76
- type: "text",
77
- text: `Stacks (${stacks.length}):\n${lines.join("\n")}`,
78
- },
79
- ],
80
- };
81
- }
82
- catch (err) {
83
- const message = err instanceof Error ? err.message : String(err);
84
- return {
85
- content: [
86
- {
87
- type: "text",
88
- text: `Failed to list stacks: ${message}. STACKBY_API_KEY and STACKBY_API_URL in use: ${getApiBaseUrl()}.`,
89
- },
90
- ],
91
- isError: true,
92
- };
93
- }
94
- });
95
- mcpServer.registerTool("list_tables", {
96
- description: "List tables in a Stackby stack. Use list_stacks first to get stack IDs.",
97
- inputSchema: {
98
- stackId: z.string().describe("Stack ID (from list_stacks)"),
99
- },
100
- }, async ({ stackId }) => {
101
- const id = stackId?.trim();
102
- if (!id) {
103
- return {
104
- content: [{ type: "text", text: "stackId is required. Use list_stacks to get stack IDs." }],
105
- isError: true,
106
- };
107
- }
108
- if (!hasApiKey()) {
109
- return {
110
- content: [{ type: "text", text: "STACKBY_API_KEY is not set. Add it to your MCP config." }],
111
- };
112
- }
113
- try {
114
- const tables = await getTables(id);
115
- const lines = tables.length === 0
116
- ? ["No tables found in this stack."]
117
- : tables.map((t) => `- ${t.name} (id: ${t.id})`);
118
- return {
119
- content: [
120
- {
121
- type: "text",
122
- text: `Tables in stack ${id} (${tables.length}):\n${lines.join("\n")}`,
123
- },
124
- ],
125
- };
126
- }
127
- catch (err) {
128
- const message = err instanceof Error ? err.message : String(err);
129
- return {
130
- content: [
131
- {
132
- type: "text",
133
- text: `Failed to list tables: ${message}. Check stackId and API access.`,
134
- },
135
- ],
136
- isError: true,
137
- };
138
- }
139
- });
140
- mcpServer.registerTool("describe_table", {
141
- description: "Get table schema: name, fields (columns with id, name, type), and views. Use list_tables to get stackId and tableId.",
142
- inputSchema: {
143
- stackId: z.string().describe("Stack ID (from list_stacks)"),
144
- tableId: z.string().describe("Table ID (from list_tables)"),
145
- },
146
- }, async ({ stackId, tableId }) => {
147
- const sId = stackId?.trim();
148
- const tId = tableId?.trim();
149
- if (!sId || !tId) {
150
- return {
151
- content: [{ type: "text", text: "stackId and tableId are required. Use list_stacks and list_tables to get IDs." }],
152
- isError: true,
153
- };
154
- }
155
- if (!hasApiKey()) {
156
- return {
157
- content: [{ type: "text", text: "STACKBY_API_KEY is not set. Add it to your MCP config." }],
158
- };
159
- }
160
- try {
161
- const schema = await describeTable(sId, tId);
162
- const fieldLines = schema.fields.length === 0
163
- ? ["(no fields)"]
164
- : schema.fields.map((f) => ` - ${f.name} (id: ${f.id}, type: ${f.type})`);
165
- const viewLines = schema.views.length === 0
166
- ? ["(no views)"]
167
- : schema.views.map((v) => ` - ${v.name} (id: ${v.id})`);
168
- const text = [
169
- `Table: ${schema.name} (id: ${schema.id})`,
170
- "",
171
- "Fields:",
172
- ...fieldLines,
173
- "",
174
- "Views:",
175
- ...viewLines,
176
- ].join("\n");
177
- return {
178
- content: [{ type: "text", text }],
179
- };
180
- }
181
- catch (err) {
182
- const message = err instanceof Error ? err.message : String(err);
183
- return {
184
- content: [
185
- { type: "text", text: `Failed to describe table: ${message}. Check stackId, tableId, and API access.` },
186
- ],
187
- isError: true,
188
- };
189
- }
190
- });
191
- mcpServer.registerTool("list_records", {
192
- description: "List rows (records) in a table. Use list_stacks and list_tables to get stackId and tableId.",
193
- inputSchema: {
194
- stackId: z.string().describe("Stack ID (from list_stacks)"),
195
- tableId: z.string().describe("Table ID (from list_tables)"),
196
- maxRecords: z.number().optional().describe("Max records to return (1–100, default 100)"),
197
- },
198
- }, async ({ stackId, tableId, maxRecords }) => {
199
- const sId = stackId?.trim();
200
- const tId = tableId?.trim();
201
- if (!sId || !tId) {
202
- return {
203
- content: [{ type: "text", text: "stackId and tableId are required. Use list_stacks and list_tables to get IDs." }],
204
- isError: true,
205
- };
206
- }
207
- if (!hasApiKey()) {
208
- return {
209
- content: [{ type: "text", text: "STACKBY_API_KEY is not set. Add it to your MCP config." }],
210
- };
211
- }
212
- try {
213
- const records = await getRowList(sId, tId, { maxRecords: maxRecords ?? 100 });
214
- const lines = records.length === 0
215
- ? ["No records found."]
216
- : records.map((r) => `- id: ${r.id} | ${JSON.stringify(r.field)}`);
217
- const text = [`Records in table ${tId} (${records.length}):`, "", ...lines].join("\n");
218
- return {
219
- content: [{ type: "text", text }],
220
- };
221
- }
222
- catch (err) {
223
- const message = err instanceof Error ? err.message : String(err);
224
- return {
225
- content: [
226
- { type: "text", text: `Failed to list records: ${message}. Check stackId, tableId, and API access.` },
227
- ],
228
- isError: true,
229
- };
230
- }
231
- });
232
- mcpServer.registerTool("search_records", {
233
- description: "Search for rows containing text in a table. Uses first column if fieldIds not provided. Use list_stacks and list_tables to get IDs.",
234
- inputSchema: {
235
- stackId: z.string().describe("Stack ID (from list_stacks)"),
236
- tableId: z.string().describe("Table ID (from list_tables)"),
237
- searchTerm: z.string().describe("Text to search for"),
238
- fieldIds: z.array(z.string()).optional().describe("Optional column IDs to search in (uses first column if omitted)"),
239
- maxRecords: z.number().optional().describe("Max records to return (default 100)"),
240
- },
241
- }, async ({ stackId, tableId, searchTerm, fieldIds, maxRecords }) => {
242
- const sId = stackId?.trim();
243
- const tId = tableId?.trim();
244
- const term = searchTerm?.trim();
245
- if (!sId || !tId || term === undefined || term === "") {
246
- return {
247
- content: [{ type: "text", text: "stackId, tableId, and searchTerm are required." }],
248
- isError: true,
249
- };
250
- }
251
- if (!hasApiKey()) {
252
- return {
253
- content: [{ type: "text", text: "STACKBY_API_KEY is not set. Add it to your MCP config." }],
254
- };
255
- }
256
- try {
257
- const columnId = fieldIds && fieldIds.length > 0 ? fieldIds[0] : undefined;
258
- const result = await searchRecords(sId, tId, term, { columnId, maxRecords });
259
- const count = result.rowIds.length;
260
- const lines = count === 0
261
- ? ["No matching records."]
262
- : result.rowIds.map((id, i) => `- id: ${id} | ${(result.rowname && result.rowname[i]) || ""}`);
263
- const text = [`Search "${term}" in table ${tId} (${count} match${count !== 1 ? "es" : ""}):`, "", ...lines].join("\n");
264
- return {
265
- content: [{ type: "text", text }],
266
- };
267
- }
268
- catch (err) {
269
- const message = err instanceof Error ? err.message : String(err);
270
- return {
271
- content: [
272
- { type: "text", text: `Failed to search records: ${message}. Check stackId, tableId, and API access.` },
273
- ],
274
- isError: true,
275
- };
276
- }
277
- });
278
- mcpServer.registerTool("get_record", {
279
- description: "Get a single row (record) by id. Use list_records or search_records to get record IDs.",
280
- inputSchema: {
281
- stackId: z.string().describe("Stack ID (from list_stacks)"),
282
- tableId: z.string().describe("Table ID (from list_tables)"),
283
- recordId: z.string().describe("Record (row) ID"),
284
- },
285
- }, async ({ stackId, tableId, recordId }) => {
286
- const sId = stackId?.trim();
287
- const tId = tableId?.trim();
288
- const rId = recordId?.trim();
289
- if (!sId || !tId || !rId) {
290
- return {
291
- content: [{ type: "text", text: "stackId, tableId, and recordId are required." }],
292
- isError: true,
293
- };
294
- }
295
- if (!hasApiKey()) {
296
- return {
297
- content: [{ type: "text", text: "STACKBY_API_KEY is not set. Add it to your MCP config." }],
298
- };
299
- }
300
- try {
301
- const record = await getRecord(sId, tId, rId);
302
- if (!record) {
303
- return {
304
- content: [{ type: "text", text: `No record found with id ${rId} in table ${tId}.` }],
305
- };
306
- }
307
- const text = [`Record ${record.id}:`, "", JSON.stringify(record.field, null, 2)].join("\n");
308
- return {
309
- content: [{ type: "text", text }],
310
- };
311
- }
312
- catch (err) {
313
- const message = err instanceof Error ? err.message : String(err);
314
- return {
315
- content: [
316
- { type: "text", text: `Failed to get record: ${message}. Check stackId, tableId, recordId, and API access.` },
317
- ],
318
- isError: true,
319
- };
320
- }
321
- });
322
- // --- Phase 3: Write tools ---
323
- mcpServer.registerTool("create_record", {
324
- description: "Create a new row (record) in a table. Use describe_table to get column names. Fields are keyed by column name.",
325
- inputSchema: {
326
- stackId: z.string().describe("Stack ID (from list_stacks)"),
327
- tableId: z.string().describe("Table ID (from list_tables)"),
328
- fields: z.record(z.string(), z.unknown()).describe("Field values keyed by column name (e.g. { \"Name\": \"Task 1\", \"Status\": \"Done\" })"),
329
- },
330
- }, async ({ stackId, tableId, fields }) => {
331
- const sId = stackId?.trim();
332
- const tId = tableId?.trim();
333
- if (!sId || !tId) {
334
- return {
335
- content: [{ type: "text", text: "stackId and tableId are required." }],
336
- isError: true,
337
- };
338
- }
339
- if (!hasApiKey()) {
340
- return {
341
- content: [{ type: "text", text: "STACKBY_API_KEY is not set. Add it to your MCP config." }],
342
- };
343
- }
344
- if (!fields || typeof fields !== "object" || Array.isArray(fields)) {
345
- return {
346
- content: [{ type: "text", text: "fields must be an object of column names to values." }],
347
- isError: true,
348
- };
349
- }
350
- try {
351
- const records = await createRow(sId, tId, fields);
352
- const created = records[0];
353
- if (!created) {
354
- return {
355
- content: [{ type: "text", text: "No record was created. Check table schema and field names." }],
356
- isError: true,
357
- };
358
- }
359
- const text = [`Created record: ${created.id}`, "", JSON.stringify(created.field, null, 2)].join("\n");
360
- return {
361
- content: [{ type: "text", text }],
362
- };
363
- }
364
- catch (err) {
365
- const message = err instanceof Error ? err.message : String(err);
366
- return {
367
- content: [
368
- { type: "text", text: `Failed to create record: ${message}. Check stackId, tableId, and field names (use describe_table).` },
369
- ],
370
- isError: true,
371
- };
372
- }
373
- });
374
- mcpServer.registerTool("update_records", {
375
- description: "Update existing rows. Provide an array of { id, fields }. At most 10 records per request. Use describe_table for column names.",
376
- inputSchema: {
377
- stackId: z.string().describe("Stack ID (from list_stacks)"),
378
- tableId: z.string().describe("Table ID (from list_tables)"),
379
- records: z
380
- .array(z.object({
381
- id: z.string().describe("Record (row) ID"),
382
- fields: z.record(z.string(), z.unknown()).describe("Field values to set (column name -> value)"),
383
- }))
384
- .min(1)
385
- .max(10)
386
- .describe("Records to update"),
387
- },
388
- }, async ({ stackId, tableId, records }) => {
389
- const sId = stackId?.trim();
390
- const tId = tableId?.trim();
391
- if (!sId || !tId) {
392
- return {
393
- content: [{ type: "text", text: "stackId and tableId are required." }],
394
- isError: true,
395
- };
396
- }
397
- if (!hasApiKey()) {
398
- return {
399
- content: [{ type: "text", text: "STACKBY_API_KEY is not set. Add it to your MCP config." }],
400
- };
401
- }
402
- try {
403
- const updated = await updateRows(sId, tId, records.map((r) => ({ id: r.id, fields: r.fields })));
404
- const lines = updated.map((r) => `- ${r.id}: ${JSON.stringify(r.field)}`);
405
- const text = [`Updated ${updated.length} record(s):`, "", ...lines].join("\n");
406
- return {
407
- content: [{ type: "text", text }],
408
- };
409
- }
410
- catch (err) {
411
- const message = err instanceof Error ? err.message : String(err);
412
- return {
413
- content: [
414
- { type: "text", text: `Failed to update records: ${message}. Check stackId, tableId, record IDs, and field names.` },
415
- ],
416
- isError: true,
417
- };
418
- }
419
- });
420
- mcpServer.registerTool("delete_records", {
421
- description: "Soft-delete rows (records) by ID. At most 10 per request. Use list_records or search_records to get IDs.",
422
- inputSchema: {
423
- stackId: z.string().describe("Stack ID (from list_stacks)"),
424
- tableId: z.string().describe("Table ID (from list_tables)"),
425
- recordIds: z.array(z.string()).min(1).max(10).describe("Record (row) IDs to delete"),
426
- },
427
- }, async ({ stackId, tableId, recordIds }) => {
428
- const sId = stackId?.trim();
429
- const tId = tableId?.trim();
430
- if (!sId || !tId) {
431
- return {
432
- content: [{ type: "text", text: "stackId and tableId are required." }],
433
- isError: true,
434
- };
435
- }
436
- if (!hasApiKey()) {
437
- return {
438
- content: [{ type: "text", text: "STACKBY_API_KEY is not set. Add it to your MCP config." }],
439
- };
440
- }
441
- try {
442
- const result = await deleteRows(sId, tId, recordIds);
443
- const list = result?.records ?? [];
444
- const lines = list.map((r) => `- ${r.id}: deleted=${r.deleted}`);
445
- const text = [`Deleted ${list.length} record(s):`, "", ...lines].join("\n");
446
- return {
447
- content: [{ type: "text", text }],
448
- };
449
- }
450
- catch (err) {
451
- const message = err instanceof Error ? err.message : String(err);
452
- return {
453
- content: [
454
- { type: "text", text: `Failed to delete records: ${message}. Check stackId, tableId, and record IDs.` },
455
- ],
456
- isError: true,
457
- };
458
- }
459
- });
460
- // --- Phase 4: Schema tools (create only; update later) ---
461
- mcpServer.registerTool("create_table", {
462
- description: "Create a new table in a stack. Use list_stacks to get stackId.",
463
- inputSchema: {
464
- stackId: z.string().describe("Stack ID (from list_stacks)"),
465
- name: z.string().describe("Table name"),
466
- },
467
- }, async ({ stackId, name }) => {
468
- const sId = stackId?.trim();
469
- const tableName = name?.trim();
470
- if (!sId || !tableName) {
471
- return {
472
- content: [{ type: "text", text: "stackId and name are required." }],
473
- isError: true,
474
- };
475
- }
476
- if (!hasApiKey()) {
477
- return {
478
- content: [{ type: "text", text: "STACKBY_API_KEY is not set. Add it to your MCP config." }],
479
- };
480
- }
481
- try {
482
- const result = await createTable(sId, tableName);
483
- const id = result?.tableId ?? result?.id ?? "unknown";
484
- const text = [`Created table: ${tableName}`, `Table ID: ${id}`].join("\n");
485
- return {
486
- content: [{ type: "text", text }],
487
- };
488
- }
489
- catch (err) {
490
- const message = err instanceof Error ? err.message : String(err);
491
- return {
492
- content: [
493
- { type: "text", text: `Failed to create table: ${message}. Check stackId and plan limits.` },
494
- ],
495
- isError: true,
496
- };
497
- }
498
- });
499
- mcpServer.registerTool("create_field", {
500
- description: "Create a new column (field) in a table. Use describe_table to see existing columns. For singleOption/multipleOptions pass options array.",
501
- inputSchema: {
502
- stackId: z.string().describe("Stack ID (from list_stacks)"),
503
- tableId: z.string().describe("Table ID (from list_tables)"),
504
- name: z.string().describe("Column name"),
505
- columnType: z.string().describe("Column type: shortText, longText, number, checkbox, dateAndTime, singleOption, multipleOptions, email, url, etc."),
506
- viewId: z.string().optional().describe("View ID (optional; first view used if omitted)"),
507
- options: z.array(z.string()).optional().describe("For singleOption/multipleOptions: choice labels"),
508
- },
509
- }, async ({ stackId, tableId, name, columnType, viewId, options }) => {
510
- const sId = stackId?.trim();
511
- const tId = tableId?.trim();
512
- const colName = name?.trim();
513
- const type = columnType?.trim();
514
- if (!sId || !tId || !colName || !type) {
515
- return {
516
- content: [{ type: "text", text: "stackId, tableId, name, and columnType are required." }],
517
- isError: true,
518
- };
519
- }
520
- if (!hasApiKey()) {
521
- return {
522
- content: [{ type: "text", text: "STACKBY_API_KEY is not set. Add it to your MCP config." }],
523
- };
524
- }
525
- try {
526
- let viewIdToUse = viewId?.trim();
527
- if (!viewIdToUse) {
528
- const views = await getTableViewList(sId, tId);
529
- viewIdToUse = views.length > 0 ? views[0].id : "";
530
- }
531
- const result = await createColumn(sId, tId, colName, type, {
532
- viewId: viewIdToUse,
533
- options: options && options.length > 0 ? options : undefined,
534
- });
535
- const id = result?.columnId ?? result?.id ?? "unknown";
536
- const text = [`Created column: ${colName}`, `Column ID: ${id}`, `Type: ${type}`].join("\n");
537
- return {
538
- content: [{ type: "text", text }],
539
- };
540
- }
541
- catch (err) {
542
- const message = err instanceof Error ? err.message : String(err);
543
- return {
544
- content: [
545
- { type: "text", text: `Failed to create field: ${message}. Check stackId, tableId, name, columnType (use describe_table for types).` },
546
- ],
547
- isError: true,
548
- };
549
- }
550
- });
551
7
  async function main() {
8
+ const mcpServer = createStackbyMcpServer();
552
9
  const transport = new StdioServerTransport();
553
10
  await mcpServer.connect(transport);
554
11
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ /**
2
+ * One-off script: list workspaces for the configured Stackby account.
3
+ * Usage: npm run build && node dist/list-workspaces.js
4
+ * Requires: STACKBY_API_KEY (or PAT) in environment.
5
+ */
6
+ import { getWorkspaces, getApiBaseUrl, hasApiKey } from "./stackby-api.js";
7
+ async function main() {
8
+ if (!hasApiKey()) {
9
+ console.error("STACKBY_API_KEY is not set. Set it in your environment or MCP config.");
10
+ process.exit(1);
11
+ }
12
+ try {
13
+ const workspaces = await getWorkspaces();
14
+ console.log(`API: ${getApiBaseUrl()}`);
15
+ console.log(`Workspaces (${workspaces.length}):`);
16
+ if (workspaces.length === 0) {
17
+ console.log(" No workspaces found.");
18
+ }
19
+ else {
20
+ for (const w of workspaces) {
21
+ console.log(` - ${w.name} (id: ${w.id})`);
22
+ }
23
+ }
24
+ }
25
+ catch (e) {
26
+ const message = e instanceof Error ? e.message : String(e);
27
+ console.error("Failed to list workspaces:", message);
28
+ process.exit(1);
29
+ }
30
+ }
31
+ main();
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Shared MCP server and tool registration (used by stdio and HTTP entry points).
3
+ */
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ export declare function createStackbyMcpServer(): McpServer;