rootr-cli 0.2.1 → 0.4.1
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/LICENSE +21 -0
- package/README.md +101 -4
- package/bin/rootr.js +109 -3
- package/lib/client.js +531 -1
- package/lib/config.js +30 -1
- package/lib/context.js +38 -0
- package/lib/mcp-tools/ask.js +1 -0
- package/lib/mcp-tools/attachments.js +111 -0
- package/lib/mcp-tools/crm.js +725 -0
- package/lib/mcp-tools/databases.js +25 -14
- package/lib/mcp-tools/documents.js +164 -3
- package/lib/mcp-tools/forms.js +215 -0
- package/lib/mcp-tools/issues.js +13 -4
- package/lib/mcp-tools/logs.js +16 -6
- package/lib/mcp-tools/misc.js +2 -0
- package/lib/mcp-tools/presentations.js +25 -9
- package/lib/mcp-tools/recordings.js +433 -0
- package/lib/mcp-tools/shared.js +44 -3
- package/lib/mcp-tools/spreadsheets.js +231 -0
- package/lib/mcp-tools/webhooks.js +6 -2
- package/lib/mcp-tools/whiteboards.js +169 -0
- package/lib/mcp-tools/workspaces.js +148 -5
- package/lib/mcp.js +98 -5
- package/package.json +22 -3
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
-
import { jsonResult, errorResult, makeClient } from './shared.js';
|
|
3
|
+
import { jsonResult, errorResult, makeClient, resolveNodeId, NODE_TARGET_HINT } from './shared.js';
|
|
4
|
+
|
|
5
|
+
/** Human text for the `databaseId` param — accepts a path just like the document tools. */
|
|
6
|
+
const DATABASE_TARGET_DESC = `DATABASE node id${NODE_TARGET_HINT} (e.g. "/제작 공정/칸반")`;
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* Resolve a `{propertyNameOrId: value}` map to `{propertyId: value}` using the
|
|
@@ -38,16 +41,18 @@ export function registerDatabaseTools(server) {
|
|
|
38
41
|
'Read a Rootr (루터) DATABASE node — its schema (properties with ids/types and select options) plus all rows ' +
|
|
39
42
|
'(each with its rowId and values). ALWAYS call this before rootr_add_row / rootr_update_row so you know the exact ' +
|
|
40
43
|
'property ids/names and, for board/kanban views, which select column holds the status. Only works on DATABASE nodes.',
|
|
44
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
41
45
|
inputSchema: {
|
|
42
|
-
databaseId: z.string().describe(
|
|
46
|
+
databaseId: z.string().describe(DATABASE_TARGET_DESC),
|
|
43
47
|
},
|
|
44
48
|
},
|
|
45
49
|
async ({ databaseId }) => {
|
|
46
50
|
try {
|
|
47
51
|
const client = makeClient();
|
|
52
|
+
const id = await resolveNodeId(client, databaseId);
|
|
48
53
|
const [database, rows] = await Promise.all([
|
|
49
|
-
client.getDatabase(
|
|
50
|
-
client.listRows(
|
|
54
|
+
client.getDatabase(id),
|
|
55
|
+
client.listRows(id),
|
|
51
56
|
]);
|
|
52
57
|
return jsonResult({ database, rows });
|
|
53
58
|
} catch (err) {
|
|
@@ -63,14 +68,15 @@ export function registerDatabaseTools(server) {
|
|
|
63
68
|
description:
|
|
64
69
|
'List the rows of a Rootr (루터) DATABASE (id + values + position), without the schema. ' +
|
|
65
70
|
'Use rootr_read_database instead when you also need the property/column definitions.',
|
|
71
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
66
72
|
inputSchema: {
|
|
67
|
-
databaseId: z.string().describe(
|
|
73
|
+
databaseId: z.string().describe(DATABASE_TARGET_DESC),
|
|
68
74
|
},
|
|
69
75
|
},
|
|
70
76
|
async ({ databaseId }) => {
|
|
71
77
|
try {
|
|
72
78
|
const client = makeClient();
|
|
73
|
-
const rows = await client.listRows(databaseId);
|
|
79
|
+
const rows = await client.listRows(await resolveNodeId(client, databaseId));
|
|
74
80
|
return jsonResult({ rows });
|
|
75
81
|
} catch (err) {
|
|
76
82
|
return errorResult(err);
|
|
@@ -87,8 +93,9 @@ export function registerDatabaseTools(server) {
|
|
|
87
93
|
'Value shapes by type: title/text/url/select/person → string; number → number; multi_select → string[]; ' +
|
|
88
94
|
'checkbox → boolean; date → { start: "YYYY-MM-DD", end? }. A select value is the OPTION NAME. ' +
|
|
89
95
|
'Call rootr_read_database first to learn the columns. Optionally set `position` to insert at an index.',
|
|
96
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
90
97
|
inputSchema: {
|
|
91
|
-
databaseId: z.string().describe(
|
|
98
|
+
databaseId: z.string().describe(DATABASE_TARGET_DESC),
|
|
92
99
|
values: z.record(z.unknown()).describe('{ propertyNameOrId: value } for the new row'),
|
|
93
100
|
position: z.number().optional().describe('0-based insert index; appended at the end if omitted'),
|
|
94
101
|
},
|
|
@@ -96,9 +103,10 @@ export function registerDatabaseTools(server) {
|
|
|
96
103
|
async ({ databaseId, values, position }) => {
|
|
97
104
|
try {
|
|
98
105
|
const client = makeClient();
|
|
99
|
-
const
|
|
106
|
+
const id = await resolveNodeId(client, databaseId);
|
|
107
|
+
const database = await client.getDatabase(id);
|
|
100
108
|
const resolved = resolveValueKeys(database.properties || [], values);
|
|
101
|
-
const row = await client.createRow(
|
|
109
|
+
const row = await client.createRow(id, { values: resolved, position });
|
|
102
110
|
return jsonResult(row);
|
|
103
111
|
} catch (err) {
|
|
104
112
|
return errorResult(err);
|
|
@@ -115,8 +123,9 @@ export function registerDatabaseTools(server) {
|
|
|
115
123
|
'`values` maps property NAME (or id) → new value; only the passed keys change, `null` clears a key, omitted keys ' +
|
|
116
124
|
'are left unchanged. To MOVE a kanban card to another column, set its select/status property to the new option name. ' +
|
|
117
125
|
'Optionally set `position` to reorder the row.',
|
|
126
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
118
127
|
inputSchema: {
|
|
119
|
-
databaseId: z.string().describe(
|
|
128
|
+
databaseId: z.string().describe(DATABASE_TARGET_DESC),
|
|
120
129
|
rowId: z.string().describe('Row id (must belong to the database)'),
|
|
121
130
|
values: z.record(z.unknown()).optional().describe('{ propertyNameOrId: newValue | null } partial patch'),
|
|
122
131
|
position: z.number().optional().describe('New 0-based position for the row'),
|
|
@@ -125,12 +134,13 @@ export function registerDatabaseTools(server) {
|
|
|
125
134
|
async ({ databaseId, rowId, values, position }) => {
|
|
126
135
|
try {
|
|
127
136
|
const client = makeClient();
|
|
137
|
+
const id = await resolveNodeId(client, databaseId);
|
|
128
138
|
let resolved;
|
|
129
139
|
if (values !== undefined) {
|
|
130
|
-
const database = await client.getDatabase(
|
|
140
|
+
const database = await client.getDatabase(id);
|
|
131
141
|
resolved = resolveValueKeys(database.properties || [], values);
|
|
132
142
|
}
|
|
133
|
-
const row = await client.updateRow(
|
|
143
|
+
const row = await client.updateRow(id, rowId, { values: resolved, position });
|
|
134
144
|
return jsonResult(row);
|
|
135
145
|
} catch (err) {
|
|
136
146
|
return errorResult(err);
|
|
@@ -145,15 +155,16 @@ export function registerDatabaseTools(server) {
|
|
|
145
155
|
description:
|
|
146
156
|
'Delete ONE row of a Rootr (루터) DATABASE by rowId (its linked page, if any, is moved to the trash). ' +
|
|
147
157
|
'Verify the row via rootr_read_database first — do not guess rowIds.',
|
|
158
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
148
159
|
inputSchema: {
|
|
149
|
-
databaseId: z.string().describe(
|
|
160
|
+
databaseId: z.string().describe(DATABASE_TARGET_DESC),
|
|
150
161
|
rowId: z.string().describe('Row id to delete (must belong to the database)'),
|
|
151
162
|
},
|
|
152
163
|
},
|
|
153
164
|
async ({ databaseId, rowId }) => {
|
|
154
165
|
try {
|
|
155
166
|
const client = makeClient();
|
|
156
|
-
await client.deleteRow(databaseId, rowId);
|
|
167
|
+
await client.deleteRow(await resolveNodeId(client, databaseId), rowId);
|
|
157
168
|
return jsonResult({ deleted: true, rowId });
|
|
158
169
|
} catch (err) {
|
|
159
170
|
return errorResult(err);
|
|
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
|
|
3
3
|
import { RootrApiError } from '../client.js';
|
|
4
4
|
import { resolveTarget, requireResolved } from '../resolve.js';
|
|
5
|
-
import { textResult, errorResult, makeClient } from './shared.js';
|
|
5
|
+
import { textResult, jsonResult, errorResult, makeClient } 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) {
|
|
@@ -13,6 +13,7 @@ export function registerDocumentTools(server) {
|
|
|
13
13
|
description:
|
|
14
14
|
'List documents and folders in the connected Rootr (루터) workspace, optionally filtered to a path prefix. ' +
|
|
15
15
|
'Use this first to discover what exists before reading or writing.',
|
|
16
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
16
17
|
inputSchema: {
|
|
17
18
|
path: z
|
|
18
19
|
.string()
|
|
@@ -30,7 +31,12 @@ export function registerDocumentTools(server) {
|
|
|
30
31
|
const p = n.path || '';
|
|
31
32
|
return p === path || p.startsWith(path.endsWith('/') ? path : path + '/');
|
|
32
33
|
})
|
|
33
|
-
.map((n) =>
|
|
34
|
+
.map((n) => {
|
|
35
|
+
const label = n.path || n.name || n.id;
|
|
36
|
+
// Keep the id visible: tools that take a node id (databases, sheets,
|
|
37
|
+
// whiteboards, …) are otherwise unreachable from a listing.
|
|
38
|
+
return n.id && label !== n.id ? `${n.type || '?'} ${label} [${n.id}]` : `${n.type || '?'} ${label}`;
|
|
39
|
+
})
|
|
34
40
|
.sort();
|
|
35
41
|
return textResult(rows.length ? rows.join('\n') : '(no matching nodes)');
|
|
36
42
|
} catch (err) {
|
|
@@ -46,6 +52,7 @@ export function registerDocumentTools(server) {
|
|
|
46
52
|
description:
|
|
47
53
|
'Read the full markdown content of a Rootr (루터) document by path (starting with "/") or by node id. ' +
|
|
48
54
|
'Read a document before editing or appending to it so you know its current structure.',
|
|
55
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
49
56
|
inputSchema: {
|
|
50
57
|
target: z.string().describe('Document path (e.g. "/notes/todo.md") or node id'),
|
|
51
58
|
},
|
|
@@ -71,6 +78,7 @@ export function registerDocumentTools(server) {
|
|
|
71
78
|
'Append content to a Rootr document, optionally under a given heading. This is a conflict-free, ' +
|
|
72
79
|
'additive operation — it never overwrites existing content, so PREFER this over rootr_write whenever ' +
|
|
73
80
|
'you just need to add new information (log entries, notes, findings) to an existing document.',
|
|
81
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
74
82
|
inputSchema: {
|
|
75
83
|
target: z.string().describe('Document path (starting with "/") or node id'),
|
|
76
84
|
content: z.string().describe('Markdown content to append'),
|
|
@@ -105,6 +113,7 @@ export function registerDocumentTools(server) {
|
|
|
105
113
|
'Fails with a clear error if the snippet is missing or ambiguous (not unique), so read the document ' +
|
|
106
114
|
'first to copy the exact text to find. Good for precise, targeted edits; use rootr_append instead when ' +
|
|
107
115
|
'you are only adding new content.',
|
|
116
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
108
117
|
inputSchema: {
|
|
109
118
|
target: z.string().describe('Document path (starting with "/") or node id'),
|
|
110
119
|
find: z.string().describe('Exact existing text to find (must be unique unless replaceAll is set)'),
|
|
@@ -133,6 +142,7 @@ export function registerDocumentTools(server) {
|
|
|
133
142
|
'Replace the ENTIRE content of a Rootr document, or create a new document at a path if it does not exist. ' +
|
|
134
143
|
'This is DESTRUCTIVE — it discards whatever is currently there. Read the document first and pass its etag ' +
|
|
135
144
|
'as ifMatch to avoid clobbering concurrent changes. Prefer rootr_append or rootr_edit for incremental changes.',
|
|
145
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
136
146
|
inputSchema: {
|
|
137
147
|
target: z.string().describe('Document path (starting with "/", created if missing) or node id'),
|
|
138
148
|
content: z.string().describe('Full new markdown content for the document'),
|
|
@@ -175,6 +185,7 @@ export function registerDocumentTools(server) {
|
|
|
175
185
|
description:
|
|
176
186
|
'Full-text search across the connected Rootr (루터) workspace. Use this to find relevant documents before ' +
|
|
177
187
|
'reading or editing them.',
|
|
188
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
178
189
|
inputSchema: {
|
|
179
190
|
query: z.string().describe('Search query text'),
|
|
180
191
|
},
|
|
@@ -183,11 +194,161 @@ export function registerDocumentTools(server) {
|
|
|
183
194
|
try {
|
|
184
195
|
const client = makeClient();
|
|
185
196
|
const results = await client.search(query);
|
|
186
|
-
const rows = results.map((r) =>
|
|
197
|
+
const rows = results.map((r) => {
|
|
198
|
+
const label = r.path || r.name || r.nodeId;
|
|
199
|
+
const id = r.nodeId && label !== r.nodeId ? ` [${r.nodeId}]` : '';
|
|
200
|
+
return `${label}${id} — ${r.snippet || ''}`;
|
|
201
|
+
});
|
|
187
202
|
return textResult(rows.length ? rows.join('\n') : '(no results)');
|
|
188
203
|
} catch (err) {
|
|
189
204
|
return errorResult(err);
|
|
190
205
|
}
|
|
191
206
|
},
|
|
192
207
|
);
|
|
208
|
+
|
|
209
|
+
server.registerTool(
|
|
210
|
+
'rootr_comment_document',
|
|
211
|
+
{
|
|
212
|
+
title: 'Comment on a Rootr document',
|
|
213
|
+
description:
|
|
214
|
+
'Add a comment to a Rootr (루터) document (optionally anchored to a block, or as a reply via parentId). ' +
|
|
215
|
+
'Comments carry a user identity, so this requires an ACCOUNT key (PAT) — a workspace-scoped key ' +
|
|
216
|
+
'(including remote-connector tokens) gets a 403; fall back to rootr_append in that case.',
|
|
217
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
218
|
+
inputSchema: {
|
|
219
|
+
target: z.string().describe('Document path (starting with "/") or node id'),
|
|
220
|
+
body: z.string().describe('Comment text (markdown)'),
|
|
221
|
+
blockId: z.string().optional().describe('Block id to anchor the comment to'),
|
|
222
|
+
parentId: z.string().optional().describe('Parent comment id, to reply in a thread'),
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
async ({ target, body, blockId, parentId }) => {
|
|
226
|
+
try {
|
|
227
|
+
const client = makeClient();
|
|
228
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
229
|
+
return jsonResult(await client.createComment(id, { body, blockId, parentId }));
|
|
230
|
+
} catch (err) {
|
|
231
|
+
return errorResult(err);
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
server.registerTool(
|
|
237
|
+
'rootr_list_document_comments',
|
|
238
|
+
{
|
|
239
|
+
title: 'List comments on a Rootr document',
|
|
240
|
+
description:
|
|
241
|
+
'List the comment threads on a Rootr (루터) document, optionally filtered to one block. Works with any key.',
|
|
242
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
243
|
+
inputSchema: {
|
|
244
|
+
target: z.string().describe('Document path (starting with "/") or node id'),
|
|
245
|
+
blockId: z.string().optional().describe('Only comments anchored to this block'),
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
async ({ target, blockId }) => {
|
|
249
|
+
try {
|
|
250
|
+
const client = makeClient();
|
|
251
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
252
|
+
return jsonResult(await client.listComments(id, { blockId }));
|
|
253
|
+
} catch (err) {
|
|
254
|
+
return errorResult(err);
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
server.registerTool(
|
|
260
|
+
'rootr_publish_document',
|
|
261
|
+
{
|
|
262
|
+
title: 'Publish a Rootr document to a public URL',
|
|
263
|
+
description:
|
|
264
|
+
'Publish a Rootr (루터) document at a public read-only URL (rootr.io/p/{slug}) — outsiders can read it ' +
|
|
265
|
+
'and propose git-style edit suggestions the owner can merge. Publishing is an owner-level action: it needs ' +
|
|
266
|
+
'MANAGE permission, which workspace-scoped keys (including remote-connector tokens) do not carry — use an ' +
|
|
267
|
+
'ACCOUNT key (PAT) of the owner, or ask the user to publish from the share dialog. ' +
|
|
268
|
+
'Returns the slug/URL. Check rootr_document_public_status first; unpublish via rootr_set_document_public.',
|
|
269
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
270
|
+
inputSchema: {
|
|
271
|
+
target: z.string().describe('Document path (starting with "/") or node id'),
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
async ({ target }) => {
|
|
275
|
+
try {
|
|
276
|
+
const client = makeClient();
|
|
277
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
278
|
+
return jsonResult(await client.publishDocument(id));
|
|
279
|
+
} catch (err) {
|
|
280
|
+
return errorResult(err);
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
server.registerTool(
|
|
286
|
+
'rootr_set_document_public',
|
|
287
|
+
{
|
|
288
|
+
title: 'Update public settings of a Rootr document',
|
|
289
|
+
description:
|
|
290
|
+
'Update a published Rootr (루터) document\'s public settings — e.g. {"isPublic": false} to unpublish. ' +
|
|
291
|
+
'Owner-level action (MANAGE): works with an account key (PAT), not with workspace-scoped/connector tokens.',
|
|
292
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
293
|
+
inputSchema: {
|
|
294
|
+
target: z.string().describe('Document path (starting with "/") or node id'),
|
|
295
|
+
isPublic: z.boolean().optional().describe('false to unpublish, true to re-enable'),
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
async ({ target, isPublic }) => {
|
|
299
|
+
try {
|
|
300
|
+
const client = makeClient();
|
|
301
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
302
|
+
return jsonResult(await client.setDocumentPublic(id, { isPublic }));
|
|
303
|
+
} catch (err) {
|
|
304
|
+
return errorResult(err);
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
server.registerTool(
|
|
310
|
+
'rootr_document_public_status',
|
|
311
|
+
{
|
|
312
|
+
title: 'Public status of a Rootr document',
|
|
313
|
+
description: 'Whether a Rootr (루터) document is published, and at which public slug/URL.',
|
|
314
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
315
|
+
inputSchema: {
|
|
316
|
+
target: z.string().describe('Document path (starting with "/") or node id'),
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
async ({ target }) => {
|
|
320
|
+
try {
|
|
321
|
+
const client = makeClient();
|
|
322
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
323
|
+
return jsonResult(await client.getDocumentPublicStatus(id));
|
|
324
|
+
} catch (err) {
|
|
325
|
+
return errorResult(err);
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
server.registerTool(
|
|
331
|
+
'rootr_duplicate_node',
|
|
332
|
+
{
|
|
333
|
+
title: 'Duplicate a Rootr node (deep copy)',
|
|
334
|
+
description:
|
|
335
|
+
'Deep-copy a Rootr (루터) node — a document or a whole folder tree — next to the original, or into ' +
|
|
336
|
+
'another workspace the user can access (targetWorkspaceId). Returns the new root node.',
|
|
337
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
338
|
+
inputSchema: {
|
|
339
|
+
target: z.string().describe('Node path (starting with "/") or node id to copy'),
|
|
340
|
+
deep: z.boolean().optional().describe('Copy children too (default true)'),
|
|
341
|
+
targetWorkspaceId: z.string().optional().describe('Destination workspace id (defaults to same workspace)'),
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
async ({ target, deep, targetWorkspaceId }) => {
|
|
345
|
+
try {
|
|
346
|
+
const client = makeClient();
|
|
347
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
348
|
+
return jsonResult(await client.duplicateNode(id, { deep, targetWorkspaceId }));
|
|
349
|
+
} catch (err) {
|
|
350
|
+
return errorResult(err);
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
);
|
|
193
354
|
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
import { jsonResult, errorResult, makeClient, requireWorkspace, resolveNodeId, NODE_TARGET_HINT, resolveOptionalNodeId } from './shared.js';
|
|
4
|
+
|
|
5
|
+
const fieldShape = z
|
|
6
|
+
.object({
|
|
7
|
+
id: z.string().optional().describe('Stable field id (also the key under response `values`). Server-generated if omitted.'),
|
|
8
|
+
name: z.string().describe('Field label shown to respondents'),
|
|
9
|
+
type: z
|
|
10
|
+
.enum(['text', 'textarea', 'number', 'select', 'multi_select', 'checkbox', 'date'])
|
|
11
|
+
.describe('Field input type'),
|
|
12
|
+
required: z.boolean().optional().describe('Whether the field must be filled in'),
|
|
13
|
+
options: z
|
|
14
|
+
.array(z.object({ name: z.string(), color: z.string() }))
|
|
15
|
+
.optional()
|
|
16
|
+
.describe('Choices for select/multi_select fields'),
|
|
17
|
+
})
|
|
18
|
+
.describe('One form field definition.');
|
|
19
|
+
|
|
20
|
+
const FIELDS_GUIDE =
|
|
21
|
+
'Fields are typed inputs shown to respondents: text | textarea | number | select | multi_select | checkbox | date; ' +
|
|
22
|
+
'select/multi_select need `options` ({name,color}[]). Read the form first (rootr_read_form) before updating so ' +
|
|
23
|
+
'you keep each field\'s `id` stable — response values are keyed by field id, so renaming/dropping an id orphans ' +
|
|
24
|
+
'past responses\' answers for that field.';
|
|
25
|
+
|
|
26
|
+
/** FORM node tools: create/read/update the field definitions, list/submit responses, manage public share links. */
|
|
27
|
+
export function registerFormTools(server) {
|
|
28
|
+
server.registerTool(
|
|
29
|
+
'rootr_create_form',
|
|
30
|
+
{
|
|
31
|
+
title: 'Create a Rootr form',
|
|
32
|
+
description:
|
|
33
|
+
'Create a new FORM node (fillable intake) in a Rootr (루터) workspace, optionally seeded with field ' +
|
|
34
|
+
'definitions. Use FORM for surveys/request queues collected from people outside the tree — set ' +
|
|
35
|
+
'`targetDatabaseId` to also mirror each response into a DATABASE\'s rows. ' +
|
|
36
|
+
FIELDS_GUIDE,
|
|
37
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
38
|
+
inputSchema: {
|
|
39
|
+
name: z.string().optional().describe('Form name/title'),
|
|
40
|
+
parentId: z.string().optional().describe(`Parent folder/node id${NODE_TARGET_HINT}`),
|
|
41
|
+
icon: z.string().optional().describe('Emoji icon for the node'),
|
|
42
|
+
fields: z.array(fieldShape).optional().describe('Initial field definitions; defaults to an empty form'),
|
|
43
|
+
targetDatabaseId: z.string().optional().describe(`DATABASE node id${NODE_TARGET_HINT} whose rows mirror this form's responses`),
|
|
44
|
+
config: z.record(z.unknown()).optional().describe('Free-form form config object (e.g. publicSubmit mode)'),
|
|
45
|
+
workspace: z.string().optional().describe('Workspace id; defaults to ROOTR_WORKSPACE/config if omitted'),
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
async ({ name, parentId, icon, fields, targetDatabaseId, config, workspace }) => {
|
|
49
|
+
try {
|
|
50
|
+
const ws = requireWorkspace(workspace);
|
|
51
|
+
const client = makeClient(ws);
|
|
52
|
+
targetDatabaseId = await resolveOptionalNodeId(client, targetDatabaseId);
|
|
53
|
+
parentId = await resolveOptionalNodeId(client, parentId);
|
|
54
|
+
const result = await client.createForm(ws, { name, parentId, icon, fields, targetDatabaseId, config });
|
|
55
|
+
return jsonResult(result);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
return errorResult(err);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
server.registerTool(
|
|
63
|
+
'rootr_read_form',
|
|
64
|
+
{
|
|
65
|
+
title: 'Read a Rootr form',
|
|
66
|
+
description:
|
|
67
|
+
'Read a Rootr (루터) FORM node — its field definitions, targetDatabaseId, and config. Call this before ' +
|
|
68
|
+
'rootr_update_form so you keep existing field ids stable, or before rootr_list_form_responses to know ' +
|
|
69
|
+
'which field ids the response `values` are keyed by.',
|
|
70
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
71
|
+
inputSchema: {
|
|
72
|
+
formId: z.string().describe(`FORM node id${NODE_TARGET_HINT}`),
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
async ({ formId }) => {
|
|
76
|
+
try {
|
|
77
|
+
const client = makeClient();
|
|
78
|
+
formId = await resolveNodeId(client, formId);
|
|
79
|
+
const result = await client.getForm(formId);
|
|
80
|
+
return jsonResult(result);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
return errorResult(err);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
server.registerTool(
|
|
88
|
+
'rootr_update_form',
|
|
89
|
+
{
|
|
90
|
+
title: 'Update a Rootr form (fields/targetDatabaseId/config)',
|
|
91
|
+
description:
|
|
92
|
+
'Merge-patch a Rootr (루터) FORM: if you pass `fields`, it REPLACES the whole fields array — call ' +
|
|
93
|
+
'rootr_read_form first and resend fields you want to keep (with their existing ids) so past responses ' +
|
|
94
|
+
'stay attributable. Pass `targetDatabaseId: null` to unlink the mirrored DATABASE. ' +
|
|
95
|
+
FIELDS_GUIDE,
|
|
96
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
97
|
+
inputSchema: {
|
|
98
|
+
formId: z.string().describe(`FORM node id${NODE_TARGET_HINT}`),
|
|
99
|
+
fields: z.array(fieldShape).optional().describe('COMPLETE fields array to set — omitted existing fields will be dropped'),
|
|
100
|
+
targetDatabaseId: z.string().nullable().optional().describe(`DATABASE node id${NODE_TARGET_HINT} to mirror responses into, or null to unlink`),
|
|
101
|
+
config: z.record(z.unknown()).optional().describe('Free-form form config object (e.g. publicSubmit mode)'),
|
|
102
|
+
icon: z.string().optional().describe('Emoji icon for the node'),
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
async ({ formId, fields, targetDatabaseId, config, icon }) => {
|
|
106
|
+
try {
|
|
107
|
+
const client = makeClient();
|
|
108
|
+
targetDatabaseId = await resolveOptionalNodeId(client, targetDatabaseId);
|
|
109
|
+
formId = await resolveNodeId(client, formId);
|
|
110
|
+
const result = await client.updateForm(formId, { fields, targetDatabaseId, config, icon });
|
|
111
|
+
return jsonResult(result);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
return errorResult(err);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
server.registerTool(
|
|
119
|
+
'rootr_list_form_responses',
|
|
120
|
+
{
|
|
121
|
+
title: 'List a Rootr form\'s submitted responses',
|
|
122
|
+
description:
|
|
123
|
+
'List all responses submitted to a Rootr (루터) FORM, most recent first. Each response has `values` keyed ' +
|
|
124
|
+
'by field id — call rootr_read_form first to map ids to field names. This is what lets an AI analyze ' +
|
|
125
|
+
'responses collected from a form.',
|
|
126
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
127
|
+
inputSchema: {
|
|
128
|
+
formId: z.string().describe(`FORM node id${NODE_TARGET_HINT}`),
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
async ({ formId }) => {
|
|
132
|
+
try {
|
|
133
|
+
const client = makeClient();
|
|
134
|
+
formId = await resolveNodeId(client, formId);
|
|
135
|
+
const result = await client.listFormResponses(formId);
|
|
136
|
+
return jsonResult({ responses: result });
|
|
137
|
+
} catch (err) {
|
|
138
|
+
return errorResult(err);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
server.registerTool(
|
|
144
|
+
'rootr_submit_form_response',
|
|
145
|
+
{
|
|
146
|
+
title: 'Submit a response to a Rootr form',
|
|
147
|
+
description:
|
|
148
|
+
'Submit one response to a Rootr (루터) FORM on behalf of the caller (authenticated submission, not the ' +
|
|
149
|
+
'public/anonymous share-link flow). `values` is keyed by field id — call rootr_read_form first to get the ' +
|
|
150
|
+
'exact field ids and types.',
|
|
151
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
152
|
+
inputSchema: {
|
|
153
|
+
formId: z.string().describe(`FORM node id${NODE_TARGET_HINT}`),
|
|
154
|
+
values: z.record(z.unknown()).describe('Response values keyed by field id'),
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
async ({ formId, values }) => {
|
|
158
|
+
try {
|
|
159
|
+
const client = makeClient();
|
|
160
|
+
formId = await resolveNodeId(client, formId);
|
|
161
|
+
const result = await client.createFormResponse(formId, values);
|
|
162
|
+
return jsonResult(result);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
return errorResult(err);
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
server.registerTool(
|
|
170
|
+
'rootr_create_form_share_link',
|
|
171
|
+
{
|
|
172
|
+
title: 'Issue a public share link for a Rootr form',
|
|
173
|
+
description:
|
|
174
|
+
'Issue a new public share link/token for a Rootr (루터) FORM so people outside the workspace can submit ' +
|
|
175
|
+
'responses (via the /f/{token} page). The form\'s public-submit mode itself is controlled separately via ' +
|
|
176
|
+
'rootr_update_form\'s config.publicSubmit ("off" | "login" | "anon").',
|
|
177
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
178
|
+
inputSchema: {
|
|
179
|
+
formId: z.string().describe(`FORM node id${NODE_TARGET_HINT}`),
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
async ({ formId }) => {
|
|
183
|
+
try {
|
|
184
|
+
const client = makeClient();
|
|
185
|
+
formId = await resolveNodeId(client, formId);
|
|
186
|
+
const result = await client.createFormShareLink(formId);
|
|
187
|
+
return jsonResult(result);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
return errorResult(err);
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
server.registerTool(
|
|
195
|
+
'rootr_list_form_share_links',
|
|
196
|
+
{
|
|
197
|
+
title: 'List a Rootr form\'s public share links',
|
|
198
|
+
description: 'List existing public share links/tokens issued for a Rootr (루터) FORM.',
|
|
199
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
200
|
+
inputSchema: {
|
|
201
|
+
formId: z.string().describe(`FORM node id${NODE_TARGET_HINT}`),
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
async ({ formId }) => {
|
|
205
|
+
try {
|
|
206
|
+
const client = makeClient();
|
|
207
|
+
formId = await resolveNodeId(client, formId);
|
|
208
|
+
const result = await client.listFormShareLinks(formId);
|
|
209
|
+
return jsonResult({ links: result });
|
|
210
|
+
} catch (err) {
|
|
211
|
+
return errorResult(err);
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
);
|
|
215
|
+
}
|
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 } from './shared.js';
|
|
3
|
+
import { textResult, jsonResult, errorResult, makeClient, requireWorkspace, resolveNodeId, NODE_TARGET_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) {
|
|
@@ -11,6 +11,7 @@ export function registerIssueTools(server) {
|
|
|
11
11
|
description:
|
|
12
12
|
'Create a new GitHub-style issue tracker document in a Rootr (루터) workspace. Returns the tracker with ' +
|
|
13
13
|
'its id, plus its default labels/types — use rootr_create_issue afterward to add issues to it.',
|
|
14
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
14
15
|
inputSchema: {
|
|
15
16
|
name: z.string().describe('Name of the issue tracker'),
|
|
16
17
|
workspace: z
|
|
@@ -38,8 +39,9 @@ export function registerIssueTools(server) {
|
|
|
38
39
|
description:
|
|
39
40
|
'List issues in a Rootr (루터) issue tracker, optionally filtered by state (OPEN/CLOSED/ALL), label id, ' +
|
|
40
41
|
'type id, or a text query. Use rootr_get_issue for full details of one issue.',
|
|
42
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
41
43
|
inputSchema: {
|
|
42
|
-
trackerId: z.string().describe(
|
|
44
|
+
trackerId: z.string().describe(`Issue tracker node id${NODE_TARGET_HINT}`),
|
|
43
45
|
state: z.string().optional().describe('OPEN | CLOSED | ALL'),
|
|
44
46
|
q: z.string().optional().describe('Free-text search within issues'),
|
|
45
47
|
label: z.string().optional().describe('Label id to filter by'),
|
|
@@ -49,6 +51,7 @@ export function registerIssueTools(server) {
|
|
|
49
51
|
async ({ trackerId, state, q, label, type }) => {
|
|
50
52
|
try {
|
|
51
53
|
const client = makeClient();
|
|
54
|
+
trackerId = await resolveNodeId(client, trackerId);
|
|
52
55
|
const result = await client.listIssues(trackerId, { state, q, label, type });
|
|
53
56
|
return jsonResult(result);
|
|
54
57
|
} catch (err) {
|
|
@@ -65,8 +68,9 @@ export function registerIssueTools(server) {
|
|
|
65
68
|
'Create a new issue in a Rootr (루터) issue tracker. labels is an array of label ids, type is a type id ' +
|
|
66
69
|
'(both from the tracker\'s custom type/label definitions — see rootr_read on the tracker or its metadata). ' +
|
|
67
70
|
'parentIssueId nests this issue under another issue (sub-issue).',
|
|
71
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
68
72
|
inputSchema: {
|
|
69
|
-
trackerId: z.string().describe(
|
|
73
|
+
trackerId: z.string().describe(`Issue tracker node id${NODE_TARGET_HINT}`),
|
|
70
74
|
title: z.string().describe('Issue title'),
|
|
71
75
|
body: z.string().optional().describe('Issue body (markdown)'),
|
|
72
76
|
labels: z.array(z.string()).optional().describe('Label ids to attach'),
|
|
@@ -77,6 +81,7 @@ export function registerIssueTools(server) {
|
|
|
77
81
|
async ({ trackerId, title, body, labels, type, parentIssueId }) => {
|
|
78
82
|
try {
|
|
79
83
|
const client = makeClient();
|
|
84
|
+
trackerId = await resolveNodeId(client, trackerId);
|
|
80
85
|
const result = await client.createIssue(trackerId, { title, body, labels, type, parentIssueId });
|
|
81
86
|
return jsonResult(result);
|
|
82
87
|
} catch (err) {
|
|
@@ -90,6 +95,7 @@ export function registerIssueTools(server) {
|
|
|
90
95
|
{
|
|
91
96
|
title: 'Get an issue',
|
|
92
97
|
description: 'Fetch full details of a single Rootr (루터) issue by id.',
|
|
98
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
93
99
|
inputSchema: {
|
|
94
100
|
issueId: z.string().describe('Issue id'),
|
|
95
101
|
},
|
|
@@ -112,6 +118,7 @@ export function registerIssueTools(server) {
|
|
|
112
118
|
description:
|
|
113
119
|
'Update arbitrary fields of a Rootr (루터) issue. To close an issue, set state to "CLOSED" and stateReason ' +
|
|
114
120
|
'to one of completed | not_planned | duplicate.',
|
|
121
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
115
122
|
inputSchema: {
|
|
116
123
|
issueId: z.string().describe('Issue id'),
|
|
117
124
|
title: z.string().optional(),
|
|
@@ -143,8 +150,9 @@ export function registerIssueTools(server) {
|
|
|
143
150
|
description:
|
|
144
151
|
'Add a comment to a Rootr (루터) issue within its tracker. Comments carry a user identity, ' +
|
|
145
152
|
'so this requires an ACCOUNT key (PAT) as ROOTR_API_KEY — a workspace key gets a 403.',
|
|
153
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
146
154
|
inputSchema: {
|
|
147
|
-
trackerId: z.string().describe(
|
|
155
|
+
trackerId: z.string().describe(`Issue tracker node id${NODE_TARGET_HINT}`),
|
|
148
156
|
issueId: z.string().describe('Issue id to comment on'),
|
|
149
157
|
body: z.string().describe('Comment body (markdown)'),
|
|
150
158
|
},
|
|
@@ -152,6 +160,7 @@ export function registerIssueTools(server) {
|
|
|
152
160
|
async ({ trackerId, issueId, body }) => {
|
|
153
161
|
try {
|
|
154
162
|
const client = makeClient();
|
|
163
|
+
trackerId = await resolveNodeId(client, trackerId);
|
|
155
164
|
const result = await client.commentIssue(trackerId, issueId, body);
|
|
156
165
|
return textResult(typeof result === 'string' ? result : JSON.stringify(result, null, 2));
|
|
157
166
|
} catch (err) {
|