@sondt2709/wikijs-mcp 0.1.0
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/LICENSE +21 -0
- package/README.md +144 -0
- package/dist/http-D7MoxUTD.js +67 -0
- package/dist/http-D7MoxUTD.js.map +1 -0
- package/dist/index.js +1020 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1020 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import pino, { destination } from "pino";
|
|
6
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
//#region src/config.ts
|
|
8
|
+
const ConfigSchema = z.object({
|
|
9
|
+
wikijsUrl: z.url({ message: "must be a valid URL like https://wiki.example.com" }),
|
|
10
|
+
wikijsToken: z.string().min(1, "must not be empty"),
|
|
11
|
+
transport: z.enum(["stdio", "http"]).default("stdio"),
|
|
12
|
+
port: z.coerce.number().int().min(1).max(65535).default(3e3),
|
|
13
|
+
logLevel: z.enum([
|
|
14
|
+
"fatal",
|
|
15
|
+
"error",
|
|
16
|
+
"warn",
|
|
17
|
+
"info",
|
|
18
|
+
"debug",
|
|
19
|
+
"trace"
|
|
20
|
+
]).default("info")
|
|
21
|
+
});
|
|
22
|
+
function loadConfig(argv = process.argv.slice(2)) {
|
|
23
|
+
const { values } = parseArgs({
|
|
24
|
+
args: argv,
|
|
25
|
+
options: {
|
|
26
|
+
url: { type: "string" },
|
|
27
|
+
token: { type: "string" },
|
|
28
|
+
transport: { type: "string" },
|
|
29
|
+
port: { type: "string" },
|
|
30
|
+
"log-level": { type: "string" }
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
const parsed = ConfigSchema.safeParse({
|
|
34
|
+
wikijsUrl: values.url ?? process.env.WIKIJS_URL,
|
|
35
|
+
wikijsToken: values.token ?? process.env.WIKIJS_TOKEN,
|
|
36
|
+
transport: values.transport ?? process.env.TRANSPORT,
|
|
37
|
+
port: values.port ?? process.env.PORT,
|
|
38
|
+
logLevel: values["log-level"] ?? process.env.LOG_LEVEL
|
|
39
|
+
});
|
|
40
|
+
if (!parsed.success) {
|
|
41
|
+
const issues = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
42
|
+
throw new Error(`Invalid configuration — ${issues}. Set WIKIJS_URL and WIKIJS_TOKEN env vars (or --url/--token flags). See .env.example.`);
|
|
43
|
+
}
|
|
44
|
+
return parsed.data;
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/logger.ts
|
|
48
|
+
function createLogger(level) {
|
|
49
|
+
return pino({
|
|
50
|
+
level,
|
|
51
|
+
base: void 0
|
|
52
|
+
}, destination(2));
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region package.json
|
|
56
|
+
var version = "0.1.0";
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/client/graphql.ts
|
|
59
|
+
var WikiJsError = class extends Error {
|
|
60
|
+
kind;
|
|
61
|
+
slug;
|
|
62
|
+
errorCode;
|
|
63
|
+
constructor(message, kind, slug, errorCode) {
|
|
64
|
+
super(message);
|
|
65
|
+
this.name = "WikiJsError";
|
|
66
|
+
this.kind = kind;
|
|
67
|
+
this.slug = slug;
|
|
68
|
+
this.errorCode = errorCode;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
function assertSucceeded(rr, action) {
|
|
72
|
+
if (!rr.succeeded) throw new WikiJsError(`Wiki.js error ${rr.slug} while ${action}: ${rr.message ?? "no message provided"}`, "application", rr.slug, rr.errorCode);
|
|
73
|
+
}
|
|
74
|
+
const AUTH_HINT = " (check that WIKIJS_TOKEN is a valid, unexpired API key and that its group has the required page permissions)";
|
|
75
|
+
var GraphQLClient = class {
|
|
76
|
+
endpoint;
|
|
77
|
+
token;
|
|
78
|
+
timeoutMs;
|
|
79
|
+
constructor(opts) {
|
|
80
|
+
this.endpoint = new URL("/graphql", opts.baseUrl).toString();
|
|
81
|
+
this.token = opts.token;
|
|
82
|
+
this.timeoutMs = opts.timeoutMs ?? 3e4;
|
|
83
|
+
}
|
|
84
|
+
async request(query, variables) {
|
|
85
|
+
let res;
|
|
86
|
+
try {
|
|
87
|
+
res = await fetch(this.endpoint, {
|
|
88
|
+
method: "POST",
|
|
89
|
+
headers: {
|
|
90
|
+
"content-type": "application/json",
|
|
91
|
+
authorization: `Bearer ${this.token}`
|
|
92
|
+
},
|
|
93
|
+
body: JSON.stringify({
|
|
94
|
+
query,
|
|
95
|
+
variables
|
|
96
|
+
}),
|
|
97
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
98
|
+
});
|
|
99
|
+
} catch (err) {
|
|
100
|
+
throw new WikiJsError(`Cannot reach Wiki.js at ${this.endpoint}: ${err.message}`, "transport");
|
|
101
|
+
}
|
|
102
|
+
if (res.status === 401 || res.status === 403) throw new WikiJsError(`Wiki.js rejected the request with HTTP ${res.status}${AUTH_HINT}`, "transport");
|
|
103
|
+
if (!res.ok) throw new WikiJsError(`Wiki.js returned HTTP ${res.status} for GraphQL request`, "transport");
|
|
104
|
+
let body;
|
|
105
|
+
try {
|
|
106
|
+
body = await res.json();
|
|
107
|
+
} catch {
|
|
108
|
+
throw new WikiJsError(`Wiki.js returned a non-JSON response (HTTP ${res.status})`, "transport");
|
|
109
|
+
}
|
|
110
|
+
if (body.errors?.length) {
|
|
111
|
+
const message = body.errors.map((e) => e.message).join("; ");
|
|
112
|
+
throw new WikiJsError(`GraphQL error: ${message}${/forbidden|unauthorized|not authorized/i.test(message) ? AUTH_HINT : ""}`, "graphql");
|
|
113
|
+
}
|
|
114
|
+
if (body.data == null) throw new WikiJsError("Wiki.js returned an empty GraphQL response", "graphql");
|
|
115
|
+
return body.data;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/client/wikijs.ts
|
|
120
|
+
function isPageNotFoundError(err) {
|
|
121
|
+
return err instanceof WikiJsError && err.kind === "graphql" && /does not exist/i.test(err.message);
|
|
122
|
+
}
|
|
123
|
+
const SearchResultSchema = z.object({
|
|
124
|
+
id: z.string(),
|
|
125
|
+
title: z.string(),
|
|
126
|
+
description: z.string(),
|
|
127
|
+
path: z.string(),
|
|
128
|
+
locale: z.string()
|
|
129
|
+
});
|
|
130
|
+
const SearchResponseSchema = z.object({
|
|
131
|
+
results: z.array(SearchResultSchema),
|
|
132
|
+
suggestions: z.array(z.string().nullable()).transform((a) => a.filter((s) => s != null)),
|
|
133
|
+
totalHits: z.number()
|
|
134
|
+
});
|
|
135
|
+
const PageListItemSchema = z.object({
|
|
136
|
+
id: z.number(),
|
|
137
|
+
path: z.string(),
|
|
138
|
+
locale: z.string(),
|
|
139
|
+
title: z.string().nullish(),
|
|
140
|
+
description: z.string().nullish(),
|
|
141
|
+
contentType: z.string(),
|
|
142
|
+
isPublished: z.boolean(),
|
|
143
|
+
isPrivate: z.boolean(),
|
|
144
|
+
createdAt: z.string(),
|
|
145
|
+
updatedAt: z.string(),
|
|
146
|
+
tags: z.array(z.string().nullable()).nullish().transform((a) => (a ?? []).filter((t) => t != null))
|
|
147
|
+
});
|
|
148
|
+
const PageDetailSchema = z.object({
|
|
149
|
+
id: z.number(),
|
|
150
|
+
path: z.string(),
|
|
151
|
+
locale: z.string(),
|
|
152
|
+
title: z.string(),
|
|
153
|
+
description: z.string(),
|
|
154
|
+
content: z.string(),
|
|
155
|
+
contentType: z.string(),
|
|
156
|
+
editor: z.string(),
|
|
157
|
+
isPublished: z.boolean(),
|
|
158
|
+
isPrivate: z.boolean(),
|
|
159
|
+
tags: z.array(z.object({ tag: z.string() }).nullable()).transform((a) => a.filter((t) => t != null).map((t) => t.tag)),
|
|
160
|
+
createdAt: z.string(),
|
|
161
|
+
updatedAt: z.string()
|
|
162
|
+
});
|
|
163
|
+
const TreeItemSchema = z.object({
|
|
164
|
+
id: z.number(),
|
|
165
|
+
path: z.string(),
|
|
166
|
+
depth: z.number(),
|
|
167
|
+
title: z.string(),
|
|
168
|
+
isFolder: z.boolean(),
|
|
169
|
+
pageId: z.number().nullish(),
|
|
170
|
+
parent: z.number().nullish(),
|
|
171
|
+
locale: z.string()
|
|
172
|
+
});
|
|
173
|
+
const TagSchema = z.object({
|
|
174
|
+
id: z.number(),
|
|
175
|
+
tag: z.string(),
|
|
176
|
+
title: z.string().nullish()
|
|
177
|
+
});
|
|
178
|
+
const PAGE_DETAIL_FIELDS = `
|
|
179
|
+
id path locale title description content contentType editor
|
|
180
|
+
isPublished isPrivate tags { tag } createdAt updatedAt
|
|
181
|
+
`;
|
|
182
|
+
const PageRefSchema = z.object({
|
|
183
|
+
id: z.number(),
|
|
184
|
+
path: z.string(),
|
|
185
|
+
locale: z.string(),
|
|
186
|
+
title: z.string()
|
|
187
|
+
});
|
|
188
|
+
const MutationPageSchema = z.object({
|
|
189
|
+
id: z.number(),
|
|
190
|
+
path: z.string(),
|
|
191
|
+
title: z.string()
|
|
192
|
+
});
|
|
193
|
+
const MutationResponseSchema = z.object({
|
|
194
|
+
responseResult: z.object({
|
|
195
|
+
succeeded: z.boolean(),
|
|
196
|
+
errorCode: z.number(),
|
|
197
|
+
slug: z.string(),
|
|
198
|
+
message: z.string().nullish()
|
|
199
|
+
}),
|
|
200
|
+
page: MutationPageSchema.nullish()
|
|
201
|
+
});
|
|
202
|
+
const HistoryEntrySchema = z.object({
|
|
203
|
+
versionId: z.number(),
|
|
204
|
+
versionDate: z.string(),
|
|
205
|
+
authorId: z.number(),
|
|
206
|
+
authorName: z.string(),
|
|
207
|
+
actionType: z.string(),
|
|
208
|
+
valueBefore: z.string().nullish(),
|
|
209
|
+
valueAfter: z.string().nullish()
|
|
210
|
+
});
|
|
211
|
+
const PageVersionSchema = z.object({
|
|
212
|
+
versionId: z.number(),
|
|
213
|
+
versionDate: z.string(),
|
|
214
|
+
action: z.string(),
|
|
215
|
+
authorName: z.string(),
|
|
216
|
+
pageId: z.number(),
|
|
217
|
+
path: z.string(),
|
|
218
|
+
locale: z.string(),
|
|
219
|
+
title: z.string(),
|
|
220
|
+
description: z.string(),
|
|
221
|
+
content: z.string(),
|
|
222
|
+
contentType: z.string(),
|
|
223
|
+
editor: z.string(),
|
|
224
|
+
isPublished: z.boolean(),
|
|
225
|
+
tags: z.array(z.string().nullable()).transform((a) => a.filter((t) => t != null)),
|
|
226
|
+
createdAt: z.string()
|
|
227
|
+
});
|
|
228
|
+
const AssetFolderSchema = z.object({
|
|
229
|
+
id: z.number(),
|
|
230
|
+
slug: z.string(),
|
|
231
|
+
name: z.string().nullish()
|
|
232
|
+
});
|
|
233
|
+
const AssetItemSchema = z.object({
|
|
234
|
+
id: z.number(),
|
|
235
|
+
filename: z.string(),
|
|
236
|
+
ext: z.string(),
|
|
237
|
+
kind: z.string(),
|
|
238
|
+
mime: z.string(),
|
|
239
|
+
fileSize: z.number(),
|
|
240
|
+
createdAt: z.string(),
|
|
241
|
+
updatedAt: z.string()
|
|
242
|
+
});
|
|
243
|
+
var WikiJsClient = class {
|
|
244
|
+
gql;
|
|
245
|
+
constructor(gql) {
|
|
246
|
+
this.gql = gql;
|
|
247
|
+
}
|
|
248
|
+
async searchPages(args) {
|
|
249
|
+
const data = await this.gql.request(`query ($query: String!, $path: String, $locale: String) {
|
|
250
|
+
pages { search(query: $query, path: $path, locale: $locale) {
|
|
251
|
+
results { id title description path locale }
|
|
252
|
+
suggestions
|
|
253
|
+
totalHits
|
|
254
|
+
} }
|
|
255
|
+
}`, args);
|
|
256
|
+
return SearchResponseSchema.parse(data.pages.search);
|
|
257
|
+
}
|
|
258
|
+
async listPages(args) {
|
|
259
|
+
const data = await this.gql.request(`query ($limit: Int, $orderBy: PageOrderBy, $orderByDirection: PageOrderByDirection, $tags: [String!], $locale: String) {
|
|
260
|
+
pages { list(limit: $limit, orderBy: $orderBy, orderByDirection: $orderByDirection, tags: $tags, locale: $locale) {
|
|
261
|
+
id path locale title description contentType isPublished isPrivate createdAt updatedAt tags
|
|
262
|
+
} }
|
|
263
|
+
}`, args);
|
|
264
|
+
return z.array(PageListItemSchema).parse(data.pages.list);
|
|
265
|
+
}
|
|
266
|
+
async getPageById(id) {
|
|
267
|
+
try {
|
|
268
|
+
const data = await this.gql.request(`query ($id: Int!) { pages { single(id: $id) { ${PAGE_DETAIL_FIELDS} } } }`, { id });
|
|
269
|
+
return data.pages.single == null ? null : PageDetailSchema.parse(data.pages.single);
|
|
270
|
+
} catch (err) {
|
|
271
|
+
if (isPageNotFoundError(err)) return null;
|
|
272
|
+
throw err;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async getPageByPath(path, locale) {
|
|
276
|
+
try {
|
|
277
|
+
const data = await this.gql.request(`query ($path: String!, $locale: String!) { pages { singleByPath(path: $path, locale: $locale) { ${PAGE_DETAIL_FIELDS} } } }`, {
|
|
278
|
+
path,
|
|
279
|
+
locale
|
|
280
|
+
});
|
|
281
|
+
return data.pages.singleByPath == null ? null : PageDetailSchema.parse(data.pages.singleByPath);
|
|
282
|
+
} catch (err) {
|
|
283
|
+
if (isPageNotFoundError(err)) return null;
|
|
284
|
+
throw err;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async getTree(args) {
|
|
288
|
+
const requestMode = args.mode === "PAGES" ? "ALL" : args.mode;
|
|
289
|
+
const data = await this.gql.request(`query ($parent: Int, $path: String, $mode: PageTreeMode!, $locale: String!, $includeAncestors: Boolean) {
|
|
290
|
+
pages { tree(parent: $parent, path: $path, mode: $mode, locale: $locale, includeAncestors: $includeAncestors) {
|
|
291
|
+
id path depth title isFolder pageId parent locale
|
|
292
|
+
} }
|
|
293
|
+
}`, {
|
|
294
|
+
...args,
|
|
295
|
+
mode: requestMode
|
|
296
|
+
});
|
|
297
|
+
const items = z.array(TreeItemSchema.nullable()).parse(data.pages.tree ?? []).filter((t) => t != null);
|
|
298
|
+
return args.mode === "PAGES" ? items.filter((t) => !t.isFolder) : items;
|
|
299
|
+
}
|
|
300
|
+
async listTags() {
|
|
301
|
+
const data = await this.gql.request(`query { pages { tags { id tag title } } }`);
|
|
302
|
+
return z.array(TagSchema.nullable()).parse(data.pages.tags).filter((t) => t != null);
|
|
303
|
+
}
|
|
304
|
+
async searchTags(query) {
|
|
305
|
+
const data = await this.gql.request(`query ($query: String!) { pages { searchTags(query: $query) } }`, { query });
|
|
306
|
+
return z.array(z.string().nullable()).parse(data.pages.searchTags).filter((t) => t != null);
|
|
307
|
+
}
|
|
308
|
+
async createPage(input) {
|
|
309
|
+
const data = await this.gql.request(`mutation ($content: String!, $description: String!, $editor: String!, $isPublished: Boolean!,
|
|
310
|
+
$isPrivate: Boolean!, $locale: String!, $path: String!, $tags: [String]!, $title: String!) {
|
|
311
|
+
pages { create(content: $content, description: $description, editor: $editor, isPublished: $isPublished,
|
|
312
|
+
isPrivate: $isPrivate, locale: $locale, path: $path, tags: $tags, title: $title) {
|
|
313
|
+
responseResult { succeeded errorCode slug message }
|
|
314
|
+
page { id path title }
|
|
315
|
+
} }
|
|
316
|
+
}`, {
|
|
317
|
+
content: input.content,
|
|
318
|
+
description: input.description ?? "",
|
|
319
|
+
editor: input.editor ?? "markdown",
|
|
320
|
+
isPublished: input.isPublished ?? true,
|
|
321
|
+
isPrivate: input.isPrivate ?? false,
|
|
322
|
+
locale: input.locale ?? "en",
|
|
323
|
+
path: input.path,
|
|
324
|
+
tags: input.tags ?? [],
|
|
325
|
+
title: input.title
|
|
326
|
+
});
|
|
327
|
+
const res = MutationResponseSchema.parse(data.pages.create);
|
|
328
|
+
assertSucceeded(res.responseResult, `creating page at ${input.path}`);
|
|
329
|
+
if (!res.page) throw new WikiJsError("Wiki.js reported success but returned no page", "application");
|
|
330
|
+
return PageRefSchema.parse({
|
|
331
|
+
...res.page,
|
|
332
|
+
locale: input.locale ?? "en"
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
async updatePage(input) {
|
|
336
|
+
const { id, ...fields } = input;
|
|
337
|
+
const current = fields.tags == null || fields.content == null || fields.isPublished == null || fields.isPrivate == null ? await this.getPageById(id) : null;
|
|
338
|
+
const tags = fields.tags ?? current?.tags ?? [];
|
|
339
|
+
const content = fields.content ?? current?.content;
|
|
340
|
+
const isPublished = fields.isPublished ?? current?.isPublished;
|
|
341
|
+
const isPrivate = fields.isPrivate ?? current?.isPrivate;
|
|
342
|
+
const data = await this.gql.request(`mutation ($id: Int!, $content: String, $description: String, $isPrivate: Boolean, $isPublished: Boolean,
|
|
343
|
+
$locale: String, $path: String, $tags: [String]!, $title: String) {
|
|
344
|
+
pages { update(id: $id, content: $content, description: $description, isPrivate: $isPrivate,
|
|
345
|
+
isPublished: $isPublished, locale: $locale, path: $path, tags: $tags, title: $title) {
|
|
346
|
+
responseResult { succeeded errorCode slug message }
|
|
347
|
+
page { id path title }
|
|
348
|
+
} }
|
|
349
|
+
}`, {
|
|
350
|
+
id,
|
|
351
|
+
...fields,
|
|
352
|
+
tags,
|
|
353
|
+
content,
|
|
354
|
+
isPublished,
|
|
355
|
+
isPrivate
|
|
356
|
+
});
|
|
357
|
+
const res = MutationResponseSchema.parse(data.pages.update);
|
|
358
|
+
assertSucceeded(res.responseResult, `updating page ${id}`);
|
|
359
|
+
if (!res.page) throw new WikiJsError("Wiki.js reported success but returned no page", "application");
|
|
360
|
+
const updated = await this.getPageById(id);
|
|
361
|
+
if (!updated) throw new WikiJsError(`Page ${id} vanished immediately after a successful update`, "application");
|
|
362
|
+
return PageRefSchema.parse({
|
|
363
|
+
id: updated.id,
|
|
364
|
+
path: updated.path,
|
|
365
|
+
locale: updated.locale,
|
|
366
|
+
title: updated.title
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
async movePage(id, destinationPath, destinationLocale) {
|
|
370
|
+
const data = await this.gql.request(`mutation ($id: Int!, $destinationPath: String!, $destinationLocale: String!) {
|
|
371
|
+
pages { move(id: $id, destinationPath: $destinationPath, destinationLocale: $destinationLocale) {
|
|
372
|
+
responseResult { succeeded errorCode slug message }
|
|
373
|
+
} }
|
|
374
|
+
}`, {
|
|
375
|
+
id,
|
|
376
|
+
destinationPath,
|
|
377
|
+
destinationLocale
|
|
378
|
+
});
|
|
379
|
+
assertSucceeded(z.object({ responseResult: MutationResponseSchema.shape.responseResult }).parse(data.pages.move).responseResult, `moving page ${id} to ${destinationLocale}/${destinationPath}`);
|
|
380
|
+
}
|
|
381
|
+
async deletePage(id) {
|
|
382
|
+
const data = await this.gql.request(`mutation ($id: Int!) {
|
|
383
|
+
pages { delete(id: $id) { responseResult { succeeded errorCode slug message } } }
|
|
384
|
+
}`, { id });
|
|
385
|
+
assertSucceeded(z.object({ responseResult: MutationResponseSchema.shape.responseResult }).parse(data.pages.delete).responseResult, `deleting page ${id}`);
|
|
386
|
+
}
|
|
387
|
+
async getHistory(id, offsetPage, offsetSize) {
|
|
388
|
+
const data = await this.gql.request(`query ($id: Int!, $offsetPage: Int, $offsetSize: Int) {
|
|
389
|
+
pages { history(id: $id, offsetPage: $offsetPage, offsetSize: $offsetSize) {
|
|
390
|
+
trail { versionId versionDate authorId authorName actionType valueBefore valueAfter }
|
|
391
|
+
total
|
|
392
|
+
} }
|
|
393
|
+
}`, {
|
|
394
|
+
id,
|
|
395
|
+
offsetPage,
|
|
396
|
+
offsetSize
|
|
397
|
+
});
|
|
398
|
+
return z.object({
|
|
399
|
+
trail: z.array(HistoryEntrySchema.nullable()).nullish().transform((a) => (a ?? []).filter((e) => e != null)),
|
|
400
|
+
total: z.number()
|
|
401
|
+
}).parse(data.pages.history);
|
|
402
|
+
}
|
|
403
|
+
async getVersion(pageId, versionId) {
|
|
404
|
+
const data = await this.gql.request(`query ($pageId: Int!, $versionId: Int!) {
|
|
405
|
+
pages { version(pageId: $pageId, versionId: $versionId) {
|
|
406
|
+
versionId versionDate action authorName pageId path locale title description
|
|
407
|
+
content contentType editor isPublished tags createdAt
|
|
408
|
+
} }
|
|
409
|
+
}`, {
|
|
410
|
+
pageId,
|
|
411
|
+
versionId
|
|
412
|
+
});
|
|
413
|
+
if (data.pages.version == null) throw new WikiJsError(`Version ${versionId} of page ${pageId} not found`, "application");
|
|
414
|
+
return PageVersionSchema.parse(data.pages.version);
|
|
415
|
+
}
|
|
416
|
+
async restoreVersion(pageId, versionId) {
|
|
417
|
+
const data = await this.gql.request(`mutation ($pageId: Int!, $versionId: Int!) {
|
|
418
|
+
pages { restore(pageId: $pageId, versionId: $versionId) {
|
|
419
|
+
responseResult { succeeded errorCode slug message }
|
|
420
|
+
} }
|
|
421
|
+
}`, {
|
|
422
|
+
pageId,
|
|
423
|
+
versionId
|
|
424
|
+
});
|
|
425
|
+
assertSucceeded(z.object({ responseResult: MutationResponseSchema.shape.responseResult }).parse(data.pages.restore).responseResult, `restoring page ${pageId} to version ${versionId}`);
|
|
426
|
+
}
|
|
427
|
+
async listAssetFolders(parentFolderId) {
|
|
428
|
+
const data = await this.gql.request(`query ($parentFolderId: Int!) { assets { folders(parentFolderId: $parentFolderId) { id slug name } } }`, { parentFolderId });
|
|
429
|
+
return z.array(AssetFolderSchema.nullable()).parse(data.assets.folders ?? []).filter((f) => f != null);
|
|
430
|
+
}
|
|
431
|
+
async listAssets(folderId, kind) {
|
|
432
|
+
const data = await this.gql.request(`query ($folderId: Int!, $kind: AssetKind!) {
|
|
433
|
+
assets { list(folderId: $folderId, kind: $kind) {
|
|
434
|
+
id filename ext kind mime fileSize createdAt updatedAt
|
|
435
|
+
} }
|
|
436
|
+
}`, {
|
|
437
|
+
folderId,
|
|
438
|
+
kind
|
|
439
|
+
});
|
|
440
|
+
return z.array(AssetItemSchema.nullable()).parse(data.assets.list ?? []).filter((a) => a != null);
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
//#endregion
|
|
444
|
+
//#region src/tools/result.ts
|
|
445
|
+
/**
|
|
446
|
+
* Wraps a tool handler to log its name and duration (ms) at debug level on
|
|
447
|
+
* every call. Never logs arguments or tokens — timing/name only.
|
|
448
|
+
*/
|
|
449
|
+
function instrument(logger, name, handler) {
|
|
450
|
+
return async (...args) => {
|
|
451
|
+
const start = performance.now();
|
|
452
|
+
try {
|
|
453
|
+
return await handler(...args);
|
|
454
|
+
} finally {
|
|
455
|
+
const durationMs = performance.now() - start;
|
|
456
|
+
logger.debug({
|
|
457
|
+
tool: name,
|
|
458
|
+
durationMs
|
|
459
|
+
}, "tool call");
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
function ok(structured, summary) {
|
|
464
|
+
const json = JSON.stringify(structured);
|
|
465
|
+
return {
|
|
466
|
+
content: [{
|
|
467
|
+
type: "text",
|
|
468
|
+
text: summary ? `${summary}\n\n${json}` : json
|
|
469
|
+
}],
|
|
470
|
+
structuredContent: structured
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
function fail(err) {
|
|
474
|
+
return {
|
|
475
|
+
content: [{
|
|
476
|
+
type: "text",
|
|
477
|
+
text: err instanceof WikiJsError || err instanceof Error ? err.message : String(err)
|
|
478
|
+
}],
|
|
479
|
+
isError: true
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Slices text content by 0-based, inclusive line range. Negative bounds
|
|
484
|
+
* count from the end (Python-style: -1 = last line). Bounds are clamped to
|
|
485
|
+
* the available lines, and an empty result is returned if start > end.
|
|
486
|
+
*/
|
|
487
|
+
function sliceLines(content, startLine, endLine) {
|
|
488
|
+
const lines = content === "" ? [] : content.split("\n");
|
|
489
|
+
const totalLines = lines.length;
|
|
490
|
+
if (totalLines === 0) return {
|
|
491
|
+
content: "",
|
|
492
|
+
totalLines: 0,
|
|
493
|
+
startLine: 0,
|
|
494
|
+
endLine: 0
|
|
495
|
+
};
|
|
496
|
+
const resolveIndex = (n) => n < 0 ? totalLines + n : n;
|
|
497
|
+
const clamp = (n) => Math.min(Math.max(n, 0), totalLines - 1);
|
|
498
|
+
const resolvedStart = clamp(resolveIndex(startLine));
|
|
499
|
+
const resolvedEnd = clamp(resolveIndex(endLine));
|
|
500
|
+
return {
|
|
501
|
+
content: resolvedStart > resolvedEnd ? "" : lines.slice(resolvedStart, resolvedEnd + 1).join("\n"),
|
|
502
|
+
totalLines,
|
|
503
|
+
startLine: resolvedStart,
|
|
504
|
+
endLine: resolvedEnd
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
//#endregion
|
|
508
|
+
//#region src/tools/discovery.ts
|
|
509
|
+
const pageSummaryShape = {
|
|
510
|
+
id: z.number(),
|
|
511
|
+
path: z.string(),
|
|
512
|
+
locale: z.string(),
|
|
513
|
+
title: z.string().nullable(),
|
|
514
|
+
description: z.string().nullable(),
|
|
515
|
+
isPublished: z.boolean(),
|
|
516
|
+
isPrivate: z.boolean(),
|
|
517
|
+
tags: z.array(z.string()),
|
|
518
|
+
createdAt: z.string(),
|
|
519
|
+
updatedAt: z.string()
|
|
520
|
+
};
|
|
521
|
+
function registerDiscoveryTools(server, wiki, logger) {
|
|
522
|
+
server.registerTool("search_pages", {
|
|
523
|
+
title: "Search pages",
|
|
524
|
+
description: "Full-text search Wiki.js pages. Returns matching pages with id, title, path and locale. Use get_page to fetch content.",
|
|
525
|
+
inputSchema: {
|
|
526
|
+
query: z.string().min(1).describe("Search keywords"),
|
|
527
|
+
path: z.string().optional().describe("Restrict results to this path prefix"),
|
|
528
|
+
locale: z.string().optional().describe("Locale code, e.g. \"en\"")
|
|
529
|
+
},
|
|
530
|
+
outputSchema: {
|
|
531
|
+
results: z.array(z.object({
|
|
532
|
+
id: z.string(),
|
|
533
|
+
title: z.string(),
|
|
534
|
+
description: z.string(),
|
|
535
|
+
path: z.string(),
|
|
536
|
+
locale: z.string()
|
|
537
|
+
})),
|
|
538
|
+
suggestions: z.array(z.string()),
|
|
539
|
+
totalHits: z.number()
|
|
540
|
+
}
|
|
541
|
+
}, instrument(logger, "search_pages", async (args) => {
|
|
542
|
+
try {
|
|
543
|
+
return ok(await wiki.searchPages(args));
|
|
544
|
+
} catch (err) {
|
|
545
|
+
return fail(err);
|
|
546
|
+
}
|
|
547
|
+
}));
|
|
548
|
+
server.registerTool("list_pages", {
|
|
549
|
+
title: "List pages",
|
|
550
|
+
description: "List Wiki.js pages with optional ordering, tag and locale filters.",
|
|
551
|
+
inputSchema: {
|
|
552
|
+
limit: z.number().int().min(1).max(500).default(50).describe("Max pages to return"),
|
|
553
|
+
orderBy: z.enum([
|
|
554
|
+
"CREATED",
|
|
555
|
+
"ID",
|
|
556
|
+
"PATH",
|
|
557
|
+
"TITLE",
|
|
558
|
+
"UPDATED"
|
|
559
|
+
]).optional(),
|
|
560
|
+
orderByDirection: z.enum(["ASC", "DESC"]).optional(),
|
|
561
|
+
tags: z.array(z.string()).optional().describe("Only pages having all these tags"),
|
|
562
|
+
locale: z.string().optional()
|
|
563
|
+
},
|
|
564
|
+
outputSchema: {
|
|
565
|
+
pages: z.array(z.object({
|
|
566
|
+
...pageSummaryShape,
|
|
567
|
+
contentType: z.string()
|
|
568
|
+
})),
|
|
569
|
+
total: z.number()
|
|
570
|
+
}
|
|
571
|
+
}, instrument(logger, "list_pages", async (args) => {
|
|
572
|
+
try {
|
|
573
|
+
const pages = await wiki.listPages(args);
|
|
574
|
+
return ok({
|
|
575
|
+
pages: pages.map((p) => ({
|
|
576
|
+
...p,
|
|
577
|
+
title: p.title ?? null,
|
|
578
|
+
description: p.description ?? null
|
|
579
|
+
})),
|
|
580
|
+
total: pages.length
|
|
581
|
+
});
|
|
582
|
+
} catch (err) {
|
|
583
|
+
return fail(err);
|
|
584
|
+
}
|
|
585
|
+
}));
|
|
586
|
+
server.registerTool("get_page", {
|
|
587
|
+
title: "Get page",
|
|
588
|
+
description: "Fetch one page by numeric id OR by path (+ locale, default \"en\"). Optionally slice its content by line range with startLine/endLine: 0-based, inclusive on both ends. Default (0..-1) returns the whole page. Negative values count from the end, Python-style (-1 = last line, -2 = second-to-last).",
|
|
589
|
+
inputSchema: {
|
|
590
|
+
id: z.number().int().optional().describe("Page id (preferred when known)"),
|
|
591
|
+
path: z.string().optional().describe("Page path, e.g. \"guides/install\""),
|
|
592
|
+
locale: z.string().default("en").describe("Locale for path lookup"),
|
|
593
|
+
startLine: z.number().int().default(0).describe("0-based start line, inclusive. Negative counts from the end."),
|
|
594
|
+
endLine: z.number().int().default(-1).describe("0-based end line, inclusive. -1 (default) means the last line.")
|
|
595
|
+
},
|
|
596
|
+
outputSchema: {
|
|
597
|
+
page: z.object({
|
|
598
|
+
id: z.number(),
|
|
599
|
+
path: z.string(),
|
|
600
|
+
locale: z.string(),
|
|
601
|
+
title: z.string(),
|
|
602
|
+
description: z.string(),
|
|
603
|
+
content: z.string(),
|
|
604
|
+
contentType: z.string(),
|
|
605
|
+
editor: z.string(),
|
|
606
|
+
isPublished: z.boolean(),
|
|
607
|
+
isPrivate: z.boolean(),
|
|
608
|
+
tags: z.array(z.string()),
|
|
609
|
+
createdAt: z.string(),
|
|
610
|
+
updatedAt: z.string()
|
|
611
|
+
}),
|
|
612
|
+
totalLines: z.number(),
|
|
613
|
+
startLine: z.number(),
|
|
614
|
+
endLine: z.number()
|
|
615
|
+
}
|
|
616
|
+
}, instrument(logger, "get_page", async (args) => {
|
|
617
|
+
try {
|
|
618
|
+
if (args.id == null && !args.path) return fail(/* @__PURE__ */ new Error("Provide either \"id\" or \"path\""));
|
|
619
|
+
const page = args.id != null ? await wiki.getPageById(args.id) : await wiki.getPageByPath(args.path, args.locale);
|
|
620
|
+
if (!page) return fail(/* @__PURE__ */ new Error(`Page not found: ${args.id != null ? `id ${args.id}` : `${args.locale}/${args.path}`}`));
|
|
621
|
+
const sliced = sliceLines(page.content, args.startLine, args.endLine);
|
|
622
|
+
return ok({
|
|
623
|
+
page: {
|
|
624
|
+
...page,
|
|
625
|
+
content: sliced.content
|
|
626
|
+
},
|
|
627
|
+
totalLines: sliced.totalLines,
|
|
628
|
+
startLine: sliced.startLine,
|
|
629
|
+
endLine: sliced.endLine
|
|
630
|
+
});
|
|
631
|
+
} catch (err) {
|
|
632
|
+
return fail(err);
|
|
633
|
+
}
|
|
634
|
+
}));
|
|
635
|
+
server.registerTool("get_page_tree", {
|
|
636
|
+
title: "Browse page tree",
|
|
637
|
+
description: "Browse the page/folder hierarchy one level at a time. Omit parent for the root level; pass a folder item id as parent to descend.",
|
|
638
|
+
inputSchema: {
|
|
639
|
+
parent: z.number().int().optional().describe("Tree item id to list children of (omit for root)"),
|
|
640
|
+
mode: z.enum([
|
|
641
|
+
"ALL",
|
|
642
|
+
"PAGES",
|
|
643
|
+
"FOLDERS"
|
|
644
|
+
]).default("ALL"),
|
|
645
|
+
locale: z.string().default("en"),
|
|
646
|
+
includeAncestors: z.boolean().optional()
|
|
647
|
+
},
|
|
648
|
+
outputSchema: { items: z.array(z.object({
|
|
649
|
+
id: z.number(),
|
|
650
|
+
path: z.string(),
|
|
651
|
+
depth: z.number(),
|
|
652
|
+
title: z.string(),
|
|
653
|
+
isFolder: z.boolean(),
|
|
654
|
+
pageId: z.number().nullable(),
|
|
655
|
+
parent: z.number().nullable(),
|
|
656
|
+
locale: z.string()
|
|
657
|
+
})) }
|
|
658
|
+
}, instrument(logger, "get_page_tree", async (args) => {
|
|
659
|
+
try {
|
|
660
|
+
return ok({ items: (await wiki.getTree({
|
|
661
|
+
...args,
|
|
662
|
+
parent: args.parent ?? 0
|
|
663
|
+
})).map((i) => ({
|
|
664
|
+
...i,
|
|
665
|
+
pageId: i.pageId ?? null,
|
|
666
|
+
parent: i.parent ?? null
|
|
667
|
+
})) });
|
|
668
|
+
} catch (err) {
|
|
669
|
+
return fail(err);
|
|
670
|
+
}
|
|
671
|
+
}));
|
|
672
|
+
server.registerTool("list_tags", {
|
|
673
|
+
title: "List tags",
|
|
674
|
+
description: "List all page tags, or search tags matching a query string.",
|
|
675
|
+
inputSchema: { query: z.string().optional().describe("If set, search tags matching this text") },
|
|
676
|
+
outputSchema: { tags: z.array(z.object({
|
|
677
|
+
tag: z.string(),
|
|
678
|
+
title: z.string().nullable()
|
|
679
|
+
})) }
|
|
680
|
+
}, instrument(logger, "list_tags", async (args) => {
|
|
681
|
+
try {
|
|
682
|
+
if (args.query) return ok({ tags: (await wiki.searchTags(args.query)).map((t) => ({
|
|
683
|
+
tag: t,
|
|
684
|
+
title: null
|
|
685
|
+
})) });
|
|
686
|
+
return ok({ tags: (await wiki.listTags()).map((t) => ({
|
|
687
|
+
tag: t.tag,
|
|
688
|
+
title: t.title ?? null
|
|
689
|
+
})) });
|
|
690
|
+
} catch (err) {
|
|
691
|
+
return fail(err);
|
|
692
|
+
}
|
|
693
|
+
}));
|
|
694
|
+
}
|
|
695
|
+
//#endregion
|
|
696
|
+
//#region src/tools/pages.ts
|
|
697
|
+
const pageRefShape = {
|
|
698
|
+
id: z.number(),
|
|
699
|
+
path: z.string(),
|
|
700
|
+
locale: z.string(),
|
|
701
|
+
title: z.string()
|
|
702
|
+
};
|
|
703
|
+
function registerPageTools(server, wiki, logger) {
|
|
704
|
+
server.registerTool("create_page", {
|
|
705
|
+
title: "Create page",
|
|
706
|
+
description: "Create a new Wiki.js page. Path must not start with the locale (use \"guides/install\", not \"en/guides/install\").",
|
|
707
|
+
inputSchema: {
|
|
708
|
+
path: z.string().min(1).describe("Page path, e.g. \"guides/install\""),
|
|
709
|
+
title: z.string().min(1),
|
|
710
|
+
content: z.string().min(1).describe("Page content (markdown by default)"),
|
|
711
|
+
description: z.string().default("").describe("Short page description"),
|
|
712
|
+
editor: z.string().default("markdown").describe("Editor type: markdown, ckeditor, code"),
|
|
713
|
+
locale: z.string().default("en"),
|
|
714
|
+
isPublished: z.boolean().default(true),
|
|
715
|
+
isPrivate: z.boolean().default(false),
|
|
716
|
+
tags: z.array(z.string()).default([])
|
|
717
|
+
},
|
|
718
|
+
outputSchema: {
|
|
719
|
+
succeeded: z.literal(true),
|
|
720
|
+
page: z.object(pageRefShape)
|
|
721
|
+
}
|
|
722
|
+
}, instrument(logger, "create_page", async (args) => {
|
|
723
|
+
try {
|
|
724
|
+
const page = await wiki.createPage(args);
|
|
725
|
+
return ok({
|
|
726
|
+
succeeded: true,
|
|
727
|
+
page
|
|
728
|
+
}, `Created page ${page.id} at ${page.locale}/${page.path}`);
|
|
729
|
+
} catch (err) {
|
|
730
|
+
return fail(err);
|
|
731
|
+
}
|
|
732
|
+
}));
|
|
733
|
+
server.registerTool("update_page", {
|
|
734
|
+
title: "Update page",
|
|
735
|
+
description: "Update an existing page by id. Only the provided fields change. Get the id from get_page, list_pages or search_pages first.",
|
|
736
|
+
inputSchema: {
|
|
737
|
+
id: z.number().int().describe("Page id"),
|
|
738
|
+
content: z.string().optional(),
|
|
739
|
+
title: z.string().optional(),
|
|
740
|
+
description: z.string().optional(),
|
|
741
|
+
isPublished: z.boolean().optional(),
|
|
742
|
+
isPrivate: z.boolean().optional(),
|
|
743
|
+
tags: z.array(z.string()).optional().describe("Replaces the whole tag list")
|
|
744
|
+
},
|
|
745
|
+
outputSchema: {
|
|
746
|
+
succeeded: z.literal(true),
|
|
747
|
+
page: z.object(pageRefShape)
|
|
748
|
+
}
|
|
749
|
+
}, instrument(logger, "update_page", async (args) => {
|
|
750
|
+
try {
|
|
751
|
+
const page = await wiki.updatePage(args);
|
|
752
|
+
return ok({
|
|
753
|
+
succeeded: true,
|
|
754
|
+
page
|
|
755
|
+
}, `Updated page ${page.id} at ${page.locale}/${page.path}`);
|
|
756
|
+
} catch (err) {
|
|
757
|
+
return fail(err);
|
|
758
|
+
}
|
|
759
|
+
}));
|
|
760
|
+
server.registerTool("move_page", {
|
|
761
|
+
title: "Move / rename page",
|
|
762
|
+
description: "Move a page to a new path (and optionally another locale). Requires the manage:pages permission on the API key.",
|
|
763
|
+
inputSchema: {
|
|
764
|
+
id: z.number().int().describe("Page id"),
|
|
765
|
+
destinationPath: z.string().min(1),
|
|
766
|
+
destinationLocale: z.string().default("en")
|
|
767
|
+
},
|
|
768
|
+
outputSchema: {
|
|
769
|
+
succeeded: z.literal(true),
|
|
770
|
+
id: z.number(),
|
|
771
|
+
destinationPath: z.string(),
|
|
772
|
+
destinationLocale: z.string()
|
|
773
|
+
}
|
|
774
|
+
}, instrument(logger, "move_page", async (args) => {
|
|
775
|
+
try {
|
|
776
|
+
await wiki.movePage(args.id, args.destinationPath, args.destinationLocale);
|
|
777
|
+
return ok({
|
|
778
|
+
succeeded: true,
|
|
779
|
+
id: args.id,
|
|
780
|
+
destinationPath: args.destinationPath,
|
|
781
|
+
destinationLocale: args.destinationLocale
|
|
782
|
+
}, `Moved page ${args.id} to ${args.destinationLocale}/${args.destinationPath}`);
|
|
783
|
+
} catch (err) {
|
|
784
|
+
return fail(err);
|
|
785
|
+
}
|
|
786
|
+
}));
|
|
787
|
+
server.registerTool("delete_page", {
|
|
788
|
+
title: "Delete page",
|
|
789
|
+
description: "Permanently delete a page by id. DESTRUCTIVE and irreversible — confirm with the user before calling this.",
|
|
790
|
+
inputSchema: { id: z.number().int().describe("Page id to delete") },
|
|
791
|
+
outputSchema: {
|
|
792
|
+
succeeded: z.literal(true),
|
|
793
|
+
id: z.number()
|
|
794
|
+
}
|
|
795
|
+
}, instrument(logger, "delete_page", async (args) => {
|
|
796
|
+
try {
|
|
797
|
+
await wiki.deletePage(args.id);
|
|
798
|
+
return ok({
|
|
799
|
+
succeeded: true,
|
|
800
|
+
id: args.id
|
|
801
|
+
}, `Deleted page ${args.id}`);
|
|
802
|
+
} catch (err) {
|
|
803
|
+
return fail(err);
|
|
804
|
+
}
|
|
805
|
+
}));
|
|
806
|
+
}
|
|
807
|
+
//#endregion
|
|
808
|
+
//#region src/tools/history.ts
|
|
809
|
+
function registerHistoryTools(server, wiki, logger) {
|
|
810
|
+
server.registerTool("get_page_history", {
|
|
811
|
+
title: "Get page history",
|
|
812
|
+
description: "List the version history trail of a page (newest first), paginated.",
|
|
813
|
+
inputSchema: {
|
|
814
|
+
id: z.number().int().describe("Page id"),
|
|
815
|
+
offsetPage: z.number().int().min(0).default(0).describe("Zero-based page of the trail"),
|
|
816
|
+
offsetSize: z.number().int().min(1).max(100).default(25).describe("Entries per page")
|
|
817
|
+
},
|
|
818
|
+
outputSchema: {
|
|
819
|
+
trail: z.array(z.object({
|
|
820
|
+
versionId: z.number(),
|
|
821
|
+
versionDate: z.string(),
|
|
822
|
+
authorId: z.number(),
|
|
823
|
+
authorName: z.string(),
|
|
824
|
+
actionType: z.string(),
|
|
825
|
+
valueBefore: z.string().nullable(),
|
|
826
|
+
valueAfter: z.string().nullable()
|
|
827
|
+
})),
|
|
828
|
+
total: z.number()
|
|
829
|
+
}
|
|
830
|
+
}, instrument(logger, "get_page_history", async (args) => {
|
|
831
|
+
try {
|
|
832
|
+
const history = await wiki.getHistory(args.id, args.offsetPage, args.offsetSize);
|
|
833
|
+
return ok({
|
|
834
|
+
trail: history.trail.map((e) => ({
|
|
835
|
+
...e,
|
|
836
|
+
valueBefore: e.valueBefore ?? null,
|
|
837
|
+
valueAfter: e.valueAfter ?? null
|
|
838
|
+
})),
|
|
839
|
+
total: history.total
|
|
840
|
+
});
|
|
841
|
+
} catch (err) {
|
|
842
|
+
return fail(err);
|
|
843
|
+
}
|
|
844
|
+
}));
|
|
845
|
+
server.registerTool("get_page_version", {
|
|
846
|
+
title: "Get page version",
|
|
847
|
+
description: "Fetch the content of a specific historical version of a page. Optionally slice its content by line range with startLine/endLine: 0-based, inclusive on both ends. Default (0..-1) returns the whole version content. Negative values count from the end, Python-style (-1 = last line, -2 = second-to-last).",
|
|
848
|
+
inputSchema: {
|
|
849
|
+
pageId: z.number().int(),
|
|
850
|
+
versionId: z.number().int().describe("From get_page_history trail"),
|
|
851
|
+
startLine: z.number().int().default(0).describe("0-based start line, inclusive. Negative counts from the end."),
|
|
852
|
+
endLine: z.number().int().default(-1).describe("0-based end line, inclusive. -1 (default) means the last line.")
|
|
853
|
+
},
|
|
854
|
+
outputSchema: {
|
|
855
|
+
version: z.object({
|
|
856
|
+
versionId: z.number(),
|
|
857
|
+
versionDate: z.string(),
|
|
858
|
+
action: z.string(),
|
|
859
|
+
authorName: z.string(),
|
|
860
|
+
pageId: z.number(),
|
|
861
|
+
path: z.string(),
|
|
862
|
+
locale: z.string(),
|
|
863
|
+
title: z.string(),
|
|
864
|
+
description: z.string(),
|
|
865
|
+
content: z.string(),
|
|
866
|
+
contentType: z.string(),
|
|
867
|
+
editor: z.string(),
|
|
868
|
+
isPublished: z.boolean(),
|
|
869
|
+
tags: z.array(z.string()),
|
|
870
|
+
createdAt: z.string()
|
|
871
|
+
}),
|
|
872
|
+
totalLines: z.number(),
|
|
873
|
+
startLine: z.number(),
|
|
874
|
+
endLine: z.number()
|
|
875
|
+
}
|
|
876
|
+
}, instrument(logger, "get_page_version", async (args) => {
|
|
877
|
+
try {
|
|
878
|
+
const version = await wiki.getVersion(args.pageId, args.versionId);
|
|
879
|
+
const sliced = sliceLines(version.content, args.startLine, args.endLine);
|
|
880
|
+
return ok({
|
|
881
|
+
version: {
|
|
882
|
+
...version,
|
|
883
|
+
content: sliced.content
|
|
884
|
+
},
|
|
885
|
+
totalLines: sliced.totalLines,
|
|
886
|
+
startLine: sliced.startLine,
|
|
887
|
+
endLine: sliced.endLine
|
|
888
|
+
});
|
|
889
|
+
} catch (err) {
|
|
890
|
+
return fail(err);
|
|
891
|
+
}
|
|
892
|
+
}));
|
|
893
|
+
server.registerTool("restore_page_version", {
|
|
894
|
+
title: "Restore page version",
|
|
895
|
+
description: "Restore a page to a previous version from its history. The current state is preserved as a new history entry.",
|
|
896
|
+
inputSchema: {
|
|
897
|
+
pageId: z.number().int(),
|
|
898
|
+
versionId: z.number().int().describe("Version to restore, from get_page_history")
|
|
899
|
+
},
|
|
900
|
+
outputSchema: {
|
|
901
|
+
succeeded: z.literal(true),
|
|
902
|
+
pageId: z.number(),
|
|
903
|
+
versionId: z.number()
|
|
904
|
+
}
|
|
905
|
+
}, instrument(logger, "restore_page_version", async (args) => {
|
|
906
|
+
try {
|
|
907
|
+
await wiki.restoreVersion(args.pageId, args.versionId);
|
|
908
|
+
return ok({
|
|
909
|
+
succeeded: true,
|
|
910
|
+
pageId: args.pageId,
|
|
911
|
+
versionId: args.versionId
|
|
912
|
+
}, `Restored page ${args.pageId} to version ${args.versionId}`);
|
|
913
|
+
} catch (err) {
|
|
914
|
+
return fail(err);
|
|
915
|
+
}
|
|
916
|
+
}));
|
|
917
|
+
}
|
|
918
|
+
//#endregion
|
|
919
|
+
//#region src/tools/assets.ts
|
|
920
|
+
function registerAssetTools(server, wiki, logger) {
|
|
921
|
+
server.registerTool("list_assets", {
|
|
922
|
+
title: "List assets",
|
|
923
|
+
description: "List uploaded assets (images/files) and subfolders in an asset folder. folderId 0 is the root folder.",
|
|
924
|
+
inputSchema: {
|
|
925
|
+
folderId: z.number().int().min(0).default(0),
|
|
926
|
+
kind: z.enum([
|
|
927
|
+
"IMAGE",
|
|
928
|
+
"BINARY",
|
|
929
|
+
"ALL"
|
|
930
|
+
]).default("ALL")
|
|
931
|
+
},
|
|
932
|
+
outputSchema: {
|
|
933
|
+
folders: z.array(z.object({
|
|
934
|
+
id: z.number(),
|
|
935
|
+
slug: z.string(),
|
|
936
|
+
name: z.string().nullable()
|
|
937
|
+
})),
|
|
938
|
+
assets: z.array(z.object({
|
|
939
|
+
id: z.number(),
|
|
940
|
+
filename: z.string(),
|
|
941
|
+
ext: z.string(),
|
|
942
|
+
kind: z.string(),
|
|
943
|
+
mime: z.string(),
|
|
944
|
+
fileSize: z.number(),
|
|
945
|
+
createdAt: z.string(),
|
|
946
|
+
updatedAt: z.string()
|
|
947
|
+
}))
|
|
948
|
+
}
|
|
949
|
+
}, instrument(logger, "list_assets", async (args) => {
|
|
950
|
+
try {
|
|
951
|
+
const [folders, assets] = await Promise.all([wiki.listAssetFolders(args.folderId), wiki.listAssets(args.folderId, args.kind)]);
|
|
952
|
+
return ok({
|
|
953
|
+
folders: folders.map((f) => ({
|
|
954
|
+
...f,
|
|
955
|
+
name: f.name ?? null
|
|
956
|
+
})),
|
|
957
|
+
assets
|
|
958
|
+
});
|
|
959
|
+
} catch (err) {
|
|
960
|
+
return fail(err);
|
|
961
|
+
}
|
|
962
|
+
}));
|
|
963
|
+
}
|
|
964
|
+
//#endregion
|
|
965
|
+
//#region src/server.ts
|
|
966
|
+
const VERSION = version;
|
|
967
|
+
function buildServer(config, logger) {
|
|
968
|
+
const wiki = new WikiJsClient(new GraphQLClient({
|
|
969
|
+
baseUrl: config.wikijsUrl,
|
|
970
|
+
token: config.wikijsToken
|
|
971
|
+
}));
|
|
972
|
+
const server = new McpServer({
|
|
973
|
+
name: "wikijs-mcp",
|
|
974
|
+
version: VERSION
|
|
975
|
+
});
|
|
976
|
+
registerDiscoveryTools(server, wiki, logger);
|
|
977
|
+
registerPageTools(server, wiki, logger);
|
|
978
|
+
registerHistoryTools(server, wiki, logger);
|
|
979
|
+
registerAssetTools(server, wiki, logger);
|
|
980
|
+
logger.debug("MCP server built");
|
|
981
|
+
return server;
|
|
982
|
+
}
|
|
983
|
+
async function probeConnection(config, logger) {
|
|
984
|
+
const wiki = new WikiJsClient(new GraphQLClient({
|
|
985
|
+
baseUrl: config.wikijsUrl,
|
|
986
|
+
token: config.wikijsToken,
|
|
987
|
+
timeoutMs: 1e4
|
|
988
|
+
}));
|
|
989
|
+
try {
|
|
990
|
+
await wiki.listPages({ limit: 1 });
|
|
991
|
+
logger.info({ url: config.wikijsUrl }, "Connected to Wiki.js");
|
|
992
|
+
} catch (err) {
|
|
993
|
+
logger.warn({
|
|
994
|
+
url: config.wikijsUrl,
|
|
995
|
+
err: err.message
|
|
996
|
+
}, "Wiki.js connectivity check failed — starting anyway");
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
//#endregion
|
|
1000
|
+
//#region src/index.ts
|
|
1001
|
+
async function main() {
|
|
1002
|
+
const config = loadConfig();
|
|
1003
|
+
const logger = createLogger(config.logLevel);
|
|
1004
|
+
await probeConnection(config, logger);
|
|
1005
|
+
if (config.transport === "http") {
|
|
1006
|
+
const { startHttpServer } = await import("./http-D7MoxUTD.js");
|
|
1007
|
+
startHttpServer(config, logger);
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
await buildServer(config, logger).connect(new StdioServerTransport());
|
|
1011
|
+
logger.info("wikijs-mcp listening on stdio");
|
|
1012
|
+
}
|
|
1013
|
+
main().catch((err) => {
|
|
1014
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
1015
|
+
process.exit(1);
|
|
1016
|
+
});
|
|
1017
|
+
//#endregion
|
|
1018
|
+
export { buildServer as t };
|
|
1019
|
+
|
|
1020
|
+
//# sourceMappingURL=index.js.map
|