aiquila-mcp 0.2.6 → 0.2.7

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/server.js CHANGED
@@ -23,6 +23,8 @@ import { mailTools } from './tools/apps/mail.js';
23
23
  import { bookmarksTools } from './tools/apps/bookmarks.js';
24
24
  import { mapsTools } from './tools/apps/maps.js';
25
25
  import { assistantTools } from './tools/apps/assistant.js';
26
+ import { translateTools } from './tools/apps/translate.js';
27
+ import { talkTools } from './tools/apps/talk.js';
26
28
  const SERVER_NAME = 'aiquila';
27
29
  const _require = createRequire(import.meta.url);
28
30
  export const SERVER_VERSION = _require('../package.json').version;
@@ -47,6 +49,8 @@ const allToolSets = [
47
49
  tagsTools,
48
50
  searchTools,
49
51
  assistantTools,
52
+ translateTools,
53
+ talkTools,
50
54
  ];
51
55
  /**
52
56
  * Create a fully-configured McpServer with all tools registered.
@@ -109,7 +109,7 @@ const listMailboxesTool = {
109
109
  },
110
110
  };
111
111
  const listMessagesTool = {
112
- name: 'list_messages',
112
+ name: 'mail_list_messages',
113
113
  description: 'List email messages in a Nextcloud Mail mailbox. Supports pagination via cursor. Returns subject, sender, date, and message IDs.',
114
114
  inputSchema: z.object({
115
115
  mailboxId: z.number().describe('The mailbox ID (from list_mailboxes)'),
@@ -174,7 +174,7 @@ const listMessagesTool = {
174
174
  },
175
175
  };
176
176
  const readMessageTool = {
177
- name: 'read_message',
177
+ name: 'mail_read_message',
178
178
  description: 'Read the full content of an email message by ID. Returns headers, body text, and attachment list.',
179
179
  inputSchema: z.object({
180
180
  messageId: z.number().describe('The message ID (from list_messages)'),
@@ -227,7 +227,7 @@ const readMessageTool = {
227
227
  },
228
228
  };
229
229
  const sendMessageTool = {
230
- name: 'send_message',
230
+ name: 'mail_send_message',
231
231
  description: 'Send an email message through Nextcloud Mail. Requires an account ID and recipient addresses.',
232
232
  inputSchema: z.object({
233
233
  accountId: z.number().describe('The mail account ID to send from (from list_mail_accounts)'),
@@ -290,7 +290,7 @@ const sendMessageTool = {
290
290
  },
291
291
  };
292
292
  const deleteMessageTool = {
293
- name: 'delete_message',
293
+ name: 'mail_delete_message',
294
294
  description: 'Delete an email message by ID. This typically moves it to trash.',
295
295
  inputSchema: z.object({
296
296
  messageId: z.number().describe('The message ID to delete'),
@@ -321,7 +321,7 @@ const deleteMessageTool = {
321
321
  },
322
322
  };
323
323
  const moveMessageTool = {
324
- name: 'move_message',
324
+ name: 'mail_move_message',
325
325
  description: 'Move an email message to a different mailbox/folder.',
326
326
  inputSchema: z.object({
327
327
  messageId: z.number().describe('The message ID to move'),
@@ -359,7 +359,7 @@ const moveMessageTool = {
359
359
  },
360
360
  };
361
361
  const setMessageFlagsTool = {
362
- name: 'set_message_flags',
362
+ name: 'mail_set_message_flags',
363
363
  description: 'Set flags on an email message (mark as read/unread, star/unstar, mark as important or junk).',
364
364
  inputSchema: z.object({
365
365
  messageId: z.number().describe('The message ID'),
@@ -0,0 +1,399 @@
1
+ import { z } from 'zod';
2
+ import { fetchOCS } from '../../client/ocs.js';
3
+ /**
4
+ * Nextcloud Talk (Spreed) Tools
5
+ *
6
+ * Bridges MCP to Nextcloud's Talk API for managing conversations,
7
+ * messages, participants, polls, and reactions.
8
+ */
9
+ // ---------------------------------------------------------------------------
10
+ // Constants
11
+ // ---------------------------------------------------------------------------
12
+ const API_V4 = '/ocs/v2.php/apps/spreed/api/v4';
13
+ const API_V1 = '/ocs/v2.php/apps/spreed/api/v1';
14
+ const CONVERSATION_TYPE_LABELS = {
15
+ 1: 'one-on-one',
16
+ 2: 'group',
17
+ 3: 'public',
18
+ 4: 'changelog',
19
+ 5: 'former one-on-one',
20
+ };
21
+ const PARTICIPANT_TYPE_LABELS = {
22
+ 1: 'owner',
23
+ 2: 'moderator',
24
+ 3: 'user',
25
+ 4: 'guest',
26
+ 5: 'public link user',
27
+ 6: 'guest moderator',
28
+ };
29
+ const ROOM_TYPE_MAP = {
30
+ 'one-on-one': 1,
31
+ group: 2,
32
+ public: 3,
33
+ };
34
+ // ---------------------------------------------------------------------------
35
+ // Helpers
36
+ // ---------------------------------------------------------------------------
37
+ function resolveRichMessage(message, params) {
38
+ if (!params)
39
+ return message;
40
+ let resolved = message;
41
+ for (const [key, param] of Object.entries(params)) {
42
+ resolved = resolved.replace(`{${key}}`, param.name ?? param.id);
43
+ }
44
+ return resolved;
45
+ }
46
+ function formatTimestamp(ts) {
47
+ const d = new Date(ts * 1000);
48
+ const pad = (n) => String(n).padStart(2, '0');
49
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
50
+ }
51
+ function text(t) {
52
+ return { content: [{ type: 'text', text: t }] };
53
+ }
54
+ function error(msg) {
55
+ return { content: [{ type: 'text', text: msg }], isError: true };
56
+ }
57
+ function wrapError(action, err) {
58
+ return error(`Error ${action}: ${err instanceof Error ? err.message : String(err)}`);
59
+ }
60
+ // ---------------------------------------------------------------------------
61
+ // list_conversations
62
+ // ---------------------------------------------------------------------------
63
+ export const listConversationsTool = {
64
+ name: 'talk_list_conversations',
65
+ description: 'List all Talk conversations the user has access to. Returns conversation tokens, names, types, and unread message counts.',
66
+ inputSchema: z.object({
67
+ includeStatus: z
68
+ .boolean()
69
+ .optional()
70
+ .describe('Include user status information (default false)'),
71
+ }),
72
+ handler: async (args) => {
73
+ try {
74
+ const queryParams = {};
75
+ if (args.includeStatus)
76
+ queryParams.includeStatus = 'true';
77
+ const data = await fetchOCS(`${API_V4}/room`, { queryParams });
78
+ const rooms = data.ocs.data ?? [];
79
+ if (rooms.length === 0)
80
+ return text('No conversations found.');
81
+ const lines = rooms.map((r) => {
82
+ const type = CONVERSATION_TYPE_LABELS[r.type] ?? `type-${r.type}`;
83
+ const last = r.lastActivity ? formatTimestamp(r.lastActivity) : 'never';
84
+ let line = `[${r.token}] ${r.displayName || r.name} (${type}, unread: ${r.unreadMessages}) — last: ${last}`;
85
+ if (args.includeStatus && r.status) {
86
+ line += ` — status: ${r.status}${r.statusMessage ? ` "${r.statusMessage}"` : ''}`;
87
+ }
88
+ return line;
89
+ });
90
+ return text(lines.join('\n'));
91
+ }
92
+ catch (err) {
93
+ return wrapError('listing conversations', err);
94
+ }
95
+ },
96
+ };
97
+ // ---------------------------------------------------------------------------
98
+ // list_messages
99
+ // ---------------------------------------------------------------------------
100
+ export const listMessagesTool = {
101
+ name: 'talk_list_messages',
102
+ description: 'List recent messages in a Talk conversation. Returns message content with timestamps and authors.',
103
+ inputSchema: z.object({
104
+ token: z.string().describe('Conversation token (from list_conversations)'),
105
+ limit: z.number().optional().describe('Number of messages to retrieve (default 50, max 200)'),
106
+ lastKnownMessageId: z
107
+ .number()
108
+ .optional()
109
+ .describe('Message ID to start from for pagination (returns messages before this ID)'),
110
+ includeSystemMessages: z
111
+ .boolean()
112
+ .optional()
113
+ .describe('Include system messages like join/leave notifications (default false)'),
114
+ }),
115
+ handler: async (args) => {
116
+ try {
117
+ const queryParams = {
118
+ lookIntoFuture: '0',
119
+ limit: String(Math.min(args.limit ?? 50, 200)),
120
+ };
121
+ if (args.lastKnownMessageId) {
122
+ queryParams.lastKnownMessageId = String(args.lastKnownMessageId);
123
+ }
124
+ const data = await fetchOCS(`${API_V4}/chat/${args.token}`, { queryParams });
125
+ let messages = data.ocs.data ?? [];
126
+ if (!args.includeSystemMessages) {
127
+ messages = messages.filter((m) => !m.systemMessage);
128
+ }
129
+ if (messages.length === 0)
130
+ return text('No messages found.');
131
+ const lines = messages.map((m) => {
132
+ const ts = formatTimestamp(m.timestamp);
133
+ const resolved = resolveRichMessage(m.message, m.messageParameters);
134
+ return `[${ts}] ${m.actorDisplayName}: ${resolved}`;
135
+ });
136
+ return text(lines.join('\n'));
137
+ }
138
+ catch (err) {
139
+ return wrapError('listing messages', err);
140
+ }
141
+ },
142
+ };
143
+ // ---------------------------------------------------------------------------
144
+ // send_message
145
+ // ---------------------------------------------------------------------------
146
+ export const sendMessageTool = {
147
+ name: 'talk_send_message',
148
+ description: 'Send a message to a Talk conversation. Supports replies and silent messages that do not trigger notifications.',
149
+ inputSchema: z.object({
150
+ token: z.string().describe('Conversation token'),
151
+ message: z.string().describe('Message text to send'),
152
+ replyTo: z.number().optional().describe('Message ID to reply to'),
153
+ silent: z
154
+ .boolean()
155
+ .optional()
156
+ .describe('Send without triggering notifications (default false)'),
157
+ }),
158
+ handler: async (args) => {
159
+ try {
160
+ const body = { message: args.message };
161
+ if (args.replyTo !== undefined)
162
+ body.replyTo = String(args.replyTo);
163
+ if (args.silent)
164
+ body.silent = 'true';
165
+ const data = await fetchOCS(`${API_V4}/chat/${args.token}`, {
166
+ method: 'POST',
167
+ body,
168
+ });
169
+ const msg = data.ocs.data;
170
+ return text(`Message sent (ID: ${msg.id}) at ${formatTimestamp(msg.timestamp)}: ${resolveRichMessage(msg.message, msg.messageParameters)}`);
171
+ }
172
+ catch (err) {
173
+ return wrapError('sending message', err);
174
+ }
175
+ },
176
+ };
177
+ // ---------------------------------------------------------------------------
178
+ // create_conversation
179
+ // ---------------------------------------------------------------------------
180
+ export const createConversationTool = {
181
+ name: 'talk_create_conversation',
182
+ description: 'Create a new Talk conversation. One-on-one requires an invite user, group/public require a name.',
183
+ inputSchema: z.object({
184
+ roomType: z.enum(['one-on-one', 'group', 'public']).describe('Type of conversation to create'),
185
+ roomName: z
186
+ .string()
187
+ .optional()
188
+ .describe('Name for the conversation (required for group and public)'),
189
+ invite: z.string().optional().describe('User ID to invite (required for one-on-one)'),
190
+ }),
191
+ handler: async (args) => {
192
+ if (args.roomType === 'one-on-one' && !args.invite) {
193
+ return error('invite is required for one-on-one conversations.');
194
+ }
195
+ if ((args.roomType === 'group' || args.roomType === 'public') && !args.roomName) {
196
+ return error('roomName is required for group and public conversations.');
197
+ }
198
+ try {
199
+ const body = {
200
+ roomType: String(ROOM_TYPE_MAP[args.roomType]),
201
+ };
202
+ if (args.roomName)
203
+ body.roomName = args.roomName;
204
+ if (args.invite)
205
+ body.invite = args.invite;
206
+ const data = await fetchOCS(`${API_V4}/room`, {
207
+ method: 'POST',
208
+ body,
209
+ });
210
+ const room = data.ocs.data;
211
+ const type = CONVERSATION_TYPE_LABELS[room.type] ?? `type-${room.type}`;
212
+ return text(`Conversation created: [${room.token}] ${room.displayName || room.name} (${type})`);
213
+ }
214
+ catch (err) {
215
+ return wrapError('creating conversation', err);
216
+ }
217
+ },
218
+ };
219
+ // ---------------------------------------------------------------------------
220
+ // list_participants
221
+ // ---------------------------------------------------------------------------
222
+ export const listParticipantsTool = {
223
+ name: 'talk_list_participants',
224
+ description: 'List all participants in a Talk conversation with their roles.',
225
+ inputSchema: z.object({
226
+ token: z.string().describe('Conversation token'),
227
+ }),
228
+ handler: async (args) => {
229
+ try {
230
+ const data = await fetchOCS(`${API_V4}/room/${args.token}/participants`);
231
+ const participants = data.ocs.data ?? [];
232
+ if (participants.length === 0)
233
+ return text('No participants found.');
234
+ const lines = participants.map((p) => {
235
+ const role = PARTICIPANT_TYPE_LABELS[p.participantType] ?? `type-${p.participantType}`;
236
+ return `${p.actorId} (${p.displayName}) — ${role}, attendeeId: ${p.attendeeId}`;
237
+ });
238
+ return text(lines.join('\n'));
239
+ }
240
+ catch (err) {
241
+ return wrapError('listing participants', err);
242
+ }
243
+ },
244
+ };
245
+ // ---------------------------------------------------------------------------
246
+ // add_participant
247
+ // ---------------------------------------------------------------------------
248
+ export const addParticipantTool = {
249
+ name: 'talk_add_participant',
250
+ description: 'Add a user, group, or email participant to a Talk conversation.',
251
+ inputSchema: z.object({
252
+ token: z.string().describe('Conversation token'),
253
+ newParticipant: z.string().describe('User ID, group ID, or email address to add'),
254
+ source: z
255
+ .enum(['users', 'groups', 'emails'])
256
+ .optional()
257
+ .describe("Participant source type (default 'users')"),
258
+ }),
259
+ handler: async (args) => {
260
+ try {
261
+ const body = { newParticipant: args.newParticipant };
262
+ if (args.source)
263
+ body.source = args.source;
264
+ await fetchOCS(`${API_V4}/room/${args.token}/participants`, {
265
+ method: 'POST',
266
+ body,
267
+ });
268
+ return text(`Participant "${args.newParticipant}" added to conversation ${args.token}.`);
269
+ }
270
+ catch (err) {
271
+ return wrapError('adding participant', err);
272
+ }
273
+ },
274
+ };
275
+ // ---------------------------------------------------------------------------
276
+ // remove_participant
277
+ // ---------------------------------------------------------------------------
278
+ export const removeParticipantTool = {
279
+ name: 'talk_remove_participant',
280
+ description: 'Remove a participant from a Talk conversation by their attendee ID (from list_participants).',
281
+ inputSchema: z.object({
282
+ token: z.string().describe('Conversation token'),
283
+ attendeeId: z.number().describe('Attendee ID of the participant to remove'),
284
+ }),
285
+ handler: async (args) => {
286
+ try {
287
+ await fetchOCS(`${API_V4}/room/${args.token}/attendees`, {
288
+ method: 'DELETE',
289
+ body: { attendeeId: String(args.attendeeId) },
290
+ });
291
+ return text(`Participant (attendeeId: ${args.attendeeId}) removed from conversation ${args.token}.`);
292
+ }
293
+ catch (err) {
294
+ return wrapError('removing participant', err);
295
+ }
296
+ },
297
+ };
298
+ // ---------------------------------------------------------------------------
299
+ // delete_message
300
+ // ---------------------------------------------------------------------------
301
+ export const deleteMessageTool = {
302
+ name: 'talk_delete_message',
303
+ description: 'Delete a message from a Talk conversation.',
304
+ inputSchema: z.object({
305
+ token: z.string().describe('Conversation token'),
306
+ messageId: z.number().describe('ID of the message to delete'),
307
+ }),
308
+ handler: async (args) => {
309
+ try {
310
+ await fetchOCS(`${API_V4}/chat/${args.token}/${args.messageId}`, {
311
+ method: 'DELETE',
312
+ });
313
+ return text(`Message ${args.messageId} deleted from conversation ${args.token}.`);
314
+ }
315
+ catch (err) {
316
+ return wrapError('deleting message', err);
317
+ }
318
+ },
319
+ };
320
+ // ---------------------------------------------------------------------------
321
+ // create_poll
322
+ // ---------------------------------------------------------------------------
323
+ export const createPollTool = {
324
+ name: 'talk_create_poll',
325
+ description: 'Create a poll in a Talk conversation. Requires at least 2 options.',
326
+ inputSchema: z.object({
327
+ token: z.string().describe('Conversation token'),
328
+ question: z.string().describe('Poll question'),
329
+ options: z.array(z.string()).min(2).describe('Poll options (minimum 2)'),
330
+ resultMode: z
331
+ .enum(['public', 'hidden'])
332
+ .optional()
333
+ .describe("Result visibility: 'public' (default) or 'hidden' until closed"),
334
+ maxVotes: z
335
+ .number()
336
+ .optional()
337
+ .describe('Maximum number of votes per participant (0 = unlimited, default 1)'),
338
+ }),
339
+ handler: async (args) => {
340
+ if (args.options.length < 2) {
341
+ return error('A poll requires at least 2 options.');
342
+ }
343
+ try {
344
+ const jsonBody = {
345
+ question: args.question,
346
+ options: args.options,
347
+ resultMode: args.resultMode === 'hidden' ? 1 : 0,
348
+ maxVotes: args.maxVotes ?? 1,
349
+ };
350
+ await fetchOCS(`${API_V1}/poll/${args.token}`, {
351
+ method: 'POST',
352
+ jsonBody,
353
+ });
354
+ return text(`Poll created in conversation ${args.token}: "${args.question}" with ${args.options.length} options.`);
355
+ }
356
+ catch (err) {
357
+ return wrapError('creating poll', err);
358
+ }
359
+ },
360
+ };
361
+ // ---------------------------------------------------------------------------
362
+ // react_to_message
363
+ // ---------------------------------------------------------------------------
364
+ export const reactToMessageTool = {
365
+ name: 'talk_react_to_message',
366
+ description: 'Add an emoji reaction to a message in a Talk conversation.',
367
+ inputSchema: z.object({
368
+ token: z.string().describe('Conversation token'),
369
+ messageId: z.number().describe('ID of the message to react to'),
370
+ reaction: z.string().describe("Emoji reaction (e.g. '👍', '❤️', '🎉')"),
371
+ }),
372
+ handler: async (args) => {
373
+ try {
374
+ await fetchOCS(`${API_V1}/reaction/${args.token}/${args.messageId}`, {
375
+ method: 'POST',
376
+ body: { reaction: args.reaction },
377
+ });
378
+ return text(`Reaction "${args.reaction}" added to message ${args.messageId} in conversation ${args.token}.`);
379
+ }
380
+ catch (err) {
381
+ return wrapError('reacting to message', err);
382
+ }
383
+ },
384
+ };
385
+ // ---------------------------------------------------------------------------
386
+ // Export
387
+ // ---------------------------------------------------------------------------
388
+ export const talkTools = [
389
+ listConversationsTool,
390
+ listMessagesTool,
391
+ sendMessageTool,
392
+ createConversationTool,
393
+ listParticipantsTool,
394
+ addParticipantTool,
395
+ removeParticipantTool,
396
+ deleteMessageTool,
397
+ createPollTool,
398
+ reactToMessageTool,
399
+ ];
@@ -0,0 +1,79 @@
1
+ import { z } from 'zod';
2
+ import { fetchOCS } from '../../client/ocs.js';
3
+ // ---------------------------------------------------------------------------
4
+ // translate_text
5
+ // ---------------------------------------------------------------------------
6
+ export const translateTextTool = {
7
+ name: 'translate_text',
8
+ description: "Translate text between languages using Nextcloud's configured translation provider. If called without text, returns available language pairs.",
9
+ inputSchema: z.object({
10
+ text: z
11
+ .string()
12
+ .optional()
13
+ .describe('Text to translate. Omit to list available language pairs.'),
14
+ fromLanguage: z.string().optional().describe("Source language code (e.g. 'en', 'de', 'fr')"),
15
+ toLanguage: z.string().optional().describe("Target language code (e.g. 'en', 'de', 'fr')"),
16
+ }),
17
+ handler: async (args) => {
18
+ try {
19
+ // List available languages mode
20
+ if (!args.text) {
21
+ const data = await fetchOCS('/ocs/v2.php/translation/languages');
22
+ const languages = data.ocs.data.languages ?? [];
23
+ return {
24
+ content: [
25
+ {
26
+ type: 'text',
27
+ text: languages.length === 0
28
+ ? 'No translation providers are configured in Nextcloud.'
29
+ : JSON.stringify(languages, null, 2),
30
+ },
31
+ ],
32
+ };
33
+ }
34
+ // Translate mode
35
+ if (!args.fromLanguage || !args.toLanguage) {
36
+ return {
37
+ content: [
38
+ {
39
+ type: 'text',
40
+ text: 'Both fromLanguage and toLanguage are required when translating text.',
41
+ },
42
+ ],
43
+ isError: true,
44
+ };
45
+ }
46
+ const data = await fetchOCS('/ocs/v2.php/translation/translate', {
47
+ method: 'POST',
48
+ body: {
49
+ text: args.text,
50
+ fromLanguage: args.fromLanguage,
51
+ toLanguage: args.toLanguage,
52
+ },
53
+ });
54
+ return {
55
+ content: [
56
+ {
57
+ type: 'text',
58
+ text: data.ocs.data.text,
59
+ },
60
+ ],
61
+ };
62
+ }
63
+ catch (error) {
64
+ return {
65
+ content: [
66
+ {
67
+ type: 'text',
68
+ text: `Error translating text: ${error instanceof Error ? error.message : String(error)}`,
69
+ },
70
+ ],
71
+ isError: true,
72
+ };
73
+ }
74
+ },
75
+ };
76
+ // ---------------------------------------------------------------------------
77
+ // Export
78
+ // ---------------------------------------------------------------------------
79
+ export const translateTools = [translateTextTool];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiquila-mcp",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "AIquila - MCP server for Nextcloud integration with Claude AI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",