aiquila-mcp 0.3.17 → 0.3.20

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.
@@ -0,0 +1,49 @@
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 News REST API v1-3.
7
+ *
8
+ * Base path: /index.php/apps/news/api/v1-3
9
+ *
10
+ * Returns plain JSON (no OCS envelope), like the Notes API.
11
+ */
12
+ export async function fetchNewsAPI(endpoint, options = {}) {
13
+ const config = getNextcloudConfig();
14
+ const auth = Buffer.from(`${config.user}:${config.password}`).toString('base64');
15
+ let url = `${config.url}/index.php/apps/news/api/v1-3${endpoint}`;
16
+ if (options.queryParams) {
17
+ const params = new URLSearchParams();
18
+ for (const [key, value] of Object.entries(options.queryParams)) {
19
+ if (value !== undefined)
20
+ params.append(key, String(value));
21
+ }
22
+ const qs = params.toString();
23
+ if (qs)
24
+ url += `?${qs}`;
25
+ }
26
+ const headers = {
27
+ Authorization: `Basic ${auth}`,
28
+ 'OCS-APIRequest': 'true',
29
+ Accept: 'application/json',
30
+ };
31
+ let body;
32
+ if (options.body !== undefined) {
33
+ body = JSON.stringify(options.body);
34
+ headers['Content-Type'] = 'application/json';
35
+ }
36
+ const method = options.method ?? 'GET';
37
+ const t0 = Date.now();
38
+ const response = await fetch(url, { method, headers, body });
39
+ logger.trace({ method, url, status: response.status, ms: Date.now() - t0 }, '[news] HTTP');
40
+ if (!response.ok) {
41
+ const text = await response.text();
42
+ throw new ApiError(response.status, response.statusText, text);
43
+ }
44
+ if (response.status === 200 &&
45
+ response.headers.get('content-type')?.includes('application/json')) {
46
+ return (await response.json());
47
+ }
48
+ return undefined;
49
+ }
@@ -15,7 +15,9 @@ import { calendarTools } from './tools/apps/calendar.js';
15
15
  import { cookbookTools } from './tools/apps/cookbook.js';
16
16
  import { deckTools } from './tools/apps/deck.js';
17
17
  import { notesTools } from './tools/apps/notes.js';
18
+ import { newsTools } from './tools/apps/news.js';
18
19
  import { aiquilaTools } from './tools/apps/aiquila.js';
20
+ import { coworkTools } from './tools/apps/cowork.js';
19
21
  import { usersTools } from './tools/apps/users.js';
20
22
  import { groupsTools } from './tools/apps/groups.js';
21
23
  import { circlesTools } from './tools/apps/circles.js';
@@ -62,11 +64,13 @@ export const TOOL_REGISTRY = [
62
64
  { category: 'security', appIds: ['aiquila'], tools: securityTools },
63
65
  { category: 'occ', appIds: ['aiquila'], tools: occTools },
64
66
  { category: 'projects', appIds: ['aiquila'], tools: projectsTools },
67
+ { category: 'cowork', appIds: ['aiquila'], tools: coworkTools },
65
68
  // Requires specific Nextcloud apps
66
69
  { category: 'calendar', appIds: ['calendar'], tools: calendarTools },
67
70
  { category: 'tasks', appIds: ['tasks'], tools: tasksTools },
68
71
  { category: 'contacts', appIds: ['contacts'], tools: contactsTools },
69
72
  { category: 'notes', appIds: ['notes'], tools: notesTools },
73
+ { category: 'news', appIds: ['news'], tools: newsTools },
70
74
  { category: 'mail', appIds: ['mail'], tools: mailTools },
71
75
  { category: 'deck', appIds: ['deck'], tools: deckTools },
72
76
  { category: 'cookbook', appIds: ['cookbook'], tools: cookbookTools },
@@ -0,0 +1,259 @@
1
+ // SPDX-License-Identifier: MIT
2
+ import { z } from 'zod';
3
+ import { fetchAiquilaAPI } from '../../client/aiquila.js';
4
+ // ── Helpers ──────────────────────────────────────────────────────────────────
5
+ function err(action, error) {
6
+ return {
7
+ content: [
8
+ {
9
+ type: 'text',
10
+ text: `Error ${action}: ${error instanceof Error ? error.message : String(error)}`,
11
+ },
12
+ ],
13
+ isError: true,
14
+ };
15
+ }
16
+ function text(body) {
17
+ return { content: [{ type: 'text', text: body }] };
18
+ }
19
+ function fmtTime(epoch) {
20
+ return epoch ? new Date(epoch * 1000).toISOString() : '—';
21
+ }
22
+ function formatCoworker(c) {
23
+ const state = !c.isActive ? 'disabled' : c.paused ? 'paused' : 'active';
24
+ const lines = [
25
+ `- **${c.title}** (ID: ${c.id}) — ${state}`,
26
+ ` task: ${c.taskType} | provider: ${c.model ?? 'default'} | input: ${c.inputPath ?? '/'}`,
27
+ ` schedule: ${c.cronSchedule} | next run: ${fmtTime(c.nextRunAt)} | last run: ${fmtTime(c.lastRunAt)}${c.lastStatus ? ` (${c.lastStatus})` : ''}`,
28
+ ];
29
+ if (c.lastError)
30
+ lines.push(` ⚠ last error: ${c.lastError}`);
31
+ return lines.join('\n');
32
+ }
33
+ // ── Tools ────────────────────────────────────────────────────────────────────
34
+ export const listCoworkersTool = {
35
+ name: 'list_coworkers',
36
+ description: "List the current user's coworkers (persistent scheduled AI tasks) with their status, provider, schedule and last run.",
37
+ inputSchema: z.object({}),
38
+ handler: async () => {
39
+ try {
40
+ const coworkers = await fetchAiquilaAPI('/coworkers');
41
+ if (coworkers.length === 0) {
42
+ return text('No coworkers configured. Use create_coworker (optionally with a templateId) to add one.');
43
+ }
44
+ return text(coworkers.map(formatCoworker).join('\n\n'));
45
+ }
46
+ catch (error) {
47
+ return err('listing coworkers', error);
48
+ }
49
+ },
50
+ };
51
+ export const listCoworkerTemplatesTool = {
52
+ name: 'list_coworker_templates',
53
+ description: 'List built-in coworker templates (e.g. image classification via Claude vision or Mistral Pixtral) and available task types.',
54
+ inputSchema: z.object({}),
55
+ handler: async () => {
56
+ try {
57
+ const data = await fetchAiquilaAPI('/coworkers/templates');
58
+ const tpls = data.templates
59
+ .map((t) => `- **${t.title}** (id: ${t.id})${t.description ? ` — ${t.description}` : ''}`)
60
+ .join('\n');
61
+ const types = data.taskTypes.map((t) => `- ${t.id} (${t.family}) — ${t.label}`).join('\n');
62
+ return text(`Templates:\n${tpls}\n\nTask types:\n${types}`);
63
+ }
64
+ catch (error) {
65
+ return err('listing templates', error);
66
+ }
67
+ },
68
+ };
69
+ export const getCoworkerTool = {
70
+ name: 'get_coworker',
71
+ description: 'Get a single coworker by ID, including its schedule and last run status.',
72
+ inputSchema: z.object({ id: z.number().describe('Coworker ID') }),
73
+ handler: async (args) => {
74
+ try {
75
+ const c = await fetchAiquilaAPI(`/coworkers/${args.id}`);
76
+ return text(formatCoworker(c));
77
+ }
78
+ catch (error) {
79
+ return err('getting coworker', error);
80
+ }
81
+ },
82
+ };
83
+ export const createCoworkerTool = {
84
+ name: 'create_coworker',
85
+ description: 'Create a coworker. Provide templateId to start from a built-in template (e.g. "classify-images-claude" or "classify-images-pixtral"), and/or explicit fields to configure a custom one. Fields override template defaults.',
86
+ inputSchema: z.object({
87
+ templateId: z
88
+ .string()
89
+ .optional()
90
+ .describe('Built-in template id (see list_coworker_templates)'),
91
+ title: z.string().optional(),
92
+ taskType: z.string().optional().describe('Task type id, e.g. "vision:classify"'),
93
+ model: z
94
+ .string()
95
+ .optional()
96
+ .describe('Provider id: "anthropic" (Claude) or "mistral" (Pixtral)'),
97
+ inputPath: z.string().optional().describe('Folder to process, e.g. "/Photos"'),
98
+ cronSchedule: z.string().optional().describe('5-field cron, e.g. "0 3 * * *"'),
99
+ maxTags: z.number().optional().describe('Max tags per image (vision:classify)'),
100
+ recursive: z.boolean().optional().describe('Include sub-folders'),
101
+ isActive: z.boolean().optional().describe('Whether the coworker is enabled'),
102
+ }),
103
+ handler: async (args) => {
104
+ try {
105
+ const body = {};
106
+ if (args.title !== undefined)
107
+ body.title = args.title;
108
+ if (args.taskType !== undefined)
109
+ body.task_type = args.taskType;
110
+ if (args.model !== undefined)
111
+ body.model = args.model;
112
+ if (args.inputPath !== undefined) {
113
+ body.input_type = 'folder';
114
+ body.input_path = args.inputPath;
115
+ }
116
+ if (args.cronSchedule !== undefined)
117
+ body.cron_schedule = args.cronSchedule;
118
+ if (args.isActive !== undefined)
119
+ body.is_active = args.isActive;
120
+ const options = {};
121
+ if (args.maxTags !== undefined)
122
+ options.maxTags = args.maxTags;
123
+ if (args.recursive !== undefined)
124
+ options.recursive = args.recursive;
125
+ if (Object.keys(options).length > 0)
126
+ body.options = options;
127
+ const endpoint = args.templateId ? '/coworkers/templates' : '/coworkers';
128
+ if (args.templateId)
129
+ body.templateId = args.templateId;
130
+ const c = await fetchAiquilaAPI(endpoint, { method: 'POST', body });
131
+ return text(`Coworker created:\n${formatCoworker(c)}`);
132
+ }
133
+ catch (error) {
134
+ return err('creating coworker', error);
135
+ }
136
+ },
137
+ };
138
+ export const updateCoworkerTool = {
139
+ name: 'update_coworker',
140
+ description: "Update a coworker's configuration (title, provider, input folder, schedule, options).",
141
+ inputSchema: z.object({
142
+ id: z.number().describe('Coworker ID'),
143
+ title: z.string().optional(),
144
+ model: z.string().optional().describe('"anthropic" or "mistral"'),
145
+ inputPath: z.string().optional(),
146
+ cronSchedule: z.string().optional(),
147
+ maxTags: z.number().optional(),
148
+ recursive: z.boolean().optional(),
149
+ }),
150
+ handler: async (args) => {
151
+ try {
152
+ const body = {};
153
+ if (args.title !== undefined)
154
+ body.title = args.title;
155
+ if (args.model !== undefined)
156
+ body.model = args.model;
157
+ if (args.inputPath !== undefined) {
158
+ body.input_type = 'folder';
159
+ body.input_path = args.inputPath;
160
+ }
161
+ if (args.cronSchedule !== undefined)
162
+ body.cron_schedule = args.cronSchedule;
163
+ const options = {};
164
+ if (args.maxTags !== undefined)
165
+ options.maxTags = args.maxTags;
166
+ if (args.recursive !== undefined)
167
+ options.recursive = args.recursive;
168
+ if (Object.keys(options).length > 0)
169
+ body.options = options;
170
+ const c = await fetchAiquilaAPI(`/coworkers/${args.id}`, { method: 'PUT', body });
171
+ return text(`Coworker updated:\n${formatCoworker(c)}`);
172
+ }
173
+ catch (error) {
174
+ return err('updating coworker', error);
175
+ }
176
+ },
177
+ };
178
+ export const deleteCoworkerTool = {
179
+ name: 'delete_coworker',
180
+ description: 'Delete a coworker and its run history.',
181
+ inputSchema: z.object({ id: z.number().describe('Coworker ID') }),
182
+ handler: async (args) => {
183
+ try {
184
+ await fetchAiquilaAPI(`/coworkers/${args.id}`, { method: 'DELETE' });
185
+ return text(`Coworker ${args.id} deleted.`);
186
+ }
187
+ catch (error) {
188
+ return err('deleting coworker', error);
189
+ }
190
+ },
191
+ };
192
+ function stateTool(name, description, action) {
193
+ return {
194
+ name,
195
+ description,
196
+ inputSchema: z.object({ id: z.number().describe('Coworker ID') }),
197
+ handler: async (args) => {
198
+ try {
199
+ if (action === 'run') {
200
+ const run = await fetchAiquilaAPI(`/coworkers/${args.id}/run`, {
201
+ method: 'POST',
202
+ });
203
+ return text(`Run finished: ${run.status} — processed ${run.itemsProcessed}/${run.itemsTotal}.` +
204
+ (run.summary ? `\n${run.summary}` : '') +
205
+ (run.error ? `\n⚠ ${run.error}` : ''));
206
+ }
207
+ const c = await fetchAiquilaAPI(`/coworkers/${args.id}/${action}`, {
208
+ method: 'POST',
209
+ });
210
+ return text(`Coworker ${action}d:\n${formatCoworker(c)}`);
211
+ }
212
+ catch (error) {
213
+ return err(`${action} coworker`, error);
214
+ }
215
+ },
216
+ };
217
+ }
218
+ export const enableCoworkerTool = stateTool('enable_coworker', 'Enable a coworker so it runs on its schedule.', 'enable');
219
+ export const disableCoworkerTool = stateTool('disable_coworker', 'Disable a coworker so it stops running on its schedule.', 'disable');
220
+ export const pauseCoworkerTool = stateTool('pause_coworker', 'Temporarily pause a coworker without disabling it.', 'pause');
221
+ export const resumeCoworkerTool = stateTool('resume_coworker', 'Resume a paused coworker.', 'resume');
222
+ export const runCoworkerTool = stateTool('run_coworker', 'Run a coworker immediately (synchronously) and return the run result.', 'run');
223
+ export const getCoworkerRunsTool = {
224
+ name: 'get_coworker_runs',
225
+ description: 'Get recent run history (progress, status, summary) for a coworker.',
226
+ inputSchema: z.object({
227
+ id: z.number().describe('Coworker ID'),
228
+ limit: z.number().optional().describe('Max runs to return (default 20)'),
229
+ }),
230
+ handler: async (args) => {
231
+ try {
232
+ const runs = await fetchAiquilaAPI(`/coworkers/${args.id}/runs`, {
233
+ queryParams: args.limit ? { limit: String(args.limit) } : undefined,
234
+ });
235
+ if (runs.length === 0)
236
+ return text('No runs yet for this coworker.');
237
+ const lines = runs.map((r) => `- #${r.id} ${r.status} — ${r.itemsProcessed}/${r.itemsTotal} — ${fmtTime(r.startedAt)}` +
238
+ (r.error ? ` — ⚠ ${r.error}` : r.summary ? ` — ${r.summary.split('\n')[0]}` : ''));
239
+ return text(lines.join('\n'));
240
+ }
241
+ catch (error) {
242
+ return err('getting coworker runs', error);
243
+ }
244
+ },
245
+ };
246
+ export const coworkTools = [
247
+ listCoworkersTool,
248
+ listCoworkerTemplatesTool,
249
+ getCoworkerTool,
250
+ createCoworkerTool,
251
+ updateCoworkerTool,
252
+ deleteCoworkerTool,
253
+ enableCoworkerTool,
254
+ disableCoworkerTool,
255
+ pauseCoworkerTool,
256
+ resumeCoworkerTool,
257
+ runCoworkerTool,
258
+ getCoworkerRunsTool,
259
+ ];
@@ -0,0 +1,411 @@
1
+ // SPDX-License-Identifier: MIT
2
+ import { z } from 'zod';
3
+ import { fetchNewsAPI } from '../../client/news.js';
4
+ // ── Helpers ─────────────────────────────────────────────────────────────────
5
+ function err(action, error) {
6
+ return {
7
+ content: [
8
+ {
9
+ type: 'text',
10
+ text: `Error ${action}: ${error instanceof Error ? error.message : String(error)}`,
11
+ },
12
+ ],
13
+ isError: true,
14
+ };
15
+ }
16
+ function text(body) {
17
+ return { content: [{ type: 'text', text: body }] };
18
+ }
19
+ /** Strip HTML tags and collapse whitespace, then truncate. */
20
+ function stripHtml(html, max = 280) {
21
+ const plain = html
22
+ .replace(/<[^>]+>/g, ' ')
23
+ .replace(/&nbsp;/g, ' ')
24
+ .replace(/\s+/g, ' ')
25
+ .trim();
26
+ return plain.length > max ? `${plain.slice(0, max)}…` : plain;
27
+ }
28
+ function formatFeed(f) {
29
+ const parts = [`- **${f.title || '(untitled)'}** — ${f.url}`];
30
+ parts.push(` ID: ${f.id} | Folder: ${f.folderId} | Unread: ${f.unreadCount}`);
31
+ if (f.lastUpdateError)
32
+ parts.push(` ⚠ Update error: ${f.lastUpdateError}`);
33
+ return parts.join('\n');
34
+ }
35
+ function formatItem(i) {
36
+ const flags = [i.unread ? 'unread' : 'read', i.starred ? 'starred' : null]
37
+ .filter(Boolean)
38
+ .join(', ');
39
+ const lines = [`- **${i.title || '(untitled)'}** (ID: ${i.id}, feed: ${i.feedId}, ${flags})`];
40
+ lines.push(` ${i.url}`);
41
+ if (i.author)
42
+ lines.push(` by ${i.author} — ${new Date(i.pubDate * 1000).toISOString()}`);
43
+ else
44
+ lines.push(` ${new Date(i.pubDate * 1000).toISOString()}`);
45
+ if (i.body)
46
+ lines.push(` ${stripHtml(i.body)}`);
47
+ return lines.join('\n');
48
+ }
49
+ // ── Feed tools ──────────────────────────────────────────────────────────────
50
+ export const listFeedsTool = {
51
+ name: 'list_feeds',
52
+ description: 'List all RSS feeds subscribed in the Nextcloud News app, with unread counts.',
53
+ inputSchema: z.object({}),
54
+ handler: async () => {
55
+ try {
56
+ const result = await fetchNewsAPI('/feeds');
57
+ if (!result.feeds || result.feeds.length === 0) {
58
+ return text('No feeds subscribed.');
59
+ }
60
+ const formatted = result.feeds.map(formatFeed).join('\n');
61
+ return text(`Feeds (${result.feeds.length}):\n\n${formatted}`);
62
+ }
63
+ catch (error) {
64
+ return err('listing feeds', error);
65
+ }
66
+ },
67
+ };
68
+ export const addFeedTool = {
69
+ name: 'add_feed',
70
+ description: 'Subscribe to a new RSS feed by URL, optionally placing it in a folder.',
71
+ inputSchema: z.object({
72
+ url: z.string().describe('The RSS/Atom feed URL to subscribe to'),
73
+ folderId: z.number().optional().describe('Folder ID to place the feed in (omit for root)'),
74
+ }),
75
+ handler: async (args) => {
76
+ try {
77
+ const body = { url: args.url };
78
+ if (args.folderId !== undefined)
79
+ body.folderId = args.folderId;
80
+ const result = await fetchNewsAPI('/feeds', { method: 'POST', body });
81
+ const feed = result.feeds?.[0];
82
+ return text(feed
83
+ ? `Subscribed to feed "${feed.title}" (ID: ${feed.id}).`
84
+ : `Subscribed to feed: ${args.url}`);
85
+ }
86
+ catch (error) {
87
+ return err('adding feed', error);
88
+ }
89
+ },
90
+ };
91
+ export const deleteFeedTool = {
92
+ name: 'delete_feed',
93
+ description: 'Delete (unsubscribe from) a feed and all its items.',
94
+ inputSchema: z.object({
95
+ feedId: z.number().describe('The feed ID to delete'),
96
+ }),
97
+ handler: async (args) => {
98
+ try {
99
+ await fetchNewsAPI(`/feeds/${args.feedId}`, { method: 'DELETE' });
100
+ return text(`Deleted feed ${args.feedId}.`);
101
+ }
102
+ catch (error) {
103
+ return err('deleting feed', error);
104
+ }
105
+ },
106
+ };
107
+ export const moveFeedTool = {
108
+ name: 'move_feed',
109
+ description: 'Move a feed into a different folder.',
110
+ inputSchema: z.object({
111
+ feedId: z.number().describe('The feed ID to move'),
112
+ folderId: z.number().describe('Destination folder ID (0 for root)'),
113
+ }),
114
+ handler: async (args) => {
115
+ try {
116
+ await fetchNewsAPI(`/feeds/${args.feedId}/move`, {
117
+ method: 'POST',
118
+ body: { folderId: args.folderId },
119
+ });
120
+ return text(`Moved feed ${args.feedId} to folder ${args.folderId}.`);
121
+ }
122
+ catch (error) {
123
+ return err('moving feed', error);
124
+ }
125
+ },
126
+ };
127
+ export const renameFeedTool = {
128
+ name: 'rename_feed',
129
+ description: 'Rename a feed.',
130
+ inputSchema: z.object({
131
+ feedId: z.number().describe('The feed ID to rename'),
132
+ feedTitle: z.string().describe('The new feed title'),
133
+ }),
134
+ handler: async (args) => {
135
+ try {
136
+ await fetchNewsAPI(`/feeds/${args.feedId}/rename`, {
137
+ method: 'POST',
138
+ body: { feedTitle: args.feedTitle },
139
+ });
140
+ return text(`Renamed feed ${args.feedId} to "${args.feedTitle}".`);
141
+ }
142
+ catch (error) {
143
+ return err('renaming feed', error);
144
+ }
145
+ },
146
+ };
147
+ export const markFeedReadTool = {
148
+ name: 'mark_feed_read',
149
+ description: 'Mark all items in a feed as read up to (and including) the given newest item ID.',
150
+ inputSchema: z.object({
151
+ feedId: z.number().describe('The feed ID'),
152
+ newestItemId: z.number().describe('Mark all items with ID <= this value as read'),
153
+ }),
154
+ handler: async (args) => {
155
+ try {
156
+ await fetchNewsAPI(`/feeds/${args.feedId}/read`, {
157
+ method: 'POST',
158
+ body: { newestItemId: args.newestItemId },
159
+ });
160
+ return text(`Marked feed ${args.feedId} read up to item ${args.newestItemId}.`);
161
+ }
162
+ catch (error) {
163
+ return err('marking feed read', error);
164
+ }
165
+ },
166
+ };
167
+ // ── Folder tools ────────────────────────────────────────────────────────────
168
+ export const listNewsFoldersTool = {
169
+ name: 'list_news_folders',
170
+ description: 'List all folders used to organize feeds in the Nextcloud News app.',
171
+ inputSchema: z.object({}),
172
+ handler: async () => {
173
+ try {
174
+ const result = await fetchNewsAPI('/folders');
175
+ if (!result.folders || result.folders.length === 0) {
176
+ return text('No folders found.');
177
+ }
178
+ const formatted = result.folders.map((f) => `- **${f.name}** (ID: ${f.id})`).join('\n');
179
+ return text(`News folders (${result.folders.length}):\n\n${formatted}`);
180
+ }
181
+ catch (error) {
182
+ return err('listing folders', error);
183
+ }
184
+ },
185
+ };
186
+ export const createNewsFolderTool = {
187
+ name: 'create_news_folder',
188
+ description: 'Create a new folder for organizing feeds in the News app.',
189
+ inputSchema: z.object({
190
+ name: z.string().describe('The folder name'),
191
+ }),
192
+ handler: async (args) => {
193
+ try {
194
+ const result = await fetchNewsAPI('/folders', {
195
+ method: 'POST',
196
+ body: { name: args.name },
197
+ });
198
+ const folder = result.folders?.[0];
199
+ return text(folder
200
+ ? `Created folder "${folder.name}" (ID: ${folder.id}).`
201
+ : `Created folder "${args.name}".`);
202
+ }
203
+ catch (error) {
204
+ return err('creating folder', error);
205
+ }
206
+ },
207
+ };
208
+ export const renameNewsFolderTool = {
209
+ name: 'rename_news_folder',
210
+ description: 'Rename a News folder.',
211
+ inputSchema: z.object({
212
+ folderId: z.number().describe('The folder ID to rename'),
213
+ name: z.string().describe('The new folder name'),
214
+ }),
215
+ handler: async (args) => {
216
+ try {
217
+ await fetchNewsAPI(`/folders/${args.folderId}`, {
218
+ method: 'PUT',
219
+ body: { name: args.name },
220
+ });
221
+ return text(`Renamed folder ${args.folderId} to "${args.name}".`);
222
+ }
223
+ catch (error) {
224
+ return err('renaming folder', error);
225
+ }
226
+ },
227
+ };
228
+ export const deleteNewsFolderTool = {
229
+ name: 'delete_news_folder',
230
+ description: 'Delete a News folder and all feeds it contains.',
231
+ inputSchema: z.object({
232
+ folderId: z.number().describe('The folder ID to delete'),
233
+ }),
234
+ handler: async (args) => {
235
+ try {
236
+ await fetchNewsAPI(`/folders/${args.folderId}`, { method: 'DELETE' });
237
+ return text(`Deleted folder ${args.folderId}.`);
238
+ }
239
+ catch (error) {
240
+ return err('deleting folder', error);
241
+ }
242
+ },
243
+ };
244
+ export const markNewsFolderReadTool = {
245
+ name: 'mark_news_folder_read',
246
+ description: 'Mark all items in a folder as read up to (and including) the given newest item ID.',
247
+ inputSchema: z.object({
248
+ folderId: z.number().describe('The folder ID'),
249
+ newestItemId: z.number().describe('Mark all items with ID <= this value as read'),
250
+ }),
251
+ handler: async (args) => {
252
+ try {
253
+ await fetchNewsAPI(`/folders/${args.folderId}/read`, {
254
+ method: 'POST',
255
+ body: { newestItemId: args.newestItemId },
256
+ });
257
+ return text(`Marked folder ${args.folderId} read up to item ${args.newestItemId}.`);
258
+ }
259
+ catch (error) {
260
+ return err('marking folder read', error);
261
+ }
262
+ },
263
+ };
264
+ // ── Item / article tools ────────────────────────────────────────────────────
265
+ export const listNewsItemsTool = {
266
+ name: 'list_news_items',
267
+ description: 'List news articles (items). Filter by feed, folder, starred, or all; supports pagination and read/unread filtering.',
268
+ inputSchema: z.object({
269
+ type: z
270
+ .enum(['feed', 'folder', 'starred', 'all'])
271
+ .optional()
272
+ .describe('Scope of items to return (default "all")'),
273
+ id: z
274
+ .number()
275
+ .optional()
276
+ .describe('Feed or folder ID when type is "feed" or "folder" (default 0)'),
277
+ batchSize: z.number().optional().describe('Max items to return (default 20, -1 for all)'),
278
+ offset: z.number().optional().describe('Return items older than this item ID (pagination)'),
279
+ getRead: z.boolean().optional().describe('Include already-read items (default false)'),
280
+ oldestFirst: z
281
+ .boolean()
282
+ .optional()
283
+ .describe('Return oldest items first (default newest first)'),
284
+ }),
285
+ handler: async (args) => {
286
+ try {
287
+ const typeMap = { feed: 0, folder: 1, starred: 2, all: 3 };
288
+ const queryParams = {
289
+ type: typeMap[args.type ?? 'all'],
290
+ id: args.id ?? 0,
291
+ batchSize: args.batchSize ?? 20,
292
+ getRead: args.getRead ?? false,
293
+ oldestFirst: args.oldestFirst ?? false,
294
+ };
295
+ if (args.offset !== undefined)
296
+ queryParams.offset = args.offset;
297
+ const result = await fetchNewsAPI('/items', { queryParams });
298
+ if (!result.items || result.items.length === 0) {
299
+ return text('No items found.');
300
+ }
301
+ const formatted = result.items.map(formatItem).join('\n\n');
302
+ return text(`Items (${result.items.length}):\n\n${formatted}`);
303
+ }
304
+ catch (error) {
305
+ return err('listing items', error);
306
+ }
307
+ },
308
+ };
309
+ export const markItemReadTool = {
310
+ name: 'mark_item_read',
311
+ description: 'Mark a single news item as read.',
312
+ inputSchema: z.object({
313
+ itemId: z.number().describe('The item ID'),
314
+ }),
315
+ handler: async (args) => {
316
+ try {
317
+ await fetchNewsAPI(`/items/${args.itemId}/read`, { method: 'POST' });
318
+ return text(`Marked item ${args.itemId} read.`);
319
+ }
320
+ catch (error) {
321
+ return err('marking item read', error);
322
+ }
323
+ },
324
+ };
325
+ export const markItemUnreadTool = {
326
+ name: 'mark_item_unread',
327
+ description: 'Mark a single news item as unread.',
328
+ inputSchema: z.object({
329
+ itemId: z.number().describe('The item ID'),
330
+ }),
331
+ handler: async (args) => {
332
+ try {
333
+ await fetchNewsAPI(`/items/${args.itemId}/unread`, { method: 'POST' });
334
+ return text(`Marked item ${args.itemId} unread.`);
335
+ }
336
+ catch (error) {
337
+ return err('marking item unread', error);
338
+ }
339
+ },
340
+ };
341
+ export const starItemTool = {
342
+ name: 'star_item',
343
+ description: 'Star (favorite) a single news item.',
344
+ inputSchema: z.object({
345
+ itemId: z.number().describe('The item ID'),
346
+ }),
347
+ handler: async (args) => {
348
+ try {
349
+ await fetchNewsAPI(`/items/${args.itemId}/star`, { method: 'POST' });
350
+ return text(`Starred item ${args.itemId}.`);
351
+ }
352
+ catch (error) {
353
+ return err('starring item', error);
354
+ }
355
+ },
356
+ };
357
+ export const unstarItemTool = {
358
+ name: 'unstar_item',
359
+ description: 'Remove the star from a single news item.',
360
+ inputSchema: z.object({
361
+ itemId: z.number().describe('The item ID'),
362
+ }),
363
+ handler: async (args) => {
364
+ try {
365
+ await fetchNewsAPI(`/items/${args.itemId}/unstar`, { method: 'POST' });
366
+ return text(`Unstarred item ${args.itemId}.`);
367
+ }
368
+ catch (error) {
369
+ return err('unstarring item', error);
370
+ }
371
+ },
372
+ };
373
+ export const markItemsReadTool = {
374
+ name: 'mark_items_read',
375
+ description: 'Mark multiple news items as read in one call.',
376
+ inputSchema: z.object({
377
+ itemIds: z.array(z.number()).describe('The item IDs to mark as read'),
378
+ }),
379
+ handler: async (args) => {
380
+ try {
381
+ await fetchNewsAPI('/items/read/multiple', {
382
+ method: 'POST',
383
+ body: { itemIds: args.itemIds },
384
+ });
385
+ return text(`Marked ${args.itemIds.length} item(s) read.`);
386
+ }
387
+ catch (error) {
388
+ return err('marking items read', error);
389
+ }
390
+ },
391
+ };
392
+ // ── Export ──────────────────────────────────────────────────────────────────
393
+ export const newsTools = [
394
+ listFeedsTool,
395
+ addFeedTool,
396
+ deleteFeedTool,
397
+ moveFeedTool,
398
+ renameFeedTool,
399
+ markFeedReadTool,
400
+ listNewsFoldersTool,
401
+ createNewsFolderTool,
402
+ renameNewsFolderTool,
403
+ deleteNewsFolderTool,
404
+ markNewsFolderReadTool,
405
+ listNewsItemsTool,
406
+ markItemReadTool,
407
+ markItemUnreadTool,
408
+ starItemTool,
409
+ unstarItemTool,
410
+ markItemsReadTool,
411
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiquila-mcp",
3
- "version": "0.3.17",
3
+ "version": "0.3.20",
4
4
  "description": "Nextcloud MCP server — files, calendar, contacts, mail, maps, notes, tasks & 120+ more tools",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",