rootr-cli 0.14.1 → 0.17.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 +78 -5
- package/bin/rootr.js +261 -11
- package/lib/client.js +74 -7
- package/lib/credentials.js +30 -14
- package/lib/mcp-tools/crm.js +4 -2
- package/lib/mcp-tools/databases.js +146 -3
- package/lib/mcp-tools/documents.js +27 -8
- package/lib/mcp-tools/forms.js +6 -3
- package/lib/mcp-tools/issues.js +3 -2
- package/lib/mcp-tools/logs.js +3 -2
- package/lib/mcp-tools/misc.js +43 -6
- package/lib/mcp-tools/pages.js +243 -0
- package/lib/mcp-tools/presentations.js +4 -2
- package/lib/mcp-tools/recordings.js +5 -2
- package/lib/mcp-tools/shared.js +9 -0
- package/lib/mcp-tools/signed.js +4 -1
- package/lib/mcp-tools/spreadsheets.js +8 -4
- package/lib/mcp-tools/templates.js +2 -1
- package/lib/mcp-tools/whiteboards.js +6 -3
- package/lib/mcp-tools/workspaces.js +8 -7
- package/lib/mcp.js +2 -0
- package/package.json +1 -1
package/lib/credentials.js
CHANGED
|
@@ -27,16 +27,22 @@ export function candidateClients(cfg = loadConfig()) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* Collect every workspace the registered keys can see, and
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
30
|
+
* Collect every workspace the registered keys can see, and which of them a
|
|
31
|
+
* key can actually open — listing is not a capability probe: a workspace key
|
|
32
|
+
* lists all of its owner's workspaces while being rejected by every one but
|
|
33
|
+
* its own. `GET /workspaces` now tags each entry with `accessible` (computed
|
|
34
|
+
* server-side from the calling key's own scope), so this reads straight off
|
|
35
|
+
* that field instead of firing one extra request per workspace. Older
|
|
36
|
+
* servers that predate the field fall back to the old per-workspace probe
|
|
37
|
+
* when `probe` is requested.
|
|
34
38
|
*/
|
|
35
39
|
export async function surveyWorkspaces(candidates, { probe = false, probeLimit = 12 } = {}) {
|
|
36
40
|
const seen = new Map();
|
|
37
41
|
const reachable = new Set();
|
|
38
42
|
let listed = 0;
|
|
39
43
|
let failures = 0;
|
|
44
|
+
let sawAccessibleField = false;
|
|
45
|
+
const lists = [];
|
|
40
46
|
for (const { client } of candidates) {
|
|
41
47
|
let list = [];
|
|
42
48
|
try {
|
|
@@ -46,19 +52,29 @@ export async function surveyWorkspaces(candidates, { probe = false, probeLimit =
|
|
|
46
52
|
failures += 1;
|
|
47
53
|
continue; // a revoked/foreign key shouldn't hide the others
|
|
48
54
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
lists.push({ client, list });
|
|
56
|
+
for (const w of list) {
|
|
57
|
+
if (!seen.has(w.id)) seen.set(w.id, w);
|
|
58
|
+
if (typeof w.accessible === 'boolean') {
|
|
59
|
+
sawAccessibleField = true;
|
|
60
|
+
if (w.accessible) reachable.add(w.id);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (!sawAccessibleField && probe) {
|
|
65
|
+
for (const { client, list } of lists) {
|
|
66
|
+
for (const w of list.slice(0, probeLimit)) {
|
|
67
|
+
if (reachable.has(w.id)) continue;
|
|
68
|
+
try {
|
|
69
|
+
await client.request(`/workspaces/${encodeURIComponent(w.id)}`);
|
|
70
|
+
reachable.add(w.id);
|
|
71
|
+
} catch {
|
|
72
|
+
// scoped key rejected here; another key may still cover it
|
|
73
|
+
}
|
|
58
74
|
}
|
|
59
75
|
}
|
|
60
76
|
}
|
|
61
|
-
return { workspaces: [...seen.values()], reachable, listed, failures };
|
|
77
|
+
return { workspaces: [...seen.values()], reachable, listed, failures, probed: sawAccessibleField || probe };
|
|
62
78
|
}
|
|
63
79
|
|
|
64
80
|
/**
|
package/lib/mcp-tools/crm.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
-
import { jsonResult, errorResult, makeClient, requireWorkspace, resolveNodeId, NODE_TARGET_HINT, resolveOptionalNodeId } from './shared.js';
|
|
3
|
+
import { jsonResult, errorResult, makeClient, requireWorkspace, resolveNodeId, NODE_TARGET_HINT, NODE_URL_HINT, resolveOptionalNodeId } from './shared.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Shared context repeated (shortened) across tool descriptions so an agent
|
|
@@ -56,7 +56,9 @@ export function registerCrmTools(server) {
|
|
|
56
56
|
'activities and tasks all live inside it. Optionally seed custom `pipelines` (name + ordered stages, each ' +
|
|
57
57
|
'with an optional win probability/color) and/or custom company/contact/deal `fields`; sane defaults are ' +
|
|
58
58
|
'used if omitted. ' +
|
|
59
|
-
CRM_CONTEXT
|
|
59
|
+
CRM_CONTEXT +
|
|
60
|
+
' ' +
|
|
61
|
+
NODE_URL_HINT,
|
|
60
62
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
61
63
|
inputSchema: {
|
|
62
64
|
name: z.string().optional().describe('CRM node name'),
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
resolveNodeId,
|
|
9
9
|
resolveOptionalNodeId,
|
|
10
10
|
NODE_TARGET_HINT,
|
|
11
|
+
NODE_URL_HINT,
|
|
11
12
|
} from './shared.js';
|
|
12
13
|
|
|
13
14
|
/** Human text for the `databaseId` param — accepts a path just like the document tools. */
|
|
@@ -135,7 +136,12 @@ export function registerDatabaseTools(server) {
|
|
|
135
136
|
'Views default to a single table view; view types are table | board | gallery | calendar | timeline | list — ' +
|
|
136
137
|
'a "board" view needs config.groupBy (a select/status property id), a "timeline"/"calendar" view needs ' +
|
|
137
138
|
'config.dateProp. Row `values` are keyed by column NAME (or id); date values are ' +
|
|
138
|
-
'{ start: "YYYY-MM-DDTHH:mm" or "YYYY-MM-DD", end? }, multi_select is string[], relation is an array of row ids.'
|
|
139
|
+
'{ start: "YYYY-MM-DDTHH:mm" or "YYYY-MM-DD", end? }, multi_select is string[], relation is an array of row ids. ' +
|
|
140
|
+
'COST NOTE: by default, every row added to this table (now or later) queues that table for graph extraction ' +
|
|
141
|
+
'(embeddings + LLM) — fine for a handful of rows, expensive for a bulk-loaded roster/log table. Pass ' +
|
|
142
|
+
'graphSync:false for tables you are about to bulk-load; flip it back with ' +
|
|
143
|
+
'`rootr_update_node({ graphSyncOff: true|false })` any time after. ' +
|
|
144
|
+
NODE_URL_HINT,
|
|
139
145
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
140
146
|
inputSchema: {
|
|
141
147
|
name: z.string().describe('Table name, e.g. "업무 목록"'),
|
|
@@ -215,10 +221,18 @@ export function registerDatabaseTools(server) {
|
|
|
215
221
|
'calculation sheet, "FORM" a per-row intake. Default "DOCUMENT". This is only the default — a single ' +
|
|
216
222
|
'row can be made something else with rootr_create_row_page.',
|
|
217
223
|
),
|
|
224
|
+
graphSync: z
|
|
225
|
+
.boolean()
|
|
226
|
+
.optional()
|
|
227
|
+
.describe(
|
|
228
|
+
'Default true. Set to false for tables you will bulk-load (rosters, logs, imports) — otherwise ' +
|
|
229
|
+
'every row added queues this table for graph extraction (embeddings + LLM), one job per row. ' +
|
|
230
|
+
'Flip it later with rootr_update_node({ graphSyncOff: true|false }).',
|
|
231
|
+
),
|
|
218
232
|
workspace: z.string().optional().describe('Workspace id; defaults to ROOTR_WORKSPACE/config if omitted'),
|
|
219
233
|
},
|
|
220
234
|
},
|
|
221
|
-
async ({ name, parentId, path, createParents, icon, properties, views, rows, rowPageType, workspace }) => {
|
|
235
|
+
async ({ name, parentId, path, createParents, icon, properties, views, rows, rowPageType, graphSync, workspace }) => {
|
|
222
236
|
try {
|
|
223
237
|
const ws = requireWorkspace(workspace);
|
|
224
238
|
const client = makeClient(ws);
|
|
@@ -233,6 +247,7 @@ export function registerDatabaseTools(server) {
|
|
|
233
247
|
views,
|
|
234
248
|
rows,
|
|
235
249
|
rowPageType,
|
|
250
|
+
graphSync,
|
|
236
251
|
});
|
|
237
252
|
return jsonResult(database);
|
|
238
253
|
} catch (err) {
|
|
@@ -271,9 +286,17 @@ export function registerDatabaseTools(server) {
|
|
|
271
286
|
'Column widths in px for this view, keyed by column NAME or id — e.g. {"공고명": 420}. ' +
|
|
272
287
|
'40~1200. Use it when a column holds long text and is being cut off. {} resets every column to auto.',
|
|
273
288
|
),
|
|
289
|
+
calendarScale: z
|
|
290
|
+
.enum(['week', 'month', 'year'])
|
|
291
|
+
.optional()
|
|
292
|
+
.describe(
|
|
293
|
+
'calendar view only: which span the calendar opens on — week / month / year. ' +
|
|
294
|
+
'Default month. The year view shows twelve mini-months at once (darker = more rows that day). ' +
|
|
295
|
+
'Saved on the view, so everyone who opens it sees that span.',
|
|
296
|
+
),
|
|
274
297
|
},
|
|
275
298
|
},
|
|
276
|
-
async ({ databaseId, name, type, groupBy, dateProp, endDateProp, filters, sort, colWidths }) => {
|
|
299
|
+
async ({ databaseId, name, type, groupBy, dateProp, endDateProp, filters, sort, colWidths, calendarScale }) => {
|
|
277
300
|
try {
|
|
278
301
|
const client = makeClient();
|
|
279
302
|
const id = await resolveNodeId(client, databaseId);
|
|
@@ -286,6 +309,7 @@ export function registerDatabaseTools(server) {
|
|
|
286
309
|
filters,
|
|
287
310
|
sort,
|
|
288
311
|
colWidths,
|
|
312
|
+
calendarScale,
|
|
289
313
|
});
|
|
290
314
|
return jsonResult(view);
|
|
291
315
|
} catch (err) {
|
|
@@ -350,6 +374,14 @@ export function registerDatabaseTools(server) {
|
|
|
350
374
|
'Column widths in px for this view, keyed by column NAME or id — e.g. {"공고명": 420}. ' +
|
|
351
375
|
'40~1200. Use it when a column holds long text and is being cut off. {} resets every column to auto.',
|
|
352
376
|
),
|
|
377
|
+
calendarScale: z
|
|
378
|
+
.enum(['week', 'month', 'year'])
|
|
379
|
+
.optional()
|
|
380
|
+
.describe(
|
|
381
|
+
'calendar view only: which span the calendar opens on — week / month / year. ' +
|
|
382
|
+
'Default month. The year view shows twelve mini-months at once (darker = more rows that day). ' +
|
|
383
|
+
'Saved on the view, so everyone who opens it sees that span.',
|
|
384
|
+
),
|
|
353
385
|
},
|
|
354
386
|
},
|
|
355
387
|
async ({
|
|
@@ -363,6 +395,7 @@ export function registerDatabaseTools(server) {
|
|
|
363
395
|
filters,
|
|
364
396
|
sort,
|
|
365
397
|
colWidths,
|
|
398
|
+
calendarScale,
|
|
366
399
|
}) => {
|
|
367
400
|
try {
|
|
368
401
|
const client = makeClient();
|
|
@@ -376,6 +409,7 @@ export function registerDatabaseTools(server) {
|
|
|
376
409
|
filters,
|
|
377
410
|
sort,
|
|
378
411
|
colWidths,
|
|
412
|
+
calendarScale,
|
|
379
413
|
});
|
|
380
414
|
return jsonResult({ view });
|
|
381
415
|
} catch (err) {
|
|
@@ -1048,4 +1082,113 @@ export function registerDatabaseTools(server) {
|
|
|
1048
1082
|
}
|
|
1049
1083
|
},
|
|
1050
1084
|
);
|
|
1085
|
+
|
|
1086
|
+
// ── DB-BUTTON — 표 위에 두는 "누르면 일하는 버튼" ──────────────────────────
|
|
1087
|
+
//
|
|
1088
|
+
// 사람은 화면에서 버튼을 만들 수 있는데 에이전트는 못 만들면, "이 표에 접수 버튼
|
|
1089
|
+
// 하나 달아 줘"에서 멈춘다. 만들 수 있으면 눌러 줄 수도 있어야 한다.
|
|
1090
|
+
|
|
1091
|
+
server.registerTool(
|
|
1092
|
+
'rootr_list_database_buttons',
|
|
1093
|
+
{
|
|
1094
|
+
title: 'List the action buttons on a database',
|
|
1095
|
+
description:
|
|
1096
|
+
'List the action buttons pinned to the top of a Rootr (루터) DATABASE. Each button is a named, ' +
|
|
1097
|
+
'ordered list of steps (add a row / set values / let AI fill a cell / open the new row) that runs ' +
|
|
1098
|
+
'when someone presses it — the repeated setup a table owner would otherwise do by hand every time. ' +
|
|
1099
|
+
'Returns `myLevel` too: editing the buttons needs MANAGE, pressing one only needs WRITE.',
|
|
1100
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
1101
|
+
inputSchema: {
|
|
1102
|
+
databaseId: z.string().describe(DATABASE_TARGET_DESC),
|
|
1103
|
+
},
|
|
1104
|
+
},
|
|
1105
|
+
async ({ databaseId }) => {
|
|
1106
|
+
try {
|
|
1107
|
+
const client = makeClient();
|
|
1108
|
+
const id = await resolveNodeId(client, databaseId);
|
|
1109
|
+
return jsonResult(await client.listDatabaseButtons(id));
|
|
1110
|
+
} catch (err) {
|
|
1111
|
+
return errorResult(err);
|
|
1112
|
+
}
|
|
1113
|
+
},
|
|
1114
|
+
);
|
|
1115
|
+
|
|
1116
|
+
server.registerTool(
|
|
1117
|
+
'rootr_set_database_buttons',
|
|
1118
|
+
{
|
|
1119
|
+
title: 'Create or edit the action buttons on a database',
|
|
1120
|
+
description:
|
|
1121
|
+
'Replace the WHOLE button list of a Rootr (루터) DATABASE (needs MANAGE). This is a full replace, not a ' +
|
|
1122
|
+
'merge — call rootr_list_database_buttons first and send back the buttons you want to KEEP, or they are ' +
|
|
1123
|
+
'dropped. Up to 12 buttons, 8 steps each.\n' +
|
|
1124
|
+
'Steps run in order and the row a step created is what the NEXT step works on, which is how ' +
|
|
1125
|
+
'"add a row → have AI write its summary → open it" becomes one press:\n' +
|
|
1126
|
+
'· {kind:"add_row", values?} — make a row, optionally pre-filled. Column keys may be NAMES or ids.\n' +
|
|
1127
|
+
'· {kind:"update_rows", target:"created"|"selected", values} — set values on the rows this button just ' +
|
|
1128
|
+
'made, or on the rows the person ticked in the table.\n' +
|
|
1129
|
+
'· {kind:"ai_fill", target, propId, prompt} — let AI write ONE cell. The prompt may quote the row with ' +
|
|
1130
|
+
'{칸이름}, e.g. "{제목}과 {메모}를 한 문장으로 요약해". If the AI fails the button keeps going.\n' +
|
|
1131
|
+
'· {kind:"open_row"} — open the row this button made (UI only; over the API it just reports the id).\n' +
|
|
1132
|
+
'Values accept @today, @today+7 and @me — the server fills them in at press time, not now.\n' +
|
|
1133
|
+
'A button never grants permission: it runs as whoever presses it.',
|
|
1134
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
1135
|
+
inputSchema: {
|
|
1136
|
+
databaseId: z.string().describe(DATABASE_TARGET_DESC),
|
|
1137
|
+
buttons: z
|
|
1138
|
+
.array(
|
|
1139
|
+
z.object({
|
|
1140
|
+
id: z.string().optional().describe('Keep an existing button by passing its id; omit to create one'),
|
|
1141
|
+
label: z.string().describe('Button text, e.g. "접수"'),
|
|
1142
|
+
icon: z.string().optional().describe('One emoji shown on the button'),
|
|
1143
|
+
steps: z
|
|
1144
|
+
.array(z.record(z.any()))
|
|
1145
|
+
.describe('Ordered steps — see the tool description for each kind (max 8)'),
|
|
1146
|
+
}),
|
|
1147
|
+
)
|
|
1148
|
+
.describe('The COMPLETE button list for this table. [] removes every button.'),
|
|
1149
|
+
},
|
|
1150
|
+
},
|
|
1151
|
+
async ({ databaseId, buttons }) => {
|
|
1152
|
+
try {
|
|
1153
|
+
const client = makeClient();
|
|
1154
|
+
const id = await resolveNodeId(client, databaseId);
|
|
1155
|
+
return jsonResult(await client.setDatabaseButtons(id, buttons));
|
|
1156
|
+
} catch (err) {
|
|
1157
|
+
return errorResult(err);
|
|
1158
|
+
}
|
|
1159
|
+
},
|
|
1160
|
+
);
|
|
1161
|
+
|
|
1162
|
+
server.registerTool(
|
|
1163
|
+
'rootr_run_database_button',
|
|
1164
|
+
{
|
|
1165
|
+
title: 'Press an action button on a database',
|
|
1166
|
+
description:
|
|
1167
|
+
'Press one button on a Rootr (루터) DATABASE and run its steps in order (needs WRITE). ' +
|
|
1168
|
+
'Pass `rowIds` only when a step targets "selected" — that is the set of rows the person would have ' +
|
|
1169
|
+
'ticked in the table; without it such a step reaches no rows and the server says so. ' +
|
|
1170
|
+
'Returns `created` (the ARRAY of row ids it made, not a count), `updated` and `filled` counts, the ' +
|
|
1171
|
+
'`openRowId` a final "open_row" step points at, and — if the AI could not write a cell — `aiFailed` ' +
|
|
1172
|
+
'with the reason (the rest of the button still ran). ' +
|
|
1173
|
+
'It runs with YOUR permissions: a button that writes a locked column stops there if you cannot edit it.',
|
|
1174
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
1175
|
+
inputSchema: {
|
|
1176
|
+
databaseId: z.string().describe(DATABASE_TARGET_DESC),
|
|
1177
|
+
buttonId: z.string().describe('Button id from rootr_list_database_buttons'),
|
|
1178
|
+
rowIds: z
|
|
1179
|
+
.array(z.string())
|
|
1180
|
+
.optional()
|
|
1181
|
+
.describe('Rows for steps whose target is "selected". Omit for buttons that only add rows.'),
|
|
1182
|
+
},
|
|
1183
|
+
},
|
|
1184
|
+
async ({ databaseId, buttonId, rowIds }) => {
|
|
1185
|
+
try {
|
|
1186
|
+
const client = makeClient();
|
|
1187
|
+
const id = await resolveNodeId(client, databaseId);
|
|
1188
|
+
return jsonResult(await client.runDatabaseButton(id, buttonId, { rowIds }));
|
|
1189
|
+
} catch (err) {
|
|
1190
|
+
return errorResult(err);
|
|
1191
|
+
}
|
|
1192
|
+
},
|
|
1193
|
+
);
|
|
1051
1194
|
}
|
|
@@ -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, NODE_TARGET_HINT } from './shared.js';
|
|
5
|
+
import { textResult, jsonResult, errorResult, makeClient, NODE_TARGET_HINT, NODE_URL_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) {
|
|
@@ -147,7 +147,8 @@ export function registerDocumentTools(server) {
|
|
|
147
147
|
description:
|
|
148
148
|
'Replace the ENTIRE content of a Rootr document, or create a new document at a path if it does not exist. ' +
|
|
149
149
|
'This is DESTRUCTIVE — it discards whatever is currently there. Read the document first and pass its etag ' +
|
|
150
|
-
'as ifMatch to avoid clobbering concurrent changes. Prefer rootr_append or rootr_edit for incremental changes.'
|
|
150
|
+
'as ifMatch to avoid clobbering concurrent changes. Prefer rootr_append or rootr_edit for incremental changes. ' +
|
|
151
|
+
`When this creates a new document, the result text includes its address (same idea as: ${NODE_URL_HINT}).`,
|
|
151
152
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
152
153
|
inputSchema: {
|
|
153
154
|
target: z.string().describe('Document path (starting with "/", created if missing) or node id, or a full rootr.io URL'),
|
|
@@ -174,7 +175,8 @@ export function registerDocumentTools(server) {
|
|
|
174
175
|
content,
|
|
175
176
|
});
|
|
176
177
|
const id = created.id || (created.node && created.node.id);
|
|
177
|
-
|
|
178
|
+
const url = created.url || (created.node && created.node.url);
|
|
179
|
+
return textResult(`created ${resolved.path} (id ${id})${url ? ` — ${url}` : ''}`);
|
|
178
180
|
}
|
|
179
181
|
const { etag } = await client.putDocument(resolved.id, content, { ifMatch });
|
|
180
182
|
return textResult(`wrote (etag ${etag || 'n/a'})`);
|
|
@@ -194,11 +196,17 @@ export function registerDocumentTools(server) {
|
|
|
194
196
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
195
197
|
inputSchema: {
|
|
196
198
|
query: z.string().describe('Search query text'),
|
|
199
|
+
// rootr_list와 같은 이유로 필요하다: 이것만 안 받으면 다중 워크스페이스
|
|
200
|
+
// 연결에서 정체 모를 오류가 난다(#99).
|
|
201
|
+
workspace: z
|
|
202
|
+
.string()
|
|
203
|
+
.optional()
|
|
204
|
+
.describe('Workspace id; defaults to ROOTR_WORKSPACE/config'),
|
|
197
205
|
},
|
|
198
206
|
},
|
|
199
|
-
async ({ query }) => {
|
|
207
|
+
async ({ query, workspace }) => {
|
|
200
208
|
try {
|
|
201
|
-
const client = makeClient();
|
|
209
|
+
const client = makeClient(workspace);
|
|
202
210
|
const results = await client.search(query);
|
|
203
211
|
const rows = results.map((r) => {
|
|
204
212
|
const label = r.path || r.name || r.nodeId;
|
|
@@ -218,13 +226,19 @@ export function registerDocumentTools(server) {
|
|
|
218
226
|
title: 'Comment on a Rootr document',
|
|
219
227
|
description:
|
|
220
228
|
'Add a comment to a Rootr (루터) document (optionally anchored to a block, or as a reply via parentId). ' +
|
|
229
|
+
'DB-ROW-COMMENT — to comment on a **row of a table**, pass the DATABASE as `target` and the ROW id as `blockId`; that is exactly what the row screen does, and the row will show it above its body. ' +
|
|
230
|
+
'Mentioning someone with @[userId] notifies them, and their notification opens that row and ' +
|
|
231
|
+
'highlights this comment. ' +
|
|
221
232
|
'Comments carry a user identity: the comment is attributed to the person who owns the key ' +
|
|
222
233
|
'(account keys and workspace keys both carry an owner). A key with no owner cannot comment.',
|
|
223
234
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
224
235
|
inputSchema: {
|
|
225
236
|
target: z.string().describe('Document path (starting with "/") or node id, or a full rootr.io URL'),
|
|
226
237
|
body: z.string().describe('Comment text (markdown)'),
|
|
227
|
-
blockId: z
|
|
238
|
+
blockId: z
|
|
239
|
+
.string()
|
|
240
|
+
.optional()
|
|
241
|
+
.describe('Block id to anchor the comment to — or a DATABASE ROW id to comment on that row'),
|
|
228
242
|
parentId: z.string().optional().describe('Parent comment id, to reply in a thread'),
|
|
229
243
|
},
|
|
230
244
|
},
|
|
@@ -244,11 +258,16 @@ export function registerDocumentTools(server) {
|
|
|
244
258
|
{
|
|
245
259
|
title: 'List comments on a Rootr document',
|
|
246
260
|
description:
|
|
247
|
-
'List the comment threads on a Rootr (루터) document, optionally filtered to one block.
|
|
261
|
+
'List the comment threads on a Rootr (루터) document, optionally filtered to one block. ' +
|
|
262
|
+
'DB-ROW-COMMENT — for the comments on ONE ROW of a table, pass the DATABASE as `target` and the ' +
|
|
263
|
+
'ROW id as `blockId`. Works with any key.',
|
|
248
264
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
249
265
|
inputSchema: {
|
|
250
266
|
target: z.string().describe('Document path (starting with "/") or node id, or a full rootr.io URL'),
|
|
251
|
-
blockId: z
|
|
267
|
+
blockId: z
|
|
268
|
+
.string()
|
|
269
|
+
.optional()
|
|
270
|
+
.describe('Only comments anchored to this block — or a DATABASE ROW id for that row\'s comments'),
|
|
252
271
|
},
|
|
253
272
|
},
|
|
254
273
|
async ({ target, blockId }) => {
|
package/lib/mcp-tools/forms.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
-
import { jsonResult, errorResult, makeClient, requireWorkspace, resolveNodeId, NODE_TARGET_HINT, resolveOptionalNodeId } from './shared.js';
|
|
3
|
+
import { jsonResult, errorResult, makeClient, requireWorkspace, resolveNodeId, NODE_TARGET_HINT, NODE_URL_HINT, resolveOptionalNodeId } from './shared.js';
|
|
4
4
|
|
|
5
5
|
const fieldShape = z
|
|
6
6
|
.object({
|
|
@@ -88,7 +88,9 @@ export function registerFormTools(server) {
|
|
|
88
88
|
'Create a new FORM node (fillable intake) in a Rootr (루터) workspace, optionally seeded with field ' +
|
|
89
89
|
'definitions. Use FORM for surveys/request queues collected from people outside the tree — set ' +
|
|
90
90
|
'`targetDatabaseId` to also mirror each response into a DATABASE\'s rows. ' +
|
|
91
|
-
FIELDS_GUIDE
|
|
91
|
+
FIELDS_GUIDE +
|
|
92
|
+
' ' +
|
|
93
|
+
NODE_URL_HINT,
|
|
92
94
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
93
95
|
inputSchema: {
|
|
94
96
|
name: z.string().optional().describe('Form name/title'),
|
|
@@ -148,7 +150,8 @@ export function registerFormTools(server) {
|
|
|
148
150
|
'rootr_read_form first and resend fields you want to keep (with their existing ids) so past responses ' +
|
|
149
151
|
'stay attributable. Pass `targetDatabaseId: null` to unlink the mirrored DATABASE. ' +
|
|
150
152
|
FIELDS_GUIDE,
|
|
151
|
-
|
|
153
|
+
// `fields`를 주면 필드 전체를 갈아끼운다 — 기존 id 를 안 보내면 응답 귀속이 끊긴다.
|
|
154
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
152
155
|
inputSchema: {
|
|
153
156
|
formId: z.string().describe(`FORM node id${NODE_TARGET_HINT}`),
|
|
154
157
|
fields: z.array(fieldShape).optional().describe('COMPLETE fields array to set — omitted existing fields will be dropped'),
|
package/lib/mcp-tools/issues.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
-
import { textResult, jsonResult, errorResult, makeClient, requireWorkspace, resolveNodeId, NODE_TARGET_HINT } from './shared.js';
|
|
3
|
+
import { textResult, jsonResult, errorResult, makeClient, requireWorkspace, resolveNodeId, NODE_TARGET_HINT, NODE_URL_HINT } from './shared.js';
|
|
4
4
|
|
|
5
5
|
/** Issue tracker tools: create tracker, list/create/get/update issues, comment. */
|
|
6
6
|
export function registerIssueTools(server) {
|
|
@@ -10,7 +10,8 @@ export function registerIssueTools(server) {
|
|
|
10
10
|
title: 'Create an issue tracker in a Rootr workspace',
|
|
11
11
|
description:
|
|
12
12
|
'Create a new GitHub-style issue tracker document in a Rootr (루터) workspace. Returns the tracker with ' +
|
|
13
|
-
'its id, plus its default labels/types — use rootr_create_issue afterward to add issues to it.'
|
|
13
|
+
'its id, plus its default labels/types — use rootr_create_issue afterward to add issues to it. ' +
|
|
14
|
+
NODE_URL_HINT,
|
|
14
15
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
15
16
|
inputSchema: {
|
|
16
17
|
name: z.string().describe('Name of the issue tracker'),
|
package/lib/mcp-tools/logs.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
-
import { jsonResult, errorResult, makeClient, requireWorkspace, resolveNodeId, NODE_TARGET_HINT, resolveOptionalNodeId } from './shared.js';
|
|
3
|
+
import { jsonResult, errorResult, makeClient, requireWorkspace, resolveNodeId, NODE_TARGET_HINT, NODE_URL_HINT, resolveOptionalNodeId } from './shared.js';
|
|
4
4
|
|
|
5
5
|
/** LOG datastore tools: create/update-fields/add-entries/query-entries/stats. */
|
|
6
6
|
export function registerLogTools(server) {
|
|
@@ -12,7 +12,8 @@ export function registerLogTools(server) {
|
|
|
12
12
|
'Create a new typed LOG store (a structured, queryable table-like node) in a Rootr (루터) workspace. ' +
|
|
13
13
|
'Each field has a name/type; type is one of string|int|float|bool|datetime|json|enum|level|relation. ' +
|
|
14
14
|
'Relation fields additionally need target (a node id to relate to) and optional many/relation flags. ' +
|
|
15
|
-
'Use rootr_add_log_entries afterward to insert rows, and rootr_query_log_entries / rootr_log_stats to read them back.'
|
|
15
|
+
'Use rootr_add_log_entries afterward to insert rows, and rootr_query_log_entries / rootr_log_stats to read them back. ' +
|
|
16
|
+
NODE_URL_HINT,
|
|
16
17
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
17
18
|
inputSchema: {
|
|
18
19
|
name: z.string().describe('Name of the LOG store'),
|
package/lib/mcp-tools/misc.js
CHANGED
|
@@ -95,11 +95,12 @@ export function registerMiscTools(server) {
|
|
|
95
95
|
{
|
|
96
96
|
title: 'Rename / re-icon / move any node',
|
|
97
97
|
description:
|
|
98
|
-
'Change a Rootr (루터) node\'s NAME, ICON, TAGS, or
|
|
99
|
-
'(DOCUMENT, FOLDER, DATABASE, SPREADSHEET, FORM, …). Until now icons could only be set when a node ' +
|
|
98
|
+
'Change a Rootr (루터) node\'s NAME, ICON, TAGS, PARENT, or graph auto-extraction setting — works for every ' +
|
|
99
|
+
'node type (DOCUMENT, FOLDER, DATABASE, SPREADSHEET, FORM, …). Until now icons could only be set when a node ' +
|
|
100
100
|
'was CREATED, so matching a migrated tree to its Notion icons was impossible without recreating ' +
|
|
101
101
|
'everything. Only the fields you pass change. `parentId: null` moves the node to the workspace root; ' +
|
|
102
|
-
'a node cannot be moved inside its own subtree.'
|
|
102
|
+
'a node cannot be moved inside its own subtree. `graphSyncOff: true` stops rows/edits from queuing this ' +
|
|
103
|
+
'node (and DATABASE tables especially) for graph extraction — useful right before a bulk load.',
|
|
103
104
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
104
105
|
inputSchema: {
|
|
105
106
|
target: z.string().describe('Node path (starting with "/") or node id'),
|
|
@@ -111,9 +112,16 @@ export function registerMiscTools(server) {
|
|
|
111
112
|
.nullable()
|
|
112
113
|
.optional()
|
|
113
114
|
.describe('Move under this folder/document/database (id or path). null = workspace root.'),
|
|
115
|
+
graphSyncOff: z
|
|
116
|
+
.boolean()
|
|
117
|
+
.optional()
|
|
118
|
+
.describe(
|
|
119
|
+
'true = stop queuing this node for graph extraction (embeddings + LLM) on every edit; false = ' +
|
|
120
|
+
're-enable it (and backfill affected documents).',
|
|
121
|
+
),
|
|
114
122
|
},
|
|
115
123
|
},
|
|
116
|
-
async ({ target, name, icon, tags, parentId }) => {
|
|
124
|
+
async ({ target, name, icon, tags, parentId, graphSyncOff }) => {
|
|
117
125
|
try {
|
|
118
126
|
const client = makeClient();
|
|
119
127
|
const id = requireResolved(await resolveTarget(client, target));
|
|
@@ -122,7 +130,7 @@ export function registerMiscTools(server) {
|
|
|
122
130
|
if (typeof parentId === 'string' && parentId) {
|
|
123
131
|
parent = requireResolved(await resolveTarget(client, parentId));
|
|
124
132
|
}
|
|
125
|
-
const node = await client.updateNode(id, { name, icon, tags, parentId: parent });
|
|
133
|
+
const node = await client.updateNode(id, { name, icon, tags, parentId: parent, graphSyncOff });
|
|
126
134
|
return jsonResult(node);
|
|
127
135
|
} catch (err) {
|
|
128
136
|
return errorResult(err);
|
|
@@ -148,7 +156,36 @@ export function registerMiscTools(server) {
|
|
|
148
156
|
const resolved = await resolveTarget(client, target);
|
|
149
157
|
const id = requireResolved(resolved);
|
|
150
158
|
await client.deleteNode(id);
|
|
151
|
-
return jsonResult({
|
|
159
|
+
return jsonResult({
|
|
160
|
+
deleted: id,
|
|
161
|
+
note: 'moved to trash, recoverable — rootr_restore_node puts it back',
|
|
162
|
+
});
|
|
163
|
+
} catch (err) {
|
|
164
|
+
return errorResult(err);
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
server.registerTool(
|
|
170
|
+
'rootr_restore_node',
|
|
171
|
+
{
|
|
172
|
+
title: 'Restore a deleted node from the trash',
|
|
173
|
+
description:
|
|
174
|
+
'Put a trashed Rootr (루터) document, folder or table back where it was — the undo for ' +
|
|
175
|
+
'rootr_delete_node, and the same thing Ctrl+Z does in the app. Pass the node id you got back ' +
|
|
176
|
+
'from the delete (a path will NOT find it: a trashed node is no longer on the tree). ' +
|
|
177
|
+
'Children that went to the trash with it come back too. Needs WRITE on the node.',
|
|
178
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
179
|
+
inputSchema: {
|
|
180
|
+
nodeId: z
|
|
181
|
+
.string()
|
|
182
|
+
.describe('Node id of the trashed item (from the rootr_delete_node result or a notification)'),
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
async ({ nodeId }) => {
|
|
186
|
+
try {
|
|
187
|
+
const client = makeClient();
|
|
188
|
+
return jsonResult(await client.restoreNode(nodeId));
|
|
152
189
|
} catch (err) {
|
|
153
190
|
return errorResult(err);
|
|
154
191
|
}
|