aiquila-mcp 0.3.12 → 0.3.13
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/dist/tools/apps/calendar.js +47 -20
- package/dist/tools/apps/mail.js +96 -1
- package/package.json +1 -1
|
@@ -175,6 +175,28 @@ function ensureVTimezone(icalData, tzid) {
|
|
|
175
175
|
const vtz = buildVTimezone(tzid);
|
|
176
176
|
return icalData.replace(/BEGIN:VEVENT/, `${vtz}\r\nBEGIN:VEVENT`);
|
|
177
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Build a DISPLAY VALARM block firing `minutes` before the event. Returns an
|
|
180
|
+
* empty string for non-positive values (those are skipped, not emitted).
|
|
181
|
+
*/
|
|
182
|
+
function buildVAlarm(minutes) {
|
|
183
|
+
if (minutes <= 0)
|
|
184
|
+
return '';
|
|
185
|
+
const totalSeconds = minutes * 60;
|
|
186
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
187
|
+
const mins = Math.floor((totalSeconds % 3600) / 60);
|
|
188
|
+
const durationStr = `-PT${hours > 0 ? hours + 'H' : ''}${mins > 0 ? mins + 'M' : ''}`;
|
|
189
|
+
return `BEGIN:VALARM\r\nACTION:DISPLAY\r\nDESCRIPTION:Reminder\r\nTRIGGER:${durationStr}\r\nEND:VALARM`;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Merge the single `alarm` and the `alarms` array into one list of reminder
|
|
193
|
+
* minutes. `alarms` entries come first, then `alarm` (if a positive number).
|
|
194
|
+
* The MCP-client-friendly split exists because a union type's anyOf JSON Schema
|
|
195
|
+
* is not reliably honored, so a dedicated array parameter is needed.
|
|
196
|
+
*/
|
|
197
|
+
function collectAlarmMinutes(alarm, alarms) {
|
|
198
|
+
return [...(alarms ?? []), ...(alarm !== undefined && alarm !== null ? [alarm] : [])];
|
|
199
|
+
}
|
|
178
200
|
// ---------------------------------------------------------------------------
|
|
179
201
|
// Parsing
|
|
180
202
|
// ---------------------------------------------------------------------------
|
|
@@ -879,7 +901,11 @@ export const createEventTool = {
|
|
|
879
901
|
alarm: z
|
|
880
902
|
.number()
|
|
881
903
|
.optional()
|
|
882
|
-
.describe('
|
|
904
|
+
.describe('Single reminder in minutes before the event (e.g. 15 for 15 min before). For multiple reminders use alarms[].'),
|
|
905
|
+
alarms: z
|
|
906
|
+
.array(z.number())
|
|
907
|
+
.optional()
|
|
908
|
+
.describe('Multiple reminders in minutes before the event (e.g. [1440, 60] for 1 day and 1 hour before). Combined with alarm if both are set.'),
|
|
883
909
|
tzid: z
|
|
884
910
|
.string()
|
|
885
911
|
.optional()
|
|
@@ -990,14 +1016,11 @@ export const createEventTool = {
|
|
|
990
1016
|
vevent += `\r\n${atLine}`;
|
|
991
1017
|
}
|
|
992
1018
|
}
|
|
993
|
-
// Add alarm
|
|
994
|
-
|
|
995
|
-
const
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
999
|
-
const durationStr = `${sign}PT${hours > 0 ? hours + 'H' : ''}${minutes > 0 ? minutes + 'M' : ''}`;
|
|
1000
|
-
vevent += `\r\nBEGIN:VALARM\r\nACTION:DISPLAY\r\nDESCRIPTION:Reminder\r\nTRIGGER:${durationStr}\r\nEND:VALARM`;
|
|
1019
|
+
// Add alarms — one VALARM block per reminder (alarm + alarms merged)
|
|
1020
|
+
for (const mins of collectAlarmMinutes(args.alarm, args.alarms)) {
|
|
1021
|
+
const valarm = buildVAlarm(mins);
|
|
1022
|
+
if (valarm)
|
|
1023
|
+
vevent += `\r\n${valarm}`;
|
|
1001
1024
|
}
|
|
1002
1025
|
vevent += `\r\nEND:VEVENT\r\nEND:VCALENDAR`;
|
|
1003
1026
|
const response = await fetchCalDAV(calDavUrl, {
|
|
@@ -1075,7 +1098,11 @@ export const updateEventTool = {
|
|
|
1075
1098
|
.number()
|
|
1076
1099
|
.nullable()
|
|
1077
1100
|
.optional()
|
|
1078
|
-
.describe('
|
|
1101
|
+
.describe('Single reminder in minutes before the event, or null to remove all existing alarms. For multiple reminders use alarms[].'),
|
|
1102
|
+
alarms: z
|
|
1103
|
+
.array(z.number())
|
|
1104
|
+
.optional()
|
|
1105
|
+
.describe('Multiple reminders in minutes before the event (e.g. [1440, 60]). Replaces existing alarms; combined with alarm if both are set.'),
|
|
1079
1106
|
attendees: z
|
|
1080
1107
|
.array(z.object({
|
|
1081
1108
|
email: z.string().describe('Attendee email'),
|
|
@@ -1138,17 +1165,17 @@ export const updateEventTool = {
|
|
|
1138
1165
|
modified = modified.replace(/END:VEVENT/, `CATEGORIES:${args.categories.map(escapeICalValue).join(',')}\r\nEND:VEVENT`);
|
|
1139
1166
|
}
|
|
1140
1167
|
}
|
|
1141
|
-
// Handle
|
|
1142
|
-
|
|
1143
|
-
|
|
1168
|
+
// Handle alarms (VALARM). Any alarm/alarms input replaces all existing
|
|
1169
|
+
// VALARM blocks; alarm:null clears them without adding new ones.
|
|
1170
|
+
if (args.alarm !== undefined || args.alarms !== undefined) {
|
|
1144
1171
|
modified = modified.replace(/BEGIN:VALARM[\s\S]*?END:VALARM\r?\n?/g, '');
|
|
1145
|
-
if (args.alarm !== null
|
|
1146
|
-
const
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1172
|
+
if (args.alarm !== null) {
|
|
1173
|
+
for (const mins of collectAlarmMinutes(args.alarm, args.alarms)) {
|
|
1174
|
+
const valarm = buildVAlarm(mins);
|
|
1175
|
+
if (valarm) {
|
|
1176
|
+
modified = modified.replace(/END:VEVENT/, `${valarm}\r\nEND:VEVENT`);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1152
1179
|
}
|
|
1153
1180
|
}
|
|
1154
1181
|
// Handle attendees
|
package/dist/tools/apps/mail.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { writeFileSync, unlinkSync } from 'node:fs';
|
|
2
4
|
import { z } from 'zod';
|
|
3
5
|
import { fetchMailAPI } from '../../client/mail.js';
|
|
4
6
|
import { fetchOCS } from '../../client/ocs.js';
|
|
@@ -232,8 +234,10 @@ const readMessageTool = {
|
|
|
232
234
|
if (attachments && attachments.length > 0) {
|
|
233
235
|
text += `\n${'─'.repeat(60)}\nAttachments:`;
|
|
234
236
|
for (const att of attachments) {
|
|
235
|
-
|
|
237
|
+
const idPart = att.id !== undefined ? `[id: ${att.id}] ` : '';
|
|
238
|
+
text += `\n • ${idPart}${att.fileName} (${att.size} bytes)`;
|
|
236
239
|
}
|
|
240
|
+
text += `\n(Use mail_get_attachment with the message ID and an attachment id to read one.)`;
|
|
237
241
|
}
|
|
238
242
|
text += `\n\nMessage ID: ${args.messageId}`;
|
|
239
243
|
return { content: [{ type: 'text', text }] };
|
|
@@ -251,6 +255,96 @@ const readMessageTool = {
|
|
|
251
255
|
}
|
|
252
256
|
},
|
|
253
257
|
};
|
|
258
|
+
const getAttachmentTool = {
|
|
259
|
+
name: 'mail_get_attachment',
|
|
260
|
+
description: 'Download an email attachment by message ID and attachment ID. ' +
|
|
261
|
+
'Returns text for text files and calendar invites (ICS), image data for images, ' +
|
|
262
|
+
'extracted text for PDFs (requires pdftotext). ' +
|
|
263
|
+
'Attachment IDs are shown in mail_read_message output.',
|
|
264
|
+
inputSchema: z.object({
|
|
265
|
+
messageId: z.number().describe('Message ID (from mail_read_message)'),
|
|
266
|
+
attachmentId: z.string().describe('Attachment ID shown in mail_read_message (e.g. "2")'),
|
|
267
|
+
}),
|
|
268
|
+
handler: async (args) => {
|
|
269
|
+
try {
|
|
270
|
+
const response = await fetchMailAPI(`/messages/${args.messageId}/attachment/${args.attachmentId}`);
|
|
271
|
+
if (!response.ok) {
|
|
272
|
+
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
|
273
|
+
}
|
|
274
|
+
const contentType = response.headers.get('content-type') ?? 'application/octet-stream';
|
|
275
|
+
const mimeType = contentType.split(';')[0].trim();
|
|
276
|
+
if (mimeType.startsWith('text/') ||
|
|
277
|
+
mimeType === 'application/json' ||
|
|
278
|
+
mimeType === 'application/ics') {
|
|
279
|
+
const text = await response.text();
|
|
280
|
+
return { content: [{ type: 'text', text }] };
|
|
281
|
+
}
|
|
282
|
+
if (mimeType.startsWith('image/')) {
|
|
283
|
+
const buffer = await response.arrayBuffer();
|
|
284
|
+
const base64 = Buffer.from(buffer).toString('base64');
|
|
285
|
+
return { content: [{ type: 'image', data: base64, mimeType }] };
|
|
286
|
+
}
|
|
287
|
+
if (mimeType === 'application/pdf') {
|
|
288
|
+
const buffer = await response.arrayBuffer();
|
|
289
|
+
const safeId = String(args.messageId).replace(/[^a-zA-Z0-9_-]/g, '') +
|
|
290
|
+
'_' +
|
|
291
|
+
String(args.attachmentId).replace(/[^a-zA-Z0-9_-]/g, '');
|
|
292
|
+
const tmpFile = `/tmp/mcp_att_${safeId}.pdf`;
|
|
293
|
+
try {
|
|
294
|
+
writeFileSync(tmpFile, Buffer.from(buffer));
|
|
295
|
+
const result = spawnSync('pdftotext', [tmpFile, '-'], { encoding: 'utf8' });
|
|
296
|
+
unlinkSync(tmpFile);
|
|
297
|
+
if (result.status === 0 && result.stdout.trim().length > 0) {
|
|
298
|
+
return {
|
|
299
|
+
content: [
|
|
300
|
+
{
|
|
301
|
+
type: 'text',
|
|
302
|
+
text: `[UNTRUSTED EXTERNAL CONTENT - PDF ATTACHMENT]\n${result.stdout}\n[END EXTERNAL CONTENT]`,
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
try {
|
|
310
|
+
unlinkSync(tmpFile);
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
/* ignore */
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
content: [
|
|
318
|
+
{
|
|
319
|
+
type: 'text',
|
|
320
|
+
text: `Attachment: ${mimeType}, ${(buffer.byteLength / 1024).toFixed(1)} KB. Could not extract text (pdftotext unavailable or empty).`,
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
const buffer = await response.arrayBuffer();
|
|
326
|
+
return {
|
|
327
|
+
content: [
|
|
328
|
+
{
|
|
329
|
+
type: 'text',
|
|
330
|
+
text: `Attachment: ${mimeType}, ${(buffer.byteLength / 1024).toFixed(1)} KB. Cannot be read inline.`,
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
return {
|
|
337
|
+
content: [
|
|
338
|
+
{
|
|
339
|
+
type: 'text',
|
|
340
|
+
text: `Error downloading attachment: ${error instanceof Error ? error.message : String(error)}`,
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
isError: true,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
};
|
|
254
348
|
const searchMessagesTool = {
|
|
255
349
|
name: 'mail_search_messages',
|
|
256
350
|
description: 'Search email messages across all mailboxes by subject or sender. ' +
|
|
@@ -502,6 +596,7 @@ export const mailTools = [
|
|
|
502
596
|
listMailboxesTool,
|
|
503
597
|
listMessagesTool,
|
|
504
598
|
readMessageTool,
|
|
599
|
+
getAttachmentTool,
|
|
505
600
|
searchMessagesTool,
|
|
506
601
|
sendMessageTool,
|
|
507
602
|
deleteMessageTool,
|