rootr-cli 0.8.1 → 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 +33 -0
- package/bin/rootr.js +289 -1
- package/lib/client.js +136 -5
- package/lib/mcp-tools/databases.js +314 -16
- 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
|
@@ -60,11 +60,17 @@ export function registerDatabaseTools(server) {
|
|
|
60
60
|
'board, a task/requirement/risk tracker, anything with trackable rows. Do NOT fake a table with a markdown ' +
|
|
61
61
|
'document, and do NOT create an empty database and then add columns one by one — `properties` order IS the ' +
|
|
62
62
|
'column order, so one call keeps the columns in exactly the order you list them. ' +
|
|
63
|
-
'The FIRST property must be type "title" (the row label). Other types: text | number | select | ' +
|
|
64
|
-
'
|
|
65
|
-
'
|
|
66
|
-
'
|
|
67
|
-
'
|
|
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.',
|
|
68
74
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
69
75
|
inputSchema: {
|
|
70
76
|
name: z.string().describe('Table name, e.g. "업무 목록"'),
|
|
@@ -77,12 +83,54 @@ export function registerDatabaseTools(server) {
|
|
|
77
83
|
z.object({
|
|
78
84
|
name: z.string().describe('Column header, e.g. "담당자"'),
|
|
79
85
|
type: z
|
|
80
|
-
.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
|
+
])
|
|
81
109
|
.describe('Column type; the FIRST property must be "title"'),
|
|
82
110
|
options: z
|
|
83
|
-
.array(z.object({ name: z.string(), color: z.string() }))
|
|
111
|
+
.array(z.object({ name: z.string(), color: z.string(), group: z.string().optional() }))
|
|
84
112
|
.optional()
|
|
85
|
-
.describe('select/multi_select only: the choices'),
|
|
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
|
+
})
|
|
131
|
+
.optional()
|
|
132
|
+
.describe('rollup only: what to gather through which relation'),
|
|
133
|
+
prefix: z.string().optional().describe('unique_id only: e.g. "TASK" → "TASK-42"'),
|
|
86
134
|
}),
|
|
87
135
|
)
|
|
88
136
|
.optional()
|
|
@@ -120,6 +168,66 @@ export function registerDatabaseTools(server) {
|
|
|
120
168
|
},
|
|
121
169
|
);
|
|
122
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
|
+
|
|
123
231
|
server.registerTool(
|
|
124
232
|
'rootr_update_view',
|
|
125
233
|
{
|
|
@@ -229,7 +337,8 @@ export function registerDatabaseTools(server) {
|
|
|
229
337
|
title: 'Add a database row',
|
|
230
338
|
description:
|
|
231
339
|
'Append a row to a Rootr (루터) DATABASE. `values` maps property NAME (or id) → value. ' +
|
|
232
|
-
'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); ' +
|
|
233
342
|
'checkbox → boolean; date → { start: "YYYY-MM-DD", end? }. A select value is the OPTION NAME. ' +
|
|
234
343
|
'Call rootr_read_database first to learn the columns. Optionally set `position` to insert at an index. ' +
|
|
235
344
|
'Pass `body` to also write the row\'s DETAIL PAGE (the note shown under a kanban card\'s properties) — ' +
|
|
@@ -328,27 +437,91 @@ export function registerDatabaseTools(server) {
|
|
|
328
437
|
description:
|
|
329
438
|
'Add ONE column to a Rootr (루터) DATABASE. The existing columns and every row are left untouched — ' +
|
|
330
439
|
'rows simply read empty for the new column until you fill them with rootr_update_row. ' +
|
|
331
|
-
'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 ' +
|
|
332
442
|
'("title" is not allowed: a database already has exactly one title column). ' +
|
|
333
|
-
'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.',
|
|
334
452
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
335
453
|
inputSchema: {
|
|
336
454
|
databaseId: z.string().describe(DATABASE_TARGET_DESC),
|
|
337
455
|
name: z.string().describe('Column header, e.g. "담당자"'),
|
|
338
456
|
type: z
|
|
339
|
-
.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
|
+
])
|
|
340
479
|
.describe('Column type'),
|
|
341
480
|
options: z
|
|
342
|
-
.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
|
+
})
|
|
343
507
|
.optional()
|
|
344
|
-
.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"'),
|
|
345
510
|
},
|
|
346
511
|
},
|
|
347
|
-
async ({ databaseId, name, type, options }) => {
|
|
512
|
+
async ({ databaseId, name, type, options, relationDatabaseId, formula, rollup, prefix }) => {
|
|
348
513
|
try {
|
|
349
514
|
const client = makeClient();
|
|
350
515
|
const id = await resolveNodeId(client, databaseId);
|
|
351
|
-
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
|
+
});
|
|
352
525
|
return jsonResult({ property });
|
|
353
526
|
} catch (err) {
|
|
354
527
|
return errorResult(err);
|
|
@@ -387,6 +560,131 @@ export function registerDatabaseTools(server) {
|
|
|
387
560
|
},
|
|
388
561
|
);
|
|
389
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
|
+
|
|
390
688
|
server.registerTool(
|
|
391
689
|
'rootr_delete_column',
|
|
392
690
|
{
|
|
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
|
|
3
3
|
import { RootrApiError } from '../client.js';
|
|
4
4
|
import { resolveTarget, requireResolved, resolveMoveDestination } from '../resolve.js';
|
|
5
|
-
import { textResult, jsonResult, errorResult, makeClient } from './shared.js';
|
|
5
|
+
import { textResult, jsonResult, errorResult, makeClient, NODE_TARGET_HINT } from './shared.js';
|
|
6
6
|
|
|
7
7
|
/** Registers the original 6 document tools: list/read/append/edit/write/search. */
|
|
8
8
|
export function registerDocumentTools(server) {
|
|
@@ -304,6 +304,24 @@ export function registerDocumentTools(server) {
|
|
|
304
304
|
.union([z.string(), z.null()])
|
|
305
305
|
.optional()
|
|
306
306
|
.describe('ISO date to auto-close the public page, or null to clear an existing expiry'),
|
|
307
|
+
// PUBLIC-LOCK / PUBLIC-ACCESS — 노션 공개 페이지에 없는 것들.
|
|
308
|
+
password: z
|
|
309
|
+
.union([z.string(), z.null()])
|
|
310
|
+
.optional()
|
|
311
|
+
.describe(
|
|
312
|
+
'Lock the page so only readers who know the link AND this password can read it (4+ chars); ' +
|
|
313
|
+
'null or "" removes the lock. Team plan and up. Locking also turns search exposure off, and it ' +
|
|
314
|
+
'cannot be turned back on while the page stays locked.',
|
|
315
|
+
),
|
|
316
|
+
allowClone: z
|
|
317
|
+
.boolean()
|
|
318
|
+
.optional()
|
|
319
|
+
.describe('Whether visitors may copy the page into their own workspace (turn off for quotes/contracts)'),
|
|
320
|
+
allowComments: z.boolean().optional().describe('Whether visitors may comment on the public page'),
|
|
321
|
+
noIndex: z
|
|
322
|
+
.boolean()
|
|
323
|
+
.optional()
|
|
324
|
+
.describe('true hides the page from search engines (Team plan and up)'),
|
|
307
325
|
skin: z
|
|
308
326
|
.enum([
|
|
309
327
|
'classic',
|
|
@@ -336,11 +354,104 @@ export function registerDocumentTools(server) {
|
|
|
336
354
|
.describe('Layout options; only the sub-fields you pass are changed'),
|
|
337
355
|
},
|
|
338
356
|
},
|
|
339
|
-
async ({ target, isPublic, allowCommits, expiresAt, skin, layout }) => {
|
|
357
|
+
async ({ target, isPublic, allowCommits, expiresAt, skin, layout, password, allowClone, allowComments, noIndex }) => {
|
|
358
|
+
try {
|
|
359
|
+
const client = makeClient();
|
|
360
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
361
|
+
// 주지 않은 값은 보내지 않는다 — 서버가 "안 보낸 것은 그대로"로 다루므로,
|
|
362
|
+
// undefined를 그대로 실어 보내면 다른 설정을 건드릴 위험이 있다.
|
|
363
|
+
const body = {};
|
|
364
|
+
for (const [k, v] of Object.entries({
|
|
365
|
+
isPublic,
|
|
366
|
+
allowCommits,
|
|
367
|
+
expiresAt,
|
|
368
|
+
skin,
|
|
369
|
+
layout,
|
|
370
|
+
password,
|
|
371
|
+
allowClone,
|
|
372
|
+
allowComments,
|
|
373
|
+
noIndex,
|
|
374
|
+
})) {
|
|
375
|
+
if (v !== undefined) body[k] = v;
|
|
376
|
+
}
|
|
377
|
+
return jsonResult(await client.setDocumentPublic(id, body));
|
|
378
|
+
} catch (err) {
|
|
379
|
+
return errorResult(err);
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
// PUBLIC-DOCS — 밖에서 온 변경 제안을 에이전트가 보고 처리할 수 있게. 노션에 없다.
|
|
385
|
+
server.registerTool(
|
|
386
|
+
'rootr_list_document_commits',
|
|
387
|
+
{
|
|
388
|
+
title: 'List change proposals on a Rootr document',
|
|
389
|
+
description:
|
|
390
|
+
'List the git-style change proposals people sent to a published Rootr (루터) document. Shows who proposed ' +
|
|
391
|
+
'what, whether the document moved on since (`stale`) and how many places clash (`conflicts`).',
|
|
392
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
393
|
+
inputSchema: {
|
|
394
|
+
target: z.string().describe('Document path (starting with "/") or node id, or a full rootr.io URL'),
|
|
395
|
+
status: z.enum(['OPEN', 'MERGED', 'REJECTED']).optional().describe('Defaults to OPEN'),
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
async ({ target, status }) => {
|
|
399
|
+
try {
|
|
400
|
+
const client = makeClient();
|
|
401
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
402
|
+
return jsonResult(await client.listDocumentCommits(id, status ?? 'OPEN'));
|
|
403
|
+
} catch (err) {
|
|
404
|
+
return errorResult(err);
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
server.registerTool(
|
|
410
|
+
'rootr_merge_document_commit',
|
|
411
|
+
{
|
|
412
|
+
title: 'Merge a change proposal',
|
|
413
|
+
description:
|
|
414
|
+
'Apply one change proposal to the document. It is a three-way MERGE, not an overwrite: only the proposer\'s ' +
|
|
415
|
+
'edits land on top of the current text, so anything the owner changed meanwhile survives. If both sides ' +
|
|
416
|
+
'touched the same lines the call is refused with the conflict count — read it, then call again with ' +
|
|
417
|
+
'force: true to take the proposal for those lines. Owner-level action (MANAGE).',
|
|
418
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
419
|
+
inputSchema: {
|
|
420
|
+
target: z.string().describe('Document path (starting with "/") or node id, or a full rootr.io URL'),
|
|
421
|
+
commitId: z.string().describe('Proposal id from rootr_list_document_commits'),
|
|
422
|
+
force: z.boolean().optional().describe('Merge even though the same lines clash (proposal wins there)'),
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
async ({ target, commitId, force }) => {
|
|
340
426
|
try {
|
|
341
427
|
const client = makeClient();
|
|
342
428
|
const id = requireResolved(await resolveTarget(client, target));
|
|
343
|
-
return jsonResult(await client.
|
|
429
|
+
return jsonResult(await client.mergeDocumentCommit(id, commitId, force === true));
|
|
430
|
+
} catch (err) {
|
|
431
|
+
return errorResult(err);
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
server.registerTool(
|
|
437
|
+
'rootr_reject_document_commit',
|
|
438
|
+
{
|
|
439
|
+
title: 'Reject a change proposal',
|
|
440
|
+
description:
|
|
441
|
+
'Turn down one change proposal. The reason (if given) is delivered to whoever proposed it — a rejection ' +
|
|
442
|
+
'with no explanation reads as being ignored. Owner-level action (MANAGE).',
|
|
443
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
444
|
+
inputSchema: {
|
|
445
|
+
target: z.string().describe('Document path (starting with "/") or node id, or a full rootr.io URL'),
|
|
446
|
+
commitId: z.string().describe('Proposal id from rootr_list_document_commits'),
|
|
447
|
+
reason: z.string().optional().describe('Sent to the proposer'),
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
async ({ target, commitId, reason }) => {
|
|
451
|
+
try {
|
|
452
|
+
const client = makeClient();
|
|
453
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
454
|
+
return jsonResult(await client.rejectDocumentCommit(id, commitId, reason));
|
|
344
455
|
} catch (err) {
|
|
345
456
|
return errorResult(err);
|
|
346
457
|
}
|
|
@@ -421,4 +532,120 @@ export function registerDocumentTools(server) {
|
|
|
421
532
|
}
|
|
422
533
|
},
|
|
423
534
|
);
|
|
535
|
+
|
|
536
|
+
// ── 공개범위·권한 (VIS-001) ─────────────────────────────────────────
|
|
537
|
+
// 화면에만 있던 "공개범위"/"권한" 창을 에이전트에게도 연다. 이게 없으면
|
|
538
|
+
// 에이전트가 문서는 만들면서 "서로 못 보게"는 사람이 따로 해야 한다.
|
|
539
|
+
|
|
540
|
+
server.registerTool(
|
|
541
|
+
'rootr_read_node_access',
|
|
542
|
+
{
|
|
543
|
+
title: "Read a node's visibility and who can access it",
|
|
544
|
+
description:
|
|
545
|
+
"Read a Rootr (루터) node's access settings: its explicit `visibility` (PUBLIC/PRIVATE/null=inherit), the " +
|
|
546
|
+
'effective value it resolves to, where an inherited value comes from, plus the permission overrides set ' +
|
|
547
|
+
'directly on it and on its ancestor folders. Requires MANAGE on the node — a caller who cannot change ' +
|
|
548
|
+
'access cannot read this either. Call before rootr_set_node_visibility / rootr_grant_node_permission.',
|
|
549
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
550
|
+
inputSchema: {
|
|
551
|
+
target: z.string().describe(`Node path (starting with "/") or node id${NODE_TARGET_HINT}`),
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
async ({ target }) => {
|
|
555
|
+
try {
|
|
556
|
+
const client = makeClient();
|
|
557
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
558
|
+
return jsonResult(await client.getNodeAccess(id));
|
|
559
|
+
} catch (err) {
|
|
560
|
+
return errorResult(err);
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
server.registerTool(
|
|
566
|
+
'rootr_set_node_visibility',
|
|
567
|
+
{
|
|
568
|
+
title: "Set a node's visibility (private / inherit)",
|
|
569
|
+
description:
|
|
570
|
+
"Set a Rootr (루터) node's explicit visibility. PRIVATE hides the node and everything under it from " +
|
|
571
|
+
"workspace members' role-default access — only the creator, workspace OWNER/ADMIN, and people granted an " +
|
|
572
|
+
'explicit permission keep it. PUBLIC opens it to the workspace. Pass `visibility: null` to clear the ' +
|
|
573
|
+
'explicit value and inherit from the nearest ancestor folder / workspace default. Existing permission ' +
|
|
574
|
+
'overrides survive, so you cannot lock yourself out. This is the API behind the "공개범위" menu, and it is ' +
|
|
575
|
+
'NOT the same as publishing to the web (that is rootr_set_document_public).',
|
|
576
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
577
|
+
inputSchema: {
|
|
578
|
+
target: z.string().describe(`Node path (starting with "/") or node id${NODE_TARGET_HINT}`),
|
|
579
|
+
visibility: z
|
|
580
|
+
.enum(['PUBLIC', 'PRIVATE'])
|
|
581
|
+
.nullable()
|
|
582
|
+
.describe('PRIVATE = restrict to the creator/admins/explicit grants, PUBLIC = open to the workspace, null = inherit'),
|
|
583
|
+
},
|
|
584
|
+
},
|
|
585
|
+
async ({ target, visibility }) => {
|
|
586
|
+
try {
|
|
587
|
+
const client = makeClient();
|
|
588
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
589
|
+
return jsonResult(await client.setNodeVisibility(id, visibility ?? null));
|
|
590
|
+
} catch (err) {
|
|
591
|
+
return errorResult(err);
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
server.registerTool(
|
|
597
|
+
'rootr_grant_node_permission',
|
|
598
|
+
{
|
|
599
|
+
title: 'Give one person or group access to a node',
|
|
600
|
+
description:
|
|
601
|
+
'Grant a workspace member (or group) an explicit permission on a Rootr (루터) node and everything under ' +
|
|
602
|
+
'it: READ (open it), WRITE (edit it), or MANAGE (also change its sharing/permissions). Use this together ' +
|
|
603
|
+
'with rootr_set_node_visibility to build "private folder, these three people" — set the folder PRIVATE, ' +
|
|
604
|
+
'then grant each person. Re-granting the same subject just changes their level. The subject must already ' +
|
|
605
|
+
'be a member of this workspace.',
|
|
606
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
607
|
+
inputSchema: {
|
|
608
|
+
target: z.string().describe(`Node path (starting with "/") or node id${NODE_TARGET_HINT}`),
|
|
609
|
+
subjectId: z.string().describe('User id (or group id when subjectType is GROUP)'),
|
|
610
|
+
level: z.enum(['READ', 'WRITE', 'MANAGE']).describe('Access level to grant'),
|
|
611
|
+
subjectType: z.enum(['USER', 'GROUP']).optional().describe('Defaults to USER'),
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
async ({ target, subjectId, level, subjectType }) => {
|
|
615
|
+
try {
|
|
616
|
+
const client = makeClient();
|
|
617
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
618
|
+
return jsonResult(await client.grantNodePermission(id, { subjectType, subjectId, level }));
|
|
619
|
+
} catch (err) {
|
|
620
|
+
return errorResult(err);
|
|
621
|
+
}
|
|
622
|
+
},
|
|
623
|
+
);
|
|
624
|
+
|
|
625
|
+
server.registerTool(
|
|
626
|
+
'rootr_revoke_node_permission',
|
|
627
|
+
{
|
|
628
|
+
title: 'Take away one person or group\'s access to a node',
|
|
629
|
+
description:
|
|
630
|
+
'Remove an explicit permission override from a Rootr (루터) node. The subject falls back to whatever they ' +
|
|
631
|
+
'would get anyway — which on a PRIVATE node is no access, and on an inherited/PUBLIC node is their normal ' +
|
|
632
|
+
'workspace role. Call rootr_read_node_access first to see which overrides exist.',
|
|
633
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
634
|
+
inputSchema: {
|
|
635
|
+
target: z.string().describe(`Node path (starting with "/") or node id${NODE_TARGET_HINT}`),
|
|
636
|
+
subjectId: z.string().describe('User id (or group id when subjectType is GROUP)'),
|
|
637
|
+
subjectType: z.enum(['USER', 'GROUP']).optional().describe('Defaults to USER'),
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
async ({ target, subjectId, subjectType }) => {
|
|
641
|
+
try {
|
|
642
|
+
const client = makeClient();
|
|
643
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
644
|
+
await client.revokeNodePermission(id, { subjectType, subjectId });
|
|
645
|
+
return jsonResult({ revoked: true, nodeId: id, subjectId });
|
|
646
|
+
} catch (err) {
|
|
647
|
+
return errorResult(err);
|
|
648
|
+
}
|
|
649
|
+
},
|
|
650
|
+
);
|
|
424
651
|
}
|