@addai/tables-mcp 0.7.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 +127 -0
- package/dist/api-client.d.ts +12 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +53 -0
- package/dist/api-client.js.map +1 -0
- package/dist/helpers/column-settings.d.ts +25 -0
- package/dist/helpers/column-settings.d.ts.map +1 -0
- package/dist/helpers/column-settings.js +96 -0
- package/dist/helpers/column-settings.js.map +1 -0
- package/dist/helpers/column-types.d.ts +8 -0
- package/dist/helpers/column-types.d.ts.map +1 -0
- package/dist/helpers/column-types.js +83 -0
- package/dist/helpers/column-types.js.map +1 -0
- package/dist/helpers/errors.d.ts +16 -0
- package/dist/helpers/errors.d.ts.map +1 -0
- package/dist/helpers/errors.js +19 -0
- package/dist/helpers/errors.js.map +1 -0
- package/dist/helpers/mapped-columns.d.ts +21 -0
- package/dist/helpers/mapped-columns.d.ts.map +1 -0
- package/dist/helpers/mapped-columns.js +119 -0
- package/dist/helpers/mapped-columns.js.map +1 -0
- package/dist/helpers/validate-options.d.ts +34 -0
- package/dist/helpers/validate-options.d.ts.map +1 -0
- package/dist/helpers/validate-options.js +120 -0
- package/dist/helpers/validate-options.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/logo.d.ts +2 -0
- package/dist/logo.d.ts.map +1 -0
- package/dist/logo.js +64 -0
- package/dist/logo.js.map +1 -0
- package/dist/preview.d.ts +3 -0
- package/dist/preview.d.ts.map +1 -0
- package/dist/preview.js +4 -0
- package/dist/preview.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +136 -0
- package/dist/server.js.map +1 -0
- package/dist/setup.d.ts +2 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +128 -0
- package/dist/setup.js.map +1 -0
- package/dist/tools/comments.d.ts +4 -0
- package/dist/tools/comments.d.ts.map +1 -0
- package/dist/tools/comments.js +63 -0
- package/dist/tools/comments.js.map +1 -0
- package/dist/tools/dashboards.d.ts +4 -0
- package/dist/tools/dashboards.d.ts.map +1 -0
- package/dist/tools/dashboards.js +560 -0
- package/dist/tools/dashboards.js.map +1 -0
- package/dist/tools/discovery.d.ts +4 -0
- package/dist/tools/discovery.d.ts.map +1 -0
- package/dist/tools/discovery.js +115 -0
- package/dist/tools/discovery.js.map +1 -0
- package/dist/tools/files.d.ts +4 -0
- package/dist/tools/files.d.ts.map +1 -0
- package/dist/tools/files.js +115 -0
- package/dist/tools/files.js.map +1 -0
- package/dist/tools/folders.d.ts +4 -0
- package/dist/tools/folders.d.ts.map +1 -0
- package/dist/tools/folders.js +164 -0
- package/dist/tools/folders.js.map +1 -0
- package/dist/tools/history.d.ts +4 -0
- package/dist/tools/history.d.ts.map +1 -0
- package/dist/tools/history.js +50 -0
- package/dist/tools/history.js.map +1 -0
- package/dist/tools/rows.d.ts +4 -0
- package/dist/tools/rows.d.ts.map +1 -0
- package/dist/tools/rows.js +244 -0
- package/dist/tools/rows.js.map +1 -0
- package/dist/tools/search.d.ts +4 -0
- package/dist/tools/search.d.ts.map +1 -0
- package/dist/tools/search.js +89 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/structure.d.ts +4 -0
- package/dist/tools/structure.d.ts.map +1 -0
- package/dist/tools/structure.js +172 -0
- package/dist/tools/structure.js.map +1 -0
- package/dist/tools/views.d.ts +4 -0
- package/dist/tools/views.d.ts.map +1 -0
- package/dist/tools/views.js +169 -0
- package/dist/tools/views.js.map +1 -0
- package/dist/types.d.ts +76 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +38 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { mcpError, mcpResult } from '../helpers/errors.js';
|
|
3
|
+
export function registerCommentTools(server, client) {
|
|
4
|
+
// ── list_comments ──
|
|
5
|
+
server.tool('list_comments', 'List comments on a row. Returns threaded comments with user info, reply counts, and emoji reactions.', {
|
|
6
|
+
table_id: z.string().uuid().describe('The table containing the row'),
|
|
7
|
+
row_id: z.string().uuid().describe('The row to get comments for'),
|
|
8
|
+
}, async ({ table_id, row_id }) => {
|
|
9
|
+
const res = await client.get(`/tables/${table_id}/rows/${row_id}/comments`);
|
|
10
|
+
if (res.error)
|
|
11
|
+
return mcpError(res.error.message);
|
|
12
|
+
return mcpResult(res.data);
|
|
13
|
+
});
|
|
14
|
+
// ── add_comment ──
|
|
15
|
+
server.tool('add_comment', 'Add a comment to a row. Supports threaded replies by providing a parent_id.', {
|
|
16
|
+
table_id: z.string().uuid().describe('The table containing the row'),
|
|
17
|
+
row_id: z.string().uuid().describe('The row to comment on'),
|
|
18
|
+
content: z.string().min(1).describe('The comment text'),
|
|
19
|
+
parent_id: z
|
|
20
|
+
.string()
|
|
21
|
+
.uuid()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe('Parent comment ID to reply to (creates a threaded reply). Max 1 level deep.'),
|
|
24
|
+
}, async ({ table_id, row_id, content, parent_id }) => {
|
|
25
|
+
const body = { content };
|
|
26
|
+
if (parent_id)
|
|
27
|
+
body.parent_id = parent_id;
|
|
28
|
+
const res = await client.post(`/tables/${table_id}/rows/${row_id}/comments`, body);
|
|
29
|
+
if (res.error)
|
|
30
|
+
return mcpError(res.error.message);
|
|
31
|
+
return mcpResult(res.data);
|
|
32
|
+
});
|
|
33
|
+
// ── update_comment ──
|
|
34
|
+
server.tool('update_comment', 'Edit a comment. You can only edit your own comments.', {
|
|
35
|
+
comment_id: z.string().uuid().describe('The comment ID to edit'),
|
|
36
|
+
content: z.string().min(1).describe('The new comment text'),
|
|
37
|
+
}, async ({ comment_id, content }) => {
|
|
38
|
+
const res = await client.patch(`/comments/${comment_id}`, { content });
|
|
39
|
+
if (res.error)
|
|
40
|
+
return mcpError(res.error.message);
|
|
41
|
+
return mcpResult(res.data);
|
|
42
|
+
});
|
|
43
|
+
// ── delete_comment ──
|
|
44
|
+
server.tool('delete_comment', 'Delete a comment and its replies/reactions. You can only delete your own comments.', {
|
|
45
|
+
comment_id: z.string().uuid().describe('The comment ID to delete'),
|
|
46
|
+
}, async ({ comment_id }) => {
|
|
47
|
+
const res = await client.delete(`/comments/${comment_id}`);
|
|
48
|
+
if (res.error)
|
|
49
|
+
return mcpError(res.error.message);
|
|
50
|
+
return mcpResult({ message: 'Comment deleted' });
|
|
51
|
+
});
|
|
52
|
+
// ── toggle_reaction ──
|
|
53
|
+
server.tool('toggle_reaction', 'Add or remove an emoji reaction on a comment. If you already reacted with that emoji, it removes it.', {
|
|
54
|
+
comment_id: z.string().uuid().describe('The comment ID to react to'),
|
|
55
|
+
emoji: z.string().min(1).describe('The emoji to toggle (e.g. "thumbsup", "heart", "fire", "eyes")'),
|
|
56
|
+
}, async ({ comment_id, emoji }) => {
|
|
57
|
+
const res = await client.post(`/comments/${comment_id}/reactions`, { emoji });
|
|
58
|
+
if (res.error)
|
|
59
|
+
return mcpError(res.error.message);
|
|
60
|
+
return mcpResult(res.data);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=comments.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comments.js","sourceRoot":"","sources":["../../src/tools/comments.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboards.d.ts","sourceRoot":"","sources":["../../src/tools/dashboards.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAwTvD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,cAAc,QAsV/E"}
|
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { mcpError, mcpResult } from '../helpers/errors.js';
|
|
3
|
+
import { buildColumnMap } from '../helpers/mapped-columns.js';
|
|
4
|
+
const DEFAULT_GRID_COLS = 12;
|
|
5
|
+
const FILTER_OPERATORS = [
|
|
6
|
+
'contains', 'not_contains', 'equals', 'not_equals', 'is', 'is_not',
|
|
7
|
+
'greater_than', 'less_than', 'greater_than_or_equal', 'less_than_or_equal',
|
|
8
|
+
'before', 'after', 'is_empty', 'is_not_empty',
|
|
9
|
+
];
|
|
10
|
+
const ScopeEnum = z.enum(['table', 'folder', 'database']);
|
|
11
|
+
const ScopeFields = {
|
|
12
|
+
scope: ScopeEnum.describe('Where the dashboard lives: "table" (single table), "folder" (table folder — multi-table), or "database" (database-wide — multi-table).'),
|
|
13
|
+
parent_id: z.string().uuid().describe('The table_id / folder_id / database_id that owns the dashboard, matching `scope`.'),
|
|
14
|
+
};
|
|
15
|
+
const PanelFilterSchema = z.object({
|
|
16
|
+
column: z.string().optional().describe('Column name (human-readable). Omit if providing columnId.'),
|
|
17
|
+
columnId: z.string().uuid().optional().describe('Column UUID. Omit if providing column name.'),
|
|
18
|
+
operator: z.enum(FILTER_OPERATORS),
|
|
19
|
+
value: z.unknown().optional(),
|
|
20
|
+
});
|
|
21
|
+
const PanelSortSchema = z.object({
|
|
22
|
+
column: z.string().optional(),
|
|
23
|
+
columnId: z.string().uuid().optional(),
|
|
24
|
+
direction: z.enum(['asc', 'desc']).default('asc'),
|
|
25
|
+
});
|
|
26
|
+
const PanelTypeEnum = z.enum(['stat', 'chart', 'view', 'embed', 'widget']);
|
|
27
|
+
const PanelInputSchema = z.object({
|
|
28
|
+
id: z.string().optional().describe('Panel ID. Auto-generated if omitted (panel_<timestamp>_<random>).'),
|
|
29
|
+
type: PanelTypeEnum,
|
|
30
|
+
title: z.string(),
|
|
31
|
+
showTitle: z.boolean().optional(),
|
|
32
|
+
table_id: z.string().uuid().optional().describe('Which table this panel reads from. Required for folder/database-scope dashboards; defaults to the dashboard\'s table for table-scope.'),
|
|
33
|
+
x: z.number().int().min(0).optional().describe('Grid X (0-indexed). Auto-placed if omitted.'),
|
|
34
|
+
y: z.number().int().min(0).optional().describe('Grid Y (0-indexed). Auto-placed if omitted.'),
|
|
35
|
+
w: z.number().int().min(1).optional().describe('Width in grid units (1-12). Defaults by type.'),
|
|
36
|
+
h: z.number().int().min(1).optional().describe('Height in grid units. Defaults by type.'),
|
|
37
|
+
minW: z.number().int().min(1).optional(),
|
|
38
|
+
minH: z.number().int().min(1).optional(),
|
|
39
|
+
config: z.record(z.string(), z.unknown()).describe('Type-specific config. Use column NAMES (not UUIDs) — tool translates. Shapes:\n' +
|
|
40
|
+
'- stat: { statType: "count"|"sum"|"average"|"min"|"max"|"unique", column?, format?: "number"|"currency"|"percentage"|"decimal", prefix?, suffix?, icon?, showChange?, changeColumn? }\n' +
|
|
41
|
+
'- chart: { chartType: "bar"|"pie"|"doughnut"|"line", groupByColumn, valueColumn?, aggregation?: "count"|"sum"|"average"|"min"|"max", showLegend?, showLabels? }\n' +
|
|
42
|
+
'- view: { viewType: "table"|"gallery"|"list"|"kanban"|"calendar"|"gantt"|"timeline"|"document", maxRows?, visibleColumns?: string[] (names) }\n' +
|
|
43
|
+
'- embed: { url, embedType?: "iframe"|"youtube"|"vimeo"|"figma"|"loom", allowFullscreen?, allowScripts? }\n' +
|
|
44
|
+
'- widget: { widgetId, widgetState?, stateMapping? }'),
|
|
45
|
+
filters: z.array(PanelFilterSchema).optional(),
|
|
46
|
+
sorts: z.array(PanelSortSchema).optional(),
|
|
47
|
+
styling: z.record(z.string(), z.unknown()).optional(),
|
|
48
|
+
customCss: z.string().optional(),
|
|
49
|
+
});
|
|
50
|
+
const DashboardStylingSchema = z.record(z.string(), z.unknown()).describe('Dashboard-level styling. Shape: { backgroundColor?, backgroundImage?, backgroundSize?, backgroundPosition?, ' +
|
|
51
|
+
'backgroundRepeat?, backgroundOpacity?, backgroundBlur?, backgroundOverlay?, customCss? }');
|
|
52
|
+
// ── path helpers ───────────────────────────────────────────────────────────────
|
|
53
|
+
function viewsBasePath(scope, parentId) {
|
|
54
|
+
if (scope === 'folder')
|
|
55
|
+
return `/folders/${parentId}/views`;
|
|
56
|
+
if (scope === 'database')
|
|
57
|
+
return `/databases/${parentId}/views`;
|
|
58
|
+
return `/tables/${parentId}/views`;
|
|
59
|
+
}
|
|
60
|
+
function viewPath(scope, parentId, viewId) {
|
|
61
|
+
return `${viewsBasePath(scope, parentId)}/${viewId}`;
|
|
62
|
+
}
|
|
63
|
+
// ── column map cache ───────────────────────────────────────────────────────────
|
|
64
|
+
class ColumnMapCache {
|
|
65
|
+
client;
|
|
66
|
+
cache = new Map();
|
|
67
|
+
constructor(client) {
|
|
68
|
+
this.client = client;
|
|
69
|
+
}
|
|
70
|
+
async get(tableId) {
|
|
71
|
+
const cached = this.cache.get(tableId);
|
|
72
|
+
if (cached)
|
|
73
|
+
return cached;
|
|
74
|
+
const map = await buildColumnMap(this.client, tableId);
|
|
75
|
+
this.cache.set(tableId, map);
|
|
76
|
+
return map;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// ── helpers ────────────────────────────────────────────────────────────────────
|
|
80
|
+
function generatePanelId() {
|
|
81
|
+
return `panel_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
82
|
+
}
|
|
83
|
+
function resolveColumnRef(ref, colMap, label) {
|
|
84
|
+
if (ref.columnId)
|
|
85
|
+
return ref.columnId;
|
|
86
|
+
if (ref.column) {
|
|
87
|
+
const id = colMap.nameToId.get(ref.column);
|
|
88
|
+
if (!id)
|
|
89
|
+
throw new Error(`Unknown column "${ref.column}" referenced in ${label}`);
|
|
90
|
+
return id;
|
|
91
|
+
}
|
|
92
|
+
throw new Error(`Column reference missing in ${label} — provide column (name) or columnId`);
|
|
93
|
+
}
|
|
94
|
+
function resolveName(name, colMap, label) {
|
|
95
|
+
if (!name)
|
|
96
|
+
throw new Error(`Missing column name in ${label}`);
|
|
97
|
+
const id = colMap.nameToId.get(name);
|
|
98
|
+
if (!id)
|
|
99
|
+
throw new Error(`Unknown column "${name}" in ${label}`);
|
|
100
|
+
return id;
|
|
101
|
+
}
|
|
102
|
+
function translatePanelConfig(type, config, colMap) {
|
|
103
|
+
const out = { ...config };
|
|
104
|
+
switch (type) {
|
|
105
|
+
case 'stat': {
|
|
106
|
+
if (typeof out.column === 'string') {
|
|
107
|
+
out.columnId = resolveName(out.column, colMap, 'stat.column');
|
|
108
|
+
delete out.column;
|
|
109
|
+
}
|
|
110
|
+
if (typeof out.changeColumn === 'string') {
|
|
111
|
+
out.changeColumnId = resolveName(out.changeColumn, colMap, 'stat.changeColumn');
|
|
112
|
+
delete out.changeColumn;
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case 'chart': {
|
|
117
|
+
if (typeof out.groupByColumn === 'string') {
|
|
118
|
+
out.groupByColumnId = resolveName(out.groupByColumn, colMap, 'chart.groupByColumn');
|
|
119
|
+
delete out.groupByColumn;
|
|
120
|
+
}
|
|
121
|
+
if (typeof out.valueColumn === 'string') {
|
|
122
|
+
out.valueColumnId = resolveName(out.valueColumn, colMap, 'chart.valueColumn');
|
|
123
|
+
delete out.valueColumn;
|
|
124
|
+
}
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
case 'view': {
|
|
128
|
+
if (Array.isArray(out.visibleColumns)) {
|
|
129
|
+
out.visibleColumns = out.visibleColumns.map((c) => typeof c === 'string' && colMap.nameToId.has(c) ? colMap.nameToId.get(c) : c);
|
|
130
|
+
}
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
default:
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
async function translatePanelForWrite(panel, cache, fallbackTableId) {
|
|
139
|
+
const tableId = panel.table_id || fallbackTableId;
|
|
140
|
+
const needsColumnMap = (panel.filters && panel.filters.length > 0) ||
|
|
141
|
+
(panel.sorts && panel.sorts.length > 0) ||
|
|
142
|
+
panel.type === 'stat' ||
|
|
143
|
+
panel.type === 'chart' ||
|
|
144
|
+
(panel.type === 'view' && Array.isArray(panel.config?.visibleColumns));
|
|
145
|
+
let colMap = null;
|
|
146
|
+
if (needsColumnMap) {
|
|
147
|
+
if (!tableId) {
|
|
148
|
+
throw new Error(`Panel "${panel.title}" references columns but has no table_id — set panel.table_id or use a table-scope dashboard.`);
|
|
149
|
+
}
|
|
150
|
+
colMap = await cache.get(tableId);
|
|
151
|
+
}
|
|
152
|
+
const config = colMap
|
|
153
|
+
? translatePanelConfig(panel.type, panel.config || {}, colMap)
|
|
154
|
+
: { ...(panel.config || {}) };
|
|
155
|
+
const filters = (panel.filters || []).map((f) => colMap
|
|
156
|
+
? { columnId: resolveColumnRef(f, colMap, 'filter'), operator: f.operator, value: f.value }
|
|
157
|
+
: { columnId: f.columnId, operator: f.operator, value: f.value });
|
|
158
|
+
const sorts = (panel.sorts || []).map((s) => colMap
|
|
159
|
+
? { columnId: resolveColumnRef(s, colMap, 'sort'), direction: s.direction }
|
|
160
|
+
: { columnId: s.columnId, direction: s.direction });
|
|
161
|
+
const out = {
|
|
162
|
+
id: panel.id || generatePanelId(),
|
|
163
|
+
type: panel.type,
|
|
164
|
+
title: panel.title,
|
|
165
|
+
showTitle: panel.showTitle ?? true,
|
|
166
|
+
config,
|
|
167
|
+
filters,
|
|
168
|
+
sorts,
|
|
169
|
+
};
|
|
170
|
+
if (tableId)
|
|
171
|
+
out.tableId = tableId;
|
|
172
|
+
for (const k of ['x', 'y', 'w', 'h', 'minW', 'minH']) {
|
|
173
|
+
if (panel[k] !== undefined)
|
|
174
|
+
out[k] = panel[k];
|
|
175
|
+
}
|
|
176
|
+
if (panel.styling)
|
|
177
|
+
out.styling = panel.styling;
|
|
178
|
+
if (panel.customCss)
|
|
179
|
+
out.customCss = panel.customCss;
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
function defaultSizeForPanel(type, config) {
|
|
183
|
+
if (type === 'stat')
|
|
184
|
+
return { w: 2, h: 2 };
|
|
185
|
+
if (type === 'chart') {
|
|
186
|
+
const ct = config?.chartType || 'bar';
|
|
187
|
+
if (ct === 'pie' || ct === 'doughnut')
|
|
188
|
+
return { w: 2, h: 2 };
|
|
189
|
+
return { w: 3, h: 2 };
|
|
190
|
+
}
|
|
191
|
+
if (type === 'view') {
|
|
192
|
+
const vt = config?.viewType || 'table';
|
|
193
|
+
if (vt === 'table' || vt === 'gallery' || vt === 'list')
|
|
194
|
+
return { w: 4, h: 3 };
|
|
195
|
+
return { w: 4, h: 4 };
|
|
196
|
+
}
|
|
197
|
+
return { w: 4, h: 4 };
|
|
198
|
+
}
|
|
199
|
+
function findAvailablePosition(existing, w, h, gridCols = DEFAULT_GRID_COLS) {
|
|
200
|
+
const placed = existing.filter((p) => typeof p.x === 'number' && typeof p.y === 'number' &&
|
|
201
|
+
typeof p.w === 'number' && typeof p.h === 'number');
|
|
202
|
+
const maxY = placed.reduce((m, p) => Math.max(m, p.y + p.h), 0);
|
|
203
|
+
for (let y = 0; y <= maxY + 1; y++) {
|
|
204
|
+
for (let x = 0; x <= gridCols - w; x++) {
|
|
205
|
+
const collides = placed.some((p) => x < p.x + p.w && x + w > p.x && y < p.y + p.h && y + h > p.y);
|
|
206
|
+
if (!collides)
|
|
207
|
+
return { x, y };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return { x: 0, y: maxY };
|
|
211
|
+
}
|
|
212
|
+
function ensurePanelPlacement(panel, existing) {
|
|
213
|
+
const size = defaultSizeForPanel(panel.type, panel.config);
|
|
214
|
+
const w = panel.w ?? size.w;
|
|
215
|
+
const h = panel.h ?? size.h;
|
|
216
|
+
let x = panel.x;
|
|
217
|
+
let y = panel.y;
|
|
218
|
+
if (x === undefined || y === undefined) {
|
|
219
|
+
const pos = findAvailablePosition(existing, w, h);
|
|
220
|
+
x = pos.x;
|
|
221
|
+
y = pos.y;
|
|
222
|
+
}
|
|
223
|
+
return { ...panel, x, y, w, h };
|
|
224
|
+
}
|
|
225
|
+
async function fetchDashboard(client, scope, parentId, viewId) {
|
|
226
|
+
const res = await client.get(viewPath(scope, parentId, viewId));
|
|
227
|
+
if (res.error)
|
|
228
|
+
return { error: res.error.message };
|
|
229
|
+
return { view: res.data };
|
|
230
|
+
}
|
|
231
|
+
async function writeDashboardSettings(client, scope, parentId, viewId, body) {
|
|
232
|
+
const res = await client.patch(viewPath(scope, parentId, viewId), body);
|
|
233
|
+
if (res.error)
|
|
234
|
+
return { error: res.error.message };
|
|
235
|
+
return { data: res.data };
|
|
236
|
+
}
|
|
237
|
+
/** For panel-level ops, figure out which table a panel is pegged to.
|
|
238
|
+
* Priority: explicit panel.table_id on the stored panel → fallback table_id (dashboard's table for table-scope).
|
|
239
|
+
*/
|
|
240
|
+
function panelTableId(panel, fallback) {
|
|
241
|
+
if (typeof panel.tableId === 'string')
|
|
242
|
+
return panel.tableId;
|
|
243
|
+
if (typeof panel.table_id === 'string')
|
|
244
|
+
return panel.table_id;
|
|
245
|
+
return fallback;
|
|
246
|
+
}
|
|
247
|
+
// ── tools ──────────────────────────────────────────────────────────────────────
|
|
248
|
+
export function registerDashboardTools(server, client) {
|
|
249
|
+
// ── list_dashboards ──
|
|
250
|
+
server.tool('list_dashboards', 'List all dashboard views on a table, folder, or database. Dashboards are views with view_type="dashboard" containing panels.', ScopeFields, async ({ scope, parent_id }) => {
|
|
251
|
+
const res = await client.get(viewsBasePath(scope, parent_id));
|
|
252
|
+
if (res.error)
|
|
253
|
+
return mcpError(res.error.message);
|
|
254
|
+
const dashboards = (res.data || []).filter((v) => v.view_type === 'dashboard');
|
|
255
|
+
return mcpResult({
|
|
256
|
+
dashboards: dashboards.map((d) => ({
|
|
257
|
+
id: d.id,
|
|
258
|
+
name: d.name,
|
|
259
|
+
scope,
|
|
260
|
+
panel_count: Array.isArray(d.view_settings?.panels)
|
|
261
|
+
? d.view_settings.panels.length
|
|
262
|
+
: 0,
|
|
263
|
+
created_date: d.created_date,
|
|
264
|
+
updated_date: d.updated_date,
|
|
265
|
+
})),
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
// ── get_dashboard ──
|
|
269
|
+
server.tool('get_dashboard', 'Get a dashboard view with all panels and settings.', {
|
|
270
|
+
...ScopeFields,
|
|
271
|
+
view_id: z.string().uuid(),
|
|
272
|
+
}, async ({ scope, parent_id, view_id }) => {
|
|
273
|
+
const { view, error } = await fetchDashboard(client, scope, parent_id, view_id);
|
|
274
|
+
if (error)
|
|
275
|
+
return mcpError(error);
|
|
276
|
+
return mcpResult(view);
|
|
277
|
+
});
|
|
278
|
+
// ── create_dashboard ──
|
|
279
|
+
server.tool('create_dashboard', 'Create a dashboard on a table, folder, or database. For folder/database scopes, set panel.table_id on each panel that references columns.', {
|
|
280
|
+
...ScopeFields,
|
|
281
|
+
name: z.string().min(1),
|
|
282
|
+
panels: z.array(PanelInputSchema).optional(),
|
|
283
|
+
dashboard_styling: DashboardStylingSchema.optional(),
|
|
284
|
+
grid_cols: z.number().int().min(1).max(24).optional(),
|
|
285
|
+
row_height: z.number().int().min(10).max(200).optional(),
|
|
286
|
+
}, async ({ scope, parent_id, name, panels, dashboard_styling, grid_cols, row_height }) => {
|
|
287
|
+
const cache = new ColumnMapCache(client);
|
|
288
|
+
const fallbackTableId = scope === 'table' ? parent_id : undefined;
|
|
289
|
+
const panelsOut = [];
|
|
290
|
+
if (panels && panels.length > 0) {
|
|
291
|
+
try {
|
|
292
|
+
for (const p of panels) {
|
|
293
|
+
const translated = await translatePanelForWrite(p, cache, fallbackTableId);
|
|
294
|
+
panelsOut.push(ensurePanelPlacement(translated, panelsOut));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch (e) {
|
|
298
|
+
return mcpError(e.message);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const view_settings = {
|
|
302
|
+
panels: panelsOut,
|
|
303
|
+
gridCols: grid_cols ?? DEFAULT_GRID_COLS,
|
|
304
|
+
rowHeight: row_height ?? 40,
|
|
305
|
+
margin: [12, 12],
|
|
306
|
+
containerPadding: [0, 0],
|
|
307
|
+
};
|
|
308
|
+
if (dashboard_styling)
|
|
309
|
+
view_settings.dashboardStyling = dashboard_styling;
|
|
310
|
+
const res = await client.post(viewsBasePath(scope, parent_id), {
|
|
311
|
+
name,
|
|
312
|
+
view_type: 'dashboard',
|
|
313
|
+
view_settings,
|
|
314
|
+
});
|
|
315
|
+
if (res.error)
|
|
316
|
+
return mcpError(res.error.message);
|
|
317
|
+
return mcpResult({ message: `Dashboard "${name}" created with ${panelsOut.length} panel(s)`, dashboard: res.data });
|
|
318
|
+
});
|
|
319
|
+
// ── update_dashboard ──
|
|
320
|
+
server.tool('update_dashboard', 'Update a dashboard\'s name, panels, or styling. When panels is provided it REPLACES all panels — use add_panel/update_panel/delete_panel for incremental changes.', {
|
|
321
|
+
...ScopeFields,
|
|
322
|
+
view_id: z.string().uuid(),
|
|
323
|
+
name: z.string().optional(),
|
|
324
|
+
panels: z.array(PanelInputSchema).optional(),
|
|
325
|
+
dashboard_styling: DashboardStylingSchema.optional(),
|
|
326
|
+
grid_cols: z.number().int().min(1).max(24).optional(),
|
|
327
|
+
row_height: z.number().int().min(10).max(200).optional(),
|
|
328
|
+
}, async ({ scope, parent_id, view_id, name, panels, dashboard_styling, grid_cols, row_height }) => {
|
|
329
|
+
const { view, error } = await fetchDashboard(client, scope, parent_id, view_id);
|
|
330
|
+
if (error)
|
|
331
|
+
return mcpError(error);
|
|
332
|
+
const current = (view?.view_settings || {});
|
|
333
|
+
const fallbackTableId = scope === 'table' ? parent_id : undefined;
|
|
334
|
+
const next = { ...current };
|
|
335
|
+
if (panels !== undefined) {
|
|
336
|
+
const cache = new ColumnMapCache(client);
|
|
337
|
+
const panelsOut = [];
|
|
338
|
+
try {
|
|
339
|
+
for (const p of panels) {
|
|
340
|
+
const translated = await translatePanelForWrite(p, cache, fallbackTableId);
|
|
341
|
+
panelsOut.push(ensurePanelPlacement(translated, panelsOut));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
catch (e) {
|
|
345
|
+
return mcpError(e.message);
|
|
346
|
+
}
|
|
347
|
+
next.panels = panelsOut;
|
|
348
|
+
}
|
|
349
|
+
if (dashboard_styling !== undefined)
|
|
350
|
+
next.dashboardStyling = dashboard_styling;
|
|
351
|
+
if (grid_cols !== undefined)
|
|
352
|
+
next.gridCols = grid_cols;
|
|
353
|
+
if (row_height !== undefined)
|
|
354
|
+
next.rowHeight = row_height;
|
|
355
|
+
const body = { view_settings: next };
|
|
356
|
+
if (name !== undefined)
|
|
357
|
+
body.name = name;
|
|
358
|
+
const { data, error: writeErr } = await writeDashboardSettings(client, scope, parent_id, view_id, body);
|
|
359
|
+
if (writeErr)
|
|
360
|
+
return mcpError(writeErr);
|
|
361
|
+
return mcpResult({ message: 'Dashboard updated', dashboard: data });
|
|
362
|
+
});
|
|
363
|
+
// ── delete_dashboard ──
|
|
364
|
+
server.tool('delete_dashboard', 'Delete a dashboard view. Cannot delete the last remaining view.', {
|
|
365
|
+
...ScopeFields,
|
|
366
|
+
view_id: z.string().uuid(),
|
|
367
|
+
}, async ({ scope, parent_id, view_id }) => {
|
|
368
|
+
const res = await client.delete(viewPath(scope, parent_id, view_id));
|
|
369
|
+
if (res.error)
|
|
370
|
+
return mcpError(res.error.message);
|
|
371
|
+
return mcpResult({ message: 'Dashboard deleted' });
|
|
372
|
+
});
|
|
373
|
+
// ── add_panel ──
|
|
374
|
+
server.tool('add_panel', 'Add a single panel to an existing dashboard. Auto-places on the grid if x/y omitted.', {
|
|
375
|
+
...ScopeFields,
|
|
376
|
+
view_id: z.string().uuid(),
|
|
377
|
+
panel: PanelInputSchema,
|
|
378
|
+
}, async ({ scope, parent_id, view_id, panel }) => {
|
|
379
|
+
const { view, error } = await fetchDashboard(client, scope, parent_id, view_id);
|
|
380
|
+
if (error)
|
|
381
|
+
return mcpError(error);
|
|
382
|
+
const settings = (view?.view_settings || {});
|
|
383
|
+
const existing = Array.isArray(settings.panels) ? settings.panels : [];
|
|
384
|
+
const fallbackTableId = scope === 'table' ? parent_id : undefined;
|
|
385
|
+
const cache = new ColumnMapCache(client);
|
|
386
|
+
let translated;
|
|
387
|
+
try {
|
|
388
|
+
translated = await translatePanelForWrite(panel, cache, fallbackTableId);
|
|
389
|
+
}
|
|
390
|
+
catch (e) {
|
|
391
|
+
return mcpError(e.message);
|
|
392
|
+
}
|
|
393
|
+
const placed = ensurePanelPlacement(translated, existing);
|
|
394
|
+
const next = { ...settings, panels: [...existing, placed] };
|
|
395
|
+
const { error: writeErr } = await writeDashboardSettings(client, scope, parent_id, view_id, { view_settings: next });
|
|
396
|
+
if (writeErr)
|
|
397
|
+
return mcpError(writeErr);
|
|
398
|
+
return mcpResult({ message: 'Panel added', panel_id: placed.id, position: { x: placed.x, y: placed.y, w: placed.w, h: placed.h } });
|
|
399
|
+
});
|
|
400
|
+
// ── update_panel ──
|
|
401
|
+
server.tool('update_panel', 'Update fields on an existing panel. Patches merge; config merges shallowly.', {
|
|
402
|
+
...ScopeFields,
|
|
403
|
+
view_id: z.string().uuid(),
|
|
404
|
+
panel_id: z.string(),
|
|
405
|
+
patch: z.object({
|
|
406
|
+
title: z.string().optional(),
|
|
407
|
+
showTitle: z.boolean().optional(),
|
|
408
|
+
config: z.record(z.string(), z.unknown()).optional(),
|
|
409
|
+
filters: z.array(PanelFilterSchema).optional(),
|
|
410
|
+
sorts: z.array(PanelSortSchema).optional(),
|
|
411
|
+
styling: z.record(z.string(), z.unknown()).optional(),
|
|
412
|
+
customCss: z.string().optional(),
|
|
413
|
+
}),
|
|
414
|
+
}, async ({ scope, parent_id, view_id, panel_id, patch }) => {
|
|
415
|
+
const { view, error } = await fetchDashboard(client, scope, parent_id, view_id);
|
|
416
|
+
if (error)
|
|
417
|
+
return mcpError(error);
|
|
418
|
+
const settings = (view?.view_settings || {});
|
|
419
|
+
const panels = Array.isArray(settings.panels) ? settings.panels : [];
|
|
420
|
+
const idx = panels.findIndex((p) => p.id === panel_id);
|
|
421
|
+
if (idx === -1)
|
|
422
|
+
return mcpError(`Panel "${panel_id}" not found on dashboard`);
|
|
423
|
+
const existing = panels[idx];
|
|
424
|
+
const fallbackTableId = scope === 'table' ? parent_id : undefined;
|
|
425
|
+
const panelTid = panelTableId(existing, fallbackTableId);
|
|
426
|
+
const cache = new ColumnMapCache(client);
|
|
427
|
+
let translatedConfig;
|
|
428
|
+
if (patch.config) {
|
|
429
|
+
if (!panelTid) {
|
|
430
|
+
// No table context — pass config through unchanged (user must provide raw IDs)
|
|
431
|
+
translatedConfig = patch.config;
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
try {
|
|
435
|
+
const colMap = await cache.get(panelTid);
|
|
436
|
+
translatedConfig = translatePanelConfig(existing.type, patch.config, colMap);
|
|
437
|
+
}
|
|
438
|
+
catch (e) {
|
|
439
|
+
return mcpError(e.message);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
let translatedFilters;
|
|
444
|
+
if (patch.filters) {
|
|
445
|
+
if (!panelTid) {
|
|
446
|
+
translatedFilters = patch.filters.map((f) => ({ columnId: f.columnId, operator: f.operator, value: f.value }));
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
const colMap = await cache.get(panelTid);
|
|
450
|
+
try {
|
|
451
|
+
translatedFilters = patch.filters.map((f) => ({
|
|
452
|
+
columnId: resolveColumnRef(f, colMap, 'filter'),
|
|
453
|
+
operator: f.operator,
|
|
454
|
+
value: f.value,
|
|
455
|
+
}));
|
|
456
|
+
}
|
|
457
|
+
catch (e) {
|
|
458
|
+
return mcpError(e.message);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
let translatedSorts;
|
|
463
|
+
if (patch.sorts) {
|
|
464
|
+
if (!panelTid) {
|
|
465
|
+
translatedSorts = patch.sorts.map((s) => ({ columnId: s.columnId, direction: s.direction }));
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
const colMap = await cache.get(panelTid);
|
|
469
|
+
try {
|
|
470
|
+
translatedSorts = patch.sorts.map((s) => ({
|
|
471
|
+
columnId: resolveColumnRef(s, colMap, 'sort'),
|
|
472
|
+
direction: s.direction,
|
|
473
|
+
}));
|
|
474
|
+
}
|
|
475
|
+
catch (e) {
|
|
476
|
+
return mcpError(e.message);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const merged = { ...existing };
|
|
481
|
+
if (patch.title !== undefined)
|
|
482
|
+
merged.title = patch.title;
|
|
483
|
+
if (patch.showTitle !== undefined)
|
|
484
|
+
merged.showTitle = patch.showTitle;
|
|
485
|
+
if (translatedConfig) {
|
|
486
|
+
merged.config = { ...(existing.config || {}), ...translatedConfig };
|
|
487
|
+
}
|
|
488
|
+
if (translatedFilters)
|
|
489
|
+
merged.filters = translatedFilters;
|
|
490
|
+
if (translatedSorts)
|
|
491
|
+
merged.sorts = translatedSorts;
|
|
492
|
+
if (patch.styling !== undefined)
|
|
493
|
+
merged.styling = patch.styling;
|
|
494
|
+
if (patch.customCss !== undefined)
|
|
495
|
+
merged.customCss = patch.customCss;
|
|
496
|
+
const nextPanels = [...panels];
|
|
497
|
+
nextPanels[idx] = merged;
|
|
498
|
+
const next = { ...settings, panels: nextPanels };
|
|
499
|
+
const { error: writeErr } = await writeDashboardSettings(client, scope, parent_id, view_id, { view_settings: next });
|
|
500
|
+
if (writeErr)
|
|
501
|
+
return mcpError(writeErr);
|
|
502
|
+
return mcpResult({ message: 'Panel updated', panel_id });
|
|
503
|
+
});
|
|
504
|
+
// ── delete_panel ──
|
|
505
|
+
server.tool('delete_panel', 'Remove a panel from a dashboard.', {
|
|
506
|
+
...ScopeFields,
|
|
507
|
+
view_id: z.string().uuid(),
|
|
508
|
+
panel_id: z.string(),
|
|
509
|
+
}, async ({ scope, parent_id, view_id, panel_id }) => {
|
|
510
|
+
const { view, error } = await fetchDashboard(client, scope, parent_id, view_id);
|
|
511
|
+
if (error)
|
|
512
|
+
return mcpError(error);
|
|
513
|
+
const settings = (view?.view_settings || {});
|
|
514
|
+
const panels = Array.isArray(settings.panels) ? settings.panels : [];
|
|
515
|
+
const nextPanels = panels.filter((p) => p.id !== panel_id);
|
|
516
|
+
if (nextPanels.length === panels.length)
|
|
517
|
+
return mcpError(`Panel "${panel_id}" not found`);
|
|
518
|
+
const next = { ...settings, panels: nextPanels };
|
|
519
|
+
const { error: writeErr } = await writeDashboardSettings(client, scope, parent_id, view_id, { view_settings: next });
|
|
520
|
+
if (writeErr)
|
|
521
|
+
return mcpError(writeErr);
|
|
522
|
+
return mcpResult({ message: 'Panel deleted', panel_id });
|
|
523
|
+
});
|
|
524
|
+
// ── move_panel ──
|
|
525
|
+
server.tool('move_panel', 'Change the position/size of a panel on the grid.', {
|
|
526
|
+
...ScopeFields,
|
|
527
|
+
view_id: z.string().uuid(),
|
|
528
|
+
panel_id: z.string(),
|
|
529
|
+
x: z.number().int().min(0).optional(),
|
|
530
|
+
y: z.number().int().min(0).optional(),
|
|
531
|
+
w: z.number().int().min(1).optional(),
|
|
532
|
+
h: z.number().int().min(1).optional(),
|
|
533
|
+
}, async ({ scope, parent_id, view_id, panel_id, x, y, w, h }) => {
|
|
534
|
+
const { view, error } = await fetchDashboard(client, scope, parent_id, view_id);
|
|
535
|
+
if (error)
|
|
536
|
+
return mcpError(error);
|
|
537
|
+
const settings = (view?.view_settings || {});
|
|
538
|
+
const panels = Array.isArray(settings.panels) ? settings.panels : [];
|
|
539
|
+
const idx = panels.findIndex((p) => p.id === panel_id);
|
|
540
|
+
if (idx === -1)
|
|
541
|
+
return mcpError(`Panel "${panel_id}" not found`);
|
|
542
|
+
const merged = { ...panels[idx] };
|
|
543
|
+
if (x !== undefined)
|
|
544
|
+
merged.x = x;
|
|
545
|
+
if (y !== undefined)
|
|
546
|
+
merged.y = y;
|
|
547
|
+
if (w !== undefined)
|
|
548
|
+
merged.w = w;
|
|
549
|
+
if (h !== undefined)
|
|
550
|
+
merged.h = h;
|
|
551
|
+
const nextPanels = [...panels];
|
|
552
|
+
nextPanels[idx] = merged;
|
|
553
|
+
const next = { ...settings, panels: nextPanels };
|
|
554
|
+
const { error: writeErr } = await writeDashboardSettings(client, scope, parent_id, view_id, { view_settings: next });
|
|
555
|
+
if (writeErr)
|
|
556
|
+
return mcpError(writeErr);
|
|
557
|
+
return mcpResult({ message: 'Panel moved', panel_id, position: { x: merged.x, y: merged.y, w: merged.w, h: merged.h } });
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
//# sourceMappingURL=dashboards.js.map
|