aiquila-mcp 0.3.2 → 0.3.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # AIquila MCP Server
2
2
 
3
- MCP (Model Context Protocol) server that gives any MCP client full access to your Nextcloud instance — files, calendar, tasks, contacts, mail, talk, maps, bookmarks, notes, and more. 198 tools across 31 categories.
3
+ MCP (Model Context Protocol) server that gives any MCP client full access to your Nextcloud instance — files, calendar, tasks, contacts, mail, talk, maps, bookmarks, notes, polls, and more. 219 tools across 32 categories.
4
4
 
5
5
  ## Quick Start
6
6
 
@@ -73,11 +73,12 @@ See the [Docker setup guide](https://github.com/elgorro/aiquila/blob/main/docs/m
73
73
  | Talk | 10 |
74
74
  | Circles | 8 |
75
75
  | Bookmarks | 13 |
76
+ | Polls | 21 |
76
77
  | Assistant | 4 |
77
78
  | Translate | 1 |
78
79
  | User Status | 5 |
79
80
  | Notifications | 4 |
80
- | **Total** | **198** |
81
+ | **Total** | **219** |
81
82
 
82
83
  ## Configuration
83
84
 
@@ -21,7 +21,10 @@ export async function fetchNotesAPI(endpoint, options = {}) {
21
21
  Accept: 'application/json',
22
22
  };
23
23
  if (options.ifMatch) {
24
- headers['If-Match'] = options.ifMatch;
24
+ // Notes API returns etag as an unquoted hash in JSON but requires a quoted
25
+ // value in the If-Match header per RFC 7232. Wrap if not already quoted.
26
+ const tag = options.ifMatch;
27
+ headers['If-Match'] = tag.startsWith('"') ? tag : `"${tag}"`;
25
28
  }
26
29
  let body;
27
30
  if (options.body !== undefined) {
@@ -0,0 +1,44 @@
1
+ // SPDX-License-Identifier: MIT
2
+ import { getNextcloudConfig } from '../tools/types.js';
3
+ import { logger } from '../logger.js';
4
+ import { ApiError } from './aiquila.js';
5
+ /**
6
+ * Make an authenticated request to the Nextcloud Polls REST API v1.0.
7
+ *
8
+ * Base path: /index.php/apps/polls/api/v1.0
9
+ */
10
+ export async function fetchPollsAPI(endpoint, options = {}) {
11
+ const config = getNextcloudConfig();
12
+ const auth = Buffer.from(`${config.user}:${config.password}`).toString('base64');
13
+ let url = `${config.url}/index.php/apps/polls/api/v1.0${endpoint}`;
14
+ if (options.queryParams) {
15
+ const params = new URLSearchParams(options.queryParams);
16
+ url += `?${params.toString()}`;
17
+ }
18
+ const headers = {
19
+ Authorization: `Basic ${auth}`,
20
+ 'OCS-APIRequest': 'true',
21
+ Accept: 'application/json',
22
+ };
23
+ let body;
24
+ if (options.body !== undefined) {
25
+ body = JSON.stringify(options.body);
26
+ headers['Content-Type'] = 'application/json';
27
+ }
28
+ const method = options.method ?? 'GET';
29
+ const t0 = Date.now();
30
+ const response = await fetch(url, { method, headers, body });
31
+ logger.trace({ method, url, status: response.status, ms: Date.now() - t0 }, '[polls] HTTP');
32
+ if (!response.ok) {
33
+ const text = await response.text();
34
+ throw new ApiError(response.status, response.statusText, text);
35
+ }
36
+ if (response.status === 204) {
37
+ return undefined;
38
+ }
39
+ const contentType = response.headers.get('content-type') ?? '';
40
+ if (contentType.includes('application/json')) {
41
+ return (await response.json());
42
+ }
43
+ return undefined;
44
+ }
@@ -34,6 +34,7 @@ import { notificationsTools } from './tools/apps/notifications.js';
34
34
  import { trashTools } from './tools/apps/trash.js';
35
35
  import { versionsTools } from './tools/apps/versions.js';
36
36
  import { projectsTools } from './tools/apps/projects.js';
37
+ import { pollsTools } from './tools/apps/polls.js';
37
38
  /**
38
39
  * Single source of truth for tool-to-Nextcloud-app mapping.
39
40
  *
@@ -72,6 +73,7 @@ export const TOOL_REGISTRY = [
72
73
  { category: 'talk', appIds: ['spreed'], tools: talkTools },
73
74
  { category: 'circles', appIds: ['circles'], tools: circlesTools },
74
75
  { category: 'bookmarks', appIds: ['bookmarks'], tools: bookmarksTools },
76
+ { category: 'polls', appIds: ['polls'], tools: pollsTools },
75
77
  { category: 'assistant', appIds: ['assistant'], tools: assistantTools },
76
78
  { category: 'translate', appIds: ['text_translate', 'translate'], tools: translateTools },
77
79
  { category: 'user_status', appIds: ['user_status'], tools: userStatusTools },
@@ -0,0 +1,683 @@
1
+ // SPDX-License-Identifier: MIT
2
+ import { z } from 'zod';
3
+ import { fetchPollsAPI, } from '../../client/polls.js';
4
+ import { handleAppError } from '../error-utils.js';
5
+ /**
6
+ * Nextcloud Polls App Tools
7
+ * Uses the Polls REST API v1.0 (/index.php/apps/polls/api/v1.0)
8
+ */
9
+ const pollsStatusMap = {
10
+ 403: 'Access denied to this poll.',
11
+ 404: 'Poll, option, share, or comment not found.',
12
+ 409: 'Conflict — the option or share already exists.',
13
+ };
14
+ function formatPoll(p) {
15
+ const cfg = p.configuration ?? { title: '(untitled)' };
16
+ const status = p.status?.deleted
17
+ ? 'deleted'
18
+ : p.status?.expired
19
+ ? 'expired'
20
+ : (cfg.expire ?? 0) < 0
21
+ ? 'closed'
22
+ : 'open';
23
+ const owner = p.owner?.displayName || p.owner?.userId || 'unknown';
24
+ return `[${p.id}] ${cfg.title} (${p.type}) — owner: ${owner}, status: ${status}`;
25
+ }
26
+ function formatPollDetailed(p) {
27
+ const cfg = p.configuration ?? { title: '(untitled)' };
28
+ const lines = [formatPoll(p)];
29
+ if (cfg.description)
30
+ lines.push(`Description: ${cfg.description}`);
31
+ if (cfg.access)
32
+ lines.push(`Access: ${cfg.access}`);
33
+ if (cfg.expire && cfg.expire > 0) {
34
+ lines.push(`Expires: ${new Date(cfg.expire * 1000).toISOString()}`);
35
+ }
36
+ if (cfg.showResults)
37
+ lines.push(`Show results: ${cfg.showResults}`);
38
+ const flags = [
39
+ cfg.anonymous ? 'anonymous' : null,
40
+ cfg.allowComment ? 'comments allowed' : null,
41
+ cfg.allowMaybe ? 'maybe allowed' : null,
42
+ cfg.allowProposals ? 'proposals allowed' : null,
43
+ cfg.useNo ? 'no-votes allowed' : null,
44
+ ].filter(Boolean);
45
+ if (flags.length)
46
+ lines.push(`Flags: ${flags.join(', ')}`);
47
+ if (p.currentUserStatus) {
48
+ const s = p.currentUserStatus;
49
+ lines.push(`You: role=${s.userRole ?? '?'}, votes=${s.countVotes ?? 0}, subscribed=${s.isSubscribed ?? false}`);
50
+ }
51
+ return lines.join('\n');
52
+ }
53
+ function formatOption(o) {
54
+ const label = o.text || o.pollOptionText || `option ${o.id}`;
55
+ const tallies = [];
56
+ if (o.yes !== undefined)
57
+ tallies.push(`yes=${o.yes}`);
58
+ if (o.no !== undefined)
59
+ tallies.push(`no=${o.no}`);
60
+ if (o.maybe !== undefined)
61
+ tallies.push(`maybe=${o.maybe}`);
62
+ const confirmed = o.confirmed ? ' (confirmed)' : '';
63
+ let timing = '';
64
+ if (o.timestamp) {
65
+ const start = new Date(o.timestamp * 1000).toISOString();
66
+ timing = ` — ${start}${o.duration ? ` +${o.duration}s` : ''}`;
67
+ }
68
+ return `[${o.id}] ${label}${timing}${confirmed}${tallies.length ? ` (${tallies.join(', ')})` : ''}`;
69
+ }
70
+ function formatVote(v) {
71
+ const who = v.userId ?? 'unknown';
72
+ const answer = v.voteAnswer ?? '?';
73
+ const what = v.optionText ?? (v.optionId !== undefined ? `option ${v.optionId}` : '');
74
+ return `${who} → ${answer}${what ? ` on ${what}` : ''}`;
75
+ }
76
+ function formatComment(c) {
77
+ const who = c.user?.displayName || c.userId || 'unknown';
78
+ const when = c.timestamp ? new Date(c.timestamp * 1000).toISOString() : (c.dt ?? '');
79
+ return `[${c.id}] ${who}${when ? ` at ${when}` : ''}: ${c.comment ?? ''}`;
80
+ }
81
+ function formatShare(s) {
82
+ const label = s.label || s.userId || '';
83
+ const who = label ? ` ${label}` : '';
84
+ return `[${s.type}] token=${s.token}${who}${s.URL ? ` — ${s.URL}` : ''}`;
85
+ }
86
+ // ─────────────────────────────────────────────────────────────────────────────
87
+ // Polls
88
+ // ─────────────────────────────────────────────────────────────────────────────
89
+ export const listPollsTool = {
90
+ name: 'list_polls',
91
+ description: 'List all polls the current user can access (own, shared, or public). Returns id, title, type, owner, and status.',
92
+ inputSchema: z.object({}),
93
+ handler: async () => {
94
+ try {
95
+ const polls = await fetchPollsAPI('/polls');
96
+ if (!polls || polls.length === 0) {
97
+ return { content: [{ type: 'text', text: 'No polls found.' }] };
98
+ }
99
+ return {
100
+ content: [
101
+ {
102
+ type: 'text',
103
+ text: `Polls (${polls.length}):\n\n${polls.map(formatPoll).join('\n')}`,
104
+ },
105
+ ],
106
+ };
107
+ }
108
+ catch (error) {
109
+ return handleAppError(error, 'Error listing polls', pollsStatusMap);
110
+ }
111
+ },
112
+ };
113
+ export const getPollTool = {
114
+ name: 'get_poll',
115
+ description: 'Get full details of a poll by ID: configuration, owner, status, and current user state.',
116
+ inputSchema: z.object({
117
+ pollId: z.number().int().describe('Poll ID (from list_polls)'),
118
+ }),
119
+ handler: async (args) => {
120
+ try {
121
+ const { poll } = await fetchPollsAPI(`/poll/${args.pollId}`);
122
+ return {
123
+ content: [{ type: 'text', text: formatPollDetailed(poll) }],
124
+ };
125
+ }
126
+ catch (error) {
127
+ return handleAppError(error, 'Error getting poll', pollsStatusMap);
128
+ }
129
+ },
130
+ };
131
+ export const createPollTool = {
132
+ name: 'create_poll',
133
+ description: 'Create a new poll. Type "textPoll" for text options (e.g. lunch choices), "datePoll" for date/time options (e.g. meeting scheduling).',
134
+ inputSchema: z.object({
135
+ title: z.string().describe('Poll title'),
136
+ type: z.enum(['textPoll', 'datePoll']).describe('Poll type'),
137
+ }),
138
+ handler: async (args) => {
139
+ try {
140
+ const { poll } = await fetchPollsAPI('/poll', {
141
+ method: 'POST',
142
+ body: { title: args.title, type: args.type },
143
+ });
144
+ return {
145
+ content: [{ type: 'text', text: `Poll created: ${formatPoll(poll)}` }],
146
+ };
147
+ }
148
+ catch (error) {
149
+ return handleAppError(error, 'Error creating poll', pollsStatusMap);
150
+ }
151
+ },
152
+ };
153
+ export const updatePollTool = {
154
+ name: 'update_poll',
155
+ description: 'Update a poll configuration. Only provided fields are changed. Set expire to 0 for no expiration, or a unix timestamp to auto-close on that date.',
156
+ inputSchema: z.object({
157
+ pollId: z.number().int().describe('Poll ID'),
158
+ title: z.string().optional(),
159
+ description: z.string().optional(),
160
+ expire: z
161
+ .number()
162
+ .int()
163
+ .optional()
164
+ .describe('Unix timestamp; 0 = no expiration; negative = close immediately'),
165
+ access: z.enum(['open', 'private']).optional(),
166
+ anonymous: z.boolean().optional(),
167
+ allowComment: z.boolean().optional(),
168
+ allowMaybe: z.boolean().optional(),
169
+ allowProposals: z.boolean().optional(),
170
+ showResults: z.enum(['never', 'always', 'closed']).optional(),
171
+ autoReminder: z.boolean().optional(),
172
+ hideBookedUp: z.boolean().optional(),
173
+ useNo: z.boolean().optional(),
174
+ maxVotesPerOption: z.number().int().min(0).optional(),
175
+ maxVotesPerUser: z.number().int().min(0).optional(),
176
+ }),
177
+ handler: async (args) => {
178
+ try {
179
+ const { pollId, ...rest } = args;
180
+ const pollBody = {};
181
+ for (const [k, v] of Object.entries(rest)) {
182
+ if (v !== undefined)
183
+ pollBody[k] = v;
184
+ }
185
+ if (Object.keys(pollBody).length === 0) {
186
+ return {
187
+ content: [{ type: 'text', text: 'No fields provided to update.' }],
188
+ isError: true,
189
+ };
190
+ }
191
+ const { poll } = await fetchPollsAPI(`/poll/${pollId}`, {
192
+ method: 'PUT',
193
+ body: { poll: pollBody },
194
+ });
195
+ return {
196
+ content: [{ type: 'text', text: `Poll updated: ${formatPoll(poll)}` }],
197
+ };
198
+ }
199
+ catch (error) {
200
+ return handleAppError(error, 'Error updating poll', pollsStatusMap);
201
+ }
202
+ },
203
+ };
204
+ export const deletePollTool = {
205
+ name: 'delete_poll',
206
+ description: 'Permanently delete a poll. This cannot be undone.',
207
+ inputSchema: z.object({
208
+ pollId: z.number().int().describe('Poll ID'),
209
+ }),
210
+ handler: async (args) => {
211
+ try {
212
+ await fetchPollsAPI(`/poll/${args.pollId}`, { method: 'DELETE' });
213
+ return {
214
+ content: [{ type: 'text', text: `Poll ${args.pollId} deleted.` }],
215
+ };
216
+ }
217
+ catch (error) {
218
+ return handleAppError(error, 'Error deleting poll', pollsStatusMap);
219
+ }
220
+ },
221
+ };
222
+ export const closePollTool = {
223
+ name: 'close_poll',
224
+ description: 'Close a poll immediately so no further votes are accepted.',
225
+ inputSchema: z.object({
226
+ pollId: z.number().int().describe('Poll ID'),
227
+ }),
228
+ handler: async (args) => {
229
+ try {
230
+ const { poll } = await fetchPollsAPI(`/poll/${args.pollId}/close`, {
231
+ method: 'PUT',
232
+ });
233
+ return {
234
+ content: [{ type: 'text', text: `Poll closed: ${formatPoll(poll)}` }],
235
+ };
236
+ }
237
+ catch (error) {
238
+ return handleAppError(error, 'Error closing poll', pollsStatusMap);
239
+ }
240
+ },
241
+ };
242
+ export const reopenPollTool = {
243
+ name: 'reopen_poll',
244
+ description: 'Reopen a previously closed poll so voting can resume.',
245
+ inputSchema: z.object({
246
+ pollId: z.number().int().describe('Poll ID'),
247
+ }),
248
+ handler: async (args) => {
249
+ try {
250
+ const { poll } = await fetchPollsAPI(`/poll/${args.pollId}/reopen`, {
251
+ method: 'PUT',
252
+ });
253
+ return {
254
+ content: [{ type: 'text', text: `Poll reopened: ${formatPoll(poll)}` }],
255
+ };
256
+ }
257
+ catch (error) {
258
+ return handleAppError(error, 'Error reopening poll', pollsStatusMap);
259
+ }
260
+ },
261
+ };
262
+ export const clonePollTool = {
263
+ name: 'clone_poll',
264
+ description: 'Clone an existing poll, copying its configuration and options into a new poll.',
265
+ inputSchema: z.object({
266
+ pollId: z.number().int().describe('Poll ID to clone'),
267
+ }),
268
+ handler: async (args) => {
269
+ try {
270
+ const { poll } = await fetchPollsAPI(`/poll/${args.pollId}/clone`, {
271
+ method: 'POST',
272
+ });
273
+ return {
274
+ content: [{ type: 'text', text: `Poll cloned: ${formatPoll(poll)}` }],
275
+ };
276
+ }
277
+ catch (error) {
278
+ return handleAppError(error, 'Error cloning poll', pollsStatusMap);
279
+ }
280
+ },
281
+ };
282
+ // ─────────────────────────────────────────────────────────────────────────────
283
+ // Options
284
+ // ─────────────────────────────────────────────────────────────────────────────
285
+ export const listPollOptionsTool = {
286
+ name: 'list_poll_options',
287
+ description: 'List all options for a poll, with vote tallies per option.',
288
+ inputSchema: z.object({
289
+ pollId: z.number().int().describe('Poll ID'),
290
+ }),
291
+ handler: async (args) => {
292
+ try {
293
+ const { options } = await fetchPollsAPI(`/poll/${args.pollId}/options`);
294
+ if (!options || options.length === 0) {
295
+ return { content: [{ type: 'text', text: 'No options for this poll.' }] };
296
+ }
297
+ return {
298
+ content: [
299
+ {
300
+ type: 'text',
301
+ text: `Options (${options.length}):\n\n${options.map(formatOption).join('\n')}`,
302
+ },
303
+ ],
304
+ };
305
+ }
306
+ catch (error) {
307
+ return handleAppError(error, 'Error listing options', pollsStatusMap);
308
+ }
309
+ },
310
+ };
311
+ export const addTextPollOptionTool = {
312
+ name: 'add_text_poll_option',
313
+ description: 'Add a text option to a textPoll (e.g. "Pizza", "Sushi"). Use add_date_poll_option for datePolls.',
314
+ inputSchema: z.object({
315
+ pollId: z.number().int().describe('Poll ID (must be a textPoll)'),
316
+ text: z.string().describe('Option text'),
317
+ }),
318
+ handler: async (args) => {
319
+ try {
320
+ const { option } = await fetchPollsAPI(`/poll/${args.pollId}/option`, {
321
+ method: 'POST',
322
+ body: { pollOptionText: args.text },
323
+ });
324
+ return {
325
+ content: [{ type: 'text', text: `Option added: ${formatOption(option)}` }],
326
+ };
327
+ }
328
+ catch (error) {
329
+ return handleAppError(error, 'Error adding text option', pollsStatusMap);
330
+ }
331
+ },
332
+ };
333
+ export const addDatePollOptionTool = {
334
+ name: 'add_date_poll_option',
335
+ description: 'Add a date/time option to a datePoll. Provide startAt as ISO-8601 (e.g. "2026-05-12T14:00:00Z") and durationSeconds (e.g. 3600 for 1 hour).',
336
+ inputSchema: z.object({
337
+ pollId: z.number().int().describe('Poll ID (must be a datePoll)'),
338
+ startAt: z.string().describe('Start date/time as ISO-8601 (e.g. "2026-05-12T14:00:00Z")'),
339
+ durationSeconds: z
340
+ .number()
341
+ .int()
342
+ .min(0)
343
+ .describe('Duration in seconds (0 = single point in time)'),
344
+ }),
345
+ handler: async (args) => {
346
+ try {
347
+ const ms = Date.parse(args.startAt);
348
+ if (Number.isNaN(ms)) {
349
+ return {
350
+ content: [
351
+ {
352
+ type: 'text',
353
+ text: `Invalid startAt "${args.startAt}": expected an ISO-8601 date/time string.`,
354
+ },
355
+ ],
356
+ isError: true,
357
+ };
358
+ }
359
+ const timestamp = Math.floor(ms / 1000);
360
+ const { option } = await fetchPollsAPI(`/poll/${args.pollId}/option`, {
361
+ method: 'POST',
362
+ body: {
363
+ option: {
364
+ text: '',
365
+ timestamp,
366
+ duration: args.durationSeconds,
367
+ },
368
+ },
369
+ });
370
+ return {
371
+ content: [{ type: 'text', text: `Option added: ${formatOption(option)}` }],
372
+ };
373
+ }
374
+ catch (error) {
375
+ return handleAppError(error, 'Error adding date option', pollsStatusMap);
376
+ }
377
+ },
378
+ };
379
+ export const deletePollOptionTool = {
380
+ name: 'delete_poll_option',
381
+ description: 'Delete an option from a poll by option ID.',
382
+ inputSchema: z.object({
383
+ optionId: z.number().int().describe('Option ID (from list_poll_options)'),
384
+ }),
385
+ handler: async (args) => {
386
+ try {
387
+ await fetchPollsAPI(`/option/${args.optionId}`, { method: 'DELETE' });
388
+ return {
389
+ content: [{ type: 'text', text: `Option ${args.optionId} deleted.` }],
390
+ };
391
+ }
392
+ catch (error) {
393
+ return handleAppError(error, 'Error deleting option', pollsStatusMap);
394
+ }
395
+ },
396
+ };
397
+ // ─────────────────────────────────────────────────────────────────────────────
398
+ // Votes
399
+ // ─────────────────────────────────────────────────────────────────────────────
400
+ export const listPollVotesTool = {
401
+ name: 'list_poll_votes',
402
+ description: "List all votes for a poll. Anonymous polls redact voters' identities.",
403
+ inputSchema: z.object({
404
+ pollId: z.number().int().describe('Poll ID'),
405
+ }),
406
+ handler: async (args) => {
407
+ try {
408
+ const { votes } = await fetchPollsAPI(`/poll/${args.pollId}/votes`);
409
+ if (!votes || votes.length === 0) {
410
+ return { content: [{ type: 'text', text: 'No votes yet.' }] };
411
+ }
412
+ return {
413
+ content: [
414
+ {
415
+ type: 'text',
416
+ text: `Votes (${votes.length}):\n\n${votes.map(formatVote).join('\n')}`,
417
+ },
418
+ ],
419
+ };
420
+ }
421
+ catch (error) {
422
+ return handleAppError(error, 'Error listing votes', pollsStatusMap);
423
+ }
424
+ },
425
+ };
426
+ export const voteOnPollTool = {
427
+ name: 'vote_on_poll',
428
+ description: 'Cast or change your vote on a poll option. setTo: "yes" to vote yes, "no" to vote no, "maybe" for a tentative yes (only if allowed).',
429
+ inputSchema: z.object({
430
+ optionId: z.number().int().describe('Option ID to vote on (from list_poll_options)'),
431
+ setTo: z.enum(['yes', 'no', 'maybe']).describe('Vote value'),
432
+ }),
433
+ handler: async (args) => {
434
+ try {
435
+ const result = await fetchPollsAPI('/vote', {
436
+ method: 'POST',
437
+ body: { optionId: args.optionId, setTo: args.setTo },
438
+ });
439
+ return {
440
+ content: [
441
+ {
442
+ type: 'text',
443
+ text: `Vote recorded: ${formatVote(result.vote)}`,
444
+ },
445
+ ],
446
+ };
447
+ }
448
+ catch (error) {
449
+ return handleAppError(error, 'Error voting', pollsStatusMap);
450
+ }
451
+ },
452
+ };
453
+ // ─────────────────────────────────────────────────────────────────────────────
454
+ // Comments
455
+ // ─────────────────────────────────────────────────────────────────────────────
456
+ export const listPollCommentsTool = {
457
+ name: 'list_poll_comments',
458
+ description: 'List all comments on a poll.',
459
+ inputSchema: z.object({
460
+ pollId: z.number().int().describe('Poll ID'),
461
+ }),
462
+ handler: async (args) => {
463
+ try {
464
+ const { comments } = await fetchPollsAPI(`/poll/${args.pollId}/comments`);
465
+ if (!comments || comments.length === 0) {
466
+ return { content: [{ type: 'text', text: 'No comments yet.' }] };
467
+ }
468
+ return {
469
+ content: [
470
+ {
471
+ type: 'text',
472
+ text: `Comments (${comments.length}):\n\n${comments.map(formatComment).join('\n')}`,
473
+ },
474
+ ],
475
+ };
476
+ }
477
+ catch (error) {
478
+ return handleAppError(error, 'Error listing comments', pollsStatusMap);
479
+ }
480
+ },
481
+ };
482
+ export const addPollCommentTool = {
483
+ name: 'add_poll_comment',
484
+ description: 'Post a comment on a poll (requires comments to be enabled).',
485
+ inputSchema: z.object({
486
+ pollId: z.number().int().describe('Poll ID'),
487
+ message: z.string().describe('Comment text'),
488
+ }),
489
+ handler: async (args) => {
490
+ try {
491
+ const { comment } = await fetchPollsAPI('/comment', {
492
+ method: 'POST',
493
+ body: { pollId: args.pollId, message: args.message },
494
+ });
495
+ return {
496
+ content: [{ type: 'text', text: `Comment added: ${formatComment(comment)}` }],
497
+ };
498
+ }
499
+ catch (error) {
500
+ return handleAppError(error, 'Error adding comment', pollsStatusMap);
501
+ }
502
+ },
503
+ };
504
+ export const deletePollCommentTool = {
505
+ name: 'delete_poll_comment',
506
+ description: 'Delete one of your poll comments by comment ID.',
507
+ inputSchema: z.object({
508
+ commentId: z.number().int().describe('Comment ID (from list_poll_comments)'),
509
+ }),
510
+ handler: async (args) => {
511
+ try {
512
+ await fetchPollsAPI(`/comment/${args.commentId}`, { method: 'DELETE' });
513
+ return {
514
+ content: [{ type: 'text', text: `Comment ${args.commentId} deleted.` }],
515
+ };
516
+ }
517
+ catch (error) {
518
+ return handleAppError(error, 'Error deleting comment', pollsStatusMap);
519
+ }
520
+ },
521
+ };
522
+ // ─────────────────────────────────────────────────────────────────────────────
523
+ // Shares
524
+ // ─────────────────────────────────────────────────────────────────────────────
525
+ export const listPollSharesTool = {
526
+ name: 'list_poll_shares',
527
+ description: 'List all shares (public link, user invitations, email invitations) for a poll.',
528
+ inputSchema: z.object({
529
+ pollId: z.number().int().describe('Poll ID'),
530
+ }),
531
+ handler: async (args) => {
532
+ try {
533
+ const { shares } = await fetchPollsAPI(`/poll/${args.pollId}/shares`);
534
+ if (!shares || shares.length === 0) {
535
+ return { content: [{ type: 'text', text: 'No shares for this poll.' }] };
536
+ }
537
+ return {
538
+ content: [
539
+ {
540
+ type: 'text',
541
+ text: `Shares (${shares.length}):\n\n${shares.map(formatShare).join('\n')}`,
542
+ },
543
+ ],
544
+ };
545
+ }
546
+ catch (error) {
547
+ return handleAppError(error, 'Error listing shares', pollsStatusMap);
548
+ }
549
+ },
550
+ };
551
+ export const addPollShareTool = {
552
+ name: 'add_poll_share',
553
+ description: 'Share a poll. type="public" creates a public link (no extra fields). type="user" invites a Nextcloud user (requires userId). type="email" invites by email (requires userId=email address, displayName).',
554
+ inputSchema: z.object({
555
+ pollId: z.number().int().describe('Poll ID'),
556
+ type: z.enum(['public', 'user', 'email']).describe('Share type'),
557
+ userId: z
558
+ .string()
559
+ .optional()
560
+ .describe('Nextcloud user ID (for type=user) or email address (for type=email)'),
561
+ displayName: z.string().optional().describe('Display name (required for type=email)'),
562
+ }),
563
+ handler: async (args) => {
564
+ try {
565
+ let body = { type: args.type };
566
+ if (args.type === 'user') {
567
+ if (!args.userId) {
568
+ return {
569
+ content: [{ type: 'text', text: 'userId is required for type="user".' }],
570
+ isError: true,
571
+ };
572
+ }
573
+ body = { type: 'user', userId: args.userId };
574
+ }
575
+ else if (args.type === 'email') {
576
+ if (!args.userId || !args.displayName) {
577
+ return {
578
+ content: [
579
+ {
580
+ type: 'text',
581
+ text: 'userId (email) and displayName are required for type="email".',
582
+ },
583
+ ],
584
+ isError: true,
585
+ };
586
+ }
587
+ body = {
588
+ type: 'email',
589
+ userId: args.userId,
590
+ displayName: args.displayName,
591
+ };
592
+ }
593
+ const { share } = await fetchPollsAPI(`/poll/${args.pollId}/share/${args.type}`, {
594
+ method: 'POST',
595
+ body,
596
+ });
597
+ return {
598
+ content: [{ type: 'text', text: `Share created: ${formatShare(share)}` }],
599
+ };
600
+ }
601
+ catch (error) {
602
+ return handleAppError(error, 'Error creating share', pollsStatusMap);
603
+ }
604
+ },
605
+ };
606
+ export const deletePollShareTool = {
607
+ name: 'delete_poll_share',
608
+ description: 'Revoke a poll share by its token (obtained from list_poll_shares or add_poll_share).',
609
+ inputSchema: z.object({
610
+ token: z.string().describe('Share token'),
611
+ }),
612
+ handler: async (args) => {
613
+ try {
614
+ await fetchPollsAPI(`/share/${args.token}`, { method: 'DELETE' });
615
+ return {
616
+ content: [{ type: 'text', text: `Share ${args.token} deleted.` }],
617
+ };
618
+ }
619
+ catch (error) {
620
+ return handleAppError(error, 'Error deleting share', pollsStatusMap);
621
+ }
622
+ },
623
+ };
624
+ // ─────────────────────────────────────────────────────────────────────────────
625
+ // Subscription
626
+ // ─────────────────────────────────────────────────────────────────────────────
627
+ export const setPollSubscriptionTool = {
628
+ name: 'set_poll_subscription',
629
+ description: 'Subscribe or unsubscribe the current user to poll notifications (new votes, comments). Use subscribe=true to subscribe, false to unsubscribe.',
630
+ inputSchema: z.object({
631
+ pollId: z.number().int().describe('Poll ID'),
632
+ subscribe: z.boolean().describe('true to subscribe, false to unsubscribe'),
633
+ }),
634
+ handler: async (args) => {
635
+ try {
636
+ await fetchPollsAPI(`/poll/${args.pollId}/subscription`, {
637
+ method: args.subscribe ? 'PUT' : 'DELETE',
638
+ });
639
+ return {
640
+ content: [
641
+ {
642
+ type: 'text',
643
+ text: args.subscribe
644
+ ? `Subscribed to poll ${args.pollId}.`
645
+ : `Unsubscribed from poll ${args.pollId}.`,
646
+ },
647
+ ],
648
+ };
649
+ }
650
+ catch (error) {
651
+ return handleAppError(error, args.subscribe ? 'Error subscribing' : 'Error unsubscribing', pollsStatusMap);
652
+ }
653
+ },
654
+ };
655
+ export const pollsTools = [
656
+ // Polls
657
+ listPollsTool,
658
+ getPollTool,
659
+ createPollTool,
660
+ updatePollTool,
661
+ deletePollTool,
662
+ closePollTool,
663
+ reopenPollTool,
664
+ clonePollTool,
665
+ // Options
666
+ listPollOptionsTool,
667
+ addTextPollOptionTool,
668
+ addDatePollOptionTool,
669
+ deletePollOptionTool,
670
+ // Votes
671
+ listPollVotesTool,
672
+ voteOnPollTool,
673
+ // Comments
674
+ listPollCommentsTool,
675
+ addPollCommentTool,
676
+ deletePollCommentTool,
677
+ // Shares
678
+ listPollSharesTool,
679
+ addPollShareTool,
680
+ deletePollShareTool,
681
+ // Subscription
682
+ setPollSubscriptionTool,
683
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiquila-mcp",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "AIquila - MCP server for Nextcloud integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",