aiquila-mcp 0.3.5 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,51 @@
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 Text app OCS API.
7
+ *
8
+ * Base path: /ocs/v2.php/apps/text
9
+ * Unwraps the OCS envelope and returns `ocs.data`.
10
+ * Throws {@link ApiError} on HTTP errors or non-success OCS status codes.
11
+ */
12
+ export async function fetchTextAPI(endpoint, options = {}) {
13
+ const config = getNextcloudConfig();
14
+ const auth = Buffer.from(`${config.user}:${config.password}`).toString('base64');
15
+ let url = `${config.url}/ocs/v2.php/apps/text${endpoint}`;
16
+ if (options.queryParams) {
17
+ const params = new URLSearchParams(options.queryParams);
18
+ url += `?${params.toString()}`;
19
+ }
20
+ const headers = {
21
+ Authorization: `Basic ${auth}`,
22
+ 'OCS-APIRequest': 'true',
23
+ Accept: 'application/json',
24
+ };
25
+ let body;
26
+ if (options.body !== undefined) {
27
+ body = JSON.stringify(options.body);
28
+ headers['Content-Type'] = 'application/json';
29
+ }
30
+ const method = options.method ?? 'GET';
31
+ const t0 = Date.now();
32
+ const response = await fetch(url, { method, headers, body });
33
+ logger.trace({ method, url, status: response.status, ms: Date.now() - t0 }, '[text] HTTP');
34
+ if (!response.ok) {
35
+ const text = await response.text();
36
+ throw new ApiError(response.status, response.statusText, text);
37
+ }
38
+ if (response.status === 204) {
39
+ return undefined;
40
+ }
41
+ const contentType = response.headers.get('content-type') ?? '';
42
+ if (!contentType.includes('application/json')) {
43
+ return undefined;
44
+ }
45
+ const json = (await response.json());
46
+ const code = json?.ocs?.meta?.statuscode;
47
+ if (code !== undefined && code !== 200 && code !== 100) {
48
+ throw new ApiError(code, json.ocs.meta.status ?? 'error', json.ocs.meta.message ?? '');
49
+ }
50
+ return json.ocs.data;
51
+ }
@@ -36,6 +36,7 @@ import { versionsTools } from './tools/apps/versions.js';
36
36
  import { projectsTools } from './tools/apps/projects.js';
37
37
  import { pollsTools } from './tools/apps/polls.js';
38
38
  import { formsTools } from './tools/apps/forms.js';
39
+ import { textTools } from './tools/apps/text.js';
39
40
  /**
40
41
  * Single source of truth for tool-to-Nextcloud-app mapping.
41
42
  *
@@ -76,6 +77,7 @@ export const TOOL_REGISTRY = [
76
77
  { category: 'bookmarks', appIds: ['bookmarks'], tools: bookmarksTools },
77
78
  { category: 'polls', appIds: ['polls'], tools: pollsTools },
78
79
  { category: 'forms', appIds: ['forms'], tools: formsTools },
80
+ { category: 'text', appIds: ['text'], tools: textTools },
79
81
  { category: 'assistant', appIds: ['assistant'], tools: assistantTools },
80
82
  { category: 'translate', appIds: ['text_translate', 'translate'], tools: translateTools },
81
83
  { category: 'user_status', appIds: ['user_status'], tools: userStatusTools },
@@ -52,17 +52,129 @@ function setICalProperty(icalData, propName, value) {
52
52
  }
53
53
  return icalData.replace(/END:VEVENT/, `${propName}:${value}\r\nEND:VEVENT`);
54
54
  }
55
- function setICalDateProperty(icalData, propName, value) {
55
+ function setICalDateProperty(icalData, propName, value, tzid) {
56
56
  const regex = new RegExp(`^${propName}(;[^:]*)?:.*$`, 'm');
57
57
  if (value === null) {
58
58
  return icalData.replace(regex, '').replace(/(\r?\n){2,}/g, '\r\n');
59
59
  }
60
- const formatted = value.length === 8 ? `${propName};VALUE=DATE:${value}` : `${propName}:${value}`;
60
+ let formatted;
61
+ if (value.length === 8) {
62
+ formatted = `${propName};VALUE=DATE:${value}`;
63
+ }
64
+ else if (tzid) {
65
+ formatted = `${propName};TZID=${tzid}:${normalizeLocalICal(value, tzid)}`;
66
+ }
67
+ else {
68
+ formatted = `${propName}:${value}`;
69
+ }
61
70
  if (regex.test(icalData)) {
62
71
  return icalData.replace(regex, formatted);
63
72
  }
64
73
  return icalData.replace(/END:VEVENT/, `${formatted}\r\nEND:VEVENT`);
65
74
  }
75
+ /**
76
+ * Format the UTC offset for a given IANA zone at a specific instant.
77
+ * Returns "+HHMM" or "-HHMM" (e.g. "+0200", "-0500", "+0000").
78
+ */
79
+ function getTimezoneOffset(tzid, date) {
80
+ const fmt = new Intl.DateTimeFormat('en-US', {
81
+ timeZone: tzid,
82
+ timeZoneName: 'longOffset',
83
+ });
84
+ const tzPart = fmt.formatToParts(date).find((p) => p.type === 'timeZoneName')?.value || 'GMT';
85
+ // tzPart is "GMT", "GMT+02:00", "GMT-05:30", or "GMT+5:45" depending on engine
86
+ const m = tzPart.match(/GMT([+-])(\d{1,2})(?::(\d{2}))?/);
87
+ if (!m)
88
+ return '+0000';
89
+ const sign = m[1];
90
+ const hh = m[2].padStart(2, '0');
91
+ const mm = m[3] ?? '00';
92
+ return `${sign}${hh}${mm}`;
93
+ }
94
+ /**
95
+ * Build a minimal but RFC 5545-conformant VTIMEZONE component for the given
96
+ * IANA zone. Uses two reference instants in the current year to detect DST.
97
+ * No external tzdata required — sufficient for iOS / Thunderbird to accept
98
+ * the event and apply the TZID correctly to upcoming occurrences.
99
+ */
100
+ export function buildVTimezone(tzid) {
101
+ const year = new Date().getUTCFullYear();
102
+ const janOffset = getTimezoneOffset(tzid, new Date(Date.UTC(year, 0, 15)));
103
+ const julOffset = getTimezoneOffset(tzid, new Date(Date.UTC(year, 6, 15)));
104
+ const lines = ['BEGIN:VTIMEZONE', `TZID:${tzid}`];
105
+ if (janOffset === julOffset) {
106
+ lines.push('BEGIN:STANDARD', 'DTSTART:19700101T000000', `TZOFFSETFROM:${janOffset}`, `TZOFFSETTO:${janOffset}`, `TZNAME:${tzid}`, 'END:STANDARD');
107
+ }
108
+ else {
109
+ // Higher numeric offset = daylight, lower = standard (works for both hemispheres)
110
+ const standardOffset = janOffset < julOffset ? janOffset : julOffset;
111
+ const daylightOffset = janOffset < julOffset ? julOffset : janOffset;
112
+ lines.push('BEGIN:STANDARD', 'DTSTART:19701101T000000', `TZOFFSETFROM:${daylightOffset}`, `TZOFFSETTO:${standardOffset}`, `TZNAME:${tzid}`, 'END:STANDARD', 'BEGIN:DAYLIGHT', 'DTSTART:19700301T000000', `TZOFFSETFROM:${standardOffset}`, `TZOFFSETTO:${daylightOffset}`, `TZNAME:${tzid}`, 'END:DAYLIGHT');
113
+ }
114
+ lines.push('END:VTIMEZONE');
115
+ return lines.join('\r\n');
116
+ }
117
+ /**
118
+ * Convert a UTC iCal datetime ("YYYYMMDDTHHmmssZ") to wall-clock time in the
119
+ * given IANA zone, returning "YYYYMMDDTHHmmss" (no Z).
120
+ */
121
+ function convertUtcICalToLocalICal(utcICal, tzid) {
122
+ const m = utcICal.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/);
123
+ if (!m)
124
+ return utcICal.replace(/Z$/, '');
125
+ const [, y, mo, d, h, mi, s] = m;
126
+ const instant = new Date(Date.UTC(+y, +mo - 1, +d, +h, +mi, +s));
127
+ const fmt = new Intl.DateTimeFormat('en-CA', {
128
+ timeZone: tzid,
129
+ year: 'numeric',
130
+ month: '2-digit',
131
+ day: '2-digit',
132
+ hour: '2-digit',
133
+ minute: '2-digit',
134
+ second: '2-digit',
135
+ hour12: false,
136
+ });
137
+ const parts = fmt.formatToParts(instant);
138
+ const get = (t) => parts.find((p) => p.type === t)?.value ?? '00';
139
+ let hour = get('hour');
140
+ if (hour === '24')
141
+ hour = '00';
142
+ return `${get('year')}${get('month')}${get('day')}T${hour}${get('minute')}${get('second')}`;
143
+ }
144
+ /**
145
+ * If `value` ends in Z, convert it to wall-clock time in `tzid`. Otherwise
146
+ * assume it is already a floating local datetime in that zone.
147
+ */
148
+ function normalizeLocalICal(value, tzid) {
149
+ if (/Z$/.test(value))
150
+ return convertUtcICalToLocalICal(value, tzid);
151
+ return value;
152
+ }
153
+ /**
154
+ * Add `hours` to a floating "YYYYMMDDTHHmmss" string, returning the same form.
155
+ */
156
+ function addHoursToLocalICal(local, hours) {
157
+ const m = local.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})$/);
158
+ if (!m)
159
+ return local;
160
+ const [, y, mo, d, h, mi, s] = m;
161
+ const dt = new Date(Date.UTC(+y, +mo - 1, +d, +h + hours, +mi, +s));
162
+ const pad = (n) => String(n).padStart(2, '0');
163
+ return (`${dt.getUTCFullYear()}${pad(dt.getUTCMonth() + 1)}${pad(dt.getUTCDate())}` +
164
+ `T${pad(dt.getUTCHours())}${pad(dt.getUTCMinutes())}${pad(dt.getUTCSeconds())}`);
165
+ }
166
+ /**
167
+ * Ensure the VCALENDAR contains a VTIMEZONE with the requested TZID. Insert
168
+ * one before the first VEVENT if not already present.
169
+ */
170
+ function ensureVTimezone(icalData, tzid) {
171
+ const escaped = tzid.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
172
+ const existing = new RegExp(`BEGIN:VTIMEZONE[\\s\\S]*?TZID:${escaped}[\\s\\S]*?END:VTIMEZONE`);
173
+ if (existing.test(icalData))
174
+ return icalData;
175
+ const vtz = buildVTimezone(tzid);
176
+ return icalData.replace(/BEGIN:VEVENT/, `${vtz}\r\nBEGIN:VEVENT`);
177
+ }
66
178
  // ---------------------------------------------------------------------------
67
179
  // Parsing
68
180
  // ---------------------------------------------------------------------------
@@ -703,6 +815,10 @@ export const createEventTool = {
703
815
  .number()
704
816
  .optional()
705
817
  .describe('Reminder in minutes before the event (e.g. 15 for 15 min before)'),
818
+ tzid: z
819
+ .string()
820
+ .optional()
821
+ .describe('IANA time zone (e.g. "Europe/Berlin"). When set, a VTIMEZONE block is emitted and dtstart/dtend use TZID instead of UTC. Required for reliable iOS Calendar and Thunderbird CalDAV sync of timed events. UTC inputs (trailing Z) are converted to wall-clock time in this zone.'),
706
822
  }),
707
823
  handler: async (args) => {
708
824
  try {
@@ -714,6 +830,8 @@ export const createEventTool = {
714
830
  await assertCalendarSupportsEvents(calendarBaseUrl, args.calendarName);
715
831
  const now = icalNow();
716
832
  const isAllDay = args.dtstart.length === 8;
833
+ const tzid = !isAllDay ? args.tzid : undefined;
834
+ const localDtstart = tzid ? normalizeLocalICal(args.dtstart, tzid) : args.dtstart;
717
835
  // Calculate default end
718
836
  let dtend = args.dtend;
719
837
  if (!dtend) {
@@ -722,18 +840,34 @@ export const createEventTool = {
722
840
  const d = new Date(parseInt(args.dtstart.slice(0, 4)), parseInt(args.dtstart.slice(4, 6)) - 1, parseInt(args.dtstart.slice(6, 8)) + 1);
723
841
  dtend = `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`;
724
842
  }
843
+ else if (tzid) {
844
+ // Timed with TZID: 1 hour after local wall time
845
+ dtend = addHoursToLocalICal(localDtstart, 1);
846
+ }
725
847
  else {
726
- // Timed: 1 hour later
848
+ // Timed: 1 hour later (UTC)
727
849
  const startDate = new Date(parseInt(args.dtstart.slice(0, 4)), parseInt(args.dtstart.slice(4, 6)) - 1, parseInt(args.dtstart.slice(6, 8)), parseInt(args.dtstart.slice(9, 11)), parseInt(args.dtstart.slice(11, 13)), parseInt(args.dtstart.slice(13, 15)));
728
850
  startDate.setHours(startDate.getHours() + 1);
729
851
  dtend = startDate.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';
730
852
  }
731
853
  }
732
- const dtstartProp = isAllDay
733
- ? `DTSTART;VALUE=DATE:${args.dtstart}`
734
- : `DTSTART:${args.dtstart}`;
735
- const dtendProp = isAllDay ? `DTEND;VALUE=DATE:${dtend}` : `DTEND:${dtend}`;
736
- let vevent = `BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//AIquila//MCP Server//EN\r\nBEGIN:VEVENT\r\nUID:${eventUid}\r\nDTSTAMP:${now}\r\nCREATED:${now}\r\nLAST-MODIFIED:${now}\r\n${dtstartProp}\r\n${dtendProp}\r\nSUMMARY:${escapeICalValue(args.summary)}`;
854
+ const localDtend = tzid ? normalizeLocalICal(dtend, tzid) : dtend;
855
+ let dtstartProp;
856
+ let dtendProp;
857
+ if (isAllDay) {
858
+ dtstartProp = `DTSTART;VALUE=DATE:${args.dtstart}`;
859
+ dtendProp = `DTEND;VALUE=DATE:${dtend}`;
860
+ }
861
+ else if (tzid) {
862
+ dtstartProp = `DTSTART;TZID=${tzid}:${localDtstart}`;
863
+ dtendProp = `DTEND;TZID=${tzid}:${localDtend}`;
864
+ }
865
+ else {
866
+ dtstartProp = `DTSTART:${args.dtstart}`;
867
+ dtendProp = `DTEND:${dtend}`;
868
+ }
869
+ const vtimezoneBlock = tzid ? `${buildVTimezone(tzid)}\r\n` : '';
870
+ let vevent = `BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//AIquila//MCP Server//EN\r\n${vtimezoneBlock}BEGIN:VEVENT\r\nUID:${eventUid}\r\nDTSTAMP:${now}\r\nCREATED:${now}\r\nLAST-MODIFIED:${now}\r\n${dtstartProp}\r\n${dtendProp}\r\nSUMMARY:${escapeICalValue(args.summary)}`;
737
871
  if (args.location) {
738
872
  vevent += `\r\nLOCATION:${escapeICalValue(args.location)}`;
739
873
  }
@@ -870,6 +1004,10 @@ export const updateEventTool = {
870
1004
  .nullable()
871
1005
  .optional()
872
1006
  .describe('Replace attendees list, or null to remove all attendees'),
1007
+ tzid: z
1008
+ .string()
1009
+ .optional()
1010
+ .describe('IANA time zone (e.g. "Europe/Berlin") for the updated dtstart/dtend. When set, a VTIMEZONE block is added (if missing) and DTSTART/DTEND use TZID instead of UTC. Required for iOS/Thunderbird CalDAV sync.'),
873
1011
  }),
874
1012
  handler: async (args) => {
875
1013
  try {
@@ -879,11 +1017,17 @@ export const updateEventTool = {
879
1017
  if (args.summary !== undefined) {
880
1018
  modified = setICalProperty(modified, 'SUMMARY', escapeICalValue(args.summary));
881
1019
  }
1020
+ // Only timed datetimes carry a TZID; all-day (length 8) values are dates.
1021
+ const dtstartTz = args.tzid && args.dtstart && args.dtstart.length > 8 ? args.tzid : undefined;
1022
+ const dtendTz = args.tzid && args.dtend && args.dtend.length > 8 ? args.tzid : undefined;
882
1023
  if (args.dtstart !== undefined) {
883
- modified = setICalDateProperty(modified, 'DTSTART', args.dtstart);
1024
+ modified = setICalDateProperty(modified, 'DTSTART', args.dtstart, dtstartTz);
884
1025
  }
885
1026
  if (args.dtend !== undefined) {
886
- modified = setICalDateProperty(modified, 'DTEND', args.dtend);
1027
+ modified = setICalDateProperty(modified, 'DTEND', args.dtend, dtendTz);
1028
+ }
1029
+ if (args.tzid && (dtstartTz || dtendTz)) {
1030
+ modified = ensureVTimezone(modified, args.tzid);
887
1031
  }
888
1032
  if (args.location !== undefined) {
889
1033
  modified = setICalProperty(modified, 'LOCATION', args.location ? escapeICalValue(args.location) : null);
@@ -0,0 +1,180 @@
1
+ // SPDX-License-Identifier: MIT
2
+ import { z } from 'zod';
3
+ import { fetchTextAPI, } from '../../client/text.js';
4
+ import { ApiError } from '../../client/aiquila.js';
5
+ import { getWebDAVClient } from '../../client/webdav.js';
6
+ import { handleAppError } from '../error-utils.js';
7
+ /**
8
+ * Nextcloud Text App Tools — workspaces (per-folder Readme.md) and direct-edit URLs.
9
+ *
10
+ * Uses the Text OCS API (/ocs/v2.php/apps/text/workspace) plus WebDAV for content I/O.
11
+ * Collaborative editing sessions are intentionally out of scope: the live editor runs
12
+ * in the user's browser via the direct-edit URL.
13
+ */
14
+ const textStatusMap = {
15
+ 400: 'Bad request — check the folder path.',
16
+ 403: 'Access denied to this folder.',
17
+ 404: 'No workspace exists for this folder.',
18
+ };
19
+ const FolderPathArg = z
20
+ .string()
21
+ .describe("Folder path relative to the user's root (e.g. '/Projects/Acme')");
22
+ function normaliseFolder(path) {
23
+ if (!path || path === '/')
24
+ return '/';
25
+ return path.replace(/\/+$/, '');
26
+ }
27
+ function joinPath(folder, name) {
28
+ const f = normaliseFolder(folder);
29
+ return f === '/' ? `/${name}` : `${f}/${name}`;
30
+ }
31
+ function formatWorkspace(file) {
32
+ return `[${file.id}] ${file.name} (${file.mimetype}) at ${file.path}`;
33
+ }
34
+ async function resolveWorkspaceFile(folder) {
35
+ try {
36
+ const data = await fetchTextAPI('/workspace', {
37
+ queryParams: { path: folder },
38
+ });
39
+ return data?.file ?? null;
40
+ }
41
+ catch (error) {
42
+ if (error instanceof ApiError && error.statusCode === 404) {
43
+ return null;
44
+ }
45
+ throw error;
46
+ }
47
+ }
48
+ export const getTextWorkspaceTool = {
49
+ name: 'get_text_workspace',
50
+ description: "Get metadata of the Text workspace file (Readme.md) for a folder. Returns the file's id, name, mimetype and path, or reports that no workspace exists yet.",
51
+ inputSchema: z.object({ path: FolderPathArg }),
52
+ handler: async (args) => {
53
+ try {
54
+ const file = await resolveWorkspaceFile(args.path);
55
+ if (!file) {
56
+ return {
57
+ content: [
58
+ {
59
+ type: 'text',
60
+ text: `No workspace file in ${normaliseFolder(args.path) || '/'}.`,
61
+ },
62
+ ],
63
+ };
64
+ }
65
+ return {
66
+ content: [{ type: 'text', text: `Workspace: ${formatWorkspace(file)}` }],
67
+ };
68
+ }
69
+ catch (error) {
70
+ return handleAppError(error, 'Error getting workspace', textStatusMap);
71
+ }
72
+ },
73
+ };
74
+ export const readTextWorkspaceTool = {
75
+ name: 'read_text_workspace',
76
+ description: "Read the content of a folder's Text workspace file (Readme.md). Returns markdown text, or a 'no workspace' message if none exists.",
77
+ inputSchema: z.object({ path: FolderPathArg }),
78
+ handler: async (args) => {
79
+ try {
80
+ const file = await resolveWorkspaceFile(args.path);
81
+ if (!file) {
82
+ return {
83
+ content: [
84
+ {
85
+ type: 'text',
86
+ text: `No workspace file in ${normaliseFolder(args.path) || '/'}.`,
87
+ },
88
+ ],
89
+ };
90
+ }
91
+ const client = getWebDAVClient();
92
+ const content = (await client.getFileContents(file.path, { format: 'text' }));
93
+ return { content: [{ type: 'text', text: content }] };
94
+ }
95
+ catch (error) {
96
+ return handleAppError(error, 'Error reading workspace', textStatusMap);
97
+ }
98
+ },
99
+ };
100
+ export const writeTextWorkspaceTool = {
101
+ name: 'write_text_workspace',
102
+ description: "Create or overwrite a folder's Text workspace file. If a workspace already exists, its existing filename is reused; otherwise Readme.md is created at the folder root.",
103
+ inputSchema: z.object({
104
+ path: FolderPathArg,
105
+ content: z.string().describe('Markdown content to write'),
106
+ }),
107
+ handler: async (args) => {
108
+ try {
109
+ const existing = await resolveWorkspaceFile(args.path);
110
+ const targetPath = existing?.path ?? joinPath(args.path, 'Readme.md');
111
+ const client = getWebDAVClient();
112
+ await client.putFileContents(targetPath, args.content, { overwrite: true });
113
+ return {
114
+ content: [
115
+ {
116
+ type: 'text',
117
+ text: existing
118
+ ? `Workspace updated at ${targetPath}.`
119
+ : `Workspace created at ${targetPath}.`,
120
+ },
121
+ ],
122
+ };
123
+ }
124
+ catch (error) {
125
+ return handleAppError(error, 'Error writing workspace', textStatusMap);
126
+ }
127
+ },
128
+ };
129
+ export const deleteTextWorkspaceTool = {
130
+ name: 'delete_text_workspace',
131
+ description: 'Delete the Text workspace file (Readme.md) for a folder. The folder itself is kept. No-op if the folder has no workspace.',
132
+ inputSchema: z.object({ path: FolderPathArg }),
133
+ handler: async (args) => {
134
+ try {
135
+ const existing = await resolveWorkspaceFile(args.path);
136
+ if (!existing) {
137
+ return {
138
+ content: [
139
+ {
140
+ type: 'text',
141
+ text: `No workspace file in ${normaliseFolder(args.path) || '/'}.`,
142
+ },
143
+ ],
144
+ };
145
+ }
146
+ const client = getWebDAVClient();
147
+ await client.deleteFile(existing.path);
148
+ return {
149
+ content: [{ type: 'text', text: `Workspace deleted at ${existing.path}.` }],
150
+ };
151
+ }
152
+ catch (error) {
153
+ return handleAppError(error, 'Error deleting workspace', textStatusMap);
154
+ }
155
+ },
156
+ };
157
+ export const getTextWorkspaceEditUrlTool = {
158
+ name: 'get_text_workspace_edit_url',
159
+ description: "Get a one-shot direct-edit URL for a folder's Text workspace. Opens the live collaborative editor in a browser. The Readme.md is created automatically if it does not exist yet. Hand the URL to a human collaborator — the MCP server does not participate in the editing session.",
160
+ inputSchema: z.object({ path: FolderPathArg }),
161
+ handler: async (args) => {
162
+ try {
163
+ const data = await fetchTextAPI('/workspace/direct', {
164
+ method: 'POST',
165
+ body: { path: args.path },
166
+ });
167
+ return { content: [{ type: 'text', text: data.url }] };
168
+ }
169
+ catch (error) {
170
+ return handleAppError(error, 'Error getting workspace edit URL', textStatusMap);
171
+ }
172
+ },
173
+ };
174
+ export const textTools = [
175
+ getTextWorkspaceTool,
176
+ readTextWorkspaceTool,
177
+ writeTextWorkspaceTool,
178
+ deleteTextWorkspaceTool,
179
+ getTextWorkspaceEditUrlTool,
180
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiquila-mcp",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "AIquila - MCP server for Nextcloud integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",