aiquila-mcp 0.2.16 → 0.2.18

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.
Files changed (55) hide show
  1. package/README.md +54 -31
  2. package/dist/auth/login.js +1 -0
  3. package/dist/auth/provider.js +1 -0
  4. package/dist/auth/store.js +12 -4
  5. package/dist/client/aiquila.js +1 -0
  6. package/dist/client/bookmarks.js +1 -0
  7. package/dist/client/caldav.js +1 -0
  8. package/dist/client/deck.js +1 -0
  9. package/dist/client/mail.js +1 -0
  10. package/dist/client/maps.js +1 -0
  11. package/dist/client/notes.js +1 -0
  12. package/dist/client/ocs.js +1 -0
  13. package/dist/client/webdav.js +1 -0
  14. package/dist/index.js +3 -2
  15. package/dist/logger.js +1 -0
  16. package/dist/server.js +1 -0
  17. package/dist/tool-registry.js +1 -0
  18. package/dist/tools/apps/absence.js +1 -0
  19. package/dist/tools/apps/aiquila.js +1 -0
  20. package/dist/tools/apps/assistant.js +1 -0
  21. package/dist/tools/apps/bookmarks.js +1 -0
  22. package/dist/tools/apps/calendar.js +2 -14
  23. package/dist/tools/apps/circles.js +1 -0
  24. package/dist/tools/apps/contacts.js +2 -14
  25. package/dist/tools/apps/cookbook.js +1 -0
  26. package/dist/tools/apps/deck.js +19 -38
  27. package/dist/tools/apps/groups.js +1 -0
  28. package/dist/tools/apps/mail.js +1 -0
  29. package/dist/tools/apps/maps.js +1 -0
  30. package/dist/tools/apps/notes.js +12 -36
  31. package/dist/tools/apps/notifications.js +1 -0
  32. package/dist/tools/apps/photos.js +1 -0
  33. package/dist/tools/apps/projects.js +1 -0
  34. package/dist/tools/apps/shares.js +1 -0
  35. package/dist/tools/apps/talk.js +1 -0
  36. package/dist/tools/apps/tasks.js +2 -10
  37. package/dist/tools/apps/translate.js +1 -0
  38. package/dist/tools/apps/trash.js +1 -0
  39. package/dist/tools/apps/user-status.js +1 -0
  40. package/dist/tools/apps/users.js +1 -0
  41. package/dist/tools/apps/versions.js +1 -0
  42. package/dist/tools/dav-utils.js +30 -0
  43. package/dist/tools/error-utils.js +30 -0
  44. package/dist/tools/system/apps.js +1 -0
  45. package/dist/tools/system/files.js +1 -0
  46. package/dist/tools/system/occ-redact.js +1 -0
  47. package/dist/tools/system/occ.js +1 -0
  48. package/dist/tools/system/search.js +1 -0
  49. package/dist/tools/system/security.js +1 -0
  50. package/dist/tools/system/status.js +1 -0
  51. package/dist/tools/system/tags.js +1 -0
  52. package/dist/tools/types.js +4 -0
  53. package/dist/transports/http.js +7 -6
  54. package/dist/transports/stdio.js +1 -0
  55. package/package.json +2 -2
package/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # AIquila MCP Server
2
2
 
3
- MCP (Model Context Protocol) server that gives Claude full access to your Nextcloud instance — files, calendar, tasks, contacts, mail, maps, bookmarks, notes, and more. 126 tools across 20 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, and more. 198 tools across 31 categories.
4
4
 
5
5
  ## Quick Start
6
6
 
7
- ### Claude Desktop (stdio)
7
+ ### Stdio (Claude Desktop, Claude Code, Cursor, etc.)
8
8
 
9
- Add to `~/.config/claude/claude_desktop_config.json`:
9
+ Add to your MCP client configuration (example for Claude Desktop `~/.config/claude/claude_desktop_config.json`):
10
10
 
11
11
  ```json
12
12
  {
@@ -26,35 +26,58 @@ Add to `~/.config/claude/claude_desktop_config.json`:
26
26
 
27
27
  Generate an App Password in Nextcloud: **Settings → Security → Devices & sessions**.
28
28
 
29
- ### Docker / Claude.ai / Claude Mobile (HTTP transport)
29
+ ### HTTP Transport (Docker, Claude.ai, remote clients)
30
30
 
31
- See the [Docker setup guide](https://github.com/elgorro/aiquila/blob/main/docs/mcp/setup.md#docker--claudeai-http-transport) for running AIquila as an HTTP server with OAuth for Claude.ai and Claude Mobile.
31
+ See the [Docker setup guide](https://github.com/elgorro/aiquila/blob/main/docs/mcp/setup.md#docker--claudeai-http-transport) for running AIquila as an HTTP server with OAuth for remote MCP clients.
32
32
 
33
33
  ## What It Can Do
34
34
 
35
- | Category | Tools |
36
- | -------------------- | ------: |
37
- | Files | 11 |
38
- | Status & Diagnostics | 3 |
39
- | App Management | 6 |
40
- | Tags | 6 |
41
- | Security | 2 |
42
- | Search | 2 |
43
- | OCC Command | 1 |
44
- | Shares | 4 |
45
- | Tasks | 6 |
46
- | Calendar | 6 |
47
- | Notes | 5 |
48
- | Contacts | 6 |
49
- | Cookbook | 6 |
50
- | Bookmarks | 13 |
51
- | Mail | 8 |
52
- | Maps | 26 |
53
- | Assistant / AI | 4 |
54
- | Users | 4 |
55
- | Groups | 4 |
56
- | AIquila | 3 |
57
- | **Total** | **126** |
35
+ ### Core (always available)
36
+
37
+ | Category | Tools |
38
+ | ---------- | ----: |
39
+ | Files | 12 |
40
+ | Status | 3 |
41
+ | Apps | 6 |
42
+ | Tags | 6 |
43
+ | Search | 2 |
44
+ | Users | 4 |
45
+ | Groups | 4 |
46
+ | Shares | 10 |
47
+ | Absence | 3 |
48
+ | Trash | 3 |
49
+ | Versions | 2 |
50
+
51
+ ### AIquila app
52
+
53
+ | Category | Tools |
54
+ | ---------- | ----: |
55
+ | AIquila | 3 |
56
+ | Security | 2 |
57
+ | OCC | 1 |
58
+ | Projects | 7 |
59
+
60
+ ### Optional Nextcloud apps
61
+
62
+ | Category | Tools |
63
+ | ------------- | ----: |
64
+ | Calendar | 6 |
65
+ | Tasks | 6 |
66
+ | Contacts | 6 |
67
+ | Notes | 5 |
68
+ | Mail | 8 |
69
+ | Deck | 12 |
70
+ | Cookbook | 6 |
71
+ | Maps | 25 |
72
+ | Photos | 11 |
73
+ | Talk | 10 |
74
+ | Circles | 8 |
75
+ | Bookmarks | 13 |
76
+ | Assistant | 4 |
77
+ | Translate | 1 |
78
+ | User Status | 5 |
79
+ | Notifications | 4 |
80
+ | **Total** | **198** |
58
81
 
59
82
  ## Configuration
60
83
 
@@ -64,7 +87,7 @@ See the [Docker setup guide](https://github.com/elgorro/aiquila/blob/main/docs/m
64
87
  | `NEXTCLOUD_USER` | Yes | |
65
88
  | `NEXTCLOUD_PASSWORD` | Yes | use an App Password |
66
89
  | `MCP_TRANSPORT` | No | `stdio` (default) or `http` |
67
- | `MCP_AUTH_ENABLED` | No | `true` to enable OAuth for Claude.ai |
90
+ | `MCP_AUTH_ENABLED` | No | `true` to enable OAuth for remote clients |
68
91
  | `MCP_AUTH_SECRET` | If auth | `openssl rand -hex 32` |
69
92
  | `MCP_AUTH_ISSUER` | If auth | public HTTPS URL of this server |
70
93
  | `LOG_LEVEL` | No | `trace`/`debug`/`info`/`warn`/`error`/`fatal` |
@@ -74,12 +97,12 @@ See the [Docker setup guide](https://github.com/elgorro/aiquila/blob/main/docs/m
74
97
  - Node.js 24+
75
98
  - A Nextcloud instance with an App Password
76
99
 
77
- Optional Nextcloud apps unlock additional tool categories: Tasks, Calendar, Contacts, Notes, Cookbook, Bookmarks, Mail, Maps.
100
+ Optional Nextcloud apps unlock additional tool categories: Tasks, Calendar, Contacts, Notes, Cookbook, Deck, Bookmarks, Mail, Maps, Photos, Talk, Circles, and more.
78
101
 
79
102
  ## Documentation
80
103
 
81
104
  - [Setup Guide](https://github.com/elgorro/aiquila/blob/main/docs/mcp/setup.md) — detailed installation and configuration
82
- - [Tools Reference](https://github.com/elgorro/aiquila/blob/main/docs/mcp/README.md) — all 126 tools documented
105
+ - [Tools Reference](https://github.com/elgorro/aiquila/blob/main/docs/mcp/README.md) — all 198 tools documented
83
106
  - [Architecture](https://github.com/elgorro/aiquila/blob/main/docs/dev/mcp-server-architecture.md) — design and internals
84
107
  - [Full Documentation](https://github.com/elgorro/aiquila/blob/main/docs/) — complete docs index
85
108
 
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { renderLoginForm } from './provider.js';
2
3
  import { logger } from '../logger.js';
3
4
  export function loginHandler(provider) {
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { createHash, timingSafeEqual } from 'node:crypto';
2
3
  import { SignJWT, jwtVerify } from 'jose';
3
4
  import { InvalidGrantError } from '@modelcontextprotocol/sdk/server/auth/errors.js';
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { randomUUID } from 'node:crypto';
2
3
  import { readFileSync, writeFileSync, mkdirSync, renameSync, unlinkSync } from 'node:fs';
3
4
  import { join } from 'node:path';
@@ -92,12 +93,13 @@ export class ClientsStore {
92
93
  }
93
94
  /**
94
95
  * Creates a ClientsStore configured from environment variables:
95
- * MCP_CLIENT_ID → pre-seeded static public PKCE client (for Claude.ai / Claude Desktop)
96
- * MCP_REGISTRATION_ENABLED=true enable dynamic POST /register
96
+ * MCP_CLIENT_ID → pre-seeded static public PKCE client (any MCP client)
97
+ * MCP_CLIENT_REDIRECT_URIS comma-separated redirect URIs for the pre-seeded client
98
+ * MCP_REGISTRATION_ENABLED=true → enable dynamic POST /register
97
99
  *
98
100
  * Note: no client_secret is stored — the SDK's clientAuth middleware requires the caller
99
101
  * to send client_secret whenever client.client_secret is set, which public OAuth clients
100
- * (e.g. Claude.ai) never do. Security is enforced via PKCE instead.
102
+ * never do. Security is enforced via PKCE instead.
101
103
  */
102
104
  static fromEnv() {
103
105
  const preseeded = [];
@@ -109,7 +111,13 @@ export class ClientsStore {
109
111
  .split(',')
110
112
  .map((u) => u.trim())
111
113
  .filter(Boolean)
112
- : ['https://claude.ai/api/mcp/auth_callback'];
114
+ : [];
115
+ if (redirectUris.length === 0) {
116
+ logger.warn({ clientId: id }, '[config] MCP_CLIENT_ID is set but MCP_CLIENT_REDIRECT_URIS is empty — ' +
117
+ 'the pre-seeded client will not be able to complete the OAuth flow. ' +
118
+ "Set MCP_CLIENT_REDIRECT_URIS to your client's callback URL, or enable " +
119
+ 'dynamic registration with MCP_REGISTRATION_ENABLED=true.');
120
+ }
113
121
  preseeded.push({
114
122
  client_id: id,
115
123
  client_id_issued_at: Math.floor(Date.now() / 1000),
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { getNextcloudConfig } from '../tools/types.js';
2
3
  import { logger } from '../logger.js';
3
4
  /**
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { getNextcloudConfig } from '../tools/types.js';
2
3
  import { logger } from '../logger.js';
3
4
  /**
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { logger } from '../logger.js';
2
3
  /**
3
4
  * Build a regex that matches an XML element with any namespace prefix.
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { getNextcloudConfig } from '../tools/types.js';
2
3
  import { logger } from '../logger.js';
3
4
  import { ApiError } from './aiquila.js';
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { getNextcloudConfig } from '../tools/types.js';
2
3
  import { logger } from '../logger.js';
3
4
  /**
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { getNextcloudConfig } from '../tools/types.js';
2
3
  import { logger } from '../logger.js';
3
4
  function buildUrl(base, endpoint, queryParams) {
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { getNextcloudConfig } from '../tools/types.js';
2
3
  import { logger } from '../logger.js';
3
4
  import { ApiError } from './aiquila.js';
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { getNextcloudConfig } from '../tools/types.js';
2
3
  import { logger } from '../logger.js';
3
4
  /**
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { createClient } from 'webdav';
2
3
  const NEXTCLOUD_URL = process.env.NEXTCLOUD_URL;
3
4
  const NEXTCLOUD_USER = process.env.NEXTCLOUD_USER;
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ // SPDX-License-Identifier: MIT
2
3
  import { startStdio } from './transports/stdio.js';
3
4
  import { startHttp } from './transports/http.js';
4
5
  import { logger } from './logger.js';
@@ -7,8 +8,8 @@ import { logger } from './logger.js';
7
8
  * Provides Model Context Protocol integration for Nextcloud
8
9
  *
9
10
  * Transport selection via MCP_TRANSPORT environment variable:
10
- * - "stdio" (default): Standard input/output transport for Claude Desktop
11
- * - "http": Streamable HTTP transport for Docker/network deployment
11
+ * - "stdio" (default): Standard I/O transport for local MCP clients (e.g. Claude Desktop, Cursor)
12
+ * - "http": Streamable HTTP transport for remote/network MCP clients
12
13
  */
13
14
  async function main() {
14
15
  const transport = process.env.MCP_TRANSPORT || 'stdio';
package/dist/logger.js CHANGED
@@ -1,2 +1,3 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import pino from 'pino';
2
3
  export const logger = pino({ level: process.env.LOG_LEVEL ?? 'info' }, process.stderr);
package/dist/server.js CHANGED
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { createRequire } from 'node:module';
2
3
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
4
  import { logger } from './logger.js';
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { fetchOCS } from './client/ocs.js';
2
3
  import { logger } from './logger.js';
3
4
  // System tools (always available)
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
  import { fetchOCS } from '../../client/ocs.js';
3
4
  import { getNextcloudConfig } from '../types.js';
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
  import { executeOCC } from '../../client/aiquila.js';
3
4
  /**
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
  import { fetchOCS } from '../../client/ocs.js';
3
4
  // NC TaskProcessing status codes
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
  import { fetchBookmarksAPI } from '../../client/bookmarks.js';
3
4
  // ── Formatters ──────────────────────────────────────────────────────────────
@@ -1,5 +1,7 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
  import { decodeXmlEntities, fetchCalDAV, nsTagContent } from '../../client/caldav.js';
4
+ import { escapeICalValue, unescapeICalValue } from '../dav-utils.js';
3
5
  import { getNextcloudConfig } from '../types.js';
4
6
  // ---------------------------------------------------------------------------
5
7
  // iCalendar helpers
@@ -7,20 +9,6 @@ import { getNextcloudConfig } from '../types.js';
7
9
  function unfoldICalLines(text) {
8
10
  return text.replace(/\r?\n[ \t]/g, '');
9
11
  }
10
- function escapeICalValue(value) {
11
- return value
12
- .replace(/\\/g, '\\\\')
13
- .replace(/;/g, '\\;')
14
- .replace(/,/g, '\\,')
15
- .replace(/\n/g, '\\n');
16
- }
17
- function unescapeICalValue(value) {
18
- return value
19
- .replace(/\\n/g, '\n')
20
- .replace(/\\,/g, ',')
21
- .replace(/\\;/g, ';')
22
- .replace(/\\\\/g, '\\');
23
- }
24
12
  function formatICalDate(icalDate) {
25
13
  if (icalDate.length === 8) {
26
14
  return `${icalDate.slice(0, 4)}-${icalDate.slice(4, 6)}-${icalDate.slice(6, 8)}`;
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
  import { fetchOCS } from '../../client/ocs.js';
3
4
  /**
@@ -1,5 +1,7 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
  import { decodeXmlEntities, fetchCalDAV, nsTagContent } from '../../client/caldav.js';
4
+ import { escapeVCardValue, unescapeVCardValue } from '../dav-utils.js';
3
5
  import { getNextcloudConfig } from '../types.js';
4
6
  // ---------------------------------------------------------------------------
5
7
  // vCard helpers
@@ -7,20 +9,6 @@ import { getNextcloudConfig } from '../types.js';
7
9
  function unfoldVCardLines(text) {
8
10
  return text.replace(/\r?\n[ \t]/g, '');
9
11
  }
10
- function escapeVCardValue(value) {
11
- return value
12
- .replace(/\\/g, '\\\\')
13
- .replace(/;/g, '\\;')
14
- .replace(/,/g, '\\,')
15
- .replace(/\n/g, '\\n');
16
- }
17
- function unescapeVCardValue(value) {
18
- return value
19
- .replace(/\\n/gi, '\n')
20
- .replace(/\\,/g, ',')
21
- .replace(/\\;/g, ';')
22
- .replace(/\\\\/g, '\\');
23
- }
24
12
  /**
25
13
  * Extract TYPE parameter from a vCard property line.
26
14
  * Handles TYPE=WORK, TYPE="WORK", type=work, etc.
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
  import { getWebDAVClient } from '../../client/webdav.js';
3
4
  /**
@@ -1,10 +1,16 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
  import { fetchDeckAPI, } from '../../client/deck.js';
3
- import { ApiError } from '../../client/aiquila.js';
4
+ import { handleAppError } from '../error-utils.js';
4
5
  /**
5
6
  * Nextcloud Deck App Tools
6
7
  * Uses the Deck REST API v1.0 (/index.php/apps/deck/api/v1.0)
7
8
  */
9
+ const deckStatusMap = {
10
+ 404: 'Not found.',
11
+ 403: 'Permission denied.',
12
+ 400: (e) => `Bad request: ${e.responseBody}`,
13
+ };
8
14
  function formatBoard(board) {
9
15
  const flags = [
10
16
  board.archived ? 'archived' : null,
@@ -62,31 +68,6 @@ function formatCardDetail(card) {
62
68
  lines.push(`Modified: ${new Date(card.lastModified * 1000).toISOString()}`);
63
69
  return lines.join('\n');
64
70
  }
65
- function handleError(error, context) {
66
- if (error instanceof ApiError) {
67
- if (error.statusCode === 404) {
68
- return { content: [{ type: 'text', text: 'Not found.' }], isError: true };
69
- }
70
- if (error.statusCode === 403) {
71
- return { content: [{ type: 'text', text: 'Permission denied.' }], isError: true };
72
- }
73
- if (error.statusCode === 400) {
74
- return {
75
- content: [{ type: 'text', text: `Bad request: ${error.responseBody}` }],
76
- isError: true,
77
- };
78
- }
79
- }
80
- return {
81
- content: [
82
- {
83
- type: 'text',
84
- text: `${context}: ${error instanceof Error ? error.message : String(error)}`,
85
- },
86
- ],
87
- isError: true,
88
- };
89
- }
90
71
  export const listBoardsTool = {
91
72
  name: 'deck_list_boards',
92
73
  description: 'List all Deck boards. Returns id, title, owner, and label count for each board.',
@@ -107,7 +88,7 @@ export const listBoardsTool = {
107
88
  };
108
89
  }
109
90
  catch (error) {
110
- return handleError(error, 'Error listing boards');
91
+ return handleAppError(error, 'Error listing boards', deckStatusMap);
111
92
  }
112
93
  },
113
94
  };
@@ -151,7 +132,7 @@ export const getBoardTool = {
151
132
  return { content: [{ type: 'text', text: lines.join('\n') }] };
152
133
  }
153
134
  catch (error) {
154
- return handleError(error, 'Error getting board');
135
+ return handleAppError(error, 'Error getting board', deckStatusMap);
155
136
  }
156
137
  },
157
138
  };
@@ -176,7 +157,7 @@ export const createBoardTool = {
176
157
  };
177
158
  }
178
159
  catch (error) {
179
- return handleError(error, 'Error creating board');
160
+ return handleAppError(error, 'Error creating board', deckStatusMap);
180
161
  }
181
162
  },
182
163
  };
@@ -216,7 +197,7 @@ export const listStacksTool = {
216
197
  };
217
198
  }
218
199
  catch (error) {
219
- return handleError(error, 'Error listing stacks');
200
+ return handleAppError(error, 'Error listing stacks', deckStatusMap);
220
201
  }
221
202
  },
222
203
  };
@@ -239,7 +220,7 @@ export const createStackTool = {
239
220
  };
240
221
  }
241
222
  catch (error) {
242
- return handleError(error, 'Error creating stack');
223
+ return handleAppError(error, 'Error creating stack', deckStatusMap);
243
224
  }
244
225
  },
245
226
  };
@@ -257,7 +238,7 @@ export const getCardTool = {
257
238
  return { content: [{ type: 'text', text: formatCardDetail(card) }] };
258
239
  }
259
240
  catch (error) {
260
- return handleError(error, 'Error getting card');
241
+ return handleAppError(error, 'Error getting card', deckStatusMap);
261
242
  }
262
243
  },
263
244
  };
@@ -288,7 +269,7 @@ export const createCardTool = {
288
269
  };
289
270
  }
290
271
  catch (error) {
291
- return handleError(error, 'Error creating card');
272
+ return handleAppError(error, 'Error creating card', deckStatusMap);
292
273
  }
293
274
  },
294
275
  };
@@ -325,7 +306,7 @@ export const updateCardTool = {
325
306
  };
326
307
  }
327
308
  catch (error) {
328
- return handleError(error, 'Error updating card');
309
+ return handleAppError(error, 'Error updating card', deckStatusMap);
329
310
  }
330
311
  },
331
312
  };
@@ -359,7 +340,7 @@ export const moveCardTool = {
359
340
  };
360
341
  }
361
342
  catch (error) {
362
- return handleError(error, 'Error moving card');
343
+ return handleAppError(error, 'Error moving card', deckStatusMap);
363
344
  }
364
345
  },
365
346
  };
@@ -386,7 +367,7 @@ export const archiveCardTool = {
386
367
  };
387
368
  }
388
369
  catch (error) {
389
- return handleError(error, `Error ${args.archive ? 'archiving' : 'unarchiving'} card`);
370
+ return handleAppError(error, `Error ${args.archive ? 'archiving' : 'unarchiving'} card`, deckStatusMap);
390
371
  }
391
372
  },
392
373
  };
@@ -412,7 +393,7 @@ export const assignLabelTool = {
412
393
  };
413
394
  }
414
395
  catch (error) {
415
- return handleError(error, 'Error assigning label');
396
+ return handleAppError(error, 'Error assigning label', deckStatusMap);
416
397
  }
417
398
  },
418
399
  };
@@ -438,7 +419,7 @@ export const assignUserTool = {
438
419
  };
439
420
  }
440
421
  catch (error) {
441
- return handleError(error, 'Error assigning user');
422
+ return handleAppError(error, 'Error assigning user', deckStatusMap);
442
423
  }
443
424
  },
444
425
  };
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
  import { fetchOCS } from '../../client/ocs.js';
3
4
  /**
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
  import { fetchMailAPI } from '../../client/mail.js';
3
4
  import { fetchOCS } from '../../client/ocs.js';
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
  import { fetchMapsExternalAPI, fetchMapsAPI } from '../../client/maps.js';
3
4
  // ── Formatters ──────────────────────────────────────────────────────────────
@@ -1,10 +1,16 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
  import { fetchNotesAPI } from '../../client/notes.js';
3
- import { ApiError } from '../../client/aiquila.js';
4
+ import { handleAppError } from '../error-utils.js';
4
5
  /**
5
6
  * Nextcloud Notes App Tools
6
7
  * Uses the Notes REST API v1 (/index.php/apps/notes/api/v1)
7
8
  */
9
+ const notesStatusMap = {
10
+ 404: 'Note not found.',
11
+ 403: 'Note is read-only.',
12
+ 412: 'Conflict: note was modified by someone else. Fetch the latest version and retry.',
13
+ };
8
14
  function formatNote(note) {
9
15
  const date = new Date(note.modified * 1000).toISOString();
10
16
  const flags = [note.favorite ? 'favorite' : null, note.readonly ? 'readonly' : null]
@@ -15,36 +21,6 @@ function formatNote(note) {
15
21
  .join(' | ');
16
22
  return `[${note.id}] ${note.title}${meta ? ` (${meta})` : ''} — modified: ${date}`;
17
23
  }
18
- function handleError(error, context) {
19
- if (error instanceof ApiError) {
20
- if (error.statusCode === 404) {
21
- return { content: [{ type: 'text', text: `Note not found.` }], isError: true };
22
- }
23
- if (error.statusCode === 403) {
24
- return { content: [{ type: 'text', text: `Note is read-only.` }], isError: true };
25
- }
26
- if (error.statusCode === 412) {
27
- return {
28
- content: [
29
- {
30
- type: 'text',
31
- text: `Conflict: note was modified by someone else. Fetch the latest version and retry.`,
32
- },
33
- ],
34
- isError: true,
35
- };
36
- }
37
- }
38
- return {
39
- content: [
40
- {
41
- type: 'text',
42
- text: `${context}: ${error instanceof Error ? error.message : String(error)}`,
43
- },
44
- ],
45
- isError: true,
46
- };
47
- }
48
24
  export const listNotesTool = {
49
25
  name: 'list_notes',
50
26
  description: 'List all notes in Nextcloud Notes. Returns id, title, category, favorite flag, and modification date.',
@@ -75,7 +51,7 @@ export const listNotesTool = {
75
51
  };
76
52
  }
77
53
  catch (error) {
78
- return handleError(error, 'Error listing notes');
54
+ return handleAppError(error, 'Error listing notes', notesStatusMap);
79
55
  }
80
56
  },
81
57
  };
@@ -98,7 +74,7 @@ export const getNoteTool = {
98
74
  };
99
75
  }
100
76
  catch (error) {
101
- return handleError(error, 'Error getting note');
77
+ return handleAppError(error, 'Error getting note', notesStatusMap);
102
78
  }
103
79
  },
104
80
  };
@@ -132,7 +108,7 @@ export const createNoteTool = {
132
108
  };
133
109
  }
134
110
  catch (error) {
135
- return handleError(error, 'Error creating note');
111
+ return handleAppError(error, 'Error creating note', notesStatusMap);
136
112
  }
137
113
  },
138
114
  };
@@ -170,7 +146,7 @@ export const updateNoteTool = {
170
146
  };
171
147
  }
172
148
  catch (error) {
173
- return handleError(error, 'Error updating note');
149
+ return handleAppError(error, 'Error updating note', notesStatusMap);
174
150
  }
175
151
  },
176
152
  };
@@ -193,7 +169,7 @@ export const deleteNoteTool = {
193
169
  };
194
170
  }
195
171
  catch (error) {
196
- return handleError(error, 'Error deleting note');
172
+ return handleAppError(error, 'Error deleting note', notesStatusMap);
197
173
  }
198
174
  },
199
175
  };
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
  import { fetchOCS } from '../../client/ocs.js';
3
4
  // ---------------------------------------------------------------------------
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
  import { fetchCalDAV, nsTagContent, decodeXmlEntities } from '../../client/caldav.js';
3
4
  import { getNextcloudConfig } from '../types.js';
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
  import { fetchAiquilaAPI } from '../../client/aiquila.js';
3
4
  function formatProject(p) {
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
  import { fetchOCS } from '../../client/ocs.js';
3
4
  const SHARE_TYPE_LABELS = {
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
  import { fetchOCS } from '../../client/ocs.js';
3
4
  /**
@@ -1,5 +1,7 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
  import { decodeXmlEntities, fetchCalDAV, nsTagContent } from '../../client/caldav.js';
4
+ import { escapeICalValue } from '../dav-utils.js';
3
5
  import { getNextcloudConfig } from '../types.js';
4
6
  // ---------------------------------------------------------------------------
5
7
  // iCalendar helpers
@@ -11,16 +13,6 @@ import { getNextcloudConfig } from '../types.js';
11
13
  function unfoldICalLines(text) {
12
14
  return text.replace(/\r?\n[ \t]/g, '');
13
15
  }
14
- /**
15
- * Escape special characters in iCalendar text values per RFC 5545.
16
- */
17
- function escapeICalValue(value) {
18
- return value
19
- .replace(/\\/g, '\\\\')
20
- .replace(/;/g, '\\;')
21
- .replace(/,/g, '\\,')
22
- .replace(/\n/g, '\\n');
23
- }
24
16
  /**
25
17
  * Format an iCalendar date string for human-readable display.
26
18
  * "20240115T103000Z" -> "2024-01-15 10:30"
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
  import { fetchOCS } from '../../client/ocs.js';
3
4
  // ---------------------------------------------------------------------------
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
  import { fetchCalDAV, decodeXmlEntities } from '../../client/caldav.js';
3
4
  import { getNextcloudConfig } from '../types.js';
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
  import { fetchOCS } from '../../client/ocs.js';
3
4
  const STATUS_TYPES = ['online', 'away', 'dnd', 'invisible', 'offline'];
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
  import { fetchOCS } from '../../client/ocs.js';
3
4
  /**
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
  import { fetchCalDAV } from '../../client/caldav.js';
3
4
  import { getNextcloudConfig } from '../types.js';
@@ -0,0 +1,30 @@
1
+ // SPDX-License-Identifier: MIT
2
+ /**
3
+ * Shared DAV escape/unescape utilities for iCalendar and vCard values.
4
+ *
5
+ * iCalendar (RFC 5545) and vCard (RFC 6350) use nearly identical text escaping.
6
+ * The only difference: vCard allows uppercase \N for newlines, so unescape is
7
+ * case-insensitive for that sequence.
8
+ */
9
+ export function escapeDavValue(value) {
10
+ return value
11
+ .replace(/\\/g, '\\\\')
12
+ .replace(/;/g, '\\;')
13
+ .replace(/,/g, '\\,')
14
+ .replace(/\n/g, '\\n');
15
+ }
16
+ export function unescapeDavValue(value, options) {
17
+ return value
18
+ .replace(options?.caseInsensitiveNewline ? /\\n/gi : /\\n/g, '\n')
19
+ .replace(/\\,/g, ',')
20
+ .replace(/\\;/g, ';')
21
+ .replace(/\\\\/g, '\\');
22
+ }
23
+ /** Escape for iCalendar (VEVENT, VTODO) properties. */
24
+ export const escapeICalValue = escapeDavValue;
25
+ /** Unescape iCalendar property values (case-sensitive \\n). */
26
+ export const unescapeICalValue = (value) => unescapeDavValue(value);
27
+ /** Escape for vCard properties. */
28
+ export const escapeVCardValue = escapeDavValue;
29
+ /** Unescape vCard property values (case-insensitive \\n per RFC 6350). */
30
+ export const unescapeVCardValue = (value) => unescapeDavValue(value, { caseInsensitiveNewline: true });
@@ -0,0 +1,30 @@
1
+ // SPDX-License-Identifier: MIT
2
+ import { ApiError } from '../client/aiquila.js';
3
+ /**
4
+ * Shared error handler for app tool modules.
5
+ *
6
+ * @param error The caught error
7
+ * @param context Human-readable context prefixed to generic messages (e.g. "Error listing notes")
8
+ * @param statusMap Optional map of HTTP status codes to user-facing messages.
9
+ * When the error is an ApiError whose status matches a key, the corresponding
10
+ * message is returned. The message may be a string or a function receiving the
11
+ * ApiError for dynamic messages (e.g. including the response body).
12
+ */
13
+ export function handleAppError(error, context, statusMap = {}) {
14
+ if (error instanceof ApiError) {
15
+ const entry = statusMap[error.statusCode];
16
+ if (entry !== undefined) {
17
+ const text = typeof entry === 'function' ? entry(error) : entry;
18
+ return { content: [{ type: 'text', text }], isError: true };
19
+ }
20
+ }
21
+ return {
22
+ content: [
23
+ {
24
+ type: 'text',
25
+ text: `${context}: ${error instanceof Error ? error.message : String(error)}`,
26
+ },
27
+ ],
28
+ isError: true,
29
+ };
30
+ }
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
  import { fetchOCS } from '../../client/ocs.js';
3
4
  import { executeOCC } from '../../client/aiquila.js';
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
  import { getWebDAVClient } from '../../client/webdav.js';
3
4
  import { fetchAiquilaAPI } from '../../client/aiquila.js';
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  /**
2
3
  * Redacts sensitive information from OCC command output.
3
4
  * Applied to stdout/stderr before returning results to the MCP client.
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
  import { executeOCC, formatOccError } from '../../client/aiquila.js';
3
4
  import { redactSensitiveOutput } from './occ-redact.js';
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
  import { fetchOCS } from '../../client/ocs.js';
3
4
  /**
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
  import { executeOCC, formatOccError } from '../../client/aiquila.js';
3
4
  /**
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
  import { fetchOCS, fetchStatus } from '../../client/ocs.js';
3
4
  import { executeOCC, formatOccError } from '../../client/aiquila.js';
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
  import { fetchCalDAV } from '../../client/caldav.js';
3
4
  import { getNextcloudConfig } from '../types.js';
@@ -1,4 +1,8 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
3
+ /**
4
+ * Shared type definitions for MCP tools
5
+ */
2
6
  /**
3
7
  * Zod schemas for common parameters
4
8
  */
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import https from 'node:https';
2
3
  import express from 'express';
3
4
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
@@ -26,8 +27,8 @@ const TLS_CERT_ERROR_CODES = new Set([
26
27
  /**
27
28
  * Advisory TLS certificate check — purely informational, never crashes the server.
28
29
  *
29
- * Claude.ai and Claude mobile require a CA-trusted cert. If the issuer URL still
30
- * has a self-signed or untrusted cert at startup this is logged as a warning so
30
+ * Most MCP clients require a CA-trusted cert. If the issuer URL still has a
31
+ * self-signed or untrusted cert at startup this is logged as a warning so
31
32
  * operators know to fix it, but the MCP server continues running normally.
32
33
  *
33
34
  * Rationale: crashing or health-check-failing on a bad cert creates a deadlock on
@@ -62,8 +63,8 @@ async function checkIssuerTls(issuerUrl) {
62
63
  const code = err.code ?? '';
63
64
  if (TLS_CERT_ERROR_CODES.has(code)) {
64
65
  logger.warn({ issuer: issuerUrl, code }, `[startup] TLS certificate not yet trusted (${code}). ` +
65
- `Claude.ai and Claude mobile require a CA-trusted certificate — self-signed certs ` +
66
- `will cause Claude to refuse the connection. ` +
66
+ `Most MCP clients require a CA-trusted certificate — self-signed certs ` +
67
+ `may cause clients to refuse the connection. ` +
67
68
  `Use Let's Encrypt (via Traefik or Caddy with a real domain) or mount a CA-signed cert. ` +
68
69
  `The MCP server will keep running; re-deploy once the cert is valid.`);
69
70
  }
@@ -149,8 +150,8 @@ export async function startHttp() {
149
150
  }
150
151
  // Stateless mode: create a new transport + server per request so each MCP
151
152
  // call is handled independently. This is required by the SDK for stateless
152
- // operation and allows distributed clients (like Claude.ai) to connect from
153
- // multiple IPs without needing a shared session.
153
+ // operation and allows distributed clients (like Claude.ai, Cursor, etc.) to
154
+ // connect from multiple IPs without needing a shared session.
154
155
  const handleMcpRequest = async (req, res) => {
155
156
  const mcpServer = await createServer();
156
157
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
2
3
  import { createServer } from '../server.js';
3
4
  import { logger } from '../logger.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aiquila-mcp",
3
- "version": "0.2.16",
4
- "description": "AIquila - MCP server for Nextcloud integration with Claude AI",
3
+ "version": "0.2.18",
4
+ "description": "AIquila - MCP server for Nextcloud integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {