aiquila-mcp 0.3.5 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/text.js +51 -0
- package/dist/tool-registry.js +2 -0
- package/dist/tools/apps/text.js +180 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/tool-registry.js
CHANGED
|
@@ -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 },
|
|
@@ -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
|
+
];
|