@seatable/mcp-seatable 0.9.5
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/.env.example +11 -0
- package/LICENSE +21 -0
- package/README.md +302 -0
- package/bin/seatable-mcp.cjs +20 -0
- package/dist/auth/tokenValidator.d.ts +10 -0
- package/dist/auth/tokenValidator.d.ts.map +1 -0
- package/dist/auth/tokenValidator.js +55 -0
- package/dist/auth/tokenValidator.js.map +1 -0
- package/dist/config/env.d.ts +67 -0
- package/dist/config/env.d.ts.map +1 -0
- package/dist/config/env.js +105 -0
- package/dist/config/env.js.map +1 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +28 -0
- package/dist/errors.js.map +1 -0
- package/dist/http/httpServer.d.ts +7 -0
- package/dist/http/httpServer.d.ts.map +1 -0
- package/dist/http/httpServer.js +211 -0
- package/dist/http/httpServer.js.map +1 -0
- package/dist/http/sseServer.d.ts +11 -0
- package/dist/http/sseServer.d.ts.map +1 -0
- package/dist/http/sseServer.js +154 -0
- package/dist/http/sseServer.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +86 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +5 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +34 -0
- package/dist/logger.js.map +1 -0
- package/dist/mcp/server.d.ts +52 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +222 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools/addRow.d.ts +3 -0
- package/dist/mcp/tools/addRow.d.ts.map +1 -0
- package/dist/mcp/tools/addRow.js +23 -0
- package/dist/mcp/tools/addRow.js.map +1 -0
- package/dist/mcp/tools/addSelectOption.d.ts +3 -0
- package/dist/mcp/tools/addSelectOption.d.ts.map +1 -0
- package/dist/mcp/tools/addSelectOption.js +27 -0
- package/dist/mcp/tools/addSelectOption.js.map +1 -0
- package/dist/mcp/tools/appendRows.d.ts +3 -0
- package/dist/mcp/tools/appendRows.d.ts.map +1 -0
- package/dist/mcp/tools/appendRows.js +27 -0
- package/dist/mcp/tools/appendRows.js.map +1 -0
- package/dist/mcp/tools/attachFileToRow.d.ts +3 -0
- package/dist/mcp/tools/attachFileToRow.d.ts.map +1 -0
- package/dist/mcp/tools/attachFileToRow.js +42 -0
- package/dist/mcp/tools/attachFileToRow.js.map +1 -0
- package/dist/mcp/tools/bulkSetSelectOptions.d.ts +3 -0
- package/dist/mcp/tools/bulkSetSelectOptions.d.ts.map +1 -0
- package/dist/mcp/tools/bulkSetSelectOptions.js +49 -0
- package/dist/mcp/tools/bulkSetSelectOptions.js.map +1 -0
- package/dist/mcp/tools/deleteRow.d.ts +3 -0
- package/dist/mcp/tools/deleteRow.d.ts.map +1 -0
- package/dist/mcp/tools/deleteRow.js +22 -0
- package/dist/mcp/tools/deleteRow.js.map +1 -0
- package/dist/mcp/tools/echoArgs.d.ts +3 -0
- package/dist/mcp/tools/echoArgs.d.ts.map +1 -0
- package/dist/mcp/tools/echoArgs.js +14 -0
- package/dist/mcp/tools/echoArgs.js.map +1 -0
- package/dist/mcp/tools/findRows.d.ts +9 -0
- package/dist/mcp/tools/findRows.d.ts.map +1 -0
- package/dist/mcp/tools/findRows.js +255 -0
- package/dist/mcp/tools/findRows.js.map +1 -0
- package/dist/mcp/tools/getRow.d.ts +3 -0
- package/dist/mcp/tools/getRow.d.ts.map +1 -0
- package/dist/mcp/tools/getRow.js +18 -0
- package/dist/mcp/tools/getRow.js.map +1 -0
- package/dist/mcp/tools/getSchema.d.ts +3 -0
- package/dist/mcp/tools/getSchema.d.ts.map +1 -0
- package/dist/mcp/tools/getSchema.js +16 -0
- package/dist/mcp/tools/getSchema.js.map +1 -0
- package/dist/mcp/tools/linkRows.d.ts +3 -0
- package/dist/mcp/tools/linkRows.d.ts.map +1 -0
- package/dist/mcp/tools/linkRows.js +23 -0
- package/dist/mcp/tools/linkRows.js.map +1 -0
- package/dist/mcp/tools/listBases.d.ts +3 -0
- package/dist/mcp/tools/listBases.d.ts.map +1 -0
- package/dist/mcp/tools/listBases.js +16 -0
- package/dist/mcp/tools/listBases.js.map +1 -0
- package/dist/mcp/tools/listCollaborators.d.ts +3 -0
- package/dist/mcp/tools/listCollaborators.d.ts.map +1 -0
- package/dist/mcp/tools/listCollaborators.js +16 -0
- package/dist/mcp/tools/listCollaborators.js.map +1 -0
- package/dist/mcp/tools/listRows.d.ts +3 -0
- package/dist/mcp/tools/listRows.d.ts.map +1 -0
- package/dist/mcp/tools/listRows.js +20 -0
- package/dist/mcp/tools/listRows.js.map +1 -0
- package/dist/mcp/tools/listTables.d.ts +3 -0
- package/dist/mcp/tools/listTables.d.ts.map +1 -0
- package/dist/mcp/tools/listTables.js +19 -0
- package/dist/mcp/tools/listTables.js.map +1 -0
- package/dist/mcp/tools/manageColumns.d.ts +3 -0
- package/dist/mcp/tools/manageColumns.d.ts.map +1 -0
- package/dist/mcp/tools/manageColumns.js +44 -0
- package/dist/mcp/tools/manageColumns.js.map +1 -0
- package/dist/mcp/tools/manageTables.d.ts +3 -0
- package/dist/mcp/tools/manageTables.d.ts.map +1 -0
- package/dist/mcp/tools/manageTables.js +36 -0
- package/dist/mcp/tools/manageTables.js.map +1 -0
- package/dist/mcp/tools/pingSeatable.d.ts +3 -0
- package/dist/mcp/tools/pingSeatable.d.ts.map +1 -0
- package/dist/mcp/tools/pingSeatable.js +37 -0
- package/dist/mcp/tools/pingSeatable.js.map +1 -0
- package/dist/mcp/tools/querySql.d.ts +3 -0
- package/dist/mcp/tools/querySql.d.ts.map +1 -0
- package/dist/mcp/tools/querySql.js +28 -0
- package/dist/mcp/tools/querySql.js.map +1 -0
- package/dist/mcp/tools/searchRows.d.ts +3 -0
- package/dist/mcp/tools/searchRows.d.ts.map +1 -0
- package/dist/mcp/tools/searchRows.js +18 -0
- package/dist/mcp/tools/searchRows.js.map +1 -0
- package/dist/mcp/tools/types.d.ts +90 -0
- package/dist/mcp/tools/types.d.ts.map +1 -0
- package/dist/mcp/tools/types.js +2 -0
- package/dist/mcp/tools/types.js.map +1 -0
- package/dist/mcp/tools/unlinkRows.d.ts +3 -0
- package/dist/mcp/tools/unlinkRows.d.ts.map +1 -0
- package/dist/mcp/tools/unlinkRows.js +23 -0
- package/dist/mcp/tools/unlinkRows.js.map +1 -0
- package/dist/mcp/tools/updateRow.d.ts +3 -0
- package/dist/mcp/tools/updateRow.d.ts.map +1 -0
- package/dist/mcp/tools/updateRow.js +32 -0
- package/dist/mcp/tools/updateRow.js.map +1 -0
- package/dist/mcp/tools/uploadFile.d.ts +3 -0
- package/dist/mcp/tools/uploadFile.d.ts.map +1 -0
- package/dist/mcp/tools/uploadFile.js +29 -0
- package/dist/mcp/tools/uploadFile.js.map +1 -0
- package/dist/mcp/tools/upsertRows.d.ts +3 -0
- package/dist/mcp/tools/upsertRows.d.ts.map +1 -0
- package/dist/mcp/tools/upsertRows.js +55 -0
- package/dist/mcp/tools/upsertRows.js.map +1 -0
- package/dist/ratelimit/connectionCounter.d.ts +11 -0
- package/dist/ratelimit/connectionCounter.d.ts.map +1 -0
- package/dist/ratelimit/connectionCounter.js +27 -0
- package/dist/ratelimit/connectionCounter.js.map +1 -0
- package/dist/ratelimit/index.d.ts +27 -0
- package/dist/ratelimit/index.d.ts.map +1 -0
- package/dist/ratelimit/index.js +50 -0
- package/dist/ratelimit/index.js.map +1 -0
- package/dist/ratelimit/rateLimiter.d.ts +18 -0
- package/dist/ratelimit/rateLimiter.d.ts.map +1 -0
- package/dist/ratelimit/rateLimiter.js +54 -0
- package/dist/ratelimit/rateLimiter.js.map +1 -0
- package/dist/schema/generic.d.ts +126 -0
- package/dist/schema/generic.d.ts.map +1 -0
- package/dist/schema/generic.js +45 -0
- package/dist/schema/generic.js.map +1 -0
- package/dist/schema/jsonSchemaToZod.d.ts +3 -0
- package/dist/schema/jsonSchemaToZod.d.ts.map +1 -0
- package/dist/schema/jsonSchemaToZod.js +53 -0
- package/dist/schema/jsonSchemaToZod.js.map +1 -0
- package/dist/schema/map.d.ts +3 -0
- package/dist/schema/map.d.ts.map +1 -0
- package/dist/schema/map.js +92 -0
- package/dist/schema/map.js.map +1 -0
- package/dist/schema/validate.d.ts +15 -0
- package/dist/schema/validate.d.ts.map +1 -0
- package/dist/schema/validate.js +170 -0
- package/dist/schema/validate.js.map +1 -0
- package/dist/seatable/client.d.ts +106 -0
- package/dist/seatable/client.d.ts.map +1 -0
- package/dist/seatable/client.js +378 -0
- package/dist/seatable/client.js.map +1 -0
- package/dist/seatable/clientRegistry.d.ts +11 -0
- package/dist/seatable/clientRegistry.d.ts.map +1 -0
- package/dist/seatable/clientRegistry.js +33 -0
- package/dist/seatable/clientRegistry.js.map +1 -0
- package/dist/seatable/contextualClient.d.ts +92 -0
- package/dist/seatable/contextualClient.d.ts.map +1 -0
- package/dist/seatable/contextualClient.js +42 -0
- package/dist/seatable/contextualClient.js.map +1 -0
- package/dist/seatable/mockClient.d.ts +68 -0
- package/dist/seatable/mockClient.d.ts.map +1 -0
- package/dist/seatable/mockClient.js +115 -0
- package/dist/seatable/mockClient.js.map +1 -0
- package/dist/seatable/tokenManager.d.ts +28 -0
- package/dist/seatable/tokenManager.d.ts.map +1 -0
- package/dist/seatable/tokenManager.js +92 -0
- package/dist/seatable/tokenManager.js.map +1 -0
- package/dist/seatable/types.d.ts +22 -0
- package/dist/seatable/types.d.ts.map +1 -0
- package/dist/seatable/types.js +3 -0
- package/dist/seatable/types.js.map +1 -0
- package/dist/seatable/utils.d.ts +7 -0
- package/dist/seatable/utils.d.ts.map +1 -0
- package/dist/seatable/utils.js +25 -0
- package/dist/seatable/utils.js.map +1 -0
- package/package.json +94 -0
package/.env.example
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# SeaTable configuration
|
|
2
|
+
SEATABLE_SERVER_URL=
|
|
3
|
+
SEATABLE_API_TOKEN=
|
|
4
|
+
# Optional: use in-memory mock client for local testing (true/1 to enable)
|
|
5
|
+
SEATABLE_MOCK=
|
|
6
|
+
# Server mode: selfhosted (default) or managed (multi-tenant HTTP with per-client auth)
|
|
7
|
+
SEATABLE_MODE=selfhosted
|
|
8
|
+
|
|
9
|
+
# Multi-base mode (selfhosted only): serve multiple bases from one process.
|
|
10
|
+
# JSON array with base_name and api_token. Use instead of SEATABLE_API_TOKEN.
|
|
11
|
+
# SEATABLE_BASES='[{"base_name":"CRM","api_token":"token_abc"},{"base_name":"Projects","api_token":"token_def"}]'
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 SeaTable GmbH
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to do so, subject to the
|
|
10
|
+
following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# mcp-seatable
|
|
2
|
+
|
|
3
|
+
> **Beta** — This project is under active development. APIs and configuration may change between releases.
|
|
4
|
+
|
|
5
|
+
The official Model Context Protocol (MCP) server for [SeaTable](https://seatable.com), built and maintained by SeaTable GmbH. It lets AI agents interact with data in your bases — reading, writing, searching, linking, and querying rows through a focused set of tools. The server intentionally focuses on data operations, not schema management (creating/deleting tables or columns), keeping the tool set lean and safe for autonomous agent use.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
The fastest way to get started depends on your setup:
|
|
10
|
+
|
|
11
|
+
- **SeaTable Cloud** — Use the hosted MCP server at `mcp.seatable.com`, no installation needed
|
|
12
|
+
- **Self-hosted SeaTable** — Run the MCP server locally via `npx` in your IDE
|
|
13
|
+
|
|
14
|
+
### SeaTable Cloud (hosted MCP server)
|
|
15
|
+
|
|
16
|
+
If you use [SeaTable Cloud](https://cloud.seatable.io), there is a hosted MCP server ready to use — no installation required. Configure your MCP client with the Streamable HTTP endpoint:
|
|
17
|
+
|
|
18
|
+
**Claude Desktop** — add to `claude_desktop_config.json`:
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"mcpServers": {
|
|
23
|
+
"seatable": {
|
|
24
|
+
"type": "streamable-http",
|
|
25
|
+
"url": "https://mcp.seatable.com/mcp",
|
|
26
|
+
"headers": {
|
|
27
|
+
"Authorization": "Bearer your-api-token"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Cursor / VSCode** — add to your MCP settings (JSON):
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"mcp.servers": {
|
|
39
|
+
"seatable": {
|
|
40
|
+
"type": "streamable-http",
|
|
41
|
+
"url": "https://mcp.seatable.com/mcp",
|
|
42
|
+
"headers": {
|
|
43
|
+
"Authorization": "Bearer your-api-token"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Self-hosted SeaTable
|
|
51
|
+
|
|
52
|
+
For self-hosted SeaTable instances, run the MCP server locally via `npx`. Your IDE starts and manages the process automatically.
|
|
53
|
+
|
|
54
|
+
**Claude Desktop** — add to `claude_desktop_config.json`:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"mcpServers": {
|
|
59
|
+
"seatable": {
|
|
60
|
+
"command": "npx",
|
|
61
|
+
"args": ["-y", "@seatable/mcp-seatable"],
|
|
62
|
+
"env": {
|
|
63
|
+
"SEATABLE_SERVER_URL": "https://your-seatable-server.com",
|
|
64
|
+
"SEATABLE_API_TOKEN": "your-api-token"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Cursor / VSCode** — add to your MCP settings (JSON):
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"mcp.servers": {
|
|
76
|
+
"seatable": {
|
|
77
|
+
"command": "npx",
|
|
78
|
+
"args": ["-y", "@seatable/mcp-seatable"],
|
|
79
|
+
"env": {
|
|
80
|
+
"SEATABLE_SERVER_URL": "https://your-seatable-server.com",
|
|
81
|
+
"SEATABLE_API_TOKEN": "your-api-token"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Deployment Options
|
|
89
|
+
|
|
90
|
+
If you need to run your own server instance — for example on your own infrastructure, with multi-base support, or in multi-tenant mode — use one of the options below.
|
|
91
|
+
|
|
92
|
+
### HTTP Server (Network Access)
|
|
93
|
+
|
|
94
|
+
Run a local HTTP server with Streamable HTTP transport:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
PORT=3001 npx -y @seatable/mcp-seatable --sse
|
|
98
|
+
|
|
99
|
+
# Health check
|
|
100
|
+
curl http://localhost:3001/health
|
|
101
|
+
|
|
102
|
+
# MCP endpoint: POST/GET/DELETE http://localhost:3001/mcp
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Multi-Base (Selfhosted)
|
|
106
|
+
|
|
107
|
+
Serve multiple bases from a single process:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
SEATABLE_SERVER_URL=https://your-seatable-server.com \
|
|
111
|
+
SEATABLE_BASES='[{"base_name":"CRM","api_token":"token_abc"},{"base_name":"Projects","api_token":"token_def"}]' \
|
|
112
|
+
npx -y @seatable/mcp-seatable
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Each tool automatically gets a `base` parameter. Use `list_bases` to see available bases.
|
|
116
|
+
|
|
117
|
+
### Managed Mode (Multi-Tenant HTTP)
|
|
118
|
+
|
|
119
|
+
For hosting an MCP endpoint where each client authenticates with their own SeaTable API token:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
SEATABLE_MODE=managed \
|
|
123
|
+
SEATABLE_SERVER_URL=https://your-seatable-server.com \
|
|
124
|
+
PORT=3000 npx -y @seatable/mcp-seatable --sse
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Clients pass their API token via `Authorization: Bearer <token>` on session initialization. The server validates the token against SeaTable and applies rate limits (60 req/min per token, 120/min per IP, 5 concurrent connections per token).
|
|
128
|
+
|
|
129
|
+
### Docker
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
docker run -d --name seatable-mcp \
|
|
133
|
+
-p 3000:3000 \
|
|
134
|
+
-e SEATABLE_SERVER_URL=https://your-seatable-server.com \
|
|
135
|
+
-e SEATABLE_API_TOKEN=your-api-token \
|
|
136
|
+
seatable/seatable-mcp:latest
|
|
137
|
+
|
|
138
|
+
# Health check
|
|
139
|
+
curl http://localhost:3000/health
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Environment Variables
|
|
143
|
+
|
|
144
|
+
Required:
|
|
145
|
+
|
|
146
|
+
- `SEATABLE_SERVER_URL` — Your SeaTable server URL
|
|
147
|
+
|
|
148
|
+
Authentication (one of these is required in selfhosted mode):
|
|
149
|
+
|
|
150
|
+
- `SEATABLE_API_TOKEN` — Single-base API token
|
|
151
|
+
- `SEATABLE_BASES` — Multi-base: JSON array (e.g. `'[{"base_name":"CRM","api_token":"..."}]'`)
|
|
152
|
+
|
|
153
|
+
Optional:
|
|
154
|
+
|
|
155
|
+
- `SEATABLE_MODE` — `selfhosted` (default) or `managed` (multi-tenant HTTP with per-client auth)
|
|
156
|
+
- `SEATABLE_MOCK=true` — Enable mock mode for offline testing
|
|
157
|
+
|
|
158
|
+
## MCP Tools
|
|
159
|
+
|
|
160
|
+
### Schema Introspection
|
|
161
|
+
|
|
162
|
+
- **`list_tables`** — Get all tables with metadata
|
|
163
|
+
- **`get_schema`** — Get complete database structure
|
|
164
|
+
- **`list_bases`** — List available bases (multi-base mode only)
|
|
165
|
+
- **`list_collaborators`** — List users with access to the base (for collaborator columns)
|
|
166
|
+
|
|
167
|
+
### Reading Data
|
|
168
|
+
|
|
169
|
+
- **`list_rows`** — Paginated row listing with sorting
|
|
170
|
+
- **`get_row`** — Retrieve specific row by ID
|
|
171
|
+
- **`find_rows`** — Client-side filtering with DSL
|
|
172
|
+
- **`search_rows`** — Search via SQL WHERE clauses
|
|
173
|
+
- **`query_sql`** — Execute SQL queries with parameterized inputs
|
|
174
|
+
|
|
175
|
+
### Writing Data
|
|
176
|
+
|
|
177
|
+
- **`add_row`** — Add single new row
|
|
178
|
+
- **`append_rows`** — Batch insert rows
|
|
179
|
+
- **`update_rows`** — Batch update rows
|
|
180
|
+
- **`upsert_rows`** — Insert or update rows by key columns
|
|
181
|
+
- **`delete_rows`** — Remove rows by ID
|
|
182
|
+
- **`upload_file`** — Upload a file or image to a row (base64-encoded)
|
|
183
|
+
|
|
184
|
+
### Linking
|
|
185
|
+
|
|
186
|
+
- **`link_rows`** — Create relationships between rows
|
|
187
|
+
- **`unlink_rows`** — Remove relationships between rows
|
|
188
|
+
|
|
189
|
+
### Utilities
|
|
190
|
+
|
|
191
|
+
- **`add_select_options`** — Add new options to single-select or multi-select columns
|
|
192
|
+
- **`ping_seatable`** — Health check with latency monitoring
|
|
193
|
+
|
|
194
|
+
## Supported Column Types
|
|
195
|
+
|
|
196
|
+
SeaTable bases can contain many different column types. The following table shows which types can be written via the API and what format to use.
|
|
197
|
+
|
|
198
|
+
| Column Type | Writable | Value Format |
|
|
199
|
+
|---|---|---|
|
|
200
|
+
| Text | Yes | `"string"` |
|
|
201
|
+
| Long Text | Yes | `"Markdown string"` |
|
|
202
|
+
| Number (incl. percent, currency) | Yes | `123.45` |
|
|
203
|
+
| Checkbox | Yes | `true` / `false` |
|
|
204
|
+
| Date | Yes | `"YYYY-MM-DD"` or `"YYYY-MM-DD HH:mm"` |
|
|
205
|
+
| Duration | Yes | `"h:mm"` or `"h:mm:ss"` |
|
|
206
|
+
| Single Select | Yes | `"option name"` |
|
|
207
|
+
| Multiple Select | Yes | `["option a", "option b"]` |
|
|
208
|
+
| Email | Yes | `"user@example.com"` |
|
|
209
|
+
| URL | Yes | `"https://..."` |
|
|
210
|
+
| Rating | Yes | `4` (integer) |
|
|
211
|
+
| Geolocation | Yes | `{"lat": 52.52, "lng": 13.40}` |
|
|
212
|
+
| Collaborator | Yes | `["0b995819003140ed8e9efe05e817b000@auth.local"]` — use `list_collaborators` to get user IDs |
|
|
213
|
+
| Link | Yes | Use `link_rows` / `unlink_rows` tools |
|
|
214
|
+
| Image / File | Yes | Use `upload_file` tool with base64-encoded data |
|
|
215
|
+
| Formula / Link Formula | No | Read-only, computed by SeaTable |
|
|
216
|
+
| Creator / Created Time / Modified Time | No | Read-only, set automatically |
|
|
217
|
+
| Auto Number | No | Read-only, set automatically |
|
|
218
|
+
| Button / Digital Signature | No | Not accessible via API |
|
|
219
|
+
|
|
220
|
+
## Tool Examples
|
|
221
|
+
|
|
222
|
+
```json
|
|
223
|
+
// List all tables
|
|
224
|
+
{ "tool": "list_tables", "args": {} }
|
|
225
|
+
|
|
226
|
+
// Get rows with pagination
|
|
227
|
+
{ "tool": "list_rows", "args": { "table": "Tasks", "page_size": 10, "order_by": "_ctime", "direction": "desc" } }
|
|
228
|
+
|
|
229
|
+
// Add rows
|
|
230
|
+
{ "tool": "append_rows", "args": { "table": "Tasks", "rows": [{ "Title": "New Task", "Status": "Todo" }] } }
|
|
231
|
+
|
|
232
|
+
// SQL query
|
|
233
|
+
{ "tool": "query_sql", "args": { "sql": "SELECT Status, COUNT(*) as count FROM Tasks GROUP BY Status" } }
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Programmatic Usage
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
import { createMcpServer } from '@seatable/mcp-seatable'
|
|
240
|
+
|
|
241
|
+
const server = await createMcpServer({
|
|
242
|
+
serverUrl: 'https://your-seatable-server.com',
|
|
243
|
+
apiToken: 'your-api-token',
|
|
244
|
+
})
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Mock Mode
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
SEATABLE_MOCK=true npm run dev
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
In-memory tables and rows for demos and tests without a live SeaTable instance.
|
|
254
|
+
|
|
255
|
+
## Development
|
|
256
|
+
|
|
257
|
+
### Prerequisites
|
|
258
|
+
|
|
259
|
+
- Node.js >= 18
|
|
260
|
+
|
|
261
|
+
### Setup
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
git clone https://github.com/seatable/seatable-mcp
|
|
265
|
+
cd seatable-mcp
|
|
266
|
+
npm install
|
|
267
|
+
cp .env.example .env # Configure your SeaTable settings
|
|
268
|
+
npm run dev # Start in watch mode
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Scripts
|
|
272
|
+
|
|
273
|
+
- `npm run dev` — Start server in watch mode (tsx)
|
|
274
|
+
- `npm run build` — Compile TypeScript
|
|
275
|
+
- `npm run start` — Run compiled server
|
|
276
|
+
- `npm test` — Run tests (vitest)
|
|
277
|
+
- `npm run lint` — Lint code
|
|
278
|
+
- `npm run typecheck` — TypeScript type check
|
|
279
|
+
|
|
280
|
+
### Testing Tools
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
node scripts/mcp-call.cjs ping_seatable '{}'
|
|
284
|
+
node scripts/mcp-call.cjs list_tables '{}'
|
|
285
|
+
node scripts/mcp-call.cjs list_rows '{"table": "Tasks", "page_size": 5}'
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Troubleshooting
|
|
289
|
+
|
|
290
|
+
| Issue | Solution |
|
|
291
|
+
|---|---|
|
|
292
|
+
| `Invalid API token` | Check `SEATABLE_API_TOKEN` |
|
|
293
|
+
| `Base not found` | Check API token permissions |
|
|
294
|
+
| `Connection timeout` | Check `SEATABLE_SERVER_URL` and network access |
|
|
295
|
+
| `Permission denied` | Ensure API token has required base permissions |
|
|
296
|
+
| `You don't have permission to perform this operation on this base.` | API token is read-only or row limit exceeded |
|
|
297
|
+
| `Asset quota exceeded.` | Storage quota reached — delete files or upgrade plan |
|
|
298
|
+
| `too many requests` | Rate-limited by SeaTable — requests are automatically retried with backoff (3 attempts) |
|
|
299
|
+
|
|
300
|
+
## License
|
|
301
|
+
|
|
302
|
+
MIT
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// CommonJS launcher that dynamically imports the ESM build and runs the CLI
|
|
4
|
+
(async () => {
|
|
5
|
+
try {
|
|
6
|
+
const mod = await import('../dist/index.js')
|
|
7
|
+
if (typeof mod.runCli === 'function') {
|
|
8
|
+
await mod.runCli()
|
|
9
|
+
return
|
|
10
|
+
}
|
|
11
|
+
if (typeof mod.default === 'function') {
|
|
12
|
+
await mod.default()
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
// Fallback: do nothing; relying on side-effect main guard in compiled code
|
|
16
|
+
} catch (err) {
|
|
17
|
+
console.error(err)
|
|
18
|
+
process.exit(1)
|
|
19
|
+
}
|
|
20
|
+
})()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare class TokenValidator {
|
|
2
|
+
private readonly serverUrl;
|
|
3
|
+
private readonly cache;
|
|
4
|
+
private cleanupInterval?;
|
|
5
|
+
constructor(serverUrl: string);
|
|
6
|
+
validate(apiToken: string): Promise<boolean>;
|
|
7
|
+
cleanup(): void;
|
|
8
|
+
destroy(): void;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=tokenValidator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokenValidator.d.ts","sourceRoot":"","sources":["../../src/auth/tokenValidator.ts"],"names":[],"mappings":"AAYA,qBAAa,cAAc;IACvB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAClC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;IACtD,OAAO,CAAC,eAAe,CAAC,CAAgC;gBAE5C,SAAS,EAAE,MAAM;IASvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAuBlD,OAAO,IAAI,IAAI;IASf,OAAO,IAAI,IAAI;CAOlB"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { logger } from '../logger.js';
|
|
3
|
+
const POSITIVE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
4
|
+
const NEGATIVE_TTL_MS = 1 * 60 * 1000; // 1 minute
|
|
5
|
+
export class TokenValidator {
|
|
6
|
+
serverUrl;
|
|
7
|
+
cache = new Map();
|
|
8
|
+
cleanupInterval;
|
|
9
|
+
constructor(serverUrl) {
|
|
10
|
+
this.serverUrl = serverUrl.replace(/\/$/, '');
|
|
11
|
+
this.cleanupInterval = setInterval(() => this.cleanup(), 60_000);
|
|
12
|
+
// Don't keep the process alive just for cleanup
|
|
13
|
+
if (this.cleanupInterval.unref) {
|
|
14
|
+
this.cleanupInterval.unref();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async validate(apiToken) {
|
|
18
|
+
const cached = this.cache.get(apiToken);
|
|
19
|
+
if (cached && Date.now() < cached.expiresAt) {
|
|
20
|
+
return cached.valid;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const url = `${this.serverUrl}/api/v2.1/dtable/app-access-token/`;
|
|
24
|
+
const res = await axios.get(url, {
|
|
25
|
+
headers: { Authorization: `Bearer ${apiToken}` },
|
|
26
|
+
timeout: 10_000,
|
|
27
|
+
});
|
|
28
|
+
const valid = !!res.data?.access_token;
|
|
29
|
+
this.cache.set(apiToken, { valid, expiresAt: Date.now() + POSITIVE_TTL_MS });
|
|
30
|
+
return valid;
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
const status = err?.response?.status;
|
|
34
|
+
logger.debug({ status }, 'Token validation failed');
|
|
35
|
+
this.cache.set(apiToken, { valid: false, expiresAt: Date.now() + NEGATIVE_TTL_MS });
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
cleanup() {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
for (const [key, entry] of this.cache) {
|
|
42
|
+
if (now >= entry.expiresAt) {
|
|
43
|
+
this.cache.delete(key);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
destroy() {
|
|
48
|
+
if (this.cleanupInterval) {
|
|
49
|
+
clearInterval(this.cleanupInterval);
|
|
50
|
+
this.cleanupInterval = undefined;
|
|
51
|
+
}
|
|
52
|
+
this.cache.clear();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=tokenValidator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokenValidator.js","sourceRoot":"","sources":["../../src/auth/tokenValidator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAOrC,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAA,CAAC,YAAY;AAClD,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAA,CAAC,WAAW;AAEjD,MAAM,OAAO,cAAc;IACN,SAAS,CAAQ;IACjB,KAAK,GAAG,IAAI,GAAG,EAAsB,CAAA;IAC9C,eAAe,CAAiC;IAExD,YAAY,SAAiB;QACzB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;QAC7C,IAAI,CAAC,eAAe,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,MAAM,CAAC,CAAA;QAChE,gDAAgD;QAChD,IAAI,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,CAAC;YAC7B,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,CAAA;QAChC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,QAAgB;QAC3B,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QACvC,IAAI,MAAM,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,SAAS,EAAE,CAAC;YAC1C,OAAO,MAAM,CAAC,KAAK,CAAA;QACvB,CAAC;QAED,IAAI,CAAC;YACD,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,SAAS,oCAAoC,CAAA;YACjE,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE;gBAC7B,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,QAAQ,EAAE,EAAE;gBAChD,OAAO,EAAE,MAAM;aAClB,CAAC,CAAA;YACF,MAAM,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,YAAY,CAAA;YACtC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,eAAe,EAAE,CAAC,CAAA;YAC5E,OAAO,KAAK,CAAA;QAChB,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAChB,MAAM,MAAM,GAAG,GAAG,EAAE,QAAQ,EAAE,MAAM,CAAA;YACpC,MAAM,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,EAAE,yBAAyB,CAAC,CAAA;YACnD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,eAAe,EAAE,CAAC,CAAA;YACnF,OAAO,KAAK,CAAA;QAChB,CAAC;IACL,CAAC;IAED,OAAO;QACH,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;QACtB,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACpC,IAAI,GAAG,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;gBACzB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;YAC1B,CAAC;QACL,CAAC;IACL,CAAC;IAED,OAAO;QACH,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YACvB,aAAa,CAAC,IAAI,CAAC,eAAe,CAAC,CAAA;YACnC,IAAI,CAAC,eAAe,GAAG,SAAS,CAAA;QACpC,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAA;IACtB,CAAC;CACJ"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const VERSION: string;
|
|
3
|
+
export declare const ServerModeSchema: z.ZodDefault<z.ZodEnum<["selfhosted", "managed"]>>;
|
|
4
|
+
export type ServerMode = z.infer<typeof ServerModeSchema>;
|
|
5
|
+
declare const EnvSchema: z.ZodEffects<z.ZodObject<{
|
|
6
|
+
SEATABLE_SERVER_URL: z.ZodString;
|
|
7
|
+
SEATABLE_MODE: z.ZodDefault<z.ZodEnum<["selfhosted", "managed"]>>;
|
|
8
|
+
SEATABLE_API_TOKEN: z.ZodOptional<z.ZodString>;
|
|
9
|
+
SEATABLE_BASES: z.ZodOptional<z.ZodString>;
|
|
10
|
+
LOG_LEVEL: z.ZodOptional<z.ZodEnum<["fatal", "error", "warn", "info", "debug", "trace"]>>;
|
|
11
|
+
HTTP_TIMEOUT_MS: z.ZodPipeline<z.ZodEffects<z.ZodOptional<z.ZodString>, number | undefined, string | undefined>, z.ZodOptional<z.ZodNumber>>;
|
|
12
|
+
SEATABLE_MOCK: z.ZodOptional<z.ZodEffects<z.ZodOptional<z.ZodString>, boolean, string | undefined>>;
|
|
13
|
+
SEATABLE_ENABLE_FIND_ROWS: z.ZodOptional<z.ZodEffects<z.ZodOptional<z.ZodString>, boolean, string | undefined>>;
|
|
14
|
+
SEATABLE_ENABLE_DEBUG_TOOLS: z.ZodOptional<z.ZodEffects<z.ZodOptional<z.ZodString>, boolean, string | undefined>>;
|
|
15
|
+
}, "strip", z.ZodTypeAny, {
|
|
16
|
+
SEATABLE_SERVER_URL: string;
|
|
17
|
+
SEATABLE_MODE: "selfhosted" | "managed";
|
|
18
|
+
SEATABLE_API_TOKEN?: string | undefined;
|
|
19
|
+
SEATABLE_BASES?: string | undefined;
|
|
20
|
+
LOG_LEVEL?: "fatal" | "error" | "warn" | "info" | "debug" | "trace" | undefined;
|
|
21
|
+
HTTP_TIMEOUT_MS?: number | undefined;
|
|
22
|
+
SEATABLE_MOCK?: boolean | undefined;
|
|
23
|
+
SEATABLE_ENABLE_FIND_ROWS?: boolean | undefined;
|
|
24
|
+
SEATABLE_ENABLE_DEBUG_TOOLS?: boolean | undefined;
|
|
25
|
+
}, {
|
|
26
|
+
SEATABLE_SERVER_URL: string;
|
|
27
|
+
SEATABLE_MODE?: "selfhosted" | "managed" | undefined;
|
|
28
|
+
SEATABLE_API_TOKEN?: string | undefined;
|
|
29
|
+
SEATABLE_BASES?: string | undefined;
|
|
30
|
+
LOG_LEVEL?: "fatal" | "error" | "warn" | "info" | "debug" | "trace" | undefined;
|
|
31
|
+
HTTP_TIMEOUT_MS?: string | undefined;
|
|
32
|
+
SEATABLE_MOCK?: string | undefined;
|
|
33
|
+
SEATABLE_ENABLE_FIND_ROWS?: string | undefined;
|
|
34
|
+
SEATABLE_ENABLE_DEBUG_TOOLS?: string | undefined;
|
|
35
|
+
}>, {
|
|
36
|
+
SEATABLE_SERVER_URL: string;
|
|
37
|
+
SEATABLE_MODE: "selfhosted" | "managed";
|
|
38
|
+
SEATABLE_API_TOKEN?: string | undefined;
|
|
39
|
+
SEATABLE_BASES?: string | undefined;
|
|
40
|
+
LOG_LEVEL?: "fatal" | "error" | "warn" | "info" | "debug" | "trace" | undefined;
|
|
41
|
+
HTTP_TIMEOUT_MS?: number | undefined;
|
|
42
|
+
SEATABLE_MOCK?: boolean | undefined;
|
|
43
|
+
SEATABLE_ENABLE_FIND_ROWS?: boolean | undefined;
|
|
44
|
+
SEATABLE_ENABLE_DEBUG_TOOLS?: boolean | undefined;
|
|
45
|
+
}, {
|
|
46
|
+
SEATABLE_SERVER_URL: string;
|
|
47
|
+
SEATABLE_MODE?: "selfhosted" | "managed" | undefined;
|
|
48
|
+
SEATABLE_API_TOKEN?: string | undefined;
|
|
49
|
+
SEATABLE_BASES?: string | undefined;
|
|
50
|
+
LOG_LEVEL?: "fatal" | "error" | "warn" | "info" | "debug" | "trace" | undefined;
|
|
51
|
+
HTTP_TIMEOUT_MS?: string | undefined;
|
|
52
|
+
SEATABLE_MOCK?: string | undefined;
|
|
53
|
+
SEATABLE_ENABLE_FIND_ROWS?: string | undefined;
|
|
54
|
+
SEATABLE_ENABLE_DEBUG_TOOLS?: string | undefined;
|
|
55
|
+
}>;
|
|
56
|
+
export type Env = z.infer<typeof EnvSchema>;
|
|
57
|
+
type EnvOverrides = Partial<Record<keyof Env, string>>;
|
|
58
|
+
export declare function setEnvOverrides(values: EnvOverrides | undefined): void;
|
|
59
|
+
export declare function clearEnvOverrides(): void;
|
|
60
|
+
export interface BaseEntry {
|
|
61
|
+
name: string;
|
|
62
|
+
apiToken: string;
|
|
63
|
+
}
|
|
64
|
+
export declare function parseBases(raw: string): BaseEntry[];
|
|
65
|
+
export declare function getEnv(): Env;
|
|
66
|
+
export {};
|
|
67
|
+
//# sourceMappingURL=env.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/config/env.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAMvB,eAAO,MAAM,OAAO,EAAE,MAAoB,CAAA;AAM1C,eAAO,MAAM,gBAAgB,oDAA0D,CAAA;AACvF,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAA;AAEzD,QAAA,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuCT,CAAA;AAEN,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAA;AAE3C,KAAK,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,GAAG,EAAE,MAAM,CAAC,CAAC,CAAA;AAStD,wBAAgB,eAAe,CAAC,MAAM,EAAE,YAAY,GAAG,SAAS,GAAG,IAAI,CAUtE;AAED,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAED,MAAM,WAAW,SAAS;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,CAuBnD;AAED,wBAAgB,MAAM,IAAI,GAAG,CAO5B"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { config as loadEnv } from 'dotenv';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
const pkg = require('../../package.json');
|
|
6
|
+
export const VERSION = pkg.version;
|
|
7
|
+
if (typeof process !== 'undefined' && process.versions?.node) {
|
|
8
|
+
loadEnv();
|
|
9
|
+
}
|
|
10
|
+
export const ServerModeSchema = z.enum(['selfhosted', 'managed']).default('selfhosted');
|
|
11
|
+
const EnvSchema = z
|
|
12
|
+
.object({
|
|
13
|
+
SEATABLE_SERVER_URL: z.string().url(),
|
|
14
|
+
SEATABLE_MODE: ServerModeSchema,
|
|
15
|
+
SEATABLE_API_TOKEN: z.string().min(1).optional(),
|
|
16
|
+
// Multi-base: JSON array, e.g. '[{"base_name":"CRM","api_token":"..."}]'
|
|
17
|
+
SEATABLE_BASES: z.string().optional(),
|
|
18
|
+
LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).optional(),
|
|
19
|
+
HTTP_TIMEOUT_MS: z
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.transform((v) => (v ? Number(v) : undefined))
|
|
23
|
+
.pipe(z.number().int().positive().optional()),
|
|
24
|
+
SEATABLE_MOCK: z
|
|
25
|
+
.string()
|
|
26
|
+
.optional()
|
|
27
|
+
.transform((v) => (v === '1' || v === 'true' ? true : false))
|
|
28
|
+
.optional(),
|
|
29
|
+
// Feature flags
|
|
30
|
+
SEATABLE_ENABLE_FIND_ROWS: z
|
|
31
|
+
.string()
|
|
32
|
+
.optional()
|
|
33
|
+
.transform((v) => (v === '1' || v === 'true' ? true : false))
|
|
34
|
+
.optional(),
|
|
35
|
+
// Debug / experimental tools (e.g. echo_args). Should NEVER be enabled in production unless explicitly needed.
|
|
36
|
+
SEATABLE_ENABLE_DEBUG_TOOLS: z
|
|
37
|
+
.string()
|
|
38
|
+
.optional()
|
|
39
|
+
.transform((v) => (v === '1' || v === 'true' ? true : false))
|
|
40
|
+
.optional(),
|
|
41
|
+
})
|
|
42
|
+
.superRefine((data, ctx) => {
|
|
43
|
+
if (data.SEATABLE_MODE === 'selfhosted' && !data.SEATABLE_API_TOKEN && !data.SEATABLE_BASES) {
|
|
44
|
+
ctx.addIssue({
|
|
45
|
+
code: z.ZodIssueCode.custom,
|
|
46
|
+
message: 'SEATABLE_API_TOKEN or SEATABLE_BASES is required in selfhosted mode',
|
|
47
|
+
path: ['SEATABLE_API_TOKEN'],
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
let overrides;
|
|
52
|
+
function buildEnvSource() {
|
|
53
|
+
const base = typeof process !== 'undefined' && process.env ? { ...process.env } : {};
|
|
54
|
+
return overrides ? { ...base, ...overrides } : base;
|
|
55
|
+
}
|
|
56
|
+
export function setEnvOverrides(values) {
|
|
57
|
+
if (!values)
|
|
58
|
+
return;
|
|
59
|
+
overrides = overrides ?? {};
|
|
60
|
+
for (const [key, value] of Object.entries(values)) {
|
|
61
|
+
if (typeof value === 'string') {
|
|
62
|
+
overrides[key] = value;
|
|
63
|
+
}
|
|
64
|
+
else if (value === undefined) {
|
|
65
|
+
delete overrides[key];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function clearEnvOverrides() {
|
|
70
|
+
overrides = undefined;
|
|
71
|
+
}
|
|
72
|
+
export function parseBases(raw) {
|
|
73
|
+
let arr;
|
|
74
|
+
try {
|
|
75
|
+
arr = JSON.parse(raw);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
throw new Error('SEATABLE_BASES must be a valid JSON array, e.g. \'[{"base_name":"CRM","api_token":"..."}]\'');
|
|
79
|
+
}
|
|
80
|
+
if (!Array.isArray(arr) || arr.length === 0) {
|
|
81
|
+
throw new Error('SEATABLE_BASES must be a non-empty JSON array');
|
|
82
|
+
}
|
|
83
|
+
return arr.map((entry, i) => {
|
|
84
|
+
if (typeof entry !== 'object' || entry === null) {
|
|
85
|
+
throw new Error(`SEATABLE_BASES[${i}]: expected an object`);
|
|
86
|
+
}
|
|
87
|
+
const obj = entry;
|
|
88
|
+
if (typeof obj.base_name !== 'string' || !obj.base_name) {
|
|
89
|
+
throw new Error(`SEATABLE_BASES[${i}]: missing or empty "base_name"`);
|
|
90
|
+
}
|
|
91
|
+
if (typeof obj.api_token !== 'string' || !obj.api_token) {
|
|
92
|
+
throw new Error(`SEATABLE_BASES[${i}]: missing or empty "api_token"`);
|
|
93
|
+
}
|
|
94
|
+
return { name: obj.base_name, apiToken: obj.api_token };
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
export function getEnv() {
|
|
98
|
+
const parsed = EnvSchema.safeParse(buildEnvSource());
|
|
99
|
+
if (!parsed.success) {
|
|
100
|
+
const issues = parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('\n');
|
|
101
|
+
throw new Error(`Invalid environment configuration:\n${issues}`);
|
|
102
|
+
}
|
|
103
|
+
return parsed.data;
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=env.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.js","sourceRoot":"","sources":["../../src/config/env.ts"],"names":[],"mappings":"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"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type ErrorCode = 'ERR_AUTH_EXPIRED' | 'ERR_SCHEMA_UNKNOWN_TABLE' | 'ERR_SCHEMA_UNKNOWN_COLUMN' | 'ERR_FILE_TOO_LARGE' | 'ERR_UPSERT_MISSING_KEY' | 'ERR_UPSERT_AMBIGUOUS' | 'ERR_TIMEOUT' | 'ERR_RATE_LIMITED' | 'ERR_SERVER' | 'ERR_BAD_REQUEST' | 'ERR_VALIDATION';
|
|
2
|
+
export type CodedError = Error & {
|
|
3
|
+
code: ErrorCode;
|
|
4
|
+
meta?: Record<string, unknown>;
|
|
5
|
+
};
|
|
6
|
+
export declare function makeError(code: ErrorCode, message: string, meta?: Record<string, unknown>): CodedError;
|
|
7
|
+
export declare function toCodedAxiosError(error: unknown, op: string): CodedError;
|
|
8
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,SAAS,GACjB,kBAAkB,GAClB,0BAA0B,GAC1B,2BAA2B,GAC3B,oBAAoB,GACpB,wBAAwB,GACxB,sBAAsB,GACtB,aAAa,GACb,kBAAkB,GAClB,YAAY,GACZ,iBAAiB,GACjB,gBAAgB,CAAA;AAEpB,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAA;AAEpF,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,UAAU,CAKtG;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,GAAG,UAAU,CAcxE"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function makeError(code, message, meta) {
|
|
2
|
+
const err = new Error(message);
|
|
3
|
+
err.code = code;
|
|
4
|
+
if (meta)
|
|
5
|
+
err.meta = meta;
|
|
6
|
+
return err;
|
|
7
|
+
}
|
|
8
|
+
export function toCodedAxiosError(error, op) {
|
|
9
|
+
const err = error;
|
|
10
|
+
const status = err.response?.status;
|
|
11
|
+
const data = err.response?.data;
|
|
12
|
+
const detail = data?.error_msg || data?.error_message || data?.detail || '';
|
|
13
|
+
const msg = (code) => detail ? `${code}: ${detail}` : code;
|
|
14
|
+
if (status === 401)
|
|
15
|
+
return makeError('ERR_AUTH_EXPIRED', msg('ERR_AUTH_EXPIRED'), { op, status, data });
|
|
16
|
+
if (status === 403)
|
|
17
|
+
return makeError('ERR_BAD_REQUEST', msg('ERR_BAD_REQUEST'), { op, status, data });
|
|
18
|
+
if (status === 408)
|
|
19
|
+
return makeError('ERR_TIMEOUT', msg('ERR_TIMEOUT'), { op, status, data });
|
|
20
|
+
if (status === 429)
|
|
21
|
+
return makeError('ERR_RATE_LIMITED', msg('ERR_RATE_LIMITED'), { op, status, data });
|
|
22
|
+
if (status && status >= 500)
|
|
23
|
+
return makeError('ERR_SERVER', msg('ERR_SERVER'), { op, status, data });
|
|
24
|
+
if (status && status >= 400)
|
|
25
|
+
return makeError('ERR_BAD_REQUEST', msg('ERR_BAD_REQUEST'), { op, status, data });
|
|
26
|
+
return makeError('ERR_SERVER', msg('ERR_SERVER'), { op, status, data });
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAiBA,MAAM,UAAU,SAAS,CAAC,IAAe,EAAE,OAAe,EAAE,IAA8B;IACxF,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,OAAO,CAAe,CAAA;IAC5C,GAAG,CAAC,IAAI,GAAG,IAAI,CAAA;IACf,IAAI,IAAI;QAAE,GAAG,CAAC,IAAI,GAAG,IAAI,CAAA;IACzB,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,KAAc,EAAE,EAAU;IAC1D,MAAM,GAAG,GAAG,KAAmB,CAAA;IAC/B,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAA;IACnC,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,EAAE,IAAW,CAAA;IACtC,MAAM,MAAM,GAAG,IAAI,EAAE,SAAS,IAAI,IAAI,EAAE,aAAa,IAAI,IAAI,EAAE,MAAM,IAAI,EAAE,CAAA;IAC3E,MAAM,GAAG,GAAG,CAAC,IAAe,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,KAAK,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;IAErE,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,SAAS,CAAC,kBAAkB,EAAE,GAAG,CAAC,kBAAkB,CAAC,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;IACvG,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,SAAS,CAAC,iBAAiB,EAAE,GAAG,CAAC,iBAAiB,CAAC,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;IACrG,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,SAAS,CAAC,aAAa,EAAE,GAAG,CAAC,aAAa,CAAC,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;IAC7F,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,SAAS,CAAC,kBAAkB,EAAE,GAAG,CAAC,kBAAkB,CAAC,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;IACvG,IAAI,MAAM,IAAI,MAAM,IAAI,GAAG;QAAE,OAAO,SAAS,CAAC,YAAY,EAAE,GAAG,CAAC,YAAY,CAAC,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;IACpG,IAAI,MAAM,IAAI,MAAM,IAAI,GAAG;QAAE,OAAO,SAAS,CAAC,iBAAiB,EAAE,GAAG,CAAC,iBAAiB,CAAC,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;IAC9G,OAAO,SAAS,CAAC,YAAY,EAAE,GAAG,CAAC,YAAY,CAAC,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;AACzE,CAAC"}
|