aiquila-mcp 0.2.8 → 0.2.10

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,40 @@
1
+ import { getNextcloudConfig } from '../tools/types.js';
2
+ import { logger } from '../logger.js';
3
+ import { ApiError } from './aiquila.js';
4
+ /**
5
+ * Make an authenticated request to the Nextcloud Deck REST API v1.0.
6
+ *
7
+ * Base path: /index.php/apps/deck/api/v1.0
8
+ */
9
+ export async function fetchDeckAPI(endpoint, options = {}) {
10
+ const config = getNextcloudConfig();
11
+ const auth = Buffer.from(`${config.user}:${config.password}`).toString('base64');
12
+ let url = `${config.url}/index.php/apps/deck/api/v1.0${endpoint}`;
13
+ if (options.queryParams) {
14
+ const params = new URLSearchParams(options.queryParams);
15
+ url += `?${params.toString()}`;
16
+ }
17
+ const headers = {
18
+ Authorization: `Basic ${auth}`,
19
+ 'OCS-APIRequest': 'true',
20
+ Accept: 'application/json',
21
+ };
22
+ let body;
23
+ if (options.body !== undefined) {
24
+ body = JSON.stringify(options.body);
25
+ headers['Content-Type'] = 'application/json';
26
+ }
27
+ const method = options.method ?? 'GET';
28
+ const t0 = Date.now();
29
+ const response = await fetch(url, { method, headers, body });
30
+ logger.trace({ method, url, status: response.status, ms: Date.now() - t0 }, '[deck] HTTP');
31
+ if (!response.ok) {
32
+ const text = await response.text();
33
+ throw new ApiError(response.status, response.statusText, text);
34
+ }
35
+ if (response.status === 200 &&
36
+ response.headers.get('content-type')?.includes('application/json')) {
37
+ return (await response.json());
38
+ }
39
+ return undefined;
40
+ }
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  import { startStdio } from './transports/stdio.js';
2
3
  import { startHttp } from './transports/http.js';
3
4
  import { logger } from './logger.js';
package/dist/server.js CHANGED
@@ -13,10 +13,13 @@ import { searchTools } from './tools/system/search.js';
13
13
  import { tasksTools } from './tools/apps/tasks.js';
14
14
  import { calendarTools } from './tools/apps/calendar.js';
15
15
  import { cookbookTools } from './tools/apps/cookbook.js';
16
+ import { deckTools } from './tools/apps/deck.js';
16
17
  import { notesTools } from './tools/apps/notes.js';
17
18
  import { aiquilaTools } from './tools/apps/aiquila.js';
18
19
  import { usersTools } from './tools/apps/users.js';
19
20
  import { groupsTools } from './tools/apps/groups.js';
21
+ import { circlesTools } from './tools/apps/circles.js';
22
+ import { photosTools } from './tools/apps/photos.js';
20
23
  import { sharesTools } from './tools/apps/shares.js';
21
24
  import { contactsTools } from './tools/apps/contacts.js';
22
25
  import { mailTools } from './tools/apps/mail.js';
@@ -39,10 +42,13 @@ const allToolSets = [
39
42
  calendarTools,
40
43
  tasksTools,
41
44
  cookbookTools,
45
+ deckTools,
42
46
  notesTools,
43
47
  aiquilaTools,
44
48
  usersTools,
45
49
  groupsTools,
50
+ circlesTools,
51
+ photosTools,
46
52
  appsTools,
47
53
  occTools,
48
54
  securityTools,
@@ -0,0 +1,285 @@
1
+ import { z } from 'zod';
2
+ import { fetchOCS } from '../../client/ocs.js';
3
+ /**
4
+ * Nextcloud Circles (Teams) Tools
5
+ *
6
+ * Manages circles/teams and their members via the Circles OCS API.
7
+ */
8
+ // ---------------------------------------------------------------------------
9
+ // Constants
10
+ // ---------------------------------------------------------------------------
11
+ const API_BASE = '/ocs/v2.php/apps/circles';
12
+ const MEMBER_TYPE_LABELS = {
13
+ 1: 'user',
14
+ 2: 'group',
15
+ 4: 'mail',
16
+ 8: 'contact',
17
+ 16: 'circle',
18
+ };
19
+ const MEMBER_LEVEL_LABELS = {
20
+ 1: 'member',
21
+ 4: 'moderator',
22
+ 8: 'admin',
23
+ 9: 'owner',
24
+ };
25
+ const CONFIG_FLAGS = [
26
+ [2, 'personal'],
27
+ [4, 'system'],
28
+ [8, 'visible'],
29
+ [16, 'open'],
30
+ [32, 'invite'],
31
+ [64, 'request-to-join'],
32
+ [128, 'friend'],
33
+ [256, 'password-protected'],
34
+ [512, 'no-owner'],
35
+ [1024, 'hidden'],
36
+ [4096, 'local'],
37
+ [32768, 'federated'],
38
+ ];
39
+ // ---------------------------------------------------------------------------
40
+ // Helpers
41
+ // ---------------------------------------------------------------------------
42
+ function text(t) {
43
+ return { content: [{ type: 'text', text: t }] };
44
+ }
45
+ function error(msg) {
46
+ return { content: [{ type: 'text', text: msg }], isError: true };
47
+ }
48
+ function wrapError(action, err) {
49
+ return error(`Error ${action}: ${err instanceof Error ? err.message : String(err)}`);
50
+ }
51
+ function formatConfigFlags(config) {
52
+ const active = CONFIG_FLAGS.filter(([bit]) => config & bit).map(([, label]) => label);
53
+ return active.length ? active.join(', ') : 'default';
54
+ }
55
+ function formatCircle(c) {
56
+ const owner = c.owner?.displayName ?? c.owner?.userId ?? 'unknown';
57
+ return `[${c.id}] ${c.displayName || c.name} (owner: ${owner}, config: ${formatConfigFlags(c.config)})`;
58
+ }
59
+ function formatMember(m) {
60
+ const type = MEMBER_TYPE_LABELS[m.userType] ?? `type-${m.userType}`;
61
+ const level = MEMBER_LEVEL_LABELS[m.level] ?? `level-${m.level}`;
62
+ return `[${m.id}] ${m.displayName || m.userId} (${type}, ${level}, status: ${m.status})`;
63
+ }
64
+ // ---------------------------------------------------------------------------
65
+ // circles_list
66
+ // ---------------------------------------------------------------------------
67
+ export const circlesListTool = {
68
+ name: 'circles_list',
69
+ description: 'List all circles/teams accessible to the current user. Returns circle IDs, names, owners, and configuration.',
70
+ inputSchema: z.object({
71
+ limit: z.number().optional().describe('Maximum number of circles to return'),
72
+ offset: z.number().optional().describe('Offset for pagination'),
73
+ }),
74
+ handler: async (args) => {
75
+ try {
76
+ const queryParams = {};
77
+ if (args.limit !== undefined)
78
+ queryParams.limit = String(args.limit);
79
+ if (args.offset !== undefined)
80
+ queryParams.offset = String(args.offset);
81
+ const result = await fetchOCS(`${API_BASE}/circles`, { queryParams });
82
+ const circles = result.ocs.data;
83
+ if (!circles.length)
84
+ return text('No circles found.');
85
+ return text(`Circles (${circles.length}):\n${circles.map(formatCircle).join('\n')}`);
86
+ }
87
+ catch (err) {
88
+ return wrapError('listing circles', err);
89
+ }
90
+ },
91
+ };
92
+ // ---------------------------------------------------------------------------
93
+ // circles_get
94
+ // ---------------------------------------------------------------------------
95
+ export const circlesGetTool = {
96
+ name: 'circles_get',
97
+ description: 'Get detailed information about a specific circle/team, including its description, owner, and configuration flags.',
98
+ inputSchema: z.object({
99
+ circleId: z.string().describe('The circle ID to retrieve'),
100
+ }),
101
+ handler: async (args) => {
102
+ try {
103
+ const result = await fetchOCS(`${API_BASE}/circles/${encodeURIComponent(args.circleId)}`);
104
+ const c = result.ocs.data;
105
+ const lines = [
106
+ `# ${c.displayName || c.name}`,
107
+ '',
108
+ `ID: ${c.id}`,
109
+ `Owner: ${c.owner?.displayName ?? c.owner?.userId ?? 'unknown'}`,
110
+ `Config: ${formatConfigFlags(c.config)}`,
111
+ ];
112
+ if (c.description)
113
+ lines.push(`Description: ${c.description}`);
114
+ if (c.members?.length) {
115
+ lines.push('', `Members (${c.members.length}):`);
116
+ lines.push(...c.members.map(formatMember));
117
+ }
118
+ return text(lines.join('\n'));
119
+ }
120
+ catch (err) {
121
+ return wrapError(`getting circle "${args.circleId}"`, err);
122
+ }
123
+ },
124
+ };
125
+ // ---------------------------------------------------------------------------
126
+ // circles_create
127
+ // ---------------------------------------------------------------------------
128
+ export const circlesCreateTool = {
129
+ name: 'circles_create',
130
+ description: 'Create a new circle/team. The current user becomes the owner. Set personal=true for a private circle only visible to the owner.',
131
+ inputSchema: z.object({
132
+ name: z.string().describe('Name for the new circle'),
133
+ personal: z.boolean().optional().describe('Create as a personal circle (default: false)'),
134
+ local: z
135
+ .boolean()
136
+ .optional()
137
+ .describe('Restrict to local instance, no federation (default: false)'),
138
+ }),
139
+ handler: async (args) => {
140
+ try {
141
+ const body = { name: args.name };
142
+ if (args.personal)
143
+ body.personal = 1;
144
+ if (args.local)
145
+ body.local = 1;
146
+ const result = await fetchOCS(`${API_BASE}/circles`, {
147
+ method: 'POST',
148
+ jsonBody: body,
149
+ });
150
+ const c = result.ocs.data;
151
+ return text(`Circle created: [${c.id}] ${c.displayName || c.name}`);
152
+ }
153
+ catch (err) {
154
+ return wrapError('creating circle', err);
155
+ }
156
+ },
157
+ };
158
+ // ---------------------------------------------------------------------------
159
+ // circles_delete
160
+ // ---------------------------------------------------------------------------
161
+ export const circlesDeleteTool = {
162
+ name: 'circles_delete',
163
+ description: 'Delete a circle/team. Requires owner privileges.',
164
+ inputSchema: z.object({
165
+ circleId: z.string().describe('The circle ID to delete'),
166
+ }),
167
+ handler: async (args) => {
168
+ try {
169
+ await fetchOCS(`${API_BASE}/circles/${encodeURIComponent(args.circleId)}`, {
170
+ method: 'DELETE',
171
+ });
172
+ return text(`Circle "${args.circleId}" has been deleted.`);
173
+ }
174
+ catch (err) {
175
+ return wrapError(`deleting circle "${args.circleId}"`, err);
176
+ }
177
+ },
178
+ };
179
+ // ---------------------------------------------------------------------------
180
+ // circles_list_members
181
+ // ---------------------------------------------------------------------------
182
+ export const circlesListMembersTool = {
183
+ name: 'circles_list_members',
184
+ description: 'List all members of a circle/team. Returns member IDs (needed for removal), user IDs, types, and permission levels.',
185
+ inputSchema: z.object({
186
+ circleId: z.string().describe('The circle ID to list members for'),
187
+ }),
188
+ handler: async (args) => {
189
+ try {
190
+ const result = await fetchOCS(`${API_BASE}/circles/${encodeURIComponent(args.circleId)}/members`);
191
+ const members = result.ocs.data;
192
+ if (!members.length)
193
+ return text('No members found in this circle.');
194
+ return text(`Members (${members.length}):\n${members.map(formatMember).join('\n')}`);
195
+ }
196
+ catch (err) {
197
+ return wrapError(`listing members of circle "${args.circleId}"`, err);
198
+ }
199
+ },
200
+ };
201
+ // ---------------------------------------------------------------------------
202
+ // circles_add_member
203
+ // ---------------------------------------------------------------------------
204
+ export const circlesAddMemberTool = {
205
+ name: 'circles_add_member',
206
+ description: 'Add a member to a circle/team. Requires moderator privileges or higher. Member type: 1=user (default), 2=group, 4=mail, 16=circle.',
207
+ inputSchema: z.object({
208
+ circleId: z.string().describe('The circle ID to add the member to'),
209
+ userId: z.string().describe('The user ID (or entity identifier) to add'),
210
+ type: z
211
+ .number()
212
+ .optional()
213
+ .describe('Member type: 1=user (default), 2=group, 4=mail, 8=contact, 16=circle'),
214
+ }),
215
+ handler: async (args) => {
216
+ try {
217
+ await fetchOCS(`${API_BASE}/circles/${encodeURIComponent(args.circleId)}/members`, {
218
+ method: 'POST',
219
+ jsonBody: { userId: args.userId, type: args.type ?? 1 },
220
+ });
221
+ const typeLabel = MEMBER_TYPE_LABELS[args.type ?? 1] ?? 'user';
222
+ return text(`Added ${typeLabel} "${args.userId}" to circle "${args.circleId}".`);
223
+ }
224
+ catch (err) {
225
+ return wrapError(`adding member "${args.userId}" to circle "${args.circleId}"`, err);
226
+ }
227
+ },
228
+ };
229
+ // ---------------------------------------------------------------------------
230
+ // circles_remove_member
231
+ // ---------------------------------------------------------------------------
232
+ export const circlesRemoveMemberTool = {
233
+ name: 'circles_remove_member',
234
+ description: 'Remove a member from a circle/team. Use circles_list_members to get the memberId. Requires moderator privileges or higher.',
235
+ inputSchema: z.object({
236
+ circleId: z.string().describe('The circle ID to remove the member from'),
237
+ memberId: z.string().describe('The member ID (from circles_list_members), not the user ID'),
238
+ }),
239
+ handler: async (args) => {
240
+ try {
241
+ await fetchOCS(`${API_BASE}/circles/${encodeURIComponent(args.circleId)}/members/${encodeURIComponent(args.memberId)}`, { method: 'DELETE' });
242
+ return text(`Member "${args.memberId}" removed from circle "${args.circleId}".`);
243
+ }
244
+ catch (err) {
245
+ return wrapError(`removing member "${args.memberId}" from circle "${args.circleId}"`, err);
246
+ }
247
+ },
248
+ };
249
+ // ---------------------------------------------------------------------------
250
+ // circles_search
251
+ // ---------------------------------------------------------------------------
252
+ export const circlesSearchTool = {
253
+ name: 'circles_search',
254
+ description: 'Search for circles/teams by name.',
255
+ inputSchema: z.object({
256
+ term: z.string().describe('Search term to match against circle names'),
257
+ }),
258
+ handler: async (args) => {
259
+ try {
260
+ const result = await fetchOCS(`${API_BASE}/search`, {
261
+ queryParams: { term: args.term },
262
+ });
263
+ const circles = result.ocs.data;
264
+ if (!circles.length)
265
+ return text(`No circles found matching "${args.term}".`);
266
+ return text(`Search results (${circles.length}):\n${circles.map(formatCircle).join('\n')}`);
267
+ }
268
+ catch (err) {
269
+ return wrapError('searching circles', err);
270
+ }
271
+ },
272
+ };
273
+ // ---------------------------------------------------------------------------
274
+ // Export
275
+ // ---------------------------------------------------------------------------
276
+ export const circlesTools = [
277
+ circlesListTool,
278
+ circlesGetTool,
279
+ circlesCreateTool,
280
+ circlesDeleteTool,
281
+ circlesListMembersTool,
282
+ circlesAddMemberTool,
283
+ circlesRemoveMemberTool,
284
+ circlesSearchTool,
285
+ ];