rootr-cli 0.4.0 → 0.5.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 +40 -3
- package/lib/client.js +18 -0
- package/lib/mcp-tools/attachments.js +3 -2
- package/lib/mcp-tools/crm.js +29 -15
- package/lib/mcp-tools/databases.js +20 -14
- package/lib/mcp-tools/documents.js +56 -6
- package/lib/mcp-tools/forms.js +19 -10
- package/lib/mcp-tools/issues.js +7 -4
- package/lib/mcp-tools/logs.js +11 -6
- package/lib/mcp-tools/presentations.js +103 -9
- package/lib/mcp-tools/recordings.js +15 -8
- package/lib/mcp-tools/shared.js +26 -0
- package/lib/mcp-tools/spreadsheets.js +15 -8
- package/lib/mcp-tools/webhooks.js +3 -2
- package/lib/mcp-tools/whiteboards.js +7 -4
- package/package.json +1 -1
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 } from './shared.js';
|
|
3
|
+
import { jsonResult, errorResult, makeClient, requireWorkspace, resolveNodeId, NODE_TARGET_HINT, resolveOptionalNodeId } from './shared.js';
|
|
4
4
|
|
|
5
5
|
const fieldShape = z
|
|
6
6
|
.object({
|
|
@@ -37,10 +37,10 @@ export function registerFormTools(server) {
|
|
|
37
37
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
38
38
|
inputSchema: {
|
|
39
39
|
name: z.string().optional().describe('Form name/title'),
|
|
40
|
-
parentId: z.string().optional().describe(
|
|
40
|
+
parentId: z.string().optional().describe(`Parent folder/node id${NODE_TARGET_HINT}`),
|
|
41
41
|
icon: z.string().optional().describe('Emoji icon for the node'),
|
|
42
42
|
fields: z.array(fieldShape).optional().describe('Initial field definitions; defaults to an empty form'),
|
|
43
|
-
targetDatabaseId: z.string().optional().describe(
|
|
43
|
+
targetDatabaseId: z.string().optional().describe(`DATABASE node id${NODE_TARGET_HINT} whose rows mirror this form's responses`),
|
|
44
44
|
config: z.record(z.unknown()).optional().describe('Free-form form config object (e.g. publicSubmit mode)'),
|
|
45
45
|
workspace: z.string().optional().describe('Workspace id; defaults to ROOTR_WORKSPACE/config if omitted'),
|
|
46
46
|
},
|
|
@@ -49,6 +49,8 @@ export function registerFormTools(server) {
|
|
|
49
49
|
try {
|
|
50
50
|
const ws = requireWorkspace(workspace);
|
|
51
51
|
const client = makeClient(ws);
|
|
52
|
+
targetDatabaseId = await resolveOptionalNodeId(client, targetDatabaseId);
|
|
53
|
+
parentId = await resolveOptionalNodeId(client, parentId);
|
|
52
54
|
const result = await client.createForm(ws, { name, parentId, icon, fields, targetDatabaseId, config });
|
|
53
55
|
return jsonResult(result);
|
|
54
56
|
} catch (err) {
|
|
@@ -67,12 +69,13 @@ export function registerFormTools(server) {
|
|
|
67
69
|
'which field ids the response `values` are keyed by.',
|
|
68
70
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
69
71
|
inputSchema: {
|
|
70
|
-
formId: z.string().describe(
|
|
72
|
+
formId: z.string().describe(`FORM node id${NODE_TARGET_HINT}`),
|
|
71
73
|
},
|
|
72
74
|
},
|
|
73
75
|
async ({ formId }) => {
|
|
74
76
|
try {
|
|
75
77
|
const client = makeClient();
|
|
78
|
+
formId = await resolveNodeId(client, formId);
|
|
76
79
|
const result = await client.getForm(formId);
|
|
77
80
|
return jsonResult(result);
|
|
78
81
|
} catch (err) {
|
|
@@ -92,9 +95,9 @@ export function registerFormTools(server) {
|
|
|
92
95
|
FIELDS_GUIDE,
|
|
93
96
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
94
97
|
inputSchema: {
|
|
95
|
-
formId: z.string().describe(
|
|
98
|
+
formId: z.string().describe(`FORM node id${NODE_TARGET_HINT}`),
|
|
96
99
|
fields: z.array(fieldShape).optional().describe('COMPLETE fields array to set — omitted existing fields will be dropped'),
|
|
97
|
-
targetDatabaseId: z.string().nullable().optional().describe(
|
|
100
|
+
targetDatabaseId: z.string().nullable().optional().describe(`DATABASE node id${NODE_TARGET_HINT} to mirror responses into, or null to unlink`),
|
|
98
101
|
config: z.record(z.unknown()).optional().describe('Free-form form config object (e.g. publicSubmit mode)'),
|
|
99
102
|
icon: z.string().optional().describe('Emoji icon for the node'),
|
|
100
103
|
},
|
|
@@ -102,6 +105,8 @@ export function registerFormTools(server) {
|
|
|
102
105
|
async ({ formId, fields, targetDatabaseId, config, icon }) => {
|
|
103
106
|
try {
|
|
104
107
|
const client = makeClient();
|
|
108
|
+
targetDatabaseId = await resolveOptionalNodeId(client, targetDatabaseId);
|
|
109
|
+
formId = await resolveNodeId(client, formId);
|
|
105
110
|
const result = await client.updateForm(formId, { fields, targetDatabaseId, config, icon });
|
|
106
111
|
return jsonResult(result);
|
|
107
112
|
} catch (err) {
|
|
@@ -120,12 +125,13 @@ export function registerFormTools(server) {
|
|
|
120
125
|
'responses collected from a form.',
|
|
121
126
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
122
127
|
inputSchema: {
|
|
123
|
-
formId: z.string().describe(
|
|
128
|
+
formId: z.string().describe(`FORM node id${NODE_TARGET_HINT}`),
|
|
124
129
|
},
|
|
125
130
|
},
|
|
126
131
|
async ({ formId }) => {
|
|
127
132
|
try {
|
|
128
133
|
const client = makeClient();
|
|
134
|
+
formId = await resolveNodeId(client, formId);
|
|
129
135
|
const result = await client.listFormResponses(formId);
|
|
130
136
|
return jsonResult({ responses: result });
|
|
131
137
|
} catch (err) {
|
|
@@ -144,13 +150,14 @@ export function registerFormTools(server) {
|
|
|
144
150
|
'exact field ids and types.',
|
|
145
151
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
146
152
|
inputSchema: {
|
|
147
|
-
formId: z.string().describe(
|
|
153
|
+
formId: z.string().describe(`FORM node id${NODE_TARGET_HINT}`),
|
|
148
154
|
values: z.record(z.unknown()).describe('Response values keyed by field id'),
|
|
149
155
|
},
|
|
150
156
|
},
|
|
151
157
|
async ({ formId, values }) => {
|
|
152
158
|
try {
|
|
153
159
|
const client = makeClient();
|
|
160
|
+
formId = await resolveNodeId(client, formId);
|
|
154
161
|
const result = await client.createFormResponse(formId, values);
|
|
155
162
|
return jsonResult(result);
|
|
156
163
|
} catch (err) {
|
|
@@ -169,12 +176,13 @@ export function registerFormTools(server) {
|
|
|
169
176
|
'rootr_update_form\'s config.publicSubmit ("off" | "login" | "anon").',
|
|
170
177
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
171
178
|
inputSchema: {
|
|
172
|
-
formId: z.string().describe(
|
|
179
|
+
formId: z.string().describe(`FORM node id${NODE_TARGET_HINT}`),
|
|
173
180
|
},
|
|
174
181
|
},
|
|
175
182
|
async ({ formId }) => {
|
|
176
183
|
try {
|
|
177
184
|
const client = makeClient();
|
|
185
|
+
formId = await resolveNodeId(client, formId);
|
|
178
186
|
const result = await client.createFormShareLink(formId);
|
|
179
187
|
return jsonResult(result);
|
|
180
188
|
} catch (err) {
|
|
@@ -190,12 +198,13 @@ export function registerFormTools(server) {
|
|
|
190
198
|
description: 'List existing public share links/tokens issued for a Rootr (루터) FORM.',
|
|
191
199
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
192
200
|
inputSchema: {
|
|
193
|
-
formId: z.string().describe(
|
|
201
|
+
formId: z.string().describe(`FORM node id${NODE_TARGET_HINT}`),
|
|
194
202
|
},
|
|
195
203
|
},
|
|
196
204
|
async ({ formId }) => {
|
|
197
205
|
try {
|
|
198
206
|
const client = makeClient();
|
|
207
|
+
formId = await resolveNodeId(client, formId);
|
|
199
208
|
const result = await client.listFormShareLinks(formId);
|
|
200
209
|
return jsonResult({ links: result });
|
|
201
210
|
} catch (err) {
|
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) {
|
|
@@ -41,7 +41,7 @@ export function registerIssueTools(server) {
|
|
|
41
41
|
'type id, or a text query. Use rootr_get_issue for full details of one issue.',
|
|
42
42
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
43
43
|
inputSchema: {
|
|
44
|
-
trackerId: z.string().describe(
|
|
44
|
+
trackerId: z.string().describe(`Issue tracker node id${NODE_TARGET_HINT}`),
|
|
45
45
|
state: z.string().optional().describe('OPEN | CLOSED | ALL'),
|
|
46
46
|
q: z.string().optional().describe('Free-text search within issues'),
|
|
47
47
|
label: z.string().optional().describe('Label id to filter by'),
|
|
@@ -51,6 +51,7 @@ export function registerIssueTools(server) {
|
|
|
51
51
|
async ({ trackerId, state, q, label, type }) => {
|
|
52
52
|
try {
|
|
53
53
|
const client = makeClient();
|
|
54
|
+
trackerId = await resolveNodeId(client, trackerId);
|
|
54
55
|
const result = await client.listIssues(trackerId, { state, q, label, type });
|
|
55
56
|
return jsonResult(result);
|
|
56
57
|
} catch (err) {
|
|
@@ -69,7 +70,7 @@ export function registerIssueTools(server) {
|
|
|
69
70
|
'parentIssueId nests this issue under another issue (sub-issue).',
|
|
70
71
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
71
72
|
inputSchema: {
|
|
72
|
-
trackerId: z.string().describe(
|
|
73
|
+
trackerId: z.string().describe(`Issue tracker node id${NODE_TARGET_HINT}`),
|
|
73
74
|
title: z.string().describe('Issue title'),
|
|
74
75
|
body: z.string().optional().describe('Issue body (markdown)'),
|
|
75
76
|
labels: z.array(z.string()).optional().describe('Label ids to attach'),
|
|
@@ -80,6 +81,7 @@ export function registerIssueTools(server) {
|
|
|
80
81
|
async ({ trackerId, title, body, labels, type, parentIssueId }) => {
|
|
81
82
|
try {
|
|
82
83
|
const client = makeClient();
|
|
84
|
+
trackerId = await resolveNodeId(client, trackerId);
|
|
83
85
|
const result = await client.createIssue(trackerId, { title, body, labels, type, parentIssueId });
|
|
84
86
|
return jsonResult(result);
|
|
85
87
|
} catch (err) {
|
|
@@ -150,7 +152,7 @@ export function registerIssueTools(server) {
|
|
|
150
152
|
'so this requires an ACCOUNT key (PAT) as ROOTR_API_KEY — a workspace key gets a 403.',
|
|
151
153
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
152
154
|
inputSchema: {
|
|
153
|
-
trackerId: z.string().describe(
|
|
155
|
+
trackerId: z.string().describe(`Issue tracker node id${NODE_TARGET_HINT}`),
|
|
154
156
|
issueId: z.string().describe('Issue id to comment on'),
|
|
155
157
|
body: z.string().describe('Comment body (markdown)'),
|
|
156
158
|
},
|
|
@@ -158,6 +160,7 @@ export function registerIssueTools(server) {
|
|
|
158
160
|
async ({ trackerId, issueId, body }) => {
|
|
159
161
|
try {
|
|
160
162
|
const client = makeClient();
|
|
163
|
+
trackerId = await resolveNodeId(client, trackerId);
|
|
161
164
|
const result = await client.commentIssue(trackerId, issueId, body);
|
|
162
165
|
return textResult(typeof result === 'string' ? result : JSON.stringify(result, null, 2));
|
|
163
166
|
} catch (err) {
|
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 } from './shared.js';
|
|
3
|
+
import { jsonResult, errorResult, makeClient, requireWorkspace, resolveNodeId, NODE_TARGET_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) {
|
|
@@ -22,7 +22,7 @@ export function registerLogTools(server) {
|
|
|
22
22
|
'Field definitions: [{name, type, ...}]. type=string|int|float|bool|datetime|json|enum|level|relation. ' +
|
|
23
23
|
'relation fields add target (node id), many?, relation?.',
|
|
24
24
|
),
|
|
25
|
-
parentId: z.string().optional().describe(
|
|
25
|
+
parentId: z.string().optional().describe(`Parent node id${NODE_TARGET_HINT}`),
|
|
26
26
|
workspace: z
|
|
27
27
|
.string()
|
|
28
28
|
.optional()
|
|
@@ -33,6 +33,7 @@ export function registerLogTools(server) {
|
|
|
33
33
|
try {
|
|
34
34
|
const ws = requireWorkspace(workspace);
|
|
35
35
|
const client = makeClient(ws);
|
|
36
|
+
parentId = await resolveOptionalNodeId(client, parentId);
|
|
36
37
|
const result = await client.createLogStore(ws, { name, fields, parentId });
|
|
37
38
|
return jsonResult(result);
|
|
38
39
|
} catch (err) {
|
|
@@ -51,7 +52,7 @@ export function registerLogTools(server) {
|
|
|
51
52
|
'Read the LOG store first (e.g. via rootr_read or a get-log call) to see its current fields before calling this.',
|
|
52
53
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
53
54
|
inputSchema: {
|
|
54
|
-
logId: z.string().describe(
|
|
55
|
+
logId: z.string().describe(`LOG store node id${NODE_TARGET_HINT}`),
|
|
55
56
|
fields: z
|
|
56
57
|
.array(z.record(z.any()))
|
|
57
58
|
.describe('COMPLETE field list to set — omitted existing fields will be dropped'),
|
|
@@ -60,6 +61,7 @@ export function registerLogTools(server) {
|
|
|
60
61
|
async ({ logId, fields }) => {
|
|
61
62
|
try {
|
|
62
63
|
const client = makeClient();
|
|
64
|
+
logId = await resolveNodeId(client, logId);
|
|
63
65
|
const result = await client.updateLogFields(logId, fields);
|
|
64
66
|
return jsonResult(result);
|
|
65
67
|
} catch (err) {
|
|
@@ -77,7 +79,7 @@ export function registerLogTools(server) {
|
|
|
77
79
|
'(shaped per the store\'s field schema). This is additive — existing entries are untouched.',
|
|
78
80
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
79
81
|
inputSchema: {
|
|
80
|
-
logId: z.string().describe(
|
|
82
|
+
logId: z.string().describe(`LOG store node id${NODE_TARGET_HINT}`),
|
|
81
83
|
entries: z
|
|
82
84
|
.array(z.record(z.any()))
|
|
83
85
|
.describe('Entries to add: [{ts?, source?, level?, message?, data?}, ...]'),
|
|
@@ -86,6 +88,7 @@ export function registerLogTools(server) {
|
|
|
86
88
|
async ({ logId, entries }) => {
|
|
87
89
|
try {
|
|
88
90
|
const client = makeClient();
|
|
91
|
+
logId = await resolveNodeId(client, logId);
|
|
89
92
|
const result = await client.addLogEntries(logId, entries);
|
|
90
93
|
return jsonResult(result);
|
|
91
94
|
} catch (err) {
|
|
@@ -103,7 +106,7 @@ export function registerLogTools(server) {
|
|
|
103
106
|
'Use this to inspect recent activity or find anomalous entries before asking rootr_ask for root-cause analysis.',
|
|
104
107
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
105
108
|
inputSchema: {
|
|
106
|
-
logId: z.string().describe(
|
|
109
|
+
logId: z.string().describe(`LOG store node id${NODE_TARGET_HINT}`),
|
|
107
110
|
from: z.string().optional().describe('ISO timestamp lower bound'),
|
|
108
111
|
to: z.string().optional().describe('ISO timestamp upper bound'),
|
|
109
112
|
source: z.string().optional().describe('Filter by source'),
|
|
@@ -116,6 +119,7 @@ export function registerLogTools(server) {
|
|
|
116
119
|
async ({ logId, from, to, source, level, anomalyOnly, limit, order }) => {
|
|
117
120
|
try {
|
|
118
121
|
const client = makeClient();
|
|
122
|
+
logId = await resolveNodeId(client, logId);
|
|
119
123
|
const result = await client.queryLogEntries(logId, {
|
|
120
124
|
from,
|
|
121
125
|
to,
|
|
@@ -142,7 +146,7 @@ export function registerLogTools(server) {
|
|
|
142
146
|
'questions without pulling raw entries.',
|
|
143
147
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
144
148
|
inputSchema: {
|
|
145
|
-
logId: z.string().describe(
|
|
149
|
+
logId: z.string().describe(`LOG store node id${NODE_TARGET_HINT}`),
|
|
146
150
|
groupBy: z.string().optional().describe('hour | day | source | level'),
|
|
147
151
|
metric: z.string().optional().describe('count | avg | max | min | sum'),
|
|
148
152
|
field: z.string().optional().describe('Numeric field name, required for avg/max/min/sum'),
|
|
@@ -153,6 +157,7 @@ export function registerLogTools(server) {
|
|
|
153
157
|
async ({ logId, groupBy, metric, field, from, to }) => {
|
|
154
158
|
try {
|
|
155
159
|
const client = makeClient();
|
|
160
|
+
logId = await resolveNodeId(client, logId);
|
|
156
161
|
const result = await client.logStats(logId, { groupBy, metric, field, from, to });
|
|
157
162
|
return jsonResult(result);
|
|
158
163
|
} catch (err) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
-
import { jsonResult, errorResult, makeClient, requireWorkspace } from './shared.js';
|
|
3
|
+
import { jsonResult, errorResult, makeClient, requireWorkspace, resolveNodeId, NODE_TARGET_HINT, resolveOptionalNodeId } from './shared.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Authoring guide, repeated (shortened) across every tool description so an
|
|
@@ -127,7 +127,7 @@ export function registerPresentationTools(server) {
|
|
|
127
127
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
128
128
|
inputSchema: {
|
|
129
129
|
name: z.string().optional().describe('Deck name/title'),
|
|
130
|
-
parentId: z.string().optional().describe(
|
|
130
|
+
parentId: z.string().optional().describe(`Parent folder/node id${NODE_TARGET_HINT}`),
|
|
131
131
|
theme: themeShape,
|
|
132
132
|
slides: z.array(slideShape).optional().describe('Initial slides, in order'),
|
|
133
133
|
config: z.record(z.unknown()).optional().describe('Free-form deck config/settings object'),
|
|
@@ -142,6 +142,7 @@ export function registerPresentationTools(server) {
|
|
|
142
142
|
try {
|
|
143
143
|
const ws = requireWorkspace(workspace);
|
|
144
144
|
const client = makeClient(ws);
|
|
145
|
+
parentId = await resolveOptionalNodeId(client, parentId);
|
|
145
146
|
const result = await client.createPresentation(ws, {
|
|
146
147
|
name,
|
|
147
148
|
parentId,
|
|
@@ -167,12 +168,13 @@ export function registerPresentationTools(server) {
|
|
|
167
168
|
'know the current slide ids and content.',
|
|
168
169
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
169
170
|
inputSchema: {
|
|
170
|
-
presentationId: z.string().describe(
|
|
171
|
+
presentationId: z.string().describe(`PRESENTATION node id${NODE_TARGET_HINT}`),
|
|
171
172
|
},
|
|
172
173
|
},
|
|
173
174
|
async ({ presentationId }) => {
|
|
174
175
|
try {
|
|
175
176
|
const client = makeClient();
|
|
177
|
+
presentationId = await resolveNodeId(client, presentationId);
|
|
176
178
|
const result = await client.getPresentation(presentationId);
|
|
177
179
|
return jsonResult(result);
|
|
178
180
|
} catch (err) {
|
|
@@ -193,7 +195,7 @@ export function registerPresentationTools(server) {
|
|
|
193
195
|
AUTHORING_GUIDE,
|
|
194
196
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
195
197
|
inputSchema: {
|
|
196
|
-
presentationId: z.string().describe(
|
|
198
|
+
presentationId: z.string().describe(`PRESENTATION node id${NODE_TARGET_HINT}`),
|
|
197
199
|
theme: themeShape,
|
|
198
200
|
slides: z
|
|
199
201
|
.array(slideShape)
|
|
@@ -206,6 +208,7 @@ export function registerPresentationTools(server) {
|
|
|
206
208
|
async ({ presentationId, theme, slides, config, icon }) => {
|
|
207
209
|
try {
|
|
208
210
|
const client = makeClient();
|
|
211
|
+
presentationId = await resolveNodeId(client, presentationId);
|
|
209
212
|
const result = await client.updatePresentation(presentationId, {
|
|
210
213
|
theme,
|
|
211
214
|
slides: ensureSlideIds(slides),
|
|
@@ -229,13 +232,14 @@ export function registerPresentationTools(server) {
|
|
|
229
232
|
AUTHORING_GUIDE,
|
|
230
233
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
231
234
|
inputSchema: {
|
|
232
|
-
presentationId: z.string().describe(
|
|
235
|
+
presentationId: z.string().describe(`PRESENTATION node id${NODE_TARGET_HINT}`),
|
|
233
236
|
slides: z.array(slideShape).describe('Slides to append, in order'),
|
|
234
237
|
},
|
|
235
238
|
},
|
|
236
239
|
async ({ presentationId, slides }) => {
|
|
237
240
|
try {
|
|
238
241
|
const client = makeClient();
|
|
242
|
+
presentationId = await resolveNodeId(client, presentationId);
|
|
239
243
|
const result = await client.appendPresentationSlides(presentationId, ensureSlideIds(slides));
|
|
240
244
|
return jsonResult(result);
|
|
241
245
|
} catch (err) {
|
|
@@ -255,7 +259,7 @@ export function registerPresentationTools(server) {
|
|
|
255
259
|
AUTHORING_GUIDE,
|
|
256
260
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
257
261
|
inputSchema: {
|
|
258
|
-
presentationId: z.string().describe(
|
|
262
|
+
presentationId: z.string().describe(`PRESENTATION node id${NODE_TARGET_HINT}`),
|
|
259
263
|
slideId: z.string().describe('Slide id to update (must belong to the deck)'),
|
|
260
264
|
slide: slideShape.describe('Partial slide fields to merge into the existing slide'),
|
|
261
265
|
},
|
|
@@ -263,6 +267,7 @@ export function registerPresentationTools(server) {
|
|
|
263
267
|
async ({ presentationId, slideId, slide }) => {
|
|
264
268
|
try {
|
|
265
269
|
const client = makeClient();
|
|
270
|
+
presentationId = await resolveNodeId(client, presentationId);
|
|
266
271
|
const result = await client.updatePresentationSlide(presentationId, slideId, slide);
|
|
267
272
|
return jsonResult(result);
|
|
268
273
|
} catch (err) {
|
|
@@ -280,13 +285,14 @@ export function registerPresentationTools(server) {
|
|
|
280
285
|
'in the new desired order — call rootr_read_presentation first to get the current ids.',
|
|
281
286
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
282
287
|
inputSchema: {
|
|
283
|
-
presentationId: z.string().describe(
|
|
288
|
+
presentationId: z.string().describe(`PRESENTATION node id${NODE_TARGET_HINT}`),
|
|
284
289
|
order: z.array(z.string()).describe('Complete ordered array of slide ids'),
|
|
285
290
|
},
|
|
286
291
|
},
|
|
287
292
|
async ({ presentationId, order }) => {
|
|
288
293
|
try {
|
|
289
294
|
const client = makeClient();
|
|
295
|
+
presentationId = await resolveNodeId(client, presentationId);
|
|
290
296
|
const result = await client.reorderPresentationSlides(presentationId, order);
|
|
291
297
|
return jsonResult(result);
|
|
292
298
|
} catch (err) {
|
|
@@ -310,7 +316,7 @@ export function registerPresentationTools(server) {
|
|
|
310
316
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
311
317
|
inputSchema: {
|
|
312
318
|
prompt: z.string().describe('English image description; ask for "no text" in the image'),
|
|
313
|
-
nodeId: z.string().optional().describe(
|
|
319
|
+
nodeId: z.string().optional().describe(`Presentation/node id${NODE_TARGET_HINT} to attach the image to (permission-checked)`),
|
|
314
320
|
workspace: z.string().optional().describe('Workspace id; defaults to ROOTR_WORKSPACE/config'),
|
|
315
321
|
},
|
|
316
322
|
},
|
|
@@ -318,6 +324,7 @@ export function registerPresentationTools(server) {
|
|
|
318
324
|
try {
|
|
319
325
|
const client = makeClient();
|
|
320
326
|
const ws = requireWorkspace(workspace);
|
|
327
|
+
nodeId = await resolveOptionalNodeId(client, nodeId);
|
|
321
328
|
const result = await client.generateImage(ws, { prompt, nodeId });
|
|
322
329
|
return jsonResult(result);
|
|
323
330
|
} catch (err) {
|
|
@@ -338,7 +345,7 @@ export function registerPresentationTools(server) {
|
|
|
338
345
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
339
346
|
inputSchema: {
|
|
340
347
|
attachmentId: z.string().describe('Source attachment id (from rootr_generate_image or an upload)'),
|
|
341
|
-
nodeId: z.string().optional().describe(
|
|
348
|
+
nodeId: z.string().optional().describe(`Presentation/node id${NODE_TARGET_HINT} to attach the result to (permission-checked)`),
|
|
342
349
|
workspace: z.string().optional().describe('Workspace id; defaults to ROOTR_WORKSPACE/config'),
|
|
343
350
|
},
|
|
344
351
|
},
|
|
@@ -346,6 +353,7 @@ export function registerPresentationTools(server) {
|
|
|
346
353
|
try {
|
|
347
354
|
const client = makeClient();
|
|
348
355
|
const ws = requireWorkspace(workspace);
|
|
356
|
+
nodeId = await resolveOptionalNodeId(client, nodeId);
|
|
349
357
|
const result = await client.removeImageBackground(ws, { attachmentId, nodeId });
|
|
350
358
|
return jsonResult(result);
|
|
351
359
|
} catch (err) {
|
|
@@ -353,4 +361,90 @@ export function registerPresentationTools(server) {
|
|
|
353
361
|
}
|
|
354
362
|
},
|
|
355
363
|
);
|
|
364
|
+
|
|
365
|
+
server.registerTool(
|
|
366
|
+
'rootr_diagram_schema',
|
|
367
|
+
{
|
|
368
|
+
title: 'Get the spec shape for a diagram type',
|
|
369
|
+
description:
|
|
370
|
+
'Return the JSON Schema and a worked example for one Rootr (루터) diagram family: architecture (components, ' +
|
|
371
|
+
'services, stores, boundaries), workflow (swimlane process with approvals/retries), sequence (call order ' +
|
|
372
|
+
'between participants), dataflow (pipelines, lineage), or lifecycle (states, retries, outcomes). ' +
|
|
373
|
+
'ALWAYS call this before rootr_create_diagram_slide for a family you have not authored yet — every family ' +
|
|
374
|
+
'has a different shape (a workflow wants lanes/nodes/edges, an architecture wants components/connections), ' +
|
|
375
|
+
'and the renderer rejects a spec that does not match. Reading the schema first turns a guess-and-fail into ' +
|
|
376
|
+
'one correct call. Pass no type to list the available families.',
|
|
377
|
+
annotations: { readOnlyHint: true },
|
|
378
|
+
inputSchema: {
|
|
379
|
+
type: z
|
|
380
|
+
.enum(['architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle'])
|
|
381
|
+
.optional()
|
|
382
|
+
.describe('Diagram family; omit to list the available families'),
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
async ({ type }) => {
|
|
386
|
+
try {
|
|
387
|
+
const client = makeClient();
|
|
388
|
+
return jsonResult(type ? await client.diagramSchema(type) : await client.diagramTypes());
|
|
389
|
+
} catch (err) {
|
|
390
|
+
return errorResult(err);
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
server.registerTool(
|
|
396
|
+
'rootr_create_diagram_slide',
|
|
397
|
+
{
|
|
398
|
+
title: 'Draw a diagram onto a slide',
|
|
399
|
+
description:
|
|
400
|
+
'Render a typed diagram spec into a Rootr (루터) presentation slide — architecture, workflow, sequence, ' +
|
|
401
|
+
'dataflow, or lifecycle. The drawing is baked into the slide, so it shows on screen AND in the PDF/PPTX ' +
|
|
402
|
+
'exports. Use this instead of the mermaid `diagram` field, which the slide viewer does not draw.\n' +
|
|
403
|
+
'Call rootr_diagram_schema first to get the exact shape for the family you want — the spec is validated ' +
|
|
404
|
+
'fail-closed and a mismatch is rejected with the offending field named and a suggested fix, which you ' +
|
|
405
|
+
'should apply and retry.\n' +
|
|
406
|
+
'Meaning must live in TEXT: `title` (one assertion-style claim, e.g. "Checkout fails when the cache is ' +
|
|
407
|
+
'cold" — not a topic label like "Checkout"), `blocks[]`, and `notes` are what connects into the knowledge ' +
|
|
408
|
+
'graph. The picture itself is NEVER indexed, so never put a fact only in the drawing.\n' +
|
|
409
|
+
'Pass `slideId` to redraw an existing slide; omit it to append a new one. Quality defaults to "standard"; ' +
|
|
410
|
+
'"showcase" adds a strict composition bar (turn run-ups, legend fit) meant for a hand-tuned hero diagram ' +
|
|
411
|
+
'and will reject specs that "standard" accepts.',
|
|
412
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
413
|
+
inputSchema: {
|
|
414
|
+
presentationId: z.string().describe(`PRESENTATION node id${NODE_TARGET_HINT}`),
|
|
415
|
+
type: z
|
|
416
|
+
.enum(['architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle'])
|
|
417
|
+
.describe('Diagram family — see rootr_diagram_schema'),
|
|
418
|
+
spec: z.record(z.any()).describe('The typed diagram spec matching that family\'s JSON Schema'),
|
|
419
|
+
title: z.string().optional().describe('Assertion-style slide title — text spine, also drawn as the heading'),
|
|
420
|
+
subtitle: z.string().optional().describe('One supporting line drawn under the title'),
|
|
421
|
+
blocks: z
|
|
422
|
+
.array(z.object({ heading: z.string().optional(), body: z.string().optional() }).passthrough())
|
|
423
|
+
.optional()
|
|
424
|
+
.describe('Text spine — the facts the diagram shows, so they reach the knowledge graph'),
|
|
425
|
+
notes: z.string().optional().describe('Speaker notes — also part of the text spine'),
|
|
426
|
+
slideId: z.string().optional().describe('Redraw this slide instead of appending a new one'),
|
|
427
|
+
quality: z.enum(['standard', 'showcase']).optional().describe('Default "standard"'),
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
async ({ presentationId, type, spec, title, subtitle, blocks, notes, slideId, quality }) => {
|
|
431
|
+
try {
|
|
432
|
+
const client = makeClient();
|
|
433
|
+
presentationId = await resolveNodeId(client, presentationId);
|
|
434
|
+
const result = await client.createDiagramSlide(presentationId, {
|
|
435
|
+
type,
|
|
436
|
+
spec,
|
|
437
|
+
title,
|
|
438
|
+
subtitle,
|
|
439
|
+
blocks,
|
|
440
|
+
notes,
|
|
441
|
+
slideId,
|
|
442
|
+
quality,
|
|
443
|
+
});
|
|
444
|
+
return jsonResult(result);
|
|
445
|
+
} catch (err) {
|
|
446
|
+
return errorResult(err);
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
);
|
|
356
450
|
}
|
|
@@ -4,7 +4,7 @@ import { basename, extname, join, isAbsolute } from 'node:path';
|
|
|
4
4
|
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
|
|
7
|
-
import { jsonResult, errorResult, makeClient, requireWorkspace } from './shared.js';
|
|
7
|
+
import { jsonResult, errorResult, makeClient, requireWorkspace, resolveNodeId, NODE_TARGET_HINT, resolveOptionalNodeId } from './shared.js';
|
|
8
8
|
import { RootrApiError } from '../client.js';
|
|
9
9
|
|
|
10
10
|
/** 100MB — the backend's hard limit on a single audio upload. */
|
|
@@ -72,7 +72,7 @@ export function registerRecordingTools(server) {
|
|
|
72
72
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
73
73
|
inputSchema: {
|
|
74
74
|
name: z.string().optional().describe('Recording name/title, e.g. "2026-07-21 주간회의"'),
|
|
75
|
-
parentId: z.string().optional().describe(
|
|
75
|
+
parentId: z.string().optional().describe(`Parent folder/node id${NODE_TARGET_HINT}`),
|
|
76
76
|
path: z
|
|
77
77
|
.string()
|
|
78
78
|
.optional()
|
|
@@ -101,6 +101,7 @@ export function registerRecordingTools(server) {
|
|
|
101
101
|
try {
|
|
102
102
|
const ws = requireWorkspace(workspace);
|
|
103
103
|
const client = makeClient(ws);
|
|
104
|
+
parentId = await resolveOptionalNodeId(client, parentId);
|
|
104
105
|
const result = await client.createRecording(ws, {
|
|
105
106
|
name,
|
|
106
107
|
parentId,
|
|
@@ -130,7 +131,7 @@ export function registerRecordingTools(server) {
|
|
|
130
131
|
'rootr_transcribe_recording.',
|
|
131
132
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
132
133
|
inputSchema: {
|
|
133
|
-
recordingId: z.string().describe(
|
|
134
|
+
recordingId: z.string().describe(`RECORDING node id${NODE_TARGET_HINT} (create one with rootr_create_recording)`),
|
|
134
135
|
filePath: z
|
|
135
136
|
.string()
|
|
136
137
|
.optional()
|
|
@@ -156,6 +157,7 @@ export function registerRecordingTools(server) {
|
|
|
156
157
|
async ({ recordingId, filePath, base64Data, filename, mimeType, durationSec }) => {
|
|
157
158
|
try {
|
|
158
159
|
const client = makeClient();
|
|
160
|
+
recordingId = await resolveNodeId(client, recordingId);
|
|
159
161
|
const data = readAudioBytes({ filePath, base64Data });
|
|
160
162
|
if (data.length > MAX_AUDIO_BYTES) {
|
|
161
163
|
throw new RootrApiError(
|
|
@@ -207,12 +209,13 @@ export function registerRecordingTools(server) {
|
|
|
207
209
|
'TRANSCRIBING = speech-to-text still running (poll again later); ' +
|
|
208
210
|
'READY = transcript available; FAILED = last transcription errored (see `error`).',
|
|
209
211
|
inputSchema: {
|
|
210
|
-
recordingId: z.string().describe(
|
|
212
|
+
recordingId: z.string().describe(`RECORDING node id${NODE_TARGET_HINT}`),
|
|
211
213
|
},
|
|
212
214
|
},
|
|
213
215
|
async ({ recordingId }) => {
|
|
214
216
|
try {
|
|
215
217
|
const client = makeClient();
|
|
218
|
+
recordingId = await resolveNodeId(client, recordingId);
|
|
216
219
|
const result = await client.getRecording(recordingId);
|
|
217
220
|
const segments = result?.transcript?.segments ?? [];
|
|
218
221
|
const names = result?.speakerNames ?? {};
|
|
@@ -240,7 +243,7 @@ export function registerRecordingTools(server) {
|
|
|
240
243
|
'{"S1":"김대표","S2":"이팀장"}. Merge-patch: only the keys you send change. The names apply to the ' +
|
|
241
244
|
'transcript everywhere (web UI, rootr_read_recording, knowledge graph).',
|
|
242
245
|
inputSchema: {
|
|
243
|
-
recordingId: z.string().describe(
|
|
246
|
+
recordingId: z.string().describe(`RECORDING node id${NODE_TARGET_HINT}`),
|
|
244
247
|
speakerNames: z
|
|
245
248
|
.record(z.string())
|
|
246
249
|
.optional()
|
|
@@ -260,6 +263,7 @@ export function registerRecordingTools(server) {
|
|
|
260
263
|
throw new RootrApiError('speakerNames 또는 expectedSpeakers 중 하나는 필요합니다.');
|
|
261
264
|
}
|
|
262
265
|
const client = makeClient();
|
|
266
|
+
recordingId = await resolveNodeId(client, recordingId);
|
|
263
267
|
const current = await client.getRecording(recordingId);
|
|
264
268
|
const merged = speakerNames
|
|
265
269
|
? { ...(current?.speakerNames ?? {}), ...speakerNames }
|
|
@@ -292,12 +296,13 @@ export function registerRecordingTools(server) {
|
|
|
292
296
|
'take a few minutes).',
|
|
293
297
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
294
298
|
inputSchema: {
|
|
295
|
-
recordingId: z.string().describe(
|
|
299
|
+
recordingId: z.string().describe(`RECORDING node id${NODE_TARGET_HINT}`),
|
|
296
300
|
},
|
|
297
301
|
},
|
|
298
302
|
async ({ recordingId }) => {
|
|
299
303
|
try {
|
|
300
304
|
const client = makeClient();
|
|
305
|
+
recordingId = await resolveNodeId(client, recordingId);
|
|
301
306
|
const result = await client.transcribeRecording(recordingId);
|
|
302
307
|
return jsonResult({
|
|
303
308
|
...result,
|
|
@@ -326,7 +331,7 @@ export function registerRecordingTools(server) {
|
|
|
326
331
|
'for Rootr\'s built-in summary.',
|
|
327
332
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
328
333
|
inputSchema: {
|
|
329
|
-
recordingId: z.string().describe(
|
|
334
|
+
recordingId: z.string().describe(`RECORDING node id${NODE_TARGET_HINT} (status must be READY)`),
|
|
330
335
|
format: z
|
|
331
336
|
.enum(['summary', 'onepager'])
|
|
332
337
|
.optional()
|
|
@@ -343,6 +348,7 @@ export function registerRecordingTools(server) {
|
|
|
343
348
|
async ({ recordingId, locale, format }) => {
|
|
344
349
|
try {
|
|
345
350
|
const client = makeClient();
|
|
351
|
+
recordingId = await resolveNodeId(client, recordingId);
|
|
346
352
|
const result = await client.summarizeRecording(recordingId, { locale, format });
|
|
347
353
|
return jsonResult(result);
|
|
348
354
|
} catch (err) {
|
|
@@ -370,7 +376,7 @@ export function registerRecordingTools(server) {
|
|
|
370
376
|
'this way lives wherever you put it (e.g. a document via rootr_write), not in the recording node.',
|
|
371
377
|
annotations: { readOnlyHint: true },
|
|
372
378
|
inputSchema: {
|
|
373
|
-
recordingId: z.string().describe(
|
|
379
|
+
recordingId: z.string().describe(`RECORDING node id${NODE_TARGET_HINT}`),
|
|
374
380
|
filePath: z
|
|
375
381
|
.string()
|
|
376
382
|
.optional()
|
|
@@ -387,6 +393,7 @@ export function registerRecordingTools(server) {
|
|
|
387
393
|
async ({ recordingId, filePath, urlOnly }) => {
|
|
388
394
|
try {
|
|
389
395
|
const client = makeClient();
|
|
396
|
+
recordingId = await resolveNodeId(client, recordingId);
|
|
390
397
|
const rec = await client.getRecording(recordingId);
|
|
391
398
|
if (!rec?.audio?.attachmentId) {
|
|
392
399
|
throw new RootrApiError('이 녹음에는 아직 오디오가 없습니다. rootr_upload_recording_audio로 먼저 올리세요.');
|
package/lib/mcp-tools/shared.js
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
import { loadConfig, keyForWorkspace } from '../config.js';
|
|
2
2
|
import { RootrClient, RootrApiError } from '../client.js';
|
|
3
3
|
import { currentClientOverride } from '../context.js';
|
|
4
|
+
import { resolveTarget, requireResolved } from '../resolve.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Suffix for the `.describe()` of any node-id parameter: every typed-node tool
|
|
8
|
+
* takes a path too, exactly like rootr_read, so an agent that only knows where
|
|
9
|
+
* a node lives in the tree never has to hunt for its uuid.
|
|
10
|
+
*/
|
|
11
|
+
export const NODE_TARGET_HINT = ', or its path starting with "/"';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve a path-or-id argument to a node id. Paths go through the by-path
|
|
15
|
+
* endpoint; anything else is already an id and is passed through untouched.
|
|
16
|
+
*/
|
|
17
|
+
export async function resolveNodeId(client, target) {
|
|
18
|
+
return requireResolved(await resolveTarget(client, target));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Same as resolveNodeId, but for optional/nullable arguments (parentId,
|
|
23
|
+
* targetDatabaseId, scopeNodeId …) — undefined/null pass through untouched so
|
|
24
|
+
* "omitted" and "explicitly cleared" keep their meaning.
|
|
25
|
+
*/
|
|
26
|
+
export async function resolveOptionalNodeId(client, target) {
|
|
27
|
+
if (target === undefined || target === null || target === '') return target;
|
|
28
|
+
return resolveNodeId(client, target);
|
|
29
|
+
}
|
|
4
30
|
|
|
5
31
|
export function textResult(text) {
|
|
6
32
|
return { content: [{ type: 'text', text }] };
|