rootr-cli 0.17.3 → 0.19.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.ko.md +107 -3
- package/README.md +86 -4
- package/bin/rootr.js +688 -13
- package/lib/client.js +250 -26
- package/lib/credentials.js +20 -2
- package/lib/mcp-tools/ask.js +6 -1
- package/lib/mcp-tools/attachments.js +31 -11
- package/lib/mcp-tools/booking.js +403 -0
- package/lib/mcp-tools/databases.js +490 -20
- package/lib/mcp-tools/documents.js +183 -16
- package/lib/mcp-tools/forms.js +63 -1
- package/lib/mcp-tools/logs.js +1 -1
- package/lib/mcp-tools/misc.js +9 -3
- package/lib/mcp-tools/pages.js +31 -7
- package/lib/mcp-tools/presentations.js +36 -12
- package/lib/mcp-tools/public.js +4 -1
- package/lib/mcp-tools/recordings.js +39 -15
- package/lib/mcp-tools/shared.js +28 -0
- package/lib/mcp-tools/workspaces.js +155 -77
- package/lib/mcp.js +3 -0
- package/lib/resolve.js +43 -1
- package/package.json +1 -1
|
@@ -1,8 +1,101 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
3
|
import { RootrApiError } from '../client.js';
|
|
4
|
+
import { t as tr } from '../i18n.js';
|
|
4
5
|
import { resolveTarget, requireResolved, resolveMoveDestination } from '../resolve.js';
|
|
5
|
-
import { textResult, jsonResult, errorResult, makeClient, NODE_TARGET_HINT, NODE_URL_HINT } from './shared.js';
|
|
6
|
+
import { textResult, jsonResult, errorResult, makeClient, resolveNodeId, NODE_TARGET_HINT, NODE_URL_HINT } from './shared.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* ISSUE-105 — 쓰기 응답의 `warnings` 를 도구 결과 글 끝에 붙인다. 서버는 HTML 주석(`<!-- … -->`)을 저장하면서 떨어뜨리는데
|
|
10
|
+
* 예전에는 200 만 돌려줘, 주석을 삽입 지점으로 쓰려던 에이전트가 다음 호출에서야(그것도 "다시 읽어라"로) 알았다.
|
|
11
|
+
*/
|
|
12
|
+
function withWarnings(text, result) {
|
|
13
|
+
const warnings = result && Array.isArray(result.warnings) ? result.warnings.filter((w) => typeof w === 'string') : [];
|
|
14
|
+
return warnings.length ? `${text}\n${warnings.map((w) => `warning: ${w}`).join('\n')}` : text;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** EMBED-API — 쓰기 도구 본문에서 표를 끼워 넣는 표기. 모델이 읽는 곳에 한 번씩 적는다. */
|
|
18
|
+
const EMBED_SYNTAX_HINT =
|
|
19
|
+
' To show a LIVE table/spreadsheet/deck/board from this workspace inside the document, put `[[embed:/Path/To/Node]]` ' +
|
|
20
|
+
'(or `[[embed:<node id>]]`) alone on its own line — the editor renders the current rows, not a copy. Paths are ' +
|
|
21
|
+
'converted to ids when saved; an unknown path is rejected with 400. See rootr_embed_node.';
|
|
22
|
+
|
|
23
|
+
/** 문서가 아닌 노드를 읽는 도구 — rootr_read 가 "Document not found" 대신 갈 곳을 말해 준다. */
|
|
24
|
+
const READ_TOOL_BY_TYPE = {
|
|
25
|
+
PRESENTATION: 'rootr_read_presentation',
|
|
26
|
+
DATABASE: 'rootr_read_database',
|
|
27
|
+
SPREADSHEET: 'rootr_read_spreadsheet',
|
|
28
|
+
WHITEBOARD: 'rootr_read_whiteboard',
|
|
29
|
+
FORM: 'rootr_read_form',
|
|
30
|
+
PAGE: 'rootr_read_page',
|
|
31
|
+
RECORDING: 'rootr_read_recording',
|
|
32
|
+
SIGNED: 'rootr_read_signed_document',
|
|
33
|
+
LOG: 'rootr_query_log_entries',
|
|
34
|
+
ISSUE_TRACKER: 'rootr_list_issues',
|
|
35
|
+
FOLDER: 'rootr_list',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* ISSUE-127 — 주소·경로가 문서가 아닌 노드(덱·표 등)로 풀렸을 때 `/documents/:id` 는 "Document not found" 라고만 답한다.
|
|
40
|
+
* 공개 주소를 받은 에이전트에게는 "없다"로 읽히므로, 무엇이고 어느 도구로 읽는지를 말한다. 문서 조회가 실제로 404 일 때만.
|
|
41
|
+
*/
|
|
42
|
+
function notADocumentError(err, resolved, target) {
|
|
43
|
+
const type = resolved?.node?.type;
|
|
44
|
+
if (!(err instanceof RootrApiError) || err.status !== 404 || !type || type === 'DOCUMENT') return null;
|
|
45
|
+
const tool = READ_TOOL_BY_TYPE[type];
|
|
46
|
+
return new RootrApiError(
|
|
47
|
+
tr(`이 대상은 문서가 아니라 ${type} 노드입니다: ${target}`, `This target is a ${type} node, not a document: ${target}`),
|
|
48
|
+
{
|
|
49
|
+
status: 404,
|
|
50
|
+
hint: tool
|
|
51
|
+
? tr(`같은 대상을 ${tool} 에 넘기세요 (노드 id: ${resolved.id}).`, `Pass the same target to ${tool} (node id: ${resolved.id}).`)
|
|
52
|
+
: tr(`노드 id: ${resolved.id}`, `Node id: ${resolved.id}`),
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* ISSUE-127 — 남의 워크스페이스의 공개 문서. 주소를 받은 누구나 브라우저로 읽는 **공개 발행본**을 그대로 돌려준다.
|
|
59
|
+
* - 비밀번호가 걸린 문서는 "잠겨 있다"고만 답한다. 통과 수단(비밀번호·통과증 인자)은 넣지 않는다.
|
|
60
|
+
* - etag·노드 id 는 주지 않는다 — 고칠 수 있는 문서가 아니다(rootr_edit/write/append 는 이 되받기를 타지 않는다).
|
|
61
|
+
*/
|
|
62
|
+
async function readPublishedCopy(client, slug, target) {
|
|
63
|
+
let pub;
|
|
64
|
+
try {
|
|
65
|
+
pub = await client.getPublicDocument(slug);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (err instanceof RootrApiError && err.status === 401 && err.body && err.body.code === 'PUBLIC_DOC_PASSWORD') {
|
|
68
|
+
throw new RootrApiError(
|
|
69
|
+
tr(`이 공개 문서는 비밀번호로 잠겨 있어 읽을 수 없습니다: ${target}`, `This public document is password-locked and cannot be read: ${target}`),
|
|
70
|
+
{
|
|
71
|
+
status: 401,
|
|
72
|
+
hint: tr(
|
|
73
|
+
'이 도구로는 비밀번호를 넣을 수 없습니다. 브라우저에서 열거나, 문서가 있는 워크스페이스의 접근 권한을 받으세요.',
|
|
74
|
+
'This tool cannot enter the password. Open it in a browser, or get access to the workspace the document lives in.',
|
|
75
|
+
),
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (err instanceof RootrApiError && err.status === 404) requireResolved({ existed: false, target });
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
if (!pub || typeof pub.content !== 'string') requireResolved({ existed: false, target });
|
|
83
|
+
const published = pub.publishedAt ? String(pub.publishedAt) : 'n/a';
|
|
84
|
+
return {
|
|
85
|
+
content: [
|
|
86
|
+
{ type: 'text', text: pub.content },
|
|
87
|
+
{
|
|
88
|
+
type: 'text',
|
|
89
|
+
text: tr(
|
|
90
|
+
`공개 발행본 · 읽기 전용 — 연결된 워크스페이스에서 찾지 못해 공개 주소가 보여 주는 판을 읽었습니다. 제목: ${pub.name ?? ''} · 발행: ${published}\n` +
|
|
91
|
+
'발행 뒤에 고친 내용은 여기 없습니다. etag 를 주지 않습니다 — rootr_edit / rootr_write / rootr_append 로 고칠 수 없는 문서입니다.',
|
|
92
|
+
`Published public copy · read-only — not found in the connected workspace, so this is the version the public URL shows. Title: ${pub.name ?? ''} · published: ${published}\n` +
|
|
93
|
+
'Edits made after publishing are not included. No etag is given — rootr_edit / rootr_write / rootr_append cannot change this document.',
|
|
94
|
+
),
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
6
99
|
|
|
7
100
|
/** Registers the original 6 document tools: list/read/append/edit/write/search. */
|
|
8
101
|
export function registerDocumentTools(server) {
|
|
@@ -57,7 +150,15 @@ export function registerDocumentTools(server) {
|
|
|
57
150
|
title: 'Read a Rootr document',
|
|
58
151
|
description:
|
|
59
152
|
'Read the full markdown content of a Rootr (루터) document by path (starting with "/") or by node id. ' +
|
|
60
|
-
'Read a document before editing or appending to it so you know its current structure.'
|
|
153
|
+
'Read a document before editing or appending to it so you know its current structure. The response\'s ' +
|
|
154
|
+
'trailing "etag" line is the current ETag — pass it as ifMatch to rootr_write to guard against clobbering ' +
|
|
155
|
+
'concurrent changes. Note: a successful rootr_edit can reformat the WHOLE document (e.g. normalizing "-" ' +
|
|
156
|
+
'list markers to "*", or table cell spacing), not just the replaced snippet — re-read before another ' +
|
|
157
|
+
'rootr_edit on the same document rather than reusing text copied from an earlier read. ' +
|
|
158
|
+
'A published public URL (rootr.io/p/{slug}) also works: a document of the connected workspace reads normally; ' +
|
|
159
|
+
'one from ANOTHER workspace comes back as its published copy, marked read-only with no etag (it cannot be ' +
|
|
160
|
+
'edited from here), and a password-locked one is refused. Reading a public URL counts as one read against ' +
|
|
161
|
+
'the plan\'s read limit (Free: 180/day), exactly like reading your own document.',
|
|
61
162
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
62
163
|
inputSchema: {
|
|
63
164
|
target: z.string().describe('Document path (e.g. "/notes/todo.md") or node id, or a full rootr.io URL'),
|
|
@@ -67,9 +168,22 @@ export function registerDocumentTools(server) {
|
|
|
67
168
|
try {
|
|
68
169
|
const client = makeClient();
|
|
69
170
|
const resolved = await resolveTarget(client, target);
|
|
171
|
+
// ISSUE-127 — 이 워크스페이스에서 못 찾은 공개 주소(/p/{slug})는 공개 발행본으로 되받는다. 읽기 도구인 여기서만.
|
|
172
|
+
if (!resolved.existed && resolved.publicSlug) return await readPublishedCopy(client, resolved.publicSlug, target);
|
|
70
173
|
const id = requireResolved(resolved);
|
|
71
|
-
|
|
72
|
-
|
|
174
|
+
let doc;
|
|
175
|
+
try {
|
|
176
|
+
doc = await client.getDocumentMarkdown(id);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
throw notADocumentError(err, resolved, target) ?? err;
|
|
179
|
+
}
|
|
180
|
+
const { content, etag } = doc;
|
|
181
|
+
return {
|
|
182
|
+
content: [
|
|
183
|
+
{ type: 'text', text: content },
|
|
184
|
+
{ type: 'text', text: `etag: ${etag || 'n/a'}` },
|
|
185
|
+
],
|
|
186
|
+
};
|
|
73
187
|
} catch (err) {
|
|
74
188
|
return errorResult(err);
|
|
75
189
|
}
|
|
@@ -87,7 +201,7 @@ export function registerDocumentTools(server) {
|
|
|
87
201
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
88
202
|
inputSchema: {
|
|
89
203
|
target: z.string().describe('Document path (starting with "/") or node id, or a full rootr.io URL'),
|
|
90
|
-
content: z.string().describe('Markdown content to append'),
|
|
204
|
+
content: z.string().describe('Markdown content to append.' + EMBED_SYNTAX_HINT),
|
|
91
205
|
underHeading: z
|
|
92
206
|
.string()
|
|
93
207
|
.optional()
|
|
@@ -103,7 +217,43 @@ export function registerDocumentTools(server) {
|
|
|
103
217
|
const resolved = await resolveTarget(client, target);
|
|
104
218
|
const id = requireResolved(resolved);
|
|
105
219
|
const result = await client.appendDocument(id, { content, underHeading });
|
|
106
|
-
return textResult(`appended (etag ${result.etag || 'n/a'})
|
|
220
|
+
return textResult(withWarnings(`appended (etag ${result.etag || 'n/a'})`, result));
|
|
221
|
+
} catch (err) {
|
|
222
|
+
return errorResult(err);
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
server.registerTool(
|
|
228
|
+
'rootr_embed_node',
|
|
229
|
+
{
|
|
230
|
+
title: 'Embed a table (or any node) inside a Rootr document',
|
|
231
|
+
description:
|
|
232
|
+
'Put a LIVE view of another node — a database/table, spreadsheet, presentation, whiteboard, form, recording, ' +
|
|
233
|
+
'log store or issue tracker — inside a document, like the editor\'s "Embed" block (Notion\'s linked database). ' +
|
|
234
|
+
'The document shows the node\'s current content, not a copy, so edits to the table appear in the document. ' +
|
|
235
|
+
'Appends at the end of the document, or at the end of a heading\'s section. Same result as writing ' +
|
|
236
|
+
'`[[embed:/Path/To/Node]]` on its own line with rootr_append/rootr_write.',
|
|
237
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
238
|
+
inputSchema: {
|
|
239
|
+
target: z.string().describe('Document to embed into — path (starting with "/"), node id, or a full rootr.io URL'),
|
|
240
|
+
node: z.string().describe('Node to embed: its id' + NODE_TARGET_HINT),
|
|
241
|
+
underHeading: z
|
|
242
|
+
.string()
|
|
243
|
+
.optional()
|
|
244
|
+
.describe('Heading to put it under, e.g. "## Data". Omit to add at the end. A missing heading is created at the end.'),
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
async ({ target, node, underHeading }) => {
|
|
248
|
+
try {
|
|
249
|
+
const client = makeClient();
|
|
250
|
+
const docId = requireResolved(await resolveTarget(client, target));
|
|
251
|
+
const nodeId = await resolveNodeId(client, node);
|
|
252
|
+
const res = await client.embedNode(docId, { node: nodeId, underHeading });
|
|
253
|
+
return textResult(
|
|
254
|
+
`embedded ${res.embedded} into the document as \`${res.marker}\` (etag ${res.etag || 'n/a'}). ` +
|
|
255
|
+
'It renders as a live view in the editor.',
|
|
256
|
+
);
|
|
107
257
|
} catch (err) {
|
|
108
258
|
return errorResult(err);
|
|
109
259
|
}
|
|
@@ -118,7 +268,12 @@ export function registerDocumentTools(server) {
|
|
|
118
268
|
'Replace an exact snippet of text ("find") with new text ("replace") inside a Rootr document. ' +
|
|
119
269
|
'Fails with a clear error if the snippet is missing or ambiguous (not unique), so read the document ' +
|
|
120
270
|
'first to copy the exact text to find. Good for precise, targeted edits; use rootr_append instead when ' +
|
|
121
|
-
'you are only adding new content.'
|
|
271
|
+
'you are only adding new content. A successful edit can reformat the WHOLE document as a side effect ' +
|
|
272
|
+
'(list markers and table spacing get normalized), so if you plan another rootr_edit right after this ' +
|
|
273
|
+
'one, rootr_read the document again first instead of reusing text from before this edit. ' +
|
|
274
|
+
'HTML comments (<!-- ... -->) are NOT kept when a document is saved, so do not use one as an insertion ' +
|
|
275
|
+
'marker — the result carries a "warning:" line when that happens; to keep adding at a fixed place use ' +
|
|
276
|
+
'rootr_append with underHeading.',
|
|
122
277
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
123
278
|
inputSchema: {
|
|
124
279
|
target: z.string().describe('Document path (starting with "/") or node id, or a full rootr.io URL'),
|
|
@@ -132,8 +287,8 @@ export function registerDocumentTools(server) {
|
|
|
132
287
|
const client = makeClient();
|
|
133
288
|
const resolved = await resolveTarget(client, target);
|
|
134
289
|
const id = requireResolved(resolved);
|
|
135
|
-
await client.patchDocumentAnchor(id, { find, replace, replaceAll });
|
|
136
|
-
return textResult('edited');
|
|
290
|
+
const result = await client.patchDocumentAnchor(id, { find, replace, replaceAll });
|
|
291
|
+
return textResult(withWarnings('edited', result));
|
|
137
292
|
} catch (err) {
|
|
138
293
|
return errorResult(err);
|
|
139
294
|
}
|
|
@@ -152,7 +307,7 @@ export function registerDocumentTools(server) {
|
|
|
152
307
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
153
308
|
inputSchema: {
|
|
154
309
|
target: z.string().describe('Document path (starting with "/", created if missing) or node id, or a full rootr.io URL'),
|
|
155
|
-
content: z.string().describe('Full new markdown content for the document'),
|
|
310
|
+
content: z.string().describe('Full new markdown content for the document.' + EMBED_SYNTAX_HINT),
|
|
156
311
|
ifMatch: z
|
|
157
312
|
.string()
|
|
158
313
|
.optional()
|
|
@@ -164,6 +319,8 @@ export function registerDocumentTools(server) {
|
|
|
164
319
|
const client = makeClient();
|
|
165
320
|
const resolved = await resolveTarget(client, target);
|
|
166
321
|
if (!resolved.existed) {
|
|
322
|
+
// ISSUE-127 — 만들 경로가 없는 대상(못 찾은 공개 주소 등)은 새 문서로 만들지 않는다(CLI `rootr write` 와 같은 답).
|
|
323
|
+
if (!resolved.path) requireResolved(resolved);
|
|
167
324
|
if (!resolved.path) throw new RootrApiError(`대상을 찾을 수 없습니다: ${target}`);
|
|
168
325
|
// creating with content IS the write — a follow-up PUT would just
|
|
169
326
|
// double the version history and extraction work
|
|
@@ -176,10 +333,13 @@ export function registerDocumentTools(server) {
|
|
|
176
333
|
});
|
|
177
334
|
const id = created.id || (created.node && created.node.id);
|
|
178
335
|
const url = created.url || (created.node && created.node.url);
|
|
179
|
-
|
|
336
|
+
// 서버가 실제로 둔 경로를 말한다 — "본인 행만" 표 바로 밑처럼 이름에 꼬리가 붙는 자리에서는 요청한 경로와 다르다.
|
|
337
|
+
// 꼬리 붙은 이름은 그 이름 그대로 적어야 다시 닿으므로(경로 풀이는 정확한 이름만 본다), 요청한 경로를 되읊으면 다음 쓰기가 또 새 문서를 만든다.
|
|
338
|
+
const at = created.path || (created.node && created.node.path) || resolved.path;
|
|
339
|
+
return textResult(`created ${at} (id ${id})${url ? ` — ${url}` : ''}`);
|
|
180
340
|
}
|
|
181
|
-
const
|
|
182
|
-
return textResult(`wrote (etag ${etag || 'n/a'})
|
|
341
|
+
const written = await client.putDocument(resolved.id, content, { ifMatch });
|
|
342
|
+
return textResult(withWarnings(`wrote (etag ${written.etag || 'n/a'})`, written));
|
|
183
343
|
} catch (err) {
|
|
184
344
|
return errorResult(err);
|
|
185
345
|
}
|
|
@@ -230,7 +390,9 @@ export function registerDocumentTools(server) {
|
|
|
230
390
|
'Mentioning someone with @[userId] notifies them, and their notification opens that row and ' +
|
|
231
391
|
'highlights this comment. ' +
|
|
232
392
|
'Comments carry a user identity: the comment is attributed to the person who owns the key ' +
|
|
233
|
-
'(account keys and workspace keys both carry an owner). A key with no owner cannot comment.'
|
|
393
|
+
'(account keys and workspace keys both carry an owner). A key with no owner cannot comment. ' +
|
|
394
|
+
'On an "own rows only" table you can only comment on rows you can see — another person\'s row answers not found. ' +
|
|
395
|
+
'A reply (parentId) always joins its parent\'s thread: without blockId it inherits the parent\'s blockId, so a reply to a row comment is that row\'s comment too.',
|
|
234
396
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
235
397
|
inputSchema: {
|
|
236
398
|
target: z.string().describe('Document path (starting with "/") or node id, or a full rootr.io URL'),
|
|
@@ -260,7 +422,8 @@ export function registerDocumentTools(server) {
|
|
|
260
422
|
description:
|
|
261
423
|
'List the comment threads on a Rootr (루터) document, optionally filtered to one block. ' +
|
|
262
424
|
'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.'
|
|
425
|
+
'ROW id as `blockId`. Works with any key. On an "own rows only" table you only get the threads of rows you can see ' +
|
|
426
|
+
'(the row author and table managers) — other rows\' threads are simply absent.',
|
|
264
427
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
265
428
|
inputSchema: {
|
|
266
429
|
target: z.string().describe('Document path (starting with "/") or node id, or a full rootr.io URL'),
|
|
@@ -487,7 +650,11 @@ export function registerDocumentTools(server) {
|
|
|
487
650
|
'rootr_document_public_status',
|
|
488
651
|
{
|
|
489
652
|
title: 'Public status of a Rootr document',
|
|
490
|
-
description:
|
|
653
|
+
description:
|
|
654
|
+
'Whether a Rootr (루터) document is published, and at which public slug/URL. ' +
|
|
655
|
+
'`closed: "PUBLIC_OWN_ROW_PAGE"` (with `isPublic: false` and a `closedReason`) means publishing is switched on but the ' +
|
|
656
|
+
'address is closed right now (the document came from a row of an "own rows only" table) — do NOT hand that URL out; ' +
|
|
657
|
+
'tell the user why and how to reopen it.',
|
|
491
658
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
492
659
|
inputSchema: {
|
|
493
660
|
target: z.string().describe('Document path (starting with "/") or node id, or a full rootr.io URL'),
|
package/lib/mcp-tools/forms.js
CHANGED
|
@@ -338,7 +338,9 @@ export function registerFormTools(server) {
|
|
|
338
338
|
'rootr_list_form_share_links',
|
|
339
339
|
{
|
|
340
340
|
title: 'List a Rootr form\'s public share links',
|
|
341
|
-
description:
|
|
341
|
+
description:
|
|
342
|
+
'List existing public share links/tokens issued for a Rootr (루터) FORM. ' +
|
|
343
|
+
'A link marked `closed: "PUBLIC_OWN_ROW_PAGE"` does not open right now (the form came from a row of an "own rows only" table) — do NOT hand it out.',
|
|
342
344
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
343
345
|
inputSchema: {
|
|
344
346
|
formId: z.string().describe(`FORM node id${NODE_TARGET_HINT}`),
|
|
@@ -355,4 +357,64 @@ export function registerFormTools(server) {
|
|
|
355
357
|
}
|
|
356
358
|
},
|
|
357
359
|
);
|
|
360
|
+
|
|
361
|
+
// ---- FORM-MAP: 질문이 표의 어느 칸으로 들어가는가 ---------------------
|
|
362
|
+
// 2026-09-18 클린베테랑 요청 6번: 질문 이름과 칸 이름이 다르면 값이 조용히 빠졌고,
|
|
363
|
+
// 폼 10개 전부가 그래서 "응답은 들어오는데 행이 안 생기는" 상태였다.
|
|
364
|
+
|
|
365
|
+
server.registerTool(
|
|
366
|
+
'rootr_form_mapping',
|
|
367
|
+
{
|
|
368
|
+
title: 'Show which table column each form question feeds',
|
|
369
|
+
description:
|
|
370
|
+
'For a Rootr (루터) FORM linked to a DATABASE, show question → column for every question, and — the ' +
|
|
371
|
+
'point of this tool — `unmatched`: the questions with NO column, whose answers are silently dropped ' +
|
|
372
|
+
'when someone submits. Check this right after linking a form to a table or renaming anything: a ' +
|
|
373
|
+
'mismatch produces no error anywhere, and the loss is usually found weeks later. ' +
|
|
374
|
+
'`how` is "manual" (someone chose it), "auto" (names matched) or "none" (dropped). ' +
|
|
375
|
+
'`lastError` carries why the most recent submission failed to reach the table, if it did.',
|
|
376
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
377
|
+
inputSchema: {
|
|
378
|
+
formId: z.string().describe(`FORM node id${NODE_TARGET_HINT}`),
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
async ({ formId }) => {
|
|
382
|
+
try {
|
|
383
|
+
const client = makeClient();
|
|
384
|
+
formId = await resolveNodeId(client, formId);
|
|
385
|
+
return jsonResult(await client.formMapping(formId));
|
|
386
|
+
} catch (err) {
|
|
387
|
+
return errorResult(err);
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
server.registerTool(
|
|
393
|
+
'rootr_set_form_mapping',
|
|
394
|
+
{
|
|
395
|
+
title: 'Wire form questions to table columns by hand',
|
|
396
|
+
description:
|
|
397
|
+
'Pin which DATABASE column each question of a Rootr (루터) FORM writes into: { "<fieldId>": "<columnId>" }. ' +
|
|
398
|
+
'Questions you leave out keep matching BY NAME, and passing null for one puts it back to automatic. ' +
|
|
399
|
+
'Use this whenever the question wording differs from the column header ("성함" vs "이름") — otherwise ' +
|
|
400
|
+
'that answer is dropped with no warning. Call rootr_form_mapping first for the ids and the current state. ' +
|
|
401
|
+
'A field or column that does not exist is refused, not ignored.',
|
|
402
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
403
|
+
inputSchema: {
|
|
404
|
+
formId: z.string().describe(`FORM node id${NODE_TARGET_HINT}`),
|
|
405
|
+
fieldMap: z
|
|
406
|
+
.record(z.string().nullable())
|
|
407
|
+
.describe('{ "<질문 id>": "<칸 id>" } — null puts that question back to name matching'),
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
async ({ formId, fieldMap }) => {
|
|
411
|
+
try {
|
|
412
|
+
const client = makeClient();
|
|
413
|
+
formId = await resolveNodeId(client, formId);
|
|
414
|
+
return jsonResult(await client.setFormMapping(formId, fieldMap));
|
|
415
|
+
} catch (err) {
|
|
416
|
+
return errorResult(err);
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
);
|
|
358
420
|
}
|
package/lib/mcp-tools/logs.js
CHANGED
|
@@ -50,7 +50,7 @@ export function registerLogTools(server) {
|
|
|
50
50
|
description:
|
|
51
51
|
'Update the field schema of an existing LOG store. WARNING: this REPLACES THE ENTIRE FIELD SET — ' +
|
|
52
52
|
'you must resend every field you want to keep, not just the ones you are changing/adding. ' +
|
|
53
|
-
'
|
|
53
|
+
'Look up its current fields before calling this — rootr_read does not return field definitions (the LOG store detail does).',
|
|
54
54
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
55
55
|
inputSchema: {
|
|
56
56
|
logId: z.string().describe(`LOG store node id${NODE_TARGET_HINT}`),
|
package/lib/mcp-tools/misc.js
CHANGED
|
@@ -147,13 +147,16 @@ export function registerMiscTools(server) {
|
|
|
147
147
|
'permanent delete. Use rootr_read/rootr_list first to confirm you have the right target.',
|
|
148
148
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
149
149
|
inputSchema: {
|
|
150
|
-
target: z.string().describe('Document/folder path (starting with "/") or node id'),
|
|
150
|
+
target: z.string().optional().describe('Document/folder path (starting with "/") or node id'),
|
|
151
|
+
nodeId: z.string().optional().describe('Alias for `target` — node id of the item to delete'),
|
|
151
152
|
},
|
|
152
153
|
},
|
|
153
|
-
async ({ target }) => {
|
|
154
|
+
async ({ target, nodeId }) => {
|
|
154
155
|
try {
|
|
156
|
+
const want = target ?? nodeId;
|
|
157
|
+
if (!want) throw new Error('target (or nodeId) is required');
|
|
155
158
|
const client = makeClient();
|
|
156
|
-
const resolved = await resolveTarget(client,
|
|
159
|
+
const resolved = await resolveTarget(client, want);
|
|
157
160
|
const id = requireResolved(resolved);
|
|
158
161
|
await client.deleteNode(id);
|
|
159
162
|
return jsonResult({
|
|
@@ -203,6 +206,9 @@ export function registerMiscTools(server) {
|
|
|
203
206
|
description:
|
|
204
207
|
'Read the READ-ACK state of a Rootr (루터) node — who must read it, the due date, who has read or ' +
|
|
205
208
|
'acknowledged, and (for a DATABASE or FOLDER) how far through the documents inside each person is. ' +
|
|
209
|
+
'Each target has `completed`; on a DATABASE/FOLDER a person is done once they have read every document THEY can open ' +
|
|
210
|
+
'(`readCount`/`readTotal` per person — on an "own rows only" table that is their own row pages plus shared documents). ' +
|
|
211
|
+
'If you cannot see every document inside, other people carry only `completed` (no counts). ' +
|
|
206
212
|
'Use it to answer "누가 아직 안 읽었나" for a 규정/공지/교육자료. If the node itself has no requirement ' +
|
|
207
213
|
'but an ancestor does, `ancestorRequirementId` names the node that carries it.',
|
|
208
214
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
package/lib/mcp-tools/pages.js
CHANGED
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
*/
|
|
26
26
|
const BLOCK_GUIDE =
|
|
27
27
|
'A page is `blocks[]` (ordered, max 200) + `config`. Each block is {id?, type, props}. ' +
|
|
28
|
-
'You assemble a page from these
|
|
28
|
+
'You assemble a page from these block types:\n' +
|
|
29
29
|
'· hero {title, subtitle?, align?:"left"|"center", background?:{kind:"color",color}|{kind:"image",attachmentId}, button?:{label,target}} — the opening banner\n' +
|
|
30
30
|
'· text {body (markdown), title?} — prose section; markdown is sanitized when rendered\n' +
|
|
31
31
|
'· features {columns?:1|2|3, items:[{title, body, icon?(emoji)}]} — "what it does" grid\n' +
|
|
@@ -38,13 +38,31 @@ const BLOCK_GUIDE =
|
|
|
38
38
|
'· footer {links:[{label, target}], contact?, copyright?}\n' +
|
|
39
39
|
'· liveStats {cards:[{label, icon?, source:{nodeId(DATABASE/LOG node), agg:"count"|"sum"|"avg"|"min"|"max", column?}}]} — numbers read LIVE from a table\n' +
|
|
40
40
|
'· liveTable {source:{nodeId}, columns?:[name], rowLimit?(1-200), orderBy?:{column, dir:"asc"|"desc"}}\n' +
|
|
41
|
-
'· liveChart {source:{nodeId}, chart
|
|
41
|
+
'· liveChart {source:{nodeId, sheet?, range?}, chart, labelColumn, valueColumn?, agg?, …} — a LIVE chart (46 kinds, see CHARTS below)\n' +
|
|
42
42
|
'· form {formNodeId(FORM node), headline?, submitLabel?} — visitors submit right on the page\n' +
|
|
43
43
|
'· custom {html, height?(40-4000, default 260)} — free HTML/CSS, rendered inside a locked-down sandboxed iframe ' +
|
|
44
44
|
'(no scripts, no same-origin, no network). Use it for a look the other blocks cannot make. Page theme colors are ' +
|
|
45
45
|
'available inside as var(--pg-bg|text|muted|faint|card|line|accent|on-accent) and var(--pg-font). Scripts and ' +
|
|
46
46
|
'on*= handlers are stripped on save — do not put behaviour here, only markup and CSS.\n' +
|
|
47
|
-
'
|
|
47
|
+
'LAYOUT: any block takes `width:"full"|"twoThirds"|"half"|"third"` — neighbouring blocks sit side by side (phones stack them). ' +
|
|
48
|
+
'That is how a page becomes a DASHBOARD: a row of `chart:"kpi"` blocks at width "third", then charts at width "half".\n' +
|
|
49
|
+
'CHARTS (liveChart): chart ∈ bar|barH|stackedBar|stackedBarH|percentBar|lollipop|dumbbell|bullet|waterfall|pareto|radar|polarBar|line|step|area|stackedArea|percentArea|combo|slope|stream|calendar|pie|donut|halfDonut|rose|funnel|treemap|sunburst|waffle|histogram|boxplot|heatmap|scatter|bubble|sankey|chord|network|tree|kpi|gauge|progress|ring|candlestick|gantt|wordCloud|pivot. ' +
|
|
50
|
+
'source = a DATABASE, SPREADSHEET or LOG node (sheet: {nodeId, sheet?(name), range?:"A1:F200"} — first row = column names, formula results are read). ' +
|
|
51
|
+
'Category charts: labelColumn (dates are bucketed: dateBucket day|week|month|quarter|year|weekday|hour, empty periods filled with 0) + ' +
|
|
52
|
+
'valueColumn & agg (count|sum|avg|min|max|median|distinct — valueColumn is optional for count/distinct, which just count the matching rows per label) OR values:[{column?, agg, label?, as?:"bar"|"line"|"area", axis?:"left"|"right"}] (several series, combo/dual axis) ' +
|
|
53
|
+
'OR seriesColumn (break down / stack by another column; heatmap, stream and pivot need it). sort auto|value-desc|value-asc|label-asc|label-desc, limit (+ others:true folds the rest into "Other"), cumulative:true. ' +
|
|
54
|
+
'kpi|gauge|progress|ring: valueColumn+agg, optional dateColumn + period (today|7d|30d|90d|week|month|quarter|year) → value vs the previous same-length period + sparkline; goal or goalColumn. ' +
|
|
55
|
+
'scatter|bubble: xColumn, yColumn, sizeColumn?, seriesColumn?. histogram|boxplot: valueColumn (+labelColumn groups for boxplot), bins?. ' +
|
|
56
|
+
'sankey|chord|network: labelColumn (from) → targetColumn (to), value = agg. treemap|sunburst|tree: labelColumn + levels:[col,…]. ' +
|
|
57
|
+
'candlestick: labelColumn + open/high/low/closeColumn. gantt: labelColumn, startColumn, endColumn?. bullet: values[0]=actual + goalColumn (or goal). wordCloud: labelColumn (+splitWords:true for free text). ' +
|
|
58
|
+
'filters:[{column, op: is|is_not|contains|not_contains|gt|gte|lt|lte|empty|not_empty|last_days, value}] (values may be "@today-30"). ' +
|
|
59
|
+
'Display: title, subtitle, height(160-1200), format auto|number|compact|percent|krw|usd|decimal1|decimal2, prefix, suffix, palette theme|vivid|pastel|ocean|sunset|forest|mono, goalLabel, thresholds:[{value,color}], showLabels, showLegend, smooth. ' +
|
|
60
|
+
'rootr_read_page_live_data shows what each chart computed — a chart that could not be drawn says why (column_missing/no_rows/needs_number).\n' +
|
|
61
|
+
'LIVE/FORM BLOCKS: `source.nodeId` / `formNodeId` must be a node in the SAME workspace that you can open — write it as ' +
|
|
62
|
+
'the node id, a full "/folder/name" path, or a name that is the ONLY table/sheet/log (or form, for `formNodeId`) with ' +
|
|
63
|
+
'that name. The server resolves it and stores the id (the response shows the id). A path or name it cannot resolve — ' +
|
|
64
|
+
'missing, not readable by you, wrong node type, or a name shared by two nodes — is rejected with 400 and NOTHING is ' +
|
|
65
|
+
'saved; it is never stored as a silently empty block. `column`/`labelColumn`/' +
|
|
48
66
|
'`valueColumn`/`orderBy.column` are the column NAMES as shown in the table (not column ids). A table or form that ' +
|
|
49
67
|
'sits in a PRIVATE subtree is silently skipped on the published page — that is deliberate, never a bug to work around.\n' +
|
|
50
68
|
'LINKS: every `target` is {kind:"url", href} (absolute http(s)://, mailto: or tel: only) or {kind:"node", nodeId} ' +
|
|
@@ -65,8 +83,11 @@ export function registerPageTools(server) {
|
|
|
65
83
|
description:
|
|
66
84
|
'Create a new PAGE node in a Rootr (루터) workspace — a web page assembled from safe typed blocks, which ' +
|
|
67
85
|
'can then be published to the web at a public address (see rootr_publish_node). Use this when someone asks ' +
|
|
68
|
-
'for a
|
|
86
|
+
'for a DASHBOARD / report / KPI board over their tables, spreadsheets or logs (46 live chart kinds, numbers stay ' +
|
|
87
|
+
'current), or for a landing page, an announcement page, an event page, a product/pricing page or a signup page. ' +
|
|
69
88
|
'Omit `blocks` to start from a hero/text/footer skeleton. ' +
|
|
89
|
+
'In the response, `nodeId` is the page\'s node id — pass THAT as `pageId` to every later page tool ' +
|
|
90
|
+
'(and in REST paths like /v1/pages/:nodeId/live-data). The response `id` is an internal page-row id and is NOT accepted there (404). ' +
|
|
70
91
|
BLOCK_GUIDE +
|
|
71
92
|
' ' +
|
|
72
93
|
NODE_URL_HINT +
|
|
@@ -222,9 +243,12 @@ export function registerPageTools(server) {
|
|
|
222
243
|
title: 'Read what a page\'s live blocks currently show',
|
|
223
244
|
description:
|
|
224
245
|
'Read the values a Rootr (루터) PAGE\'s live blocks (liveStats/liveTable/liveChart) are showing right now — ' +
|
|
225
|
-
'the aggregates, rows and chart
|
|
226
|
-
'
|
|
227
|
-
'
|
|
246
|
+
'the aggregates, rows and chart data computed from their source tables, spreadsheets or logs. Use it after ' +
|
|
247
|
+
'building a dashboard to check every chart actually draws. Each chart carries `data` shaped by kind ' +
|
|
248
|
+
'(category {labels, series[]} · single {value, previous, goal, spark} · xy · distribution · tree · flow · ohlc · range). ' +
|
|
249
|
+
'A chart that cannot draw says why: `{shape:"empty", reason:"column_missing"|"no_rows"|"needs_number", column}` — ' +
|
|
250
|
+
'fix that column name or filter. A block missing from the result means its source was not found, is deleted, ' +
|
|
251
|
+
'or sits in a PRIVATE subtree.',
|
|
228
252
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
229
253
|
inputSchema: {
|
|
230
254
|
pageId: z.string().describe(`PAGE node id${NODE_TARGET_HINT}`),
|
|
@@ -10,7 +10,7 @@ import { jsonResult, errorResult, makeClient, requireWorkspace, resolveNodeId, N
|
|
|
10
10
|
const AUTHORING_GUIDE =
|
|
11
11
|
'Authoring guide: give each slide ONE assertion-style title (a claim, e.g. "Latency dropped 40% after the cache fix" — ' +
|
|
12
12
|
'not a topic label like "Latency"), plus blocks[] (bullet-like {id, heading?, body?, icon?} items) and, where useful, ' +
|
|
13
|
-
'a diagram ({type:"mermaid", code}) or images. Canvas is 16:9 (1280x720). Meaning must live in TEXT — ' +
|
|
13
|
+
'a diagram ({type:"mermaid", code} — drawn on "content" slides) or images. Canvas is 16:9 (1280x720). Meaning must live in TEXT — ' +
|
|
14
14
|
'title/blocks[].heading/blocks[].body/notes are what auto-connects into the knowledge graph; diagrams/code/html and the ' +
|
|
15
15
|
'image pixels themselves are visual-only and are NOT indexed, so never put facts ONLY in a diagram or picture.\n' +
|
|
16
16
|
'Images: `image` is the single primary/background image (cover, section, full-bleed); `images[]` holds additional ' +
|
|
@@ -19,8 +19,10 @@ const AUTHORING_GUIDE =
|
|
|
19
19
|
'an https URL, or a data: URI — you cannot upload the file bytes through these MCP tools, only reference the resulting URL. ' +
|
|
20
20
|
'To get a REAL image, call rootr_generate_image with an English prompt (say "no text"), then put the returned `url` ' +
|
|
21
21
|
'into the slide image\'s `src`; rootr_remove_image_background cuts out an image\'s background. ' +
|
|
22
|
-
'`placement`
|
|
23
|
-
'
|
|
22
|
+
'Set `placement` on the primary `image`: "full" = background (full-bleed on cover/section/closing, faint on content); ' +
|
|
23
|
+
'"right" / "top" = beside / above the blocks, content slides only; other values are ignored and a content slide without ' +
|
|
24
|
+
'placement does not draw the image. For `images[]`, x/y/w/h give freeform PPT-style placement in 1280x720 canvas ' +
|
|
25
|
+
'coords (drawn only when both x and y are set). ALWAYS add an `alt` caption to every image — alt text is part of the graph text spine, so a captioned image ' +
|
|
24
26
|
'connects into the knowledge graph while an uncaptioned one does not.\n' +
|
|
25
27
|
'Use kind to mark each slide\'s role: "cover" (deck title slide), "section" (chapter divider), "content" (body slide, ' +
|
|
26
28
|
'the default), "closing" (last slide / call to action).\n' +
|
|
@@ -54,7 +56,13 @@ const imageShape = z
|
|
|
54
56
|
.optional()
|
|
55
57
|
.describe('Caption / alt text. Part of the knowledge-graph text spine — ALWAYS set this so the image is indexed.'),
|
|
56
58
|
prompt: z.string().optional().describe('English AI-generation prompt, no text baked into the image (used when src is absent)'),
|
|
57
|
-
placement: z
|
|
59
|
+
placement: z
|
|
60
|
+
.string()
|
|
61
|
+
.optional()
|
|
62
|
+
.describe(
|
|
63
|
+
'Where the primary `image` goes: "full" (background — full-bleed on cover/section/closing, faint on content) | ' +
|
|
64
|
+
'"right" | "top" (content slides only). Other values are ignored.',
|
|
65
|
+
),
|
|
58
66
|
x: z.number().optional().describe('Freeform placement X in 1280x720 canvas coords'),
|
|
59
67
|
y: z.number().optional().describe('Freeform placement Y in 1280x720 canvas coords'),
|
|
60
68
|
w: z.number().optional().describe('Freeform placement width in canvas coords'),
|
|
@@ -66,7 +74,14 @@ const slideShape = z
|
|
|
66
74
|
.object({
|
|
67
75
|
id: z.string().optional().describe('Unique slide id, e.g. "SLD-001". Auto-generated if omitted.'),
|
|
68
76
|
kind: z.string().optional().describe('cover | section | content | closing'),
|
|
69
|
-
layout: z
|
|
77
|
+
layout: z
|
|
78
|
+
.string()
|
|
79
|
+
.optional()
|
|
80
|
+
.describe(
|
|
81
|
+
'Block arrangement on content slides: "two-col" (needs 2+ blocks) | "kpi-grid" (alias "kpi"). ' +
|
|
82
|
+
'Any other value silently falls back to the default arrangement. On cover/section slides it is printed as ' +
|
|
83
|
+
'the small label above the title.',
|
|
84
|
+
),
|
|
70
85
|
title: z.string().optional().describe('Assertion-style title (the claim this slide makes) — text spine'),
|
|
71
86
|
blocks: z
|
|
72
87
|
.array(
|
|
@@ -82,7 +97,10 @@ const slideShape = z
|
|
|
82
97
|
diagram: z
|
|
83
98
|
.object({ type: z.literal('mermaid').optional(), code: z.string() })
|
|
84
99
|
.optional()
|
|
85
|
-
.describe(
|
|
100
|
+
.describe(
|
|
101
|
+
'Mermaid diagram, drawn below the blocks on "content" slides (ignored on cover/section/closing and when the ' +
|
|
102
|
+
'slide has baked html); visual only, not indexed — keep the facts in title/blocks too',
|
|
103
|
+
),
|
|
86
104
|
image: imageShape
|
|
87
105
|
.optional()
|
|
88
106
|
.describe('Single primary/background image (cover, section, full-bleed). See imageShape fields.'),
|
|
@@ -242,7 +260,12 @@ export function registerPresentationTools(server) {
|
|
|
242
260
|
try {
|
|
243
261
|
const client = makeClient();
|
|
244
262
|
presentationId = await resolveNodeId(client, presentationId);
|
|
245
|
-
|
|
263
|
+
// ISSUE-185 — do NOT mint ids here: numbering by this call's own
|
|
264
|
+
// array index always collides with the deck's existing slides
|
|
265
|
+
// (e.g. restamping "SLD-001" onto a deck that already has one).
|
|
266
|
+
// The server fills any omitted slide/block id, avoiding the deck's
|
|
267
|
+
// current ids (see PresentationsService.appendSlides).
|
|
268
|
+
const result = await client.appendPresentationSlides(presentationId, slides);
|
|
246
269
|
return jsonResult(result);
|
|
247
270
|
} catch (err) {
|
|
248
271
|
return errorResult(err);
|
|
@@ -328,8 +351,8 @@ export function registerPresentationTools(server) {
|
|
|
328
351
|
},
|
|
329
352
|
async ({ prompt, nodeId, workspace }) => {
|
|
330
353
|
try {
|
|
331
|
-
const
|
|
332
|
-
const
|
|
354
|
+
const ws = requireWorkspace(workspace); // ISSUE-61 — 워크스페이스를 먼저 정하고 그 키로(attachments.js 와 같은 결함)
|
|
355
|
+
const client = makeClient(ws);
|
|
333
356
|
nodeId = await resolveOptionalNodeId(client, nodeId);
|
|
334
357
|
const result = await client.generateImage(ws, { prompt, nodeId });
|
|
335
358
|
return jsonResult(result);
|
|
@@ -357,8 +380,8 @@ export function registerPresentationTools(server) {
|
|
|
357
380
|
},
|
|
358
381
|
async ({ attachmentId, nodeId, workspace }) => {
|
|
359
382
|
try {
|
|
360
|
-
const
|
|
361
|
-
const
|
|
383
|
+
const ws = requireWorkspace(workspace); // ISSUE-61 — 위와 같다
|
|
384
|
+
const client = makeClient(ws);
|
|
362
385
|
nodeId = await resolveOptionalNodeId(client, nodeId);
|
|
363
386
|
const result = await client.removeImageBackground(ws, { attachmentId, nodeId });
|
|
364
387
|
return jsonResult(result);
|
|
@@ -405,7 +428,8 @@ export function registerPresentationTools(server) {
|
|
|
405
428
|
description:
|
|
406
429
|
'Render a typed diagram spec into a Rootr (루터) presentation slide — architecture, workflow, sequence, ' +
|
|
407
430
|
'dataflow, or lifecycle. The drawing is baked into the slide, so it shows on screen AND in the PDF/PPTX ' +
|
|
408
|
-
'exports.
|
|
431
|
+
'exports. Prefer this over the mermaid `diagram` field when the diagram has to look designed — a plain ' +
|
|
432
|
+
'"content" slide also draws its mermaid `diagram`, but with mermaid\'s default look.\n' +
|
|
409
433
|
'Call rootr_diagram_schema first to get the exact shape for the family you want — the spec is validated ' +
|
|
410
434
|
'fail-closed and a mismatch is rejected with the offending field named and a suggested fix, which you ' +
|
|
411
435
|
'should apply and retry.\n' +
|
package/lib/mcp-tools/public.js
CHANGED
|
@@ -86,7 +86,8 @@ export function registerPublicTools(server) {
|
|
|
86
86
|
'`{"password": "..."}` locks the page (the content is not sent at all until the password is entered — it is ' +
|
|
87
87
|
'not hidden in the browser), `{"password": null}` unlocks; ' +
|
|
88
88
|
'`{"expiresAt": "2026-12-31T00:00:00Z"}` auto-closes on that date, `null` clears it; ' +
|
|
89
|
-
'`{"noIndex": true}` asks search engines to stay away
|
|
89
|
+
'`{"noIndex": true}` asks search engines to stay away (Team plan and up — a Free workspace gets 403 ' +
|
|
90
|
+
'PLAN_UPGRADE_REQUIRED; a password-locked page is always hidden and cannot be re-exposed until unlocked); ' +
|
|
90
91
|
'`{"design": {"layout": "board", "skin": "paper"}}` changes how a published TABLE looks — it does not ' +
|
|
91
92
|
'have to be a table (cards, gallery, list, board, timeline, calendar, chart, ranking). ' +
|
|
92
93
|
'For DOCUMENT nodes use rootr_set_document_public instead — documents keep their own settings (skins, ' +
|
|
@@ -129,6 +130,8 @@ export function registerPublicTools(server) {
|
|
|
129
130
|
'currently open, when it expires, whether a password is set (the password itself is never returned), whether ' +
|
|
130
131
|
'cloning is allowed, and whether the pages underneath went out with it. ' +
|
|
131
132
|
'`publishable: false` means this type cannot be published at all (FOLDER). ' +
|
|
133
|
+
'`closed: "PUBLIC_OWN_ROW_PAGE"` (with `isPublic: false` and a `closedReason`) means publishing is switched on but the ' +
|
|
134
|
+
'address is closed right now — do NOT hand that URL out; tell the user why and how to reopen it. ' +
|
|
132
135
|
'Call this before publishing so you do not tell the user a page is new when it already had a URL.',
|
|
133
136
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
134
137
|
inputSchema: {
|