@henrikogard/auroradocs-mcp 0.1.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 +201 -0
- package/NOTICE +3 -0
- package/README.md +239 -0
- package/dist/auroraClient.d.ts +165 -0
- package/dist/auroraClient.js +706 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +62 -0
- package/dist/planningTools.d.ts +76 -0
- package/dist/planningTools.js +159 -0
- package/dist/toolCatalog.d.ts +39 -0
- package/dist/toolCatalog.js +423 -0
- package/dist/tools.d.ts +105 -0
- package/dist/tools.js +640 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/package.json +54 -0
package/dist/tools.js
ADDED
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tools.ts — Tool execution logic for the MCP server.
|
|
3
|
+
*
|
|
4
|
+
* SECURITY: Every tool call receives the workspaceId and passes it through
|
|
5
|
+
* to AuroraCloud client helpers which verify ownership before any read or write.
|
|
6
|
+
* No cross-workspace or cross-user data leakage is possible.
|
|
7
|
+
*/
|
|
8
|
+
import { listObjects, getObject, getContent, setContent, appendContentText, listProperties, listMembers, listTaskLists, listTaskStatuses, createObject, updateObjectTitle, deleteObject, upsertProperty, getTaskProps, updateTaskProps, getWorkspaceKnowledgeObjectServer, listWorkspaceRecentKnowledgeServer, listWorkspaceRelatedKnowledgeServer, listPlanningTasks, createPlanningTimeBlock, readCanvasContent, searchWorkspaceKnowledgeServer, } from './auroraClient.js';
|
|
9
|
+
import { buildWeekPlan, normalizeCanvasContent } from './planningTools.js';
|
|
10
|
+
import { getMcpToolCoverageAudit, getMcpWorkflowRecipes } from './toolCatalog.js';
|
|
11
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
12
|
+
function readString(value) {
|
|
13
|
+
if (typeof value !== 'string')
|
|
14
|
+
return null;
|
|
15
|
+
const trimmed = value.trim();
|
|
16
|
+
return trimmed.length ? trimmed : null;
|
|
17
|
+
}
|
|
18
|
+
function readStringArray(value) {
|
|
19
|
+
if (Array.isArray(value))
|
|
20
|
+
return value.map((v) => String(v).trim()).filter(Boolean);
|
|
21
|
+
if (typeof value === 'string')
|
|
22
|
+
return value.split(',').map((v) => v.trim()).filter(Boolean);
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
function todayDateKey() {
|
|
26
|
+
const now = new Date();
|
|
27
|
+
const year = now.getFullYear();
|
|
28
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
29
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
30
|
+
return `${year}-${month}-${day}`;
|
|
31
|
+
}
|
|
32
|
+
function isDateKey(value) {
|
|
33
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
34
|
+
if (!match)
|
|
35
|
+
return false;
|
|
36
|
+
const [, yearRaw, monthRaw, dayRaw] = match;
|
|
37
|
+
const year = Number(yearRaw);
|
|
38
|
+
const month = Number(monthRaw);
|
|
39
|
+
const day = Number(dayRaw);
|
|
40
|
+
const date = new Date(year, month - 1, day);
|
|
41
|
+
return date.getFullYear() === year
|
|
42
|
+
&& date.getMonth() === month - 1
|
|
43
|
+
&& date.getDate() === day;
|
|
44
|
+
}
|
|
45
|
+
function isTimeKey(value) {
|
|
46
|
+
return /^([01]\d|2[0-3]):[0-5]\d$/.test(value);
|
|
47
|
+
}
|
|
48
|
+
function weekPlanningDueDateForDay(dateKey, existingDueDate) {
|
|
49
|
+
const time = existingDueDate?.match(/^\d{4}-\d{2}-\d{2}T(\d{2}:\d{2})/)?.[1];
|
|
50
|
+
return time ? `${dateKey}T${time}` : dateKey;
|
|
51
|
+
}
|
|
52
|
+
function buildWeekPlanningTimeBlockTaskInput(input) {
|
|
53
|
+
const title = input.title.trim() || 'Time block';
|
|
54
|
+
const durationMinutes = Number.isFinite(input.durationMinutes) && input.durationMinutes > 0
|
|
55
|
+
? Math.round(input.durationMinutes)
|
|
56
|
+
: 30;
|
|
57
|
+
return {
|
|
58
|
+
title,
|
|
59
|
+
due_date: `${input.dateKey}T${input.startTime}`,
|
|
60
|
+
labels: ['time-block', `duration:${durationMinutes}m`],
|
|
61
|
+
description: `Duration: ${durationMinutes} minutes`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function matchTaskList(query, lists) {
|
|
65
|
+
const q = query.trim().toLowerCase();
|
|
66
|
+
if (!q)
|
|
67
|
+
return null;
|
|
68
|
+
const exact = lists.find((l) => l.id.toLowerCase() === q || l.name.trim().toLowerCase() === q);
|
|
69
|
+
if (exact)
|
|
70
|
+
return exact;
|
|
71
|
+
const partial = lists.filter((l) => l.name.toLowerCase().includes(q));
|
|
72
|
+
return partial.length === 1 ? partial[0] : null;
|
|
73
|
+
}
|
|
74
|
+
function matchMember(query, members) {
|
|
75
|
+
const q = query.trim().toLowerCase();
|
|
76
|
+
if (!q)
|
|
77
|
+
return null;
|
|
78
|
+
const exact = members.find((m) => {
|
|
79
|
+
const name = m.name?.trim().toLowerCase() ?? '';
|
|
80
|
+
const email = m.email.trim().toLowerCase();
|
|
81
|
+
const local = email.split('@')[0] ?? '';
|
|
82
|
+
return m.id.toLowerCase() === q || email === q || local === q || name === q;
|
|
83
|
+
});
|
|
84
|
+
if (exact)
|
|
85
|
+
return exact;
|
|
86
|
+
const partial = members.filter((m) => {
|
|
87
|
+
const name = m.name?.trim().toLowerCase() ?? '';
|
|
88
|
+
const email = m.email.trim().toLowerCase();
|
|
89
|
+
return name.includes(q) || email.includes(q);
|
|
90
|
+
});
|
|
91
|
+
return partial.length === 1 ? partial[0] : null;
|
|
92
|
+
}
|
|
93
|
+
function normalizePriority(value) {
|
|
94
|
+
const v = value.toLowerCase();
|
|
95
|
+
if (v === 'low')
|
|
96
|
+
return 'Low';
|
|
97
|
+
if (v === 'medium')
|
|
98
|
+
return 'Medium';
|
|
99
|
+
if (v === 'high')
|
|
100
|
+
return 'High';
|
|
101
|
+
if (v === 'urgent')
|
|
102
|
+
return 'Urgent';
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
// ── Tool execution ───────────────────────────────────────────────────────────
|
|
106
|
+
export async function executeToolCall(name, input, workspaceId) {
|
|
107
|
+
try {
|
|
108
|
+
switch (name) {
|
|
109
|
+
// ── Read tools ─────────────────────────────────────────────────────
|
|
110
|
+
case 'search':
|
|
111
|
+
case 'search_objects': {
|
|
112
|
+
const query = String(input['query'] ?? '').toLowerCase();
|
|
113
|
+
const typeFilter = input['type'] ? String(input['type']) : undefined;
|
|
114
|
+
const all = await listObjects(workspaceId, typeFilter);
|
|
115
|
+
const matches = all
|
|
116
|
+
.filter((o) => (o.title ?? '').toLowerCase().includes(query))
|
|
117
|
+
.slice(0, 10)
|
|
118
|
+
.map((o) => ({ id: o.id, title: o.title, type: o.type, icon: o.icon }));
|
|
119
|
+
return { type: 'objects', objects: matches };
|
|
120
|
+
}
|
|
121
|
+
case 'list_objects': {
|
|
122
|
+
const typeFilter = input['type'] ? String(input['type']) : undefined;
|
|
123
|
+
const limit = typeof input['limit'] === 'number' ? input['limit'] : 20;
|
|
124
|
+
const all = await listObjects(workspaceId, typeFilter);
|
|
125
|
+
const results = all
|
|
126
|
+
.filter((o) => !o.is_template)
|
|
127
|
+
.slice(0, limit)
|
|
128
|
+
.map((o) => ({ id: o.id, title: o.title, type: o.type, icon: o.icon }));
|
|
129
|
+
return { type: 'objects', objects: results };
|
|
130
|
+
}
|
|
131
|
+
case 'list_recent': {
|
|
132
|
+
const typeFilter = input['type'] ? String(input['type']) : undefined;
|
|
133
|
+
const limit = typeof input['limit'] === 'number' ? input['limit'] : 20;
|
|
134
|
+
const all = await listObjects(workspaceId, typeFilter);
|
|
135
|
+
const results = all
|
|
136
|
+
.filter((o) => !o.is_template)
|
|
137
|
+
.slice(0, limit)
|
|
138
|
+
.map((o) => ({ id: o.id, title: o.title, type: o.type, icon: o.icon }));
|
|
139
|
+
return { type: 'objects', objects: results };
|
|
140
|
+
}
|
|
141
|
+
case 'wiki_search': {
|
|
142
|
+
const query = readString(input['query']);
|
|
143
|
+
if (!query)
|
|
144
|
+
return { type: 'error', message: 'Query is required' };
|
|
145
|
+
const limit = typeof input['limit'] === 'number' ? input['limit'] : 20;
|
|
146
|
+
const sources = await searchWorkspaceKnowledgeServer(workspaceId, query, limit);
|
|
147
|
+
return { type: 'knowledge_sources', sources };
|
|
148
|
+
}
|
|
149
|
+
case 'wiki_get_page': {
|
|
150
|
+
const id = readString(input['id']);
|
|
151
|
+
if (!id)
|
|
152
|
+
return { type: 'error', message: 'Object ID is required' };
|
|
153
|
+
const includeFullText = Boolean(input['includeFullText']);
|
|
154
|
+
const source = await getWorkspaceKnowledgeObjectServer(workspaceId, id, includeFullText);
|
|
155
|
+
if (!source)
|
|
156
|
+
return { type: 'error', message: `Object ${id} not found in this workspace` };
|
|
157
|
+
return { type: 'knowledge_sources', sources: [source] };
|
|
158
|
+
}
|
|
159
|
+
case 'wiki_related': {
|
|
160
|
+
const id = readString(input['id']);
|
|
161
|
+
if (!id)
|
|
162
|
+
return { type: 'error', message: 'Object ID is required' };
|
|
163
|
+
const limit = typeof input['limit'] === 'number' ? input['limit'] : 6;
|
|
164
|
+
const sources = await listWorkspaceRelatedKnowledgeServer(workspaceId, id, limit);
|
|
165
|
+
return { type: 'knowledge_sources', sources };
|
|
166
|
+
}
|
|
167
|
+
case 'wiki_recent': {
|
|
168
|
+
const limit = typeof input['limit'] === 'number' ? input['limit'] : 6;
|
|
169
|
+
const sources = await listWorkspaceRecentKnowledgeServer(workspaceId, limit);
|
|
170
|
+
return { type: 'knowledge_sources', sources };
|
|
171
|
+
}
|
|
172
|
+
case 'get_object': {
|
|
173
|
+
const id = String(input['id'] ?? '');
|
|
174
|
+
const obj = await getObject(id, workspaceId);
|
|
175
|
+
if (!obj)
|
|
176
|
+
return { type: 'error', message: `Object ${id} not found in this workspace` };
|
|
177
|
+
const content = await getContent(id, workspaceId);
|
|
178
|
+
// Detect E2EE content
|
|
179
|
+
const displayContent = content && (content.startsWith('v1:') || content.startsWith('v2:'))
|
|
180
|
+
? '[Content is end-to-end encrypted and cannot be read by the MCP server]'
|
|
181
|
+
: content;
|
|
182
|
+
// Include properties
|
|
183
|
+
const rawProps = await listProperties([id], workspaceId);
|
|
184
|
+
const properties = {};
|
|
185
|
+
for (const p of rawProps) {
|
|
186
|
+
properties[p.key] =
|
|
187
|
+
p.value_text
|
|
188
|
+
?? p.value_date
|
|
189
|
+
?? (p.value_num != null ? String(p.value_num) : null)
|
|
190
|
+
?? (p.value_bool != null ? String(p.value_bool) : null)
|
|
191
|
+
?? p.value_ref
|
|
192
|
+
?? '';
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
type: 'object',
|
|
196
|
+
object: { id: obj.id, title: obj.title, type: obj.type, icon: obj.icon },
|
|
197
|
+
content: displayContent ?? null,
|
|
198
|
+
properties,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
case 'list_workspace_members': {
|
|
202
|
+
const members = await listMembers(workspaceId);
|
|
203
|
+
return { type: 'members', members };
|
|
204
|
+
}
|
|
205
|
+
case 'list_task_lists': {
|
|
206
|
+
const lists = await listTaskLists(workspaceId);
|
|
207
|
+
return { type: 'task_lists', task_lists: lists.map((l) => ({ id: l.id, name: l.name })) };
|
|
208
|
+
}
|
|
209
|
+
case 'list_task_statuses': {
|
|
210
|
+
const statuses = await listTaskStatuses(workspaceId);
|
|
211
|
+
return { type: 'task_statuses', statuses };
|
|
212
|
+
}
|
|
213
|
+
case 'get_mcp_tool_coverage':
|
|
214
|
+
return { type: 'mcp_tool_coverage', audit: getMcpToolCoverageAudit() };
|
|
215
|
+
case 'get_mcp_workflow_recipes':
|
|
216
|
+
return { type: 'mcp_workflow_recipes', recipes: getMcpWorkflowRecipes() };
|
|
217
|
+
case 'list_week_plan': {
|
|
218
|
+
const anchorDate = readString(input['anchor_date']) ?? todayDateKey();
|
|
219
|
+
if (!isDateKey(anchorDate)) {
|
|
220
|
+
return { type: 'error', message: 'anchor_date must be a valid date formatted YYYY-MM-DD' };
|
|
221
|
+
}
|
|
222
|
+
const includeUnscheduled = input['include_unscheduled'] !== false;
|
|
223
|
+
const unscheduledLimit = typeof input['unscheduled_limit'] === 'number' ? input['unscheduled_limit'] : 12;
|
|
224
|
+
const tasks = await listPlanningTasks(workspaceId);
|
|
225
|
+
return {
|
|
226
|
+
type: 'week_plan',
|
|
227
|
+
plan: buildWeekPlan(tasks.map((task) => ({
|
|
228
|
+
id: task.id,
|
|
229
|
+
title: task.title,
|
|
230
|
+
status: task.status,
|
|
231
|
+
due_date: task.due_date,
|
|
232
|
+
updated_at: task.updated_at,
|
|
233
|
+
labels: task.labels,
|
|
234
|
+
})), {
|
|
235
|
+
anchorDate,
|
|
236
|
+
includeUnscheduled,
|
|
237
|
+
unscheduledLimit,
|
|
238
|
+
}),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
case 'read_canvas': {
|
|
242
|
+
const id = readString(input['id']);
|
|
243
|
+
if (!id)
|
|
244
|
+
return { type: 'error', message: 'Canvas object ID is required' };
|
|
245
|
+
const result = await readCanvasContent(workspaceId, id);
|
|
246
|
+
if (!result)
|
|
247
|
+
return { type: 'error', message: `Canvas ${id} not found in this workspace` };
|
|
248
|
+
const includeText = input['include_text'] !== false;
|
|
249
|
+
return {
|
|
250
|
+
type: 'canvas',
|
|
251
|
+
canvas: normalizeCanvasContent({ id: result.object.id, title: result.object.title, type: result.object.type }, result.contentJson, { includeText }),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
case 'schedule_task_block': {
|
|
255
|
+
const mode = readString(input['mode']);
|
|
256
|
+
const date = readString(input['date']);
|
|
257
|
+
if (mode !== 'schedule_existing_task' && mode !== 'create_time_block') {
|
|
258
|
+
return { type: 'error', message: 'mode must be schedule_existing_task or create_time_block' };
|
|
259
|
+
}
|
|
260
|
+
if (!date || !isDateKey(date)) {
|
|
261
|
+
return { type: 'error', message: 'date must be a valid date formatted YYYY-MM-DD' };
|
|
262
|
+
}
|
|
263
|
+
const startTime = readString(input['start_time']);
|
|
264
|
+
if (startTime && !isTimeKey(startTime)) {
|
|
265
|
+
return { type: 'error', message: 'start_time must be a valid time formatted HH:mm' };
|
|
266
|
+
}
|
|
267
|
+
if (mode === 'schedule_existing_task') {
|
|
268
|
+
const taskId = readString(input['task_id']);
|
|
269
|
+
if (!taskId)
|
|
270
|
+
return { type: 'error', message: 'task_id is required for schedule_existing_task' };
|
|
271
|
+
const object = await getObject(taskId, workspaceId);
|
|
272
|
+
if (!object)
|
|
273
|
+
return { type: 'error', message: `Object ${taskId} not found in this workspace` };
|
|
274
|
+
if (object.type !== 'task')
|
|
275
|
+
return { type: 'error', message: `${taskId} is not a task` };
|
|
276
|
+
const props = await getTaskProps(taskId, workspaceId);
|
|
277
|
+
if (props.status === 'Done')
|
|
278
|
+
return { type: 'error', message: 'Completed tasks cannot be scheduled' };
|
|
279
|
+
const dueDate = startTime ? `${date}T${startTime}` : weekPlanningDueDateForDay(date, props.due_date);
|
|
280
|
+
await updateTaskProps(taskId, workspaceId, { due_date: dueDate });
|
|
281
|
+
return { type: 'scheduled_task_block', id: taskId, title: object.title, due_date: dueDate, mode };
|
|
282
|
+
}
|
|
283
|
+
const title = readString(input['title']);
|
|
284
|
+
if (!title)
|
|
285
|
+
return { type: 'error', message: 'title is required for create_time_block' };
|
|
286
|
+
const durationRaw = input['duration_minutes'];
|
|
287
|
+
if (durationRaw !== undefined
|
|
288
|
+
&& (typeof durationRaw !== 'number' || !Number.isFinite(durationRaw) || durationRaw <= 0)) {
|
|
289
|
+
return { type: 'error', message: 'duration_minutes must be a positive number' };
|
|
290
|
+
}
|
|
291
|
+
const blockInput = buildWeekPlanningTimeBlockTaskInput({
|
|
292
|
+
title,
|
|
293
|
+
dateKey: date,
|
|
294
|
+
startTime: startTime ?? '09:00',
|
|
295
|
+
durationMinutes: typeof durationRaw === 'number' ? durationRaw : 30,
|
|
296
|
+
});
|
|
297
|
+
const object = await createPlanningTimeBlock(workspaceId, {
|
|
298
|
+
title: blockInput.title,
|
|
299
|
+
dueDate: blockInput.due_date,
|
|
300
|
+
labels: blockInput.labels,
|
|
301
|
+
description: blockInput.description,
|
|
302
|
+
});
|
|
303
|
+
return { type: 'scheduled_task_block', id: object.id, title: object.title, due_date: blockInput.due_date, mode };
|
|
304
|
+
}
|
|
305
|
+
// ── Write tools ────────────────────────────────────────────────────
|
|
306
|
+
case 'create_object': {
|
|
307
|
+
const type = String(input['type'] ?? 'page');
|
|
308
|
+
if (type === 'task')
|
|
309
|
+
return { type: 'error', message: 'Use create_task for tasks' };
|
|
310
|
+
const title = String(input['title'] ?? 'Untitled');
|
|
311
|
+
const obj = await createObject(workspaceId, type, title);
|
|
312
|
+
return { type: 'created', id: obj.id, title: obj.title ?? title };
|
|
313
|
+
}
|
|
314
|
+
case 'create_task': {
|
|
315
|
+
const title = readString(input['title']);
|
|
316
|
+
if (!title)
|
|
317
|
+
return { type: 'error', message: 'Task title is required' };
|
|
318
|
+
const obj = await createObject(workspaceId, 'task', title);
|
|
319
|
+
// Resolve task fields
|
|
320
|
+
const patch = await resolveTaskInput(input, workspaceId);
|
|
321
|
+
if (Object.keys(patch).length > 0) {
|
|
322
|
+
await updateTaskProps(obj.id, workspaceId, patch);
|
|
323
|
+
}
|
|
324
|
+
const taskListName = patch.task_list_id
|
|
325
|
+
? (await listTaskLists(workspaceId)).find((l) => l.id === patch.task_list_id)?.name ?? null
|
|
326
|
+
: null;
|
|
327
|
+
return { type: 'task_created', id: obj.id, title, status: patch.status ?? null, task_list_name: taskListName };
|
|
328
|
+
}
|
|
329
|
+
case 'update_task': {
|
|
330
|
+
const id = readString(input['id']);
|
|
331
|
+
if (!id)
|
|
332
|
+
return { type: 'error', message: 'Task ID is required' };
|
|
333
|
+
const obj = await getObject(id, workspaceId);
|
|
334
|
+
if (!obj)
|
|
335
|
+
return { type: 'error', message: `Object ${id} not found in this workspace` };
|
|
336
|
+
if (obj.type !== 'task')
|
|
337
|
+
return { type: 'error', message: `${id} is not a task` };
|
|
338
|
+
const changedFields = [];
|
|
339
|
+
if ('title' in input) {
|
|
340
|
+
const newTitle = readString(input['title']);
|
|
341
|
+
if (!newTitle)
|
|
342
|
+
return { type: 'error', message: 'Task title cannot be empty' };
|
|
343
|
+
await updateObjectTitle(id, newTitle, workspaceId);
|
|
344
|
+
changedFields.push('title');
|
|
345
|
+
}
|
|
346
|
+
const patch = await resolveTaskInput(input, workspaceId);
|
|
347
|
+
if (Object.keys(patch).length > 0) {
|
|
348
|
+
await updateTaskProps(id, workspaceId, patch);
|
|
349
|
+
if (patch.status !== undefined)
|
|
350
|
+
changedFields.push('status');
|
|
351
|
+
if (patch.priority !== undefined)
|
|
352
|
+
changedFields.push('priority');
|
|
353
|
+
if (patch.due_date !== undefined)
|
|
354
|
+
changedFields.push('due_date');
|
|
355
|
+
if (patch.assignees !== undefined)
|
|
356
|
+
changedFields.push('assignees');
|
|
357
|
+
if (patch.labels !== undefined)
|
|
358
|
+
changedFields.push('labels');
|
|
359
|
+
if (patch.description !== undefined)
|
|
360
|
+
changedFields.push('description');
|
|
361
|
+
if (patch.task_list_id !== undefined)
|
|
362
|
+
changedFields.push('task_list');
|
|
363
|
+
}
|
|
364
|
+
return { type: 'task_updated', id, title: obj.title ?? id, changed_fields: changedFields };
|
|
365
|
+
}
|
|
366
|
+
case 'update_object_title': {
|
|
367
|
+
const id = String(input['id'] ?? '');
|
|
368
|
+
const title = String(input['title'] ?? '');
|
|
369
|
+
await updateObjectTitle(id, title, workspaceId);
|
|
370
|
+
return { type: 'updated', id, title };
|
|
371
|
+
}
|
|
372
|
+
case 'update_object': {
|
|
373
|
+
const id = readString(input['id']);
|
|
374
|
+
if (!id)
|
|
375
|
+
return { type: 'error', message: 'Object ID is required' };
|
|
376
|
+
const obj = await getObject(id, workspaceId);
|
|
377
|
+
if (!obj)
|
|
378
|
+
return { type: 'error', message: `Object ${id} not found in this workspace` };
|
|
379
|
+
const changedFields = [];
|
|
380
|
+
if ('title' in input) {
|
|
381
|
+
const title = readString(input['title']);
|
|
382
|
+
if (!title)
|
|
383
|
+
return { type: 'error', message: 'Object title cannot be empty' };
|
|
384
|
+
await updateObjectTitle(id, title, workspaceId);
|
|
385
|
+
changedFields.push('title');
|
|
386
|
+
}
|
|
387
|
+
if ('text' in input || 'content' in input) {
|
|
388
|
+
const text = readString(input['text'] ?? input['content']);
|
|
389
|
+
if (!text)
|
|
390
|
+
return { type: 'error', message: 'Object content cannot be empty' };
|
|
391
|
+
await setContent(id, workspaceId, textToTipTapDoc(text));
|
|
392
|
+
changedFields.push('content');
|
|
393
|
+
}
|
|
394
|
+
return changedFields.length
|
|
395
|
+
? { type: 'object_updated', id, changed_fields: changedFields }
|
|
396
|
+
: { type: 'no_op', message: `No updates supplied for ${id}` };
|
|
397
|
+
}
|
|
398
|
+
case 'set_content': {
|
|
399
|
+
const id = String(input['id'] ?? '');
|
|
400
|
+
const text = String(input['text'] ?? '');
|
|
401
|
+
// Build a minimal TipTap document from plain text
|
|
402
|
+
const doc = textToTipTapDoc(text);
|
|
403
|
+
await setContent(id, workspaceId, doc);
|
|
404
|
+
return { type: 'content_set', id };
|
|
405
|
+
}
|
|
406
|
+
case 'append_block': {
|
|
407
|
+
const id = readString(input['id']);
|
|
408
|
+
const text = readString(input['text'] ?? input['content']);
|
|
409
|
+
if (!id)
|
|
410
|
+
return { type: 'error', message: 'Object ID is required' };
|
|
411
|
+
if (!text)
|
|
412
|
+
return { type: 'error', message: 'Text is required' };
|
|
413
|
+
await appendContentText(id, workspaceId, text);
|
|
414
|
+
return { type: 'content_appended', id };
|
|
415
|
+
}
|
|
416
|
+
case 'delete_object': {
|
|
417
|
+
const id = String(input['id'] ?? '');
|
|
418
|
+
await deleteObject(id, workspaceId);
|
|
419
|
+
return { type: 'deleted', id };
|
|
420
|
+
}
|
|
421
|
+
case 'set_property': {
|
|
422
|
+
const objectId = String(input['object_id'] ?? '');
|
|
423
|
+
const key = String(input['key'] ?? '');
|
|
424
|
+
const valueType = String(input['value_type'] ?? 'text');
|
|
425
|
+
const value = String(input['value'] ?? '');
|
|
426
|
+
await upsertProperty(objectId, workspaceId, key, valueType, value);
|
|
427
|
+
return { type: 'property_set', objectId, key, value };
|
|
428
|
+
}
|
|
429
|
+
case 'navigate_to':
|
|
430
|
+
return { type: 'no_op', message: 'Navigation is not supported in the MCP server context.' };
|
|
431
|
+
default:
|
|
432
|
+
return { type: 'error', message: `Unknown tool: ${name}` };
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
catch (err) {
|
|
436
|
+
return { type: 'error', message: err instanceof Error ? err.message : String(err) };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// ── Task input resolution ────────────────────────────────────────────────────
|
|
440
|
+
async function resolveTaskInput(input, workspaceId) {
|
|
441
|
+
const patch = {};
|
|
442
|
+
if ('status' in input) {
|
|
443
|
+
const raw = readString(input['status']);
|
|
444
|
+
if (raw) {
|
|
445
|
+
const statuses = await listTaskStatuses(workspaceId);
|
|
446
|
+
const match = statuses.find((s) => s.toLowerCase() === raw.toLowerCase());
|
|
447
|
+
patch.status = match ?? raw;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if ('priority' in input) {
|
|
451
|
+
const raw = readString(input['priority']);
|
|
452
|
+
if (raw === null)
|
|
453
|
+
patch.priority = null;
|
|
454
|
+
else {
|
|
455
|
+
const normalized = normalizePriority(raw);
|
|
456
|
+
if (!normalized)
|
|
457
|
+
throw new Error(`Unknown priority: ${raw}`);
|
|
458
|
+
patch.priority = normalized;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if ('due_date' in input) {
|
|
462
|
+
const raw = readString(input['due_date']);
|
|
463
|
+
// Normalize space-separated datetime to ISO
|
|
464
|
+
patch.due_date = raw && /^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}$/.test(raw) ? raw.replace(/\s+/, 'T') : raw;
|
|
465
|
+
}
|
|
466
|
+
if ('description' in input) {
|
|
467
|
+
patch.description = readString(input['description']);
|
|
468
|
+
}
|
|
469
|
+
if ('labels' in input || 'tags' in input) {
|
|
470
|
+
patch.labels = readStringArray(input['labels'] ?? input['tags']);
|
|
471
|
+
}
|
|
472
|
+
if ('task_list' in input) {
|
|
473
|
+
const query = readString(input['task_list']);
|
|
474
|
+
if (query === null) {
|
|
475
|
+
patch.task_list_id = null;
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
const lists = await listTaskLists(workspaceId);
|
|
479
|
+
const match = matchTaskList(query, lists);
|
|
480
|
+
if (!match)
|
|
481
|
+
throw new Error(`Could not resolve task list: ${query}`);
|
|
482
|
+
patch.task_list_id = match.id;
|
|
483
|
+
// Apply default status if not explicitly set
|
|
484
|
+
if (patch.status === undefined && match.default_status) {
|
|
485
|
+
patch.status = match.default_status;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if ('assignees' in input || 'assignee' in input) {
|
|
490
|
+
const queries = [...readStringArray(input['assignees']), ...readStringArray(input['assignee'])];
|
|
491
|
+
if (!queries.length) {
|
|
492
|
+
patch.assignees = [];
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
const members = await listMembers(workspaceId);
|
|
496
|
+
const resolved = queries.map((q) => {
|
|
497
|
+
const m = matchMember(q, members);
|
|
498
|
+
if (!m)
|
|
499
|
+
throw new Error(`Could not resolve assignee: ${q}`);
|
|
500
|
+
return m.id;
|
|
501
|
+
});
|
|
502
|
+
patch.assignees = [...new Set(resolved)];
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return patch;
|
|
506
|
+
}
|
|
507
|
+
// ── Content helpers ──────────────────────────────────────────────────────────
|
|
508
|
+
function textToTipTapDoc(text) {
|
|
509
|
+
const paragraphs = text.split('\n').map((line) => ({
|
|
510
|
+
type: 'paragraph',
|
|
511
|
+
content: line ? [{ type: 'text', text: line }] : [],
|
|
512
|
+
}));
|
|
513
|
+
return { type: 'doc', content: paragraphs };
|
|
514
|
+
}
|
|
515
|
+
// ── Format result for MCP text response ──────────────────────────────────────
|
|
516
|
+
export function formatToolResult(result) {
|
|
517
|
+
switch (result.type) {
|
|
518
|
+
case 'objects':
|
|
519
|
+
if (!result.objects.length)
|
|
520
|
+
return 'No objects found.';
|
|
521
|
+
return result.objects.map((o) => `- [${o.type}] ${o.title ?? '(Untitled)'} (${o.id})`).join('\n');
|
|
522
|
+
case 'object': {
|
|
523
|
+
const propLines = Object.entries(result.properties)
|
|
524
|
+
.map(([k, v]) => ` ${k}: ${v}`)
|
|
525
|
+
.join('\n');
|
|
526
|
+
return [
|
|
527
|
+
`**${result.object.title ?? 'Untitled'}** (${result.object.type}, ${result.object.id})`,
|
|
528
|
+
propLines ? `\nProperties:\n${propLines}` : '',
|
|
529
|
+
result.content ? `\n\n${result.content}` : '\n*(no content)*',
|
|
530
|
+
].join('');
|
|
531
|
+
}
|
|
532
|
+
case 'created':
|
|
533
|
+
return `Created: "${result.title}" (${result.id})`;
|
|
534
|
+
case 'task_created':
|
|
535
|
+
return `Task created: "${result.title}" (${result.id})${result.status ? ` [${result.status}]` : ''}${result.task_list_name ? ` in ${result.task_list_name}` : ''}`;
|
|
536
|
+
case 'task_updated':
|
|
537
|
+
return `Task updated: "${result.title}" (${result.id}) — changed: ${result.changed_fields.join(', ') || 'nothing'}`;
|
|
538
|
+
case 'object_updated':
|
|
539
|
+
return `Object updated: ${result.id} — changed: ${result.changed_fields.join(', ') || 'nothing'}`;
|
|
540
|
+
case 'updated':
|
|
541
|
+
return `Updated: "${result.title}" (${result.id})`;
|
|
542
|
+
case 'deleted':
|
|
543
|
+
return `Deleted: ${result.id} (soft-deleted)`;
|
|
544
|
+
case 'content_set':
|
|
545
|
+
return `Content set on ${result.id}`;
|
|
546
|
+
case 'content_appended':
|
|
547
|
+
return `Content appended to ${result.id}`;
|
|
548
|
+
case 'property_set':
|
|
549
|
+
return `Set property "${result.key}" = "${result.value}" on ${result.objectId}`;
|
|
550
|
+
case 'knowledge_sources':
|
|
551
|
+
if (!result.sources.length)
|
|
552
|
+
return 'No matching workspace sources.';
|
|
553
|
+
return result.sources.map((source, index) => {
|
|
554
|
+
const header = `[${index + 1}] ${source.title ?? '(Untitled)'} (${source.objectType}, ${source.objectId})`;
|
|
555
|
+
const lines = [
|
|
556
|
+
header,
|
|
557
|
+
` sourceId: ${source.sourceId}`,
|
|
558
|
+
` deepLink: ${source.deepLink}`,
|
|
559
|
+
` updatedAt: ${source.updatedAt}`,
|
|
560
|
+
` availability: ${source.availability}`,
|
|
561
|
+
];
|
|
562
|
+
if (source.snippet)
|
|
563
|
+
lines.push(` snippet: ${source.snippet}`);
|
|
564
|
+
if (source.matchedFields.length)
|
|
565
|
+
lines.push(` matchedFields: ${source.matchedFields.join(', ')}`);
|
|
566
|
+
if (source.plainText) {
|
|
567
|
+
lines.push(' content:');
|
|
568
|
+
lines.push(...source.plainText.split('\n').map((line) => ` ${line}`));
|
|
569
|
+
}
|
|
570
|
+
return lines.join('\n');
|
|
571
|
+
}).join('\n\n');
|
|
572
|
+
case 'mcp_tool_coverage':
|
|
573
|
+
return JSON.stringify(result.audit, null, 2);
|
|
574
|
+
case 'mcp_workflow_recipes':
|
|
575
|
+
return result.recipes.map((recipe) => [
|
|
576
|
+
`## ${recipe.id}: ${recipe.title}`,
|
|
577
|
+
recipe.goal,
|
|
578
|
+
`Scopes: ${recipe.requiredScopes.join(', ')}`,
|
|
579
|
+
`Tools: ${recipe.toolSteps.join(' -> ')}`,
|
|
580
|
+
`Prompt: ${recipe.prompt}`,
|
|
581
|
+
].join('\n')).join('\n\n');
|
|
582
|
+
case 'week_plan':
|
|
583
|
+
return [
|
|
584
|
+
`Week ${result.plan.range.start} to ${result.plan.range.end}`,
|
|
585
|
+
...result.plan.days.map((day) => {
|
|
586
|
+
const tasks = day.tasks.length
|
|
587
|
+
? day.tasks.map((task) => {
|
|
588
|
+
const block = task.timeBlock.isTimeBlock
|
|
589
|
+
? ` [time block${task.timeBlock.durationMinutes ? `, ${task.timeBlock.durationMinutes}m` : ''}]`
|
|
590
|
+
: '';
|
|
591
|
+
return ` - ${task.title ?? '(Untitled)'} (${task.id})${task.due_date ? ` due ${task.due_date}` : ''}${block}`;
|
|
592
|
+
}).join('\n')
|
|
593
|
+
: ' - No scheduled tasks';
|
|
594
|
+
return `${day.date}\n${tasks}`;
|
|
595
|
+
}),
|
|
596
|
+
result.plan.unscheduled.length
|
|
597
|
+
? `Unscheduled\n${result.plan.unscheduled.map((task) => ` - ${task.title ?? '(Untitled)'} (${task.id})`).join('\n')}`
|
|
598
|
+
: 'Unscheduled\n - None',
|
|
599
|
+
].join('\n\n');
|
|
600
|
+
case 'canvas': {
|
|
601
|
+
const cardLines = result.canvas.cards.length
|
|
602
|
+
? result.canvas.cards.map((card) => {
|
|
603
|
+
const ref = card.objectId ? ` object=${card.objectId}${card.objectTitle ? ` (${card.objectTitle})` : ''}` : '';
|
|
604
|
+
const text = card.text ? ` text="${card.text}"` : '';
|
|
605
|
+
return `- ${card.id} [${card.type}] x=${card.x ?? '?'} y=${card.y ?? '?'} w=${card.width ?? '?'} h=${card.height ?? '?'}${ref}${text}`;
|
|
606
|
+
})
|
|
607
|
+
: ['- No cards'];
|
|
608
|
+
const edgeLines = result.canvas.edges.length
|
|
609
|
+
? result.canvas.edges.map((edge) => `- ${edge.id}: ${edge.fromCard ?? '?'} -> ${edge.toCard ?? '?'}${edge.label ? ` (${edge.label})` : ''}`)
|
|
610
|
+
: ['- No edges'];
|
|
611
|
+
const warningLines = result.canvas.warnings.length
|
|
612
|
+
? ['Warnings:', ...result.canvas.warnings.map((warning) => `- ${warning}`)]
|
|
613
|
+
: [];
|
|
614
|
+
return [
|
|
615
|
+
`Canvas: ${result.canvas.canvas.title ?? '(Untitled)'} (${result.canvas.canvas.id})`,
|
|
616
|
+
'Cards:',
|
|
617
|
+
...cardLines,
|
|
618
|
+
'Edges:',
|
|
619
|
+
...edgeLines,
|
|
620
|
+
...warningLines,
|
|
621
|
+
].join('\n');
|
|
622
|
+
}
|
|
623
|
+
case 'scheduled_task_block':
|
|
624
|
+
return `Scheduled: ${result.title ?? '(Untitled)'} (${result.id}) due ${result.due_date} via ${result.mode}`;
|
|
625
|
+
case 'members':
|
|
626
|
+
if (!result.members.length)
|
|
627
|
+
return 'No members found.';
|
|
628
|
+
return result.members.map((m) => `- ${m.name ?? m.email} (${m.role}, ${m.id})`).join('\n');
|
|
629
|
+
case 'task_lists':
|
|
630
|
+
if (!result.task_lists.length)
|
|
631
|
+
return 'No task lists found.';
|
|
632
|
+
return result.task_lists.map((l) => `- ${l.name} (${l.id})`).join('\n');
|
|
633
|
+
case 'task_statuses':
|
|
634
|
+
return result.statuses.join(', ');
|
|
635
|
+
case 'no_op':
|
|
636
|
+
return result.message;
|
|
637
|
+
case 'error':
|
|
638
|
+
return `Error: ${result.message}`;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const SERVER_VERSION: string;
|
package/dist/version.js
ADDED