stackby-mcp-server 0.2.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 +104 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +558 -0
- package/dist/stackby-api.d.ts +118 -0
- package/dist/stackby-api.js +250 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Stackby MCP Server
|
|
2
|
+
|
|
3
|
+
Model Context Protocol (MCP) server for Stackby. Exposes tools so AI clients (Cursor, Claude Desktop, Cline, ChatGPT with MCP) can work with Stackby data.
|
|
4
|
+
|
|
5
|
+
**Auth:** Stackby Developer API via `STACKBY_API_KEY` (API key or Personal Access Token). See [CONFIG](https://github.com/stackby/Stackby_API/blob/production/MCP_SERVER/docs/CONFIG.md) in the planning repo (or sibling `Stackby_API/MCP_SERVER/docs/CONFIG.md`).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Install (one-click)
|
|
10
|
+
|
|
11
|
+
**1. Add to Cursor** — Edit `~/.cursor/mcp.json` (Mac/Linux) or `%USERPROFILE%\.cursor\mcp.json` (Windows):
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"mcpServers": {
|
|
16
|
+
"stackby": {
|
|
17
|
+
"command": "npx",
|
|
18
|
+
"args": ["-y", "stackby-mcp-server"],
|
|
19
|
+
"env": {
|
|
20
|
+
"STACKBY_API_KEY": "your-api-key-or-pat",
|
|
21
|
+
"STACKBY_API_URL": "http://localhost:3000"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Replace `your-api-key-or-pat` with your Stackby API key or PAT. Set `STACKBY_API_URL` to your Stackby API base URL (omit for default). Restart Cursor.
|
|
29
|
+
|
|
30
|
+
**2. Or install globally:** `npm install -g stackby-mcp-server` then use `"command": "stackby-mcp-server"` in `mcp.json`.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Tools (11)
|
|
35
|
+
|
|
36
|
+
| Tool | Description |
|
|
37
|
+
|------|-------------|
|
|
38
|
+
| `list_stacks` | List stacks (bases) the user can access. |
|
|
39
|
+
| `list_tables` | List tables in a stack. |
|
|
40
|
+
| `describe_table` | Table schema: fields (columns), views. |
|
|
41
|
+
| `list_records` | List rows in a table (with optional maxRecords). |
|
|
42
|
+
| `search_records` | Search rows by text. |
|
|
43
|
+
| `get_record` | Get one row by ID. |
|
|
44
|
+
| `create_record` | Create a row (fields keyed by column name). |
|
|
45
|
+
| `update_records` | Update rows (array of `{ id, fields }`, max 10). |
|
|
46
|
+
| `delete_records` | Soft-delete rows by ID (max 10). |
|
|
47
|
+
| `create_table` | Create a new table in a stack. |
|
|
48
|
+
| `create_field` | Create a new column (field) in a table. |
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Setup
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm install
|
|
56
|
+
npm run build
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**How to verify the build:** After `npm run build` you should see `Build OK. Output in dist/`. Run `npm start` — the server runs over stdio (no visible output; it waits for Cursor/Claude to connect).
|
|
60
|
+
|
|
61
|
+
## Verify in Cursor (Step 1.2)
|
|
62
|
+
|
|
63
|
+
1. Open Cursor **Settings** → **MCP** (or edit the config file directly).
|
|
64
|
+
2. Add the Stackby MCP server. Use **project** or **user** config.
|
|
65
|
+
|
|
66
|
+
**Option A — User config** (`~/.cursor/mcp.json` on Mac/Linux, or `%USERPROFILE%\\.cursor\\mcp.json` on Windows):
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"mcpServers": {
|
|
71
|
+
"stackby": {
|
|
72
|
+
"command": "node",
|
|
73
|
+
"args": ["C:\\Users\\Admin\\Desktop\\Stackby\\stackby-mcp-server\\dist\\index.js"],
|
|
74
|
+
"env": {}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Option B — If you use `npx` from the project folder:** (from a terminal in `stackby-mcp-server` run `node dist/index.js`; Cursor can use that path in `args`.)
|
|
81
|
+
|
|
82
|
+
Use the **full path** to `dist/index.js` in `args` so Cursor can spawn the server.
|
|
83
|
+
3. Restart Cursor (or reload the window).
|
|
84
|
+
4. Set `STACKBY_API_KEY` (and optionally `STACKBY_API_URL`) in the `env` object. In a chat, check the **tools** list — you should see all 11 tools: **list_stacks**, **list_tables**, **describe_table**, **list_records**, **search_records**, **get_record**, **create_record**, **update_records**, **delete_records**, **create_table**, **create_field**.
|
|
85
|
+
|
|
86
|
+
## Run (stdio — for Cursor / Claude)
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
STACKBY_API_KEY=your_api_key STACKBY_API_URL=https://api.stackby.com npm start
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Or one-click: `npx stackby-mcp-server` (after npm publish)
|
|
93
|
+
|
|
94
|
+
## Config
|
|
95
|
+
|
|
96
|
+
| Env | Required | Description |
|
|
97
|
+
|-----|----------|-------------|
|
|
98
|
+
| `STACKBY_API_KEY` | Yes | Stackby API key (or PAT when implemented). |
|
|
99
|
+
|
|
100
|
+
**Full config** (Cursor, Claude Desktop, Cline, HTTP transport): see `Stackby_API/MCP_SERVER/docs/CONFIG.md` in the sibling repo.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
Planning and design: `Stackby_API/MCP_SERVER/` (sibling repo).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stackby MCP Server — entry point.
|
|
3
|
+
* list_stacks, list_tables call real Stackby API.
|
|
4
|
+
*/
|
|
5
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
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
|
+
async function main() {
|
|
552
|
+
const transport = new StdioServerTransport();
|
|
553
|
+
await mcpServer.connect(transport);
|
|
554
|
+
}
|
|
555
|
+
main().catch((err) => {
|
|
556
|
+
console.error(err);
|
|
557
|
+
process.exit(1);
|
|
558
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stackby API client for MCP server.
|
|
3
|
+
* Uses dedicated MCP API only: /api/v1/mcp/* (existing developer API is unchanged).
|
|
4
|
+
*/
|
|
5
|
+
export declare function hasApiKey(): boolean;
|
|
6
|
+
/** Returns the base URL actually in use (for error messages). */
|
|
7
|
+
export declare function getApiBaseUrl(): string;
|
|
8
|
+
export interface Workspace {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
}
|
|
12
|
+
export interface Stack {
|
|
13
|
+
stackId: string;
|
|
14
|
+
workspaceId: string;
|
|
15
|
+
stackName: string;
|
|
16
|
+
info?: string;
|
|
17
|
+
color?: string;
|
|
18
|
+
icon?: string;
|
|
19
|
+
createdAt?: string;
|
|
20
|
+
}
|
|
21
|
+
/** GET /api/v1/mcp/workspaces — list workspaces (MCP API only). */
|
|
22
|
+
export declare function getWorkspaces(): Promise<Workspace[]>;
|
|
23
|
+
/** POST /api/v1/mcp/stacks — list stacks in a workspace. Body: { workspaceId }. */
|
|
24
|
+
export declare function getStacks(workspaceId: string, workspaceName?: string): Promise<{
|
|
25
|
+
list: Stack[];
|
|
26
|
+
workspaceName: string;
|
|
27
|
+
}>;
|
|
28
|
+
/** Fetch all stacks across all workspaces (for list_stacks tool). */
|
|
29
|
+
export declare function getAllStacks(): Promise<Array<Stack & {
|
|
30
|
+
workspaceName?: string;
|
|
31
|
+
}>>;
|
|
32
|
+
export interface Table {
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
}
|
|
36
|
+
/** GET /api/v1/mcp/stacks/:stackId/tables — list tables in a stack (MCP API only). */
|
|
37
|
+
export declare function getTables(stackId: string): Promise<Table[]>;
|
|
38
|
+
export interface TableField {
|
|
39
|
+
id: string;
|
|
40
|
+
name: string;
|
|
41
|
+
type: string;
|
|
42
|
+
key?: string;
|
|
43
|
+
label?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface TableView {
|
|
46
|
+
id: string;
|
|
47
|
+
name: string;
|
|
48
|
+
tableId: string;
|
|
49
|
+
}
|
|
50
|
+
/** GET /api/v1/mcp/stacks/:stackId/tables/:tableId/columns — list columns (MCP API only). */
|
|
51
|
+
export declare function getTableColumns(stackId: string, tableId: string): Promise<TableField[]>;
|
|
52
|
+
/** GET /api/v1/mcp/stacks/:stackId/tables/:tableId/views — list all visible views (MCP API only). */
|
|
53
|
+
export declare function getTableViewList(stackId: string, tableId: string): Promise<TableView[]>;
|
|
54
|
+
export interface DescribeTableResult {
|
|
55
|
+
id: string;
|
|
56
|
+
name: string;
|
|
57
|
+
fields: TableField[];
|
|
58
|
+
views: TableView[];
|
|
59
|
+
}
|
|
60
|
+
/** Describe one table: name (from tablelist), fields (columnlist), views (viewlist). */
|
|
61
|
+
export declare function describeTable(stackId: string, tableId: string): Promise<DescribeTableResult>;
|
|
62
|
+
export interface TableRecord {
|
|
63
|
+
id: string;
|
|
64
|
+
field: Record<string, unknown>;
|
|
65
|
+
}
|
|
66
|
+
/** GET /api/v1/mcp/stacks/:stackId/tables/:tableId/rows — list rows (MCP API). */
|
|
67
|
+
export declare function getRowList(stackId: string, tableId: string, opts?: {
|
|
68
|
+
maxRecords?: number;
|
|
69
|
+
offset?: number;
|
|
70
|
+
rowIds?: string[];
|
|
71
|
+
}): Promise<TableRecord[]>;
|
|
72
|
+
/** GET /api/v1/mcp/stacks/:stackId/tables/:tableId/rows/:recordId — get one record (MCP API). */
|
|
73
|
+
export declare function getRecord(stackId: string, tableId: string, recordId: string): Promise<TableRecord | null>;
|
|
74
|
+
export interface SearchRecordsResult {
|
|
75
|
+
rowIds: string[];
|
|
76
|
+
rowname: string[];
|
|
77
|
+
fields: Array<Record<string, unknown>>;
|
|
78
|
+
}
|
|
79
|
+
/** POST /api/v1/mcp/stacks/:stackId/tables/:tableId/search — search rows (MCP API). */
|
|
80
|
+
export declare function searchRecords(stackId: string, tableId: string, searchTerm: string, opts?: {
|
|
81
|
+
columnId?: string;
|
|
82
|
+
maxRecords?: number;
|
|
83
|
+
}): Promise<SearchRecordsResult>;
|
|
84
|
+
/** POST /api/v1/mcp/stacks/:stackId/tables/:tableId/rows — create row(s) (MCP API). */
|
|
85
|
+
export declare function createRow(stackId: string, tableId: string, fields: Record<string, unknown>): Promise<TableRecord[]>;
|
|
86
|
+
/** POST /api/v1/mcp/stacks/:stackId/tables/:tableId/rows/update — update rows (MCP API). */
|
|
87
|
+
export declare function updateRows(stackId: string, tableId: string, records: Array<{
|
|
88
|
+
id: string;
|
|
89
|
+
fields: Record<string, unknown>;
|
|
90
|
+
}>): Promise<TableRecord[]>;
|
|
91
|
+
/** DELETE /api/v1/mcp/stacks/:stackId/tables/:tableId/rows — soft-delete rows (MCP API). */
|
|
92
|
+
export declare function deleteRows(stackId: string, tableId: string, recordIds: string[]): Promise<{
|
|
93
|
+
records: Array<{
|
|
94
|
+
id: string;
|
|
95
|
+
deleted: boolean;
|
|
96
|
+
}>;
|
|
97
|
+
}>;
|
|
98
|
+
export interface CreateTableResult {
|
|
99
|
+
tableId?: string;
|
|
100
|
+
name?: string;
|
|
101
|
+
[key: string]: unknown;
|
|
102
|
+
}
|
|
103
|
+
/** POST /api/v1/mcp/stacks/:stackId/tables — create a table in a stack (MCP API). */
|
|
104
|
+
export declare function createTable(stackId: string, name: string, _description?: string): Promise<CreateTableResult>;
|
|
105
|
+
/** Column types supported by POST /api/v1/columnCreate/:columnType (devapi). */
|
|
106
|
+
export declare const COLUMN_TYPES: readonly ["shortText", "longText", "number", "checkbox", "dateAndTime", "time", "singleOption", "multipleOptions", "email", "url", "phoneNumber", "rating", "duration", "autoNumber", "createdTime", "updatedTime", "createdBy", "updatedBy", "attachment", "link", "lookup", "lookupCount", "aggregation", "formula", "checkList", "location", "barcode", "signature"];
|
|
107
|
+
export type ColumnType = (typeof COLUMN_TYPES)[number];
|
|
108
|
+
export interface CreateColumnResult {
|
|
109
|
+
columnId?: string;
|
|
110
|
+
tableId?: string;
|
|
111
|
+
name?: string;
|
|
112
|
+
[key: string]: unknown;
|
|
113
|
+
}
|
|
114
|
+
/** POST /api/v1/mcp/columns — create a column (MCP API). Body: stackId, tableId, name, columnType, viewId?, options? */
|
|
115
|
+
export declare function createColumn(stackId: string, tableId: string, name: string, columnType: string, opts?: {
|
|
116
|
+
viewId?: string;
|
|
117
|
+
options?: string[];
|
|
118
|
+
}): Promise<CreateColumnResult>;
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stackby API client for MCP server.
|
|
3
|
+
* Uses dedicated MCP API only: /api/v1/mcp/* (existing developer API is unchanged).
|
|
4
|
+
*/
|
|
5
|
+
const BASE_URL = process.env.STACKBY_API_URL || "https://stackby.com";
|
|
6
|
+
const API_KEY = process.env.STACKBY_API_KEY || "";
|
|
7
|
+
const MCP_API = "/api/v1/mcp";
|
|
8
|
+
export function hasApiKey() {
|
|
9
|
+
return Boolean(API_KEY && API_KEY.trim().length > 0);
|
|
10
|
+
}
|
|
11
|
+
/** Returns the base URL actually in use (for error messages). */
|
|
12
|
+
export function getApiBaseUrl() {
|
|
13
|
+
return BASE_URL.replace(/\/$/, "");
|
|
14
|
+
}
|
|
15
|
+
function authHeaders() {
|
|
16
|
+
const key = API_KEY.trim();
|
|
17
|
+
if (!key) {
|
|
18
|
+
throw new Error("STACKBY_API_KEY is not set. Set it in your MCP config (e.g. Cursor mcp.json env).");
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
"Content-Type": "application/json",
|
|
22
|
+
"x-api-key": key,
|
|
23
|
+
...(key.startsWith("pat_") ? { Authorization: `Bearer ${key}` } : {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async function request(path, options = {}) {
|
|
27
|
+
const url = path.startsWith("http") ? path : `${BASE_URL.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
|
|
28
|
+
const res = await fetch(url, {
|
|
29
|
+
...options,
|
|
30
|
+
headers: { ...authHeaders(), ...options.headers },
|
|
31
|
+
});
|
|
32
|
+
const body = await res.json().catch(() => ({}));
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
const msg = body?.error ?? body?.message ?? res.statusText;
|
|
35
|
+
throw new Error(`Stackby API ${res.status}: ${msg}`);
|
|
36
|
+
}
|
|
37
|
+
return body;
|
|
38
|
+
}
|
|
39
|
+
/** GET /api/v1/mcp/workspaces — list workspaces (MCP API only). */
|
|
40
|
+
export async function getWorkspaces() {
|
|
41
|
+
const out = await request(`${MCP_API}/workspaces`, { method: "GET" });
|
|
42
|
+
return Array.isArray(out.data) ? out.data : [];
|
|
43
|
+
}
|
|
44
|
+
/** POST /api/v1/mcp/stacks — list stacks in a workspace. Body: { workspaceId }. */
|
|
45
|
+
export async function getStacks(workspaceId, workspaceName) {
|
|
46
|
+
const out = await request(`${MCP_API}/stacks`, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
body: JSON.stringify({ workspaceId }),
|
|
49
|
+
});
|
|
50
|
+
const raw = Array.isArray(out.data) ? out.data : [];
|
|
51
|
+
const list = raw.map((s) => ({
|
|
52
|
+
stackId: s.stackId ?? "",
|
|
53
|
+
stackName: s.stackName,
|
|
54
|
+
workspaceId: s.workspaceId,
|
|
55
|
+
color: s.color,
|
|
56
|
+
icon: s.icon,
|
|
57
|
+
}));
|
|
58
|
+
return { list, workspaceName: workspaceName ?? "" };
|
|
59
|
+
}
|
|
60
|
+
/** Fetch all stacks across all workspaces (for list_stacks tool). */
|
|
61
|
+
export async function getAllStacks() {
|
|
62
|
+
const workspaces = await getWorkspaces();
|
|
63
|
+
const all = [];
|
|
64
|
+
for (const ws of workspaces) {
|
|
65
|
+
try {
|
|
66
|
+
const { list, workspaceName } = await getStacks(ws.id, ws.name);
|
|
67
|
+
for (const s of list) {
|
|
68
|
+
all.push({ ...s, workspaceName });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
// skip workspace on error (e.g. no access)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return all;
|
|
76
|
+
}
|
|
77
|
+
/** GET /api/v1/mcp/stacks/:stackId/tables — list tables in a stack (MCP API only). */
|
|
78
|
+
export async function getTables(stackId) {
|
|
79
|
+
const out = await request(`${MCP_API}/stacks/${encodeURIComponent(stackId)}/tables`, { method: "GET" });
|
|
80
|
+
return Array.isArray(out.data) ? out.data : [];
|
|
81
|
+
}
|
|
82
|
+
/** GET /api/v1/mcp/stacks/:stackId/tables/:tableId/columns — list columns (MCP API only). */
|
|
83
|
+
export async function getTableColumns(stackId, tableId) {
|
|
84
|
+
const path = `${MCP_API}/stacks/${encodeURIComponent(stackId)}/tables/${encodeURIComponent(tableId)}/columns`;
|
|
85
|
+
const out = await request(path, { method: "GET" });
|
|
86
|
+
return Array.isArray(out.data) ? out.data : [];
|
|
87
|
+
}
|
|
88
|
+
/** GET /api/v1/mcp/stacks/:stackId/tables/:tableId/views — list all visible views (MCP API only). */
|
|
89
|
+
export async function getTableViewList(stackId, tableId) {
|
|
90
|
+
const path = `${MCP_API}/stacks/${encodeURIComponent(stackId)}/tables/${encodeURIComponent(tableId)}/views`;
|
|
91
|
+
const out = await request(path, { method: "GET" });
|
|
92
|
+
return Array.isArray(out.data) ? out.data : [];
|
|
93
|
+
}
|
|
94
|
+
/** Describe one table: name (from tablelist), fields (columnlist), views (viewlist). */
|
|
95
|
+
export async function describeTable(stackId, tableId) {
|
|
96
|
+
const [tables, fields, views] = await Promise.all([
|
|
97
|
+
getTables(stackId),
|
|
98
|
+
getTableColumns(stackId, tableId),
|
|
99
|
+
getTableViewList(stackId, tableId),
|
|
100
|
+
]);
|
|
101
|
+
const tableMeta = tables.find((t) => t.id === tableId);
|
|
102
|
+
return {
|
|
103
|
+
id: tableId,
|
|
104
|
+
name: tableMeta?.name ?? tableId,
|
|
105
|
+
fields,
|
|
106
|
+
views,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/** GET /api/v1/mcp/stacks/:stackId/tables/:tableId/rows — list rows (MCP API). */
|
|
110
|
+
export async function getRowList(stackId, tableId, opts = {}) {
|
|
111
|
+
const maxRecords = Math.min(Math.max(1, opts.maxRecords ?? 100), 100);
|
|
112
|
+
const offset = Math.max(0, opts.offset ?? 0);
|
|
113
|
+
let path = `${MCP_API}/stacks/${encodeURIComponent(stackId)}/tables/${encodeURIComponent(tableId)}/rows?maxrecord=${maxRecords}&offset=${offset}`;
|
|
114
|
+
if (opts.rowIds && opts.rowIds.length > 0) {
|
|
115
|
+
path += `&rowIds=${encodeURIComponent(opts.rowIds.join(","))}`;
|
|
116
|
+
}
|
|
117
|
+
const out = await request(path, { method: "GET" });
|
|
118
|
+
return Array.isArray(out.data) ? out.data : [];
|
|
119
|
+
}
|
|
120
|
+
/** GET /api/v1/mcp/stacks/:stackId/tables/:tableId/rows/:recordId — get one record (MCP API). */
|
|
121
|
+
export async function getRecord(stackId, tableId, recordId) {
|
|
122
|
+
const path = `${MCP_API}/stacks/${encodeURIComponent(stackId)}/tables/${encodeURIComponent(tableId)}/rows/${encodeURIComponent(recordId)}`;
|
|
123
|
+
const out = await request(path, { method: "GET" });
|
|
124
|
+
const data = out.data;
|
|
125
|
+
if (Array.isArray(data))
|
|
126
|
+
return data.length > 0 ? data[0] : null;
|
|
127
|
+
return data && typeof data === "object" ? data : null;
|
|
128
|
+
}
|
|
129
|
+
/** POST /api/v1/mcp/stacks/:stackId/tables/:tableId/search — search rows (MCP API). */
|
|
130
|
+
export async function searchRecords(stackId, tableId, searchTerm, opts = {}) {
|
|
131
|
+
let columnId = opts.columnId?.trim();
|
|
132
|
+
if (!columnId) {
|
|
133
|
+
const columns = await getTableColumns(stackId, tableId);
|
|
134
|
+
columnId = columns.length > 0 ? columns[0].id : "";
|
|
135
|
+
}
|
|
136
|
+
const maxRecord = Math.min(Math.max(1, opts.maxRecords ?? 100), 99999);
|
|
137
|
+
const path = `${MCP_API}/stacks/${encodeURIComponent(stackId)}/tables/${encodeURIComponent(tableId)}/search`;
|
|
138
|
+
const body = { search: searchTerm, columnId: columnId || undefined, maxRecords: maxRecord };
|
|
139
|
+
const out = await request(path, {
|
|
140
|
+
method: "POST",
|
|
141
|
+
body: JSON.stringify(body),
|
|
142
|
+
});
|
|
143
|
+
const data = out.data;
|
|
144
|
+
const first = Array.isArray(data) ? data[0] : data;
|
|
145
|
+
if (!first || !first.rowIds) {
|
|
146
|
+
return { rowIds: [], rowname: [], fields: [] };
|
|
147
|
+
}
|
|
148
|
+
const f = first;
|
|
149
|
+
return { rowIds: f.rowIds ?? [], rowname: f.rowname ?? [], fields: f.fields ?? [] };
|
|
150
|
+
}
|
|
151
|
+
// --- Write APIs (Phase 3) ---
|
|
152
|
+
/** POST /api/v1/mcp/stacks/:stackId/tables/:tableId/rows — create row(s) (MCP API). */
|
|
153
|
+
export async function createRow(stackId, tableId, fields) {
|
|
154
|
+
const path = `${MCP_API}/stacks/${encodeURIComponent(stackId)}/tables/${encodeURIComponent(tableId)}/rows`;
|
|
155
|
+
const body = { records: [{ field: fields }] };
|
|
156
|
+
const out = await request(path, {
|
|
157
|
+
method: "POST",
|
|
158
|
+
body: JSON.stringify(body),
|
|
159
|
+
});
|
|
160
|
+
return Array.isArray(out.data) ? out.data : [];
|
|
161
|
+
}
|
|
162
|
+
/** POST /api/v1/mcp/stacks/:stackId/tables/:tableId/rows/update — update rows (MCP API). */
|
|
163
|
+
export async function updateRows(stackId, tableId, records) {
|
|
164
|
+
if (records.length === 0)
|
|
165
|
+
return [];
|
|
166
|
+
if (records.length > 10)
|
|
167
|
+
throw new Error("update_records supports at most 10 records per request.");
|
|
168
|
+
const path = `${MCP_API}/stacks/${encodeURIComponent(stackId)}/tables/${encodeURIComponent(tableId)}/rows/update`;
|
|
169
|
+
const body = {
|
|
170
|
+
records: records.map((r) => ({ id: r.id, field: r.fields })),
|
|
171
|
+
};
|
|
172
|
+
const out = await request(path, {
|
|
173
|
+
method: "POST",
|
|
174
|
+
body: JSON.stringify(body),
|
|
175
|
+
});
|
|
176
|
+
return Array.isArray(out.data) ? out.data : [];
|
|
177
|
+
}
|
|
178
|
+
/** DELETE /api/v1/mcp/stacks/:stackId/tables/:tableId/rows — soft-delete rows (MCP API). */
|
|
179
|
+
export async function deleteRows(stackId, tableId, recordIds) {
|
|
180
|
+
if (recordIds.length === 0)
|
|
181
|
+
return { records: [] };
|
|
182
|
+
if (recordIds.length > 10)
|
|
183
|
+
throw new Error("delete_records supports at most 10 records per request.");
|
|
184
|
+
const query = recordIds.map((id) => `rowIds=${encodeURIComponent(id)}`).join("&");
|
|
185
|
+
const path = `${MCP_API}/stacks/${encodeURIComponent(stackId)}/tables/${encodeURIComponent(tableId)}/rows?${query}`;
|
|
186
|
+
const out = await request(path, {
|
|
187
|
+
method: "DELETE",
|
|
188
|
+
});
|
|
189
|
+
return out.data ?? { records: [] };
|
|
190
|
+
}
|
|
191
|
+
/** POST /api/v1/mcp/stacks/:stackId/tables — create a table in a stack (MCP API). */
|
|
192
|
+
export async function createTable(stackId, name, _description) {
|
|
193
|
+
const path = `${MCP_API}/stacks/${encodeURIComponent(stackId)}/tables`;
|
|
194
|
+
const body = { name: name.trim() };
|
|
195
|
+
const out = await request(path, {
|
|
196
|
+
method: "POST",
|
|
197
|
+
body: JSON.stringify(body),
|
|
198
|
+
});
|
|
199
|
+
return out.data ?? {};
|
|
200
|
+
}
|
|
201
|
+
/** Column types supported by POST /api/v1/columnCreate/:columnType (devapi). */
|
|
202
|
+
export const COLUMN_TYPES = [
|
|
203
|
+
"shortText",
|
|
204
|
+
"longText",
|
|
205
|
+
"number",
|
|
206
|
+
"checkbox",
|
|
207
|
+
"dateAndTime",
|
|
208
|
+
"time",
|
|
209
|
+
"singleOption",
|
|
210
|
+
"multipleOptions",
|
|
211
|
+
"email",
|
|
212
|
+
"url",
|
|
213
|
+
"phoneNumber",
|
|
214
|
+
"rating",
|
|
215
|
+
"duration",
|
|
216
|
+
"autoNumber",
|
|
217
|
+
"createdTime",
|
|
218
|
+
"updatedTime",
|
|
219
|
+
"createdBy",
|
|
220
|
+
"updatedBy",
|
|
221
|
+
"attachment",
|
|
222
|
+
"link",
|
|
223
|
+
"lookup",
|
|
224
|
+
"lookupCount",
|
|
225
|
+
"aggregation",
|
|
226
|
+
"formula",
|
|
227
|
+
"checkList",
|
|
228
|
+
"location",
|
|
229
|
+
"barcode",
|
|
230
|
+
"signature",
|
|
231
|
+
];
|
|
232
|
+
/** POST /api/v1/mcp/columns — create a column (MCP API). Body: stackId, tableId, name, columnType, viewId?, options? */
|
|
233
|
+
export async function createColumn(stackId, tableId, name, columnType, opts = {}) {
|
|
234
|
+
const path = `${MCP_API}/columns`;
|
|
235
|
+
const body = {
|
|
236
|
+
stackId,
|
|
237
|
+
tableId,
|
|
238
|
+
name: name.trim(),
|
|
239
|
+
columnType,
|
|
240
|
+
viewId: opts.viewId ?? "",
|
|
241
|
+
};
|
|
242
|
+
if (opts.options && opts.options.length > 0) {
|
|
243
|
+
body.options = opts.options;
|
|
244
|
+
}
|
|
245
|
+
const out = await request(path, {
|
|
246
|
+
method: "POST",
|
|
247
|
+
body: JSON.stringify(body),
|
|
248
|
+
});
|
|
249
|
+
return out.data ?? {};
|
|
250
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "stackby-mcp-server",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "MCP server for Stackby — list stacks, tables, records; create/update/delete rows and schema via AI clients (Cursor, Claude, Cline).",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": { "stackby-mcp-server": "dist/index.js" },
|
|
7
|
+
"files": ["dist"],
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc && node -e \"console.log('\\nBuild OK. Output in dist/'); console.log('Run npm start to run the server.');\"",
|
|
10
|
+
"start": "node dist/index.js",
|
|
11
|
+
"dev": "tsc && node dist/index.js",
|
|
12
|
+
"prepare": "npm run build"
|
|
13
|
+
},
|
|
14
|
+
"engines": { "node": ">=18" },
|
|
15
|
+
"keywords": ["mcp", "stackby", "model-context-protocol", "cursor", "claude", "ai"],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"type": "module",
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
20
|
+
"zod": "^3.23.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^20.10.0",
|
|
24
|
+
"typescript": "^5.3.0"
|
|
25
|
+
}
|
|
26
|
+
}
|