rootr-cli 0.8.0 → 0.9.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 +42 -4
- package/bin/rootr.js +375 -1
- package/lib/client.js +147 -12
- package/lib/mcp-tools/databases.js +346 -25
- package/lib/mcp-tools/documents.js +230 -3
- package/lib/mcp-tools/forms.js +87 -2
- package/lib/mcp-tools/misc.js +40 -0
- package/lib/mcp-tools/workspaces.js +8 -1
- package/package.json +1 -1
|
@@ -13,6 +13,15 @@ import {
|
|
|
13
13
|
/** Human text for the `databaseId` param — accepts a path just like the document tools. */
|
|
14
14
|
const DATABASE_TARGET_DESC = `DATABASE node id${NODE_TARGET_HINT} (e.g. "/제작 공정/칸반")`;
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* DB-BODY — 행의 상세 내용(본문 메모). 화면에서 칸반 카드를 열면 속성 아래에 붙는 그 글이며,
|
|
18
|
+
* 실체는 표 밑에 달리는 문서 하나다. 예전에는 REST 2단계로만 닿을 수 있어서 CLI·MCP에서는
|
|
19
|
+
* 아예 손댈 수 없었다.
|
|
20
|
+
*/
|
|
21
|
+
const BODY_DESC =
|
|
22
|
+
"The row's detail body — the note shown under a kanban card's properties, in markdown " +
|
|
23
|
+
'(행의 상세 내용/본문 메모). Creates the row\'s page on demand; replaces the whole body.';
|
|
24
|
+
|
|
16
25
|
/**
|
|
17
26
|
* Resolve a `{propertyNameOrId: value}` map to `{propertyId: value}` using the
|
|
18
27
|
* database schema, so agents can address columns by their human name OR their
|
|
@@ -51,11 +60,17 @@ export function registerDatabaseTools(server) {
|
|
|
51
60
|
'board, a task/requirement/risk tracker, anything with trackable rows. Do NOT fake a table with a markdown ' +
|
|
52
61
|
'document, and do NOT create an empty database and then add columns one by one — `properties` order IS the ' +
|
|
53
62
|
'column order, so one call keeps the columns in exactly the order you list them. ' +
|
|
54
|
-
'The FIRST property must be type "title" (the row label). Other types: text | number | select | ' +
|
|
55
|
-
'
|
|
56
|
-
'
|
|
57
|
-
'
|
|
58
|
-
'
|
|
63
|
+
'The FIRST property must be type "title" (the row label). Other types: text | number | select | multi_select | ' +
|
|
64
|
+
'status | date | checkbox | url | email | phone | person | files | relation | formula | rollup | ' +
|
|
65
|
+
'created_time | last_edited_time | created_by | last_edited_by | unique_id. ' +
|
|
66
|
+
'select/multi_select/status take options:[{name,color,group?}] (group ∈ todo|in_progress|done, status only); ' +
|
|
67
|
+
'relation takes relationDatabaseId; formula takes formula ("{수량} * {단가}"); rollup takes ' +
|
|
68
|
+
'rollup:{relation,property?,function}; unique_id takes prefix ("TASK" → "TASK-42"). The last seven types are ' +
|
|
69
|
+
'filled in by the server and take no row value. ' +
|
|
70
|
+
'Views default to a single table view; view types are table | board | gallery | calendar | timeline | list — ' +
|
|
71
|
+
'a "board" view needs config.groupBy (a select/status property id), a "timeline"/"calendar" view needs ' +
|
|
72
|
+
'config.dateProp. Row `values` are keyed by column NAME (or id); date values are ' +
|
|
73
|
+
'{ start: "YYYY-MM-DDTHH:mm" or "YYYY-MM-DD", end? }, multi_select is string[], relation is an array of row ids.',
|
|
59
74
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
60
75
|
inputSchema: {
|
|
61
76
|
name: z.string().describe('Table name, e.g. "업무 목록"'),
|
|
@@ -68,12 +83,54 @@ export function registerDatabaseTools(server) {
|
|
|
68
83
|
z.object({
|
|
69
84
|
name: z.string().describe('Column header, e.g. "담당자"'),
|
|
70
85
|
type: z
|
|
71
|
-
.enum([
|
|
86
|
+
.enum([
|
|
87
|
+
'title',
|
|
88
|
+
'text',
|
|
89
|
+
'number',
|
|
90
|
+
'select',
|
|
91
|
+
'multi_select',
|
|
92
|
+
'status',
|
|
93
|
+
'date',
|
|
94
|
+
'checkbox',
|
|
95
|
+
'url',
|
|
96
|
+
'email',
|
|
97
|
+
'phone',
|
|
98
|
+
'person',
|
|
99
|
+
'files',
|
|
100
|
+
'relation',
|
|
101
|
+
'formula',
|
|
102
|
+
'rollup',
|
|
103
|
+
'created_time',
|
|
104
|
+
'last_edited_time',
|
|
105
|
+
'created_by',
|
|
106
|
+
'last_edited_by',
|
|
107
|
+
'unique_id',
|
|
108
|
+
])
|
|
72
109
|
.describe('Column type; the FIRST property must be "title"'),
|
|
73
110
|
options: z
|
|
74
|
-
.array(z.object({ name: z.string(), color: z.string() }))
|
|
111
|
+
.array(z.object({ name: z.string(), color: z.string(), group: z.string().optional() }))
|
|
112
|
+
.optional()
|
|
113
|
+
.describe('select/multi_select/status only: the choices'),
|
|
114
|
+
relationDatabaseId: z.string().optional().describe('relation only: the table this column points at'),
|
|
115
|
+
formula: z.string().optional().describe('formula only: e.g. "{수량} * {단가}"'),
|
|
116
|
+
rollup: z
|
|
117
|
+
.object({
|
|
118
|
+
relation: z.string(),
|
|
119
|
+
property: z.string().optional(),
|
|
120
|
+
function: z.enum([
|
|
121
|
+
'count',
|
|
122
|
+
'count_values',
|
|
123
|
+
'sum',
|
|
124
|
+
'average',
|
|
125
|
+
'min',
|
|
126
|
+
'max',
|
|
127
|
+
'show_original',
|
|
128
|
+
'percent_checked',
|
|
129
|
+
]),
|
|
130
|
+
})
|
|
75
131
|
.optional()
|
|
76
|
-
.describe('
|
|
132
|
+
.describe('rollup only: what to gather through which relation'),
|
|
133
|
+
prefix: z.string().optional().describe('unique_id only: e.g. "TASK" → "TASK-42"'),
|
|
77
134
|
}),
|
|
78
135
|
)
|
|
79
136
|
.optional()
|
|
@@ -111,6 +168,66 @@ export function registerDatabaseTools(server) {
|
|
|
111
168
|
},
|
|
112
169
|
);
|
|
113
170
|
|
|
171
|
+
server.registerTool(
|
|
172
|
+
'rootr_create_view',
|
|
173
|
+
{
|
|
174
|
+
title: 'Add a view to an existing database',
|
|
175
|
+
description:
|
|
176
|
+
'Add ONE more view (table / board / gallery / calendar / timeline) to a Rootr (루터) DATABASE that ' +
|
|
177
|
+
'already exists. Until now views could only be set when the table was CREATED, so a Notion table with ' +
|
|
178
|
+
'four views could only be migrated with one. Existing views are untouched — the server merges, unlike ' +
|
|
179
|
+
'a full PATCH which replaces the whole views array and silently drops anything you leave out. ' +
|
|
180
|
+
'A board needs `groupBy` (a select column); calendar/timeline need `dateProp`. Columns may be given ' +
|
|
181
|
+
'by NAME or id.',
|
|
182
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
183
|
+
inputSchema: {
|
|
184
|
+
databaseId: z.string().describe(DATABASE_TARGET_DESC),
|
|
185
|
+
name: z.string().describe('View name shown on the tab, e.g. "일정"'),
|
|
186
|
+
type: z
|
|
187
|
+
.enum(['table', 'board', 'gallery', 'calendar', 'timeline'])
|
|
188
|
+
.describe('View layout'),
|
|
189
|
+
groupBy: z.string().optional().describe('board view: the select column to group by (name or id)'),
|
|
190
|
+
dateProp: z.string().optional().describe('calendar/timeline view: the date column (name or id)'),
|
|
191
|
+
endDateProp: z.string().optional().describe('timeline view: the end-date column (name or id)'),
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
async ({ databaseId, name, type, groupBy, dateProp, endDateProp }) => {
|
|
195
|
+
try {
|
|
196
|
+
const client = makeClient();
|
|
197
|
+
const id = await resolveNodeId(client, databaseId);
|
|
198
|
+
const view = await client.createDatabaseView(id, { name, type, groupBy, dateProp, endDateProp });
|
|
199
|
+
return jsonResult(view);
|
|
200
|
+
} catch (err) {
|
|
201
|
+
return errorResult(err);
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
server.registerTool(
|
|
207
|
+
'rootr_delete_view',
|
|
208
|
+
{
|
|
209
|
+
title: 'Delete one view from a database',
|
|
210
|
+
description:
|
|
211
|
+
'Remove ONE view from a Rootr (루터) DATABASE. Rows and column values are NOT touched — only that ' +
|
|
212
|
+
'view\'s layout is removed. The last remaining view cannot be deleted (there would be nothing to look at).',
|
|
213
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
214
|
+
inputSchema: {
|
|
215
|
+
databaseId: z.string().describe(DATABASE_TARGET_DESC),
|
|
216
|
+
viewId: z.string().describe('View id or view NAME (from rootr_read_database)'),
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
async ({ databaseId, viewId }) => {
|
|
220
|
+
try {
|
|
221
|
+
const client = makeClient();
|
|
222
|
+
const id = await resolveNodeId(client, databaseId);
|
|
223
|
+
await client.deleteDatabaseView(id, viewId);
|
|
224
|
+
return jsonResult({ deleted: viewId });
|
|
225
|
+
} catch (err) {
|
|
226
|
+
return errorResult(err);
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
);
|
|
230
|
+
|
|
114
231
|
server.registerTool(
|
|
115
232
|
'rootr_update_view',
|
|
116
233
|
{
|
|
@@ -164,15 +281,19 @@ export function registerDatabaseTools(server) {
|
|
|
164
281
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
165
282
|
inputSchema: {
|
|
166
283
|
databaseId: z.string().describe(DATABASE_TARGET_DESC),
|
|
284
|
+
includeBody: z
|
|
285
|
+
.boolean()
|
|
286
|
+
.optional()
|
|
287
|
+
.describe("true → also return each row's detail body (the note under a card). Off by default."),
|
|
167
288
|
},
|
|
168
289
|
},
|
|
169
|
-
async ({ databaseId }) => {
|
|
290
|
+
async ({ databaseId, includeBody }) => {
|
|
170
291
|
try {
|
|
171
292
|
const client = makeClient();
|
|
172
293
|
const id = await resolveNodeId(client, databaseId);
|
|
173
294
|
const [database, rows] = await Promise.all([
|
|
174
295
|
client.getDatabase(id),
|
|
175
|
-
client.listRows(id),
|
|
296
|
+
client.listRows(id, { withBody: includeBody === true }),
|
|
176
297
|
]);
|
|
177
298
|
return jsonResult({ database, rows });
|
|
178
299
|
} catch (err) {
|
|
@@ -191,12 +312,18 @@ export function registerDatabaseTools(server) {
|
|
|
191
312
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
192
313
|
inputSchema: {
|
|
193
314
|
databaseId: z.string().describe(DATABASE_TARGET_DESC),
|
|
315
|
+
includeBody: z
|
|
316
|
+
.boolean()
|
|
317
|
+
.optional()
|
|
318
|
+
.describe("true → also return each row's detail body (the note under a card). Off by default."),
|
|
194
319
|
},
|
|
195
320
|
},
|
|
196
|
-
async ({ databaseId }) => {
|
|
321
|
+
async ({ databaseId, includeBody }) => {
|
|
197
322
|
try {
|
|
198
323
|
const client = makeClient();
|
|
199
|
-
const rows = await client.listRows(await resolveNodeId(client, databaseId)
|
|
324
|
+
const rows = await client.listRows(await resolveNodeId(client, databaseId), {
|
|
325
|
+
withBody: includeBody === true,
|
|
326
|
+
});
|
|
200
327
|
return jsonResult({ rows });
|
|
201
328
|
} catch (err) {
|
|
202
329
|
return errorResult(err);
|
|
@@ -210,23 +337,27 @@ export function registerDatabaseTools(server) {
|
|
|
210
337
|
title: 'Add a database row',
|
|
211
338
|
description:
|
|
212
339
|
'Append a row to a Rootr (루터) DATABASE. `values` maps property NAME (or id) → value. ' +
|
|
213
|
-
'Value shapes by type: title/text/url/select
|
|
340
|
+
'Value shapes by type: title/text/url/select → string; number → number; multi_select → string[]; ' +
|
|
341
|
+
'person → string[] (a LIST of names — a meeting has several attendees; one bare name is also accepted); ' +
|
|
214
342
|
'checkbox → boolean; date → { start: "YYYY-MM-DD", end? }. A select value is the OPTION NAME. ' +
|
|
215
|
-
'Call rootr_read_database first to learn the columns. Optionally set `position` to insert at an index.'
|
|
343
|
+
'Call rootr_read_database first to learn the columns. Optionally set `position` to insert at an index. ' +
|
|
344
|
+
'Pass `body` to also write the row\'s DETAIL PAGE (the note shown under a kanban card\'s properties) — ' +
|
|
345
|
+
'that page is created on demand, so a row with `body` is the one-call equivalent of "메모 추가".',
|
|
216
346
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
217
347
|
inputSchema: {
|
|
218
348
|
databaseId: z.string().describe(DATABASE_TARGET_DESC),
|
|
219
349
|
values: z.record(z.unknown()).describe('{ propertyNameOrId: value } for the new row'),
|
|
220
350
|
position: z.number().optional().describe('0-based insert index; appended at the end if omitted'),
|
|
351
|
+
body: z.string().optional().describe(BODY_DESC),
|
|
221
352
|
},
|
|
222
353
|
},
|
|
223
|
-
async ({ databaseId, values, position }) => {
|
|
354
|
+
async ({ databaseId, values, position, body }) => {
|
|
224
355
|
try {
|
|
225
356
|
const client = makeClient();
|
|
226
357
|
const id = await resolveNodeId(client, databaseId);
|
|
227
358
|
const database = await client.getDatabase(id);
|
|
228
359
|
const resolved = resolveValueKeys(database.properties || [], values);
|
|
229
|
-
const row = await client.createRow(id, { values: resolved, position });
|
|
360
|
+
const row = await client.createRow(id, { values: resolved, position, body });
|
|
230
361
|
return jsonResult(row);
|
|
231
362
|
} catch (err) {
|
|
232
363
|
return errorResult(err);
|
|
@@ -249,9 +380,10 @@ export function registerDatabaseTools(server) {
|
|
|
249
380
|
rowId: z.string().describe('Row id (must belong to the database)'),
|
|
250
381
|
values: z.record(z.unknown()).optional().describe('{ propertyNameOrId: newValue | null } partial patch'),
|
|
251
382
|
position: z.number().optional().describe('New 0-based position for the row'),
|
|
383
|
+
body: z.string().optional().describe(BODY_DESC),
|
|
252
384
|
},
|
|
253
385
|
},
|
|
254
|
-
async ({ databaseId, rowId, values, position }) => {
|
|
386
|
+
async ({ databaseId, rowId, values, position, body }) => {
|
|
255
387
|
try {
|
|
256
388
|
const client = makeClient();
|
|
257
389
|
const id = await resolveNodeId(client, databaseId);
|
|
@@ -260,7 +392,7 @@ export function registerDatabaseTools(server) {
|
|
|
260
392
|
const database = await client.getDatabase(id);
|
|
261
393
|
resolved = resolveValueKeys(database.properties || [], values);
|
|
262
394
|
}
|
|
263
|
-
const row = await client.updateRow(id, rowId, { values: resolved, position });
|
|
395
|
+
const row = await client.updateRow(id, rowId, { values: resolved, position, body });
|
|
264
396
|
return jsonResult(row);
|
|
265
397
|
} catch (err) {
|
|
266
398
|
return errorResult(err);
|
|
@@ -305,27 +437,91 @@ export function registerDatabaseTools(server) {
|
|
|
305
437
|
description:
|
|
306
438
|
'Add ONE column to a Rootr (루터) DATABASE. The existing columns and every row are left untouched — ' +
|
|
307
439
|
'rows simply read empty for the new column until you fill them with rootr_update_row. ' +
|
|
308
|
-
'type ∈ text | number | select | multi_select | date | checkbox | url | person ' +
|
|
440
|
+
'type ∈ text | number | select | multi_select | status | date | checkbox | url | email | phone | person | files | ' +
|
|
441
|
+
'relation | formula | rollup | created_time | last_edited_time | created_by | last_edited_by | unique_id ' +
|
|
309
442
|
'("title" is not allowed: a database already has exactly one title column). ' +
|
|
310
|
-
'For select/multi_select pass options: [{ name, color }]
|
|
443
|
+
'For select/multi_select/status pass options: [{ name, color, group? }] (group ∈ todo|in_progress|done, status only). ' +
|
|
444
|
+
'For relation pass relationDatabaseId (the node id of the table it points at). ' +
|
|
445
|
+
'For formula pass formula, e.g. "{수량} * {단가}" or "if({금액} >= 5000, \\"큰 건\\", \\"작은 건\\")" — ' +
|
|
446
|
+
'available functions: if, concat, length, upper, lower, contains, abs, round, floor, ceil, min, max, dateBetween, now, empty. ' +
|
|
447
|
+
'For rollup pass rollup: { relation, property?, function } (function ∈ count|count_values|sum|average|min|max|show_original|percent_checked). ' +
|
|
448
|
+
'For unique_id pass prefix to get "TASK-42" style ids. ' +
|
|
449
|
+
'created_time / last_edited_time / created_by / last_edited_by / unique_id / formula / rollup are filled in by the server — ' +
|
|
450
|
+
'they take no value and rootr_update_row will refuse to set them. ' +
|
|
451
|
+
'Returns the created column WITH ITS id.',
|
|
311
452
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
312
453
|
inputSchema: {
|
|
313
454
|
databaseId: z.string().describe(DATABASE_TARGET_DESC),
|
|
314
455
|
name: z.string().describe('Column header, e.g. "담당자"'),
|
|
315
456
|
type: z
|
|
316
|
-
.enum([
|
|
457
|
+
.enum([
|
|
458
|
+
'text',
|
|
459
|
+
'number',
|
|
460
|
+
'select',
|
|
461
|
+
'multi_select',
|
|
462
|
+
'status',
|
|
463
|
+
'date',
|
|
464
|
+
'checkbox',
|
|
465
|
+
'url',
|
|
466
|
+
'email',
|
|
467
|
+
'phone',
|
|
468
|
+
'person',
|
|
469
|
+
'files',
|
|
470
|
+
'relation',
|
|
471
|
+
'formula',
|
|
472
|
+
'rollup',
|
|
473
|
+
'created_time',
|
|
474
|
+
'last_edited_time',
|
|
475
|
+
'created_by',
|
|
476
|
+
'last_edited_by',
|
|
477
|
+
'unique_id',
|
|
478
|
+
])
|
|
317
479
|
.describe('Column type'),
|
|
318
480
|
options: z
|
|
319
|
-
.array(z.object({ name: z.string(), color: z.string() }))
|
|
481
|
+
.array(z.object({ name: z.string(), color: z.string(), group: z.string().optional() }))
|
|
482
|
+
.optional()
|
|
483
|
+
.describe('select/multi_select/status only: the choices, e.g. [{ name: "진행중", color: "blue" }]'),
|
|
484
|
+
relationDatabaseId: z
|
|
485
|
+
.string()
|
|
486
|
+
.optional()
|
|
487
|
+
.describe('relation only: the node id of the database this column points at'),
|
|
488
|
+
formula: z
|
|
489
|
+
.string()
|
|
490
|
+
.optional()
|
|
491
|
+
.describe('formula only: the expression, e.g. "{수량} * {단가}"'),
|
|
492
|
+
rollup: z
|
|
493
|
+
.object({
|
|
494
|
+
relation: z.string().describe('the relation column (id or name) to follow'),
|
|
495
|
+
property: z.string().optional().describe('the column to read in the linked table'),
|
|
496
|
+
function: z.enum([
|
|
497
|
+
'count',
|
|
498
|
+
'count_values',
|
|
499
|
+
'sum',
|
|
500
|
+
'average',
|
|
501
|
+
'min',
|
|
502
|
+
'max',
|
|
503
|
+
'show_original',
|
|
504
|
+
'percent_checked',
|
|
505
|
+
]),
|
|
506
|
+
})
|
|
320
507
|
.optional()
|
|
321
|
-
.describe('
|
|
508
|
+
.describe('rollup only: what to gather through which relation'),
|
|
509
|
+
prefix: z.string().optional().describe('unique_id only: prefix for the number, e.g. "TASK" → "TASK-42"'),
|
|
322
510
|
},
|
|
323
511
|
},
|
|
324
|
-
async ({ databaseId, name, type, options }) => {
|
|
512
|
+
async ({ databaseId, name, type, options, relationDatabaseId, formula, rollup, prefix }) => {
|
|
325
513
|
try {
|
|
326
514
|
const client = makeClient();
|
|
327
515
|
const id = await resolveNodeId(client, databaseId);
|
|
328
|
-
const property = await client.addColumn(id, {
|
|
516
|
+
const property = await client.addColumn(id, {
|
|
517
|
+
name,
|
|
518
|
+
type,
|
|
519
|
+
options,
|
|
520
|
+
relationDatabaseId,
|
|
521
|
+
formula,
|
|
522
|
+
rollup,
|
|
523
|
+
prefix,
|
|
524
|
+
});
|
|
329
525
|
return jsonResult({ property });
|
|
330
526
|
} catch (err) {
|
|
331
527
|
return errorResult(err);
|
|
@@ -364,6 +560,131 @@ export function registerDatabaseTools(server) {
|
|
|
364
560
|
},
|
|
365
561
|
);
|
|
366
562
|
|
|
563
|
+
// ── DB-PERM-001 / DB-WATCH — 노션에 없는 두 가지: 자물쇠와 알림 ────────────
|
|
564
|
+
//
|
|
565
|
+
// 표를 만들 수 있으면 잠글 수도, 누구에게 알릴지 정할 수도 있어야 한다. 이게 없으면
|
|
566
|
+
// 에이전트가 "지원서 표 만들어 줘. 서로 못 보게 하고, 새 지원 들어오면 김승수한테
|
|
567
|
+
// 알려 줘" 같은 부탁의 절반만 해내고 만다.
|
|
568
|
+
|
|
569
|
+
server.registerTool(
|
|
570
|
+
'rootr_read_database_permissions',
|
|
571
|
+
{
|
|
572
|
+
title: 'Read a database\'s locks',
|
|
573
|
+
description:
|
|
574
|
+
'Read the three locks on a Rootr (루터) DATABASE — schema lock (only managers change columns/views), ' +
|
|
575
|
+
'own-rows-only (people see just what they submitted), and manager-only columns. Notion has none of these.',
|
|
576
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
577
|
+
inputSchema: { databaseId: z.string().describe(DATABASE_TARGET_DESC) },
|
|
578
|
+
},
|
|
579
|
+
async ({ databaseId }) => {
|
|
580
|
+
try {
|
|
581
|
+
const client = makeClient();
|
|
582
|
+
const id = await resolveNodeId(client, databaseId);
|
|
583
|
+
return jsonResult(await client.getDatabasePermissions(id));
|
|
584
|
+
} catch (err) {
|
|
585
|
+
return errorResult(err);
|
|
586
|
+
}
|
|
587
|
+
},
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
server.registerTool(
|
|
591
|
+
'rootr_set_database_permissions',
|
|
592
|
+
{
|
|
593
|
+
title: 'Lock a database',
|
|
594
|
+
description:
|
|
595
|
+
'Set the locks on a Rootr (루터) DATABASE. `schemaLock: "managers"` stops editors from changing columns and ' +
|
|
596
|
+
'views; `rowScope: "own"` hides other people\'s rows (application forms, report boxes); `lockedProps` names ' +
|
|
597
|
+
'the columns only managers may edit (approval flags, final scores). Omitted fields keep their current value. ' +
|
|
598
|
+
'Requires MANAGE on the table.',
|
|
599
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
600
|
+
inputSchema: {
|
|
601
|
+
databaseId: z.string().describe(DATABASE_TARGET_DESC),
|
|
602
|
+
schemaLock: z.enum(['open', 'managers']).optional().describe('Who may change columns and views'),
|
|
603
|
+
rowScope: z.enum(['all', 'own']).optional().describe('"own" = each person sees only the rows they created'),
|
|
604
|
+
lockedProps: z
|
|
605
|
+
.array(z.string())
|
|
606
|
+
.optional()
|
|
607
|
+
.describe('Column ids (or names) only managers may edit'),
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
async ({ databaseId, schemaLock, rowScope, lockedProps }) => {
|
|
611
|
+
try {
|
|
612
|
+
const client = makeClient();
|
|
613
|
+
const id = await resolveNodeId(client, databaseId);
|
|
614
|
+
const body = {};
|
|
615
|
+
if (schemaLock !== undefined) body.schemaLock = schemaLock;
|
|
616
|
+
if (rowScope !== undefined) body.rowScope = rowScope;
|
|
617
|
+
if (lockedProps !== undefined) body.lockedProps = lockedProps;
|
|
618
|
+
return jsonResult(await client.setDatabasePermissions(id, body));
|
|
619
|
+
} catch (err) {
|
|
620
|
+
return errorResult(err);
|
|
621
|
+
}
|
|
622
|
+
},
|
|
623
|
+
);
|
|
624
|
+
|
|
625
|
+
server.registerTool(
|
|
626
|
+
'rootr_read_database_notifications',
|
|
627
|
+
{
|
|
628
|
+
title: 'Read a database\'s notification rules',
|
|
629
|
+
description:
|
|
630
|
+
'Read who gets notified when rows are added, changed or deleted in a Rootr (루터) DATABASE. ' +
|
|
631
|
+
'Notion cannot do this — there each person must turn on watching for themselves.',
|
|
632
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
633
|
+
inputSchema: { databaseId: z.string().describe(DATABASE_TARGET_DESC) },
|
|
634
|
+
},
|
|
635
|
+
async ({ databaseId }) => {
|
|
636
|
+
try {
|
|
637
|
+
const client = makeClient();
|
|
638
|
+
const id = await resolveNodeId(client, databaseId);
|
|
639
|
+
return jsonResult(await client.getDatabaseNotifications(id));
|
|
640
|
+
} catch (err) {
|
|
641
|
+
return errorResult(err);
|
|
642
|
+
}
|
|
643
|
+
},
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
server.registerTool(
|
|
647
|
+
'rootr_set_database_notifications',
|
|
648
|
+
{
|
|
649
|
+
title: 'Set who gets notified about a database',
|
|
650
|
+
description:
|
|
651
|
+
'Replace the notification rules on a Rootr (루터) DATABASE. Each rule names the people to notify (`userIds`), ' +
|
|
652
|
+
'when (`events`: created / updated / deleted), and optionally a filter so only some rows ping — ' +
|
|
653
|
+
'`{propId, op: "is"|"contains"|"changed", value}` (e.g. status is 긴급, owner changed). ' +
|
|
654
|
+
'Rules replace the existing set; pass [] to clear. Requires MANAGE. A person is never notified about their own change.',
|
|
655
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
656
|
+
inputSchema: {
|
|
657
|
+
databaseId: z.string().describe(DATABASE_TARGET_DESC),
|
|
658
|
+
watchers: z
|
|
659
|
+
.array(
|
|
660
|
+
z.object({
|
|
661
|
+
id: z.string().optional(),
|
|
662
|
+
userIds: z.array(z.string()).describe('Workspace member user ids to notify'),
|
|
663
|
+
events: z.array(z.enum(['created', 'updated', 'deleted'])),
|
|
664
|
+
filter: z
|
|
665
|
+
.object({
|
|
666
|
+
propId: z.string(),
|
|
667
|
+
op: z.enum(['is', 'contains', 'changed']),
|
|
668
|
+
value: z.string().optional(),
|
|
669
|
+
})
|
|
670
|
+
.optional(),
|
|
671
|
+
label: z.string().optional(),
|
|
672
|
+
}),
|
|
673
|
+
)
|
|
674
|
+
.describe('The complete rule set (replaces what is stored)'),
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
async ({ databaseId, watchers }) => {
|
|
678
|
+
try {
|
|
679
|
+
const client = makeClient();
|
|
680
|
+
const id = await resolveNodeId(client, databaseId);
|
|
681
|
+
return jsonResult(await client.setDatabaseNotifications(id, watchers));
|
|
682
|
+
} catch (err) {
|
|
683
|
+
return errorResult(err);
|
|
684
|
+
}
|
|
685
|
+
},
|
|
686
|
+
);
|
|
687
|
+
|
|
367
688
|
server.registerTool(
|
|
368
689
|
'rootr_delete_column',
|
|
369
690
|
{
|