@templmf/temp-solf-lmf 0.0.137 → 0.0.139

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/confMCP.js ADDED
@@ -0,0 +1,551 @@
1
+ import axios from "axios";
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { z } from "zod";
4
+ import LRUCache from "lru-cache";
5
+ import { htmlToText } from "html-to-text";
6
+ import BM25 from "wink-bm25-text-search";
7
+
8
+ const server = new Server({
9
+ name: "confluence-7.4.7",
10
+ version: "1.0.0"
11
+ });
12
+
13
+ const baseURL = process.env.CONFLUENCE_BASE_URL;
14
+ const authorization = process.env.CONFLUENCE_AUTHORIZATION;
15
+
16
+ if (!baseURL || !authorization) {
17
+ throw new Error(
18
+ "CONFLUENCE_BASE_URL and CONFLUENCE_AUTHORIZATION are required"
19
+ );
20
+ }
21
+
22
+ const client = axios.create({
23
+ baseURL,
24
+ timeout: 30000,
25
+ headers: {
26
+ Authorization: authorization,
27
+ Accept: "application/json"
28
+ }
29
+ });
30
+
31
+ const pageCache = new LRUCache({
32
+ max: 100,
33
+ ttl: 1000 * 60 * 10
34
+ });
35
+
36
+ function stripHtml(html = "") {
37
+ return html
38
+ .replace(/<[^>]+>/g, " ")
39
+ .replace(/\s+/g, " ")
40
+ .trim();
41
+ }
42
+
43
+ function buildCql({
44
+ keywords = [],
45
+ spaces = [],
46
+ contributors = []
47
+ }) {
48
+ const clauses = [];
49
+
50
+ if (spaces.length) {
51
+ clauses.push(
52
+ "(" +
53
+ spaces
54
+ .map((v) => `space="${v}"`)
55
+ .join(" OR ") +
56
+ ")"
57
+ );
58
+ }
59
+
60
+ if (contributors.length) {
61
+ clauses.push(
62
+ "(" +
63
+ contributors
64
+ .map((v) => `contributor="${v}"`)
65
+ .join(" OR ") +
66
+ ")"
67
+ );
68
+ }
69
+
70
+ if (keywords.length) {
71
+ clauses.push(
72
+ "(" +
73
+ keywords
74
+ .map((v) => `text~"${v}"`)
75
+ .join(" OR ") +
76
+ ")"
77
+ );
78
+ }
79
+
80
+ return clauses.join(" AND ");
81
+ }
82
+
83
+ async function getPageById(pageId) {
84
+ const cached = pageCache.get(pageId);
85
+
86
+ if (cached) {
87
+ return cached;
88
+ }
89
+
90
+ const { data } = await client.get(
91
+ `/rest/api/content/${pageId}`,
92
+ {
93
+ params: {
94
+ expand: "body.storage,version,space"
95
+ }
96
+ }
97
+ );
98
+
99
+ const result = {
100
+ id: data.id,
101
+ title: data.title,
102
+ space: data.space?.key,
103
+ version: data.version?.number,
104
+ url: `${baseURL}${data._links?.webui}`,
105
+ content: data.body?.storage?.value || ""
106
+ };
107
+
108
+ pageCache.set(pageId, result);
109
+
110
+ return result;
111
+ }
112
+
113
+ async function getPageByTitle(
114
+ title,
115
+ space
116
+ ) {
117
+ const params = {
118
+ title,
119
+ expand: "body.storage"
120
+ };
121
+
122
+ if (space) {
123
+ params.spaceKey = space;
124
+ }
125
+
126
+ const { data } = await client.get(
127
+ "/rest/api/content",
128
+ {
129
+ params
130
+ }
131
+ );
132
+
133
+ const page = data.results?.[0];
134
+
135
+ if (!page) {
136
+ return null;
137
+ }
138
+
139
+ return {
140
+ id: page.id,
141
+ title: page.title,
142
+ url: `${baseURL}${page._links?.webui}`
143
+ };
144
+ }
145
+
146
+ async function getSpaces() {
147
+ const { data } = await client.get(
148
+ "/rest/api/space"
149
+ );
150
+
151
+ return data.results.map((v) => ({
152
+ key: v.key,
153
+ name: v.name
154
+ }));
155
+ }
156
+
157
+ async function searchPages({
158
+ keywords,
159
+ spaces = [],
160
+ contributors = [],
161
+ limit = 10
162
+ }) {
163
+ const cql = buildCql({
164
+ keywords,
165
+ spaces,
166
+ contributors
167
+ });
168
+
169
+ const { data } = await client.get(
170
+ "/rest/api/search",
171
+ {
172
+ params: {
173
+ cql,
174
+ limit
175
+ }
176
+ }
177
+ );
178
+
179
+ return data.results.map((item) => ({
180
+ pageId: item.content?.id,
181
+ title: item.title,
182
+ url: `${baseURL}${item.url}`
183
+ }));
184
+ }
185
+
186
+ server.registerTool(
187
+ "get_page_by_id",
188
+ {
189
+ title: "Get Page By Id",
190
+ description:
191
+ "Get Confluence page content by page id",
192
+ inputSchema: {
193
+ pageId: z.string()
194
+ }
195
+ },
196
+ async ({ pageId }) => {
197
+ return await getPageById(pageId);
198
+ }
199
+ );
200
+
201
+ server.registerTool(
202
+ "get_page_by_title",
203
+ {
204
+ title: "Get Page By Title",
205
+ description:
206
+ "Find page by title",
207
+ inputSchema: {
208
+ title: z.string(),
209
+ space: z.string().optional()
210
+ }
211
+ },
212
+ async ({ title, space }) => {
213
+ return await getPageByTitle(
214
+ title,
215
+ space
216
+ );
217
+ }
218
+ );
219
+
220
+ server.registerTool(
221
+ "get_spaces",
222
+ {
223
+ title: "Get Spaces",
224
+ description:
225
+ "List all spaces"
226
+ },
227
+ async () => {
228
+ return await getSpaces();
229
+ }
230
+ );
231
+
232
+ server.registerTool(
233
+ "search_pages",
234
+ {
235
+ title: "Search Pages",
236
+ description:
237
+ "Search pages by keywords, spaces and contributors",
238
+ inputSchema: {
239
+ keywords: z
240
+ .array(z.string())
241
+ .min(1),
242
+
243
+ spaces: z
244
+ .array(z.string())
245
+ .optional(),
246
+
247
+ contributors: z
248
+ .array(z.string())
249
+ .optional(),
250
+
251
+ limit: z.number().default(10)
252
+ }
253
+ },
254
+ async (args) => {
255
+ return await searchPages(args);
256
+ }
257
+ );
258
+
259
+ async function getChildren(pageId) {
260
+ const { data } = await client.get(
261
+ `/rest/api/content/${pageId}/child/page`
262
+ );
263
+
264
+ return data.results.map((v) => ({
265
+ id: v.id,
266
+ title: v.title
267
+ }));
268
+ }
269
+
270
+ async function buildTree(pageId, depth) {
271
+ const page = await getPageById(pageId);
272
+
273
+ if (depth <= 0) {
274
+ return {
275
+ id: page.id,
276
+ title: page.title
277
+ };
278
+ }
279
+
280
+ const children = await getChildren(pageId);
281
+
282
+ return {
283
+ id: page.id,
284
+ title: page.title,
285
+ children: await Promise.all(
286
+ children.map((v) =>
287
+ buildTree(v.id, depth - 1)
288
+ )
289
+ )
290
+ };
291
+ }
292
+
293
+ function chunkText(
294
+ text,
295
+ size = 1200,
296
+ overlap = 200
297
+ ) {
298
+ const chunks = [];
299
+
300
+ let start = 0;
301
+
302
+ while (start < text.length) {
303
+ chunks.push(
304
+ text.slice(start, start + size)
305
+ );
306
+
307
+ start += size - overlap;
308
+ }
309
+
310
+ return chunks;
311
+ }
312
+
313
+ function buildQueries(question) {
314
+ const queries = [question];
315
+
316
+ queries.push(
317
+ ...question
318
+ .split(/[ ,,、]/)
319
+ .map((v) => v.trim())
320
+ .filter(Boolean)
321
+ );
322
+
323
+ return [...new Set(queries)];
324
+ }
325
+
326
+ async function hybridSearch({
327
+ question,
328
+ spaces,
329
+ contributors,
330
+ maxPages = 10
331
+ }) {
332
+ const queries = buildQueries(question);
333
+
334
+ const weights = [
335
+ 1,
336
+ 0.8,
337
+ 0.6,
338
+ 0.4,
339
+ 0.2
340
+ ];
341
+
342
+ const scoreMap = new Map();
343
+
344
+ for (let i = 0; i < queries.length; i++) {
345
+ const pages = await searchPages({
346
+ keywords: [queries[i]],
347
+ spaces,
348
+ contributors,
349
+ limit: 20
350
+ });
351
+
352
+ for (const page of pages) {
353
+ const old =
354
+ scoreMap.get(page.pageId) || {
355
+ ...page,
356
+ score: 0
357
+ };
358
+
359
+ old.score += weights[i] || 0.1;
360
+
361
+ scoreMap.set(page.pageId, old);
362
+ }
363
+ }
364
+
365
+ return [...scoreMap.values()]
366
+ .sort((a, b) => b.score - a.score)
367
+ .slice(0, maxPages);
368
+ }
369
+
370
+ async function askConfluence({
371
+ question,
372
+ spaces,
373
+ contributors,
374
+ maxPages = 10,
375
+ maxChunks = 15
376
+ }) {
377
+ const pages = await hybridSearch({
378
+ question,
379
+ spaces,
380
+ contributors,
381
+ maxPages
382
+ });
383
+
384
+ const documents = [];
385
+
386
+ for (const page of pages) {
387
+ const detail = await getPageById(
388
+ page.pageId
389
+ );
390
+
391
+ const text = htmlToText(
392
+ detail.content,
393
+ {
394
+ wordwrap: false
395
+ }
396
+ );
397
+
398
+ const chunks = chunkText(text);
399
+
400
+ chunks.forEach((chunk) => {
401
+ documents.push({
402
+ pageId: detail.id,
403
+ title: detail.title,
404
+ url: detail.url,
405
+ chunk
406
+ });
407
+ });
408
+ }
409
+
410
+ const engine = BM25();
411
+
412
+ engine.defineConfig({
413
+ fldWeights: {
414
+ chunk: 1
415
+ }
416
+ });
417
+
418
+ engine.definePrepTasks([]);
419
+
420
+ engine.defineField("chunk");
421
+
422
+ engine.defineRef("id");
423
+
424
+ engine.configure();
425
+
426
+ documents.forEach((doc, index) => {
427
+ engine.addDoc(index, {
428
+ chunk: doc.chunk
429
+ });
430
+ });
431
+
432
+ engine.consolidate();
433
+
434
+ const results = engine.search(
435
+ question,
436
+ maxChunks
437
+ );
438
+
439
+ return results.map((v) => {
440
+ const doc = documents[v[0]];
441
+
442
+ return {
443
+ title: doc.title,
444
+ url: doc.url,
445
+ score: v[1],
446
+ content: doc.chunk
447
+ };
448
+ });
449
+ }
450
+
451
+ async function exploreConfluence(
452
+ topic
453
+ ) {
454
+ const pages = await searchPages({
455
+ keywords: [topic],
456
+ limit: 5
457
+ });
458
+
459
+ const result = [];
460
+
461
+ for (const page of pages) {
462
+ const children = await getChildren(
463
+ page.pageId
464
+ );
465
+
466
+ result.push({
467
+ page,
468
+ children
469
+ });
470
+ }
471
+
472
+ return result;
473
+ }
474
+
475
+ server.registerTool(
476
+ "get_children",
477
+ {
478
+ title: "Get Children",
479
+ description:
480
+ "Get child pages",
481
+ inputSchema: {
482
+ pageId: z.string()
483
+ }
484
+ },
485
+ async ({ pageId }) =>
486
+ getChildren(pageId)
487
+ );
488
+
489
+ server.registerTool(
490
+ "get_page_tree",
491
+ {
492
+ title: "Get Page Tree",
493
+ description:
494
+ "Get page tree recursively",
495
+ inputSchema: {
496
+ rootPageId: z.string(),
497
+ depth: z.number().default(2)
498
+ }
499
+ },
500
+ async ({
501
+ rootPageId,
502
+ depth
503
+ }) =>
504
+ buildTree(rootPageId, depth)
505
+ );
506
+
507
+ server.registerTool(
508
+ "ask_confluence",
509
+ {
510
+ title: "Ask Confluence",
511
+ description:
512
+ "Retrieve relevant contexts from Confluence",
513
+
514
+ inputSchema: {
515
+ question: z.string(),
516
+
517
+ spaces: z
518
+ .array(z.string())
519
+ .optional(),
520
+
521
+ contributors: z
522
+ .array(z.string())
523
+ .optional(),
524
+
525
+ maxPages: z
526
+ .number()
527
+ .default(10),
528
+
529
+ maxChunks: z
530
+ .number()
531
+ .default(15)
532
+ }
533
+ },
534
+ async (args) =>
535
+ askConfluence(args)
536
+ );
537
+
538
+ server.registerTool(
539
+ "explore_confluence",
540
+ {
541
+ title: "Explore Topic",
542
+ description:
543
+ "Explore topic related pages",
544
+
545
+ inputSchema: {
546
+ topic: z.string()
547
+ }
548
+ },
549
+ async ({ topic }) =>
550
+ exploreConfluence(topic)
551
+ );
@@ -0,0 +1,151 @@
1
+ {
2
+ "tools": [
3
+ {
4
+ "name": "get_page_by_id",
5
+ "description": "根据页面 ID 获取 Confluence 页面内容",
6
+ "parameters": [
7
+ {
8
+ "name": "pageId",
9
+ "type": "string",
10
+ "required": true,
11
+ "description": "Confluence 页面 ID"
12
+ }
13
+ ]
14
+ },
15
+ {
16
+ "name": "get_page_by_title",
17
+ "description": "根据标题查找页面",
18
+ "parameters": [
19
+ {
20
+ "name": "title",
21
+ "type": "string",
22
+ "required": true,
23
+ "description": "要查找的页面标题"
24
+ },
25
+ {
26
+ "name": "space",
27
+ "type": "string",
28
+ "required": false,
29
+ "description": "空间标识,缩小搜索范围(如 DEV, OPS)"
30
+ }
31
+ ]
32
+ },
33
+ {
34
+ "name": "get_spaces",
35
+ "description": "列出所有空间",
36
+ "parameters": []
37
+ },
38
+ {
39
+ "name": "search_pages",
40
+ "description": "按关键词、空间和贡献者搜索页面",
41
+ "parameters": [
42
+ {
43
+ "name": "keywords",
44
+ "type": "array<string>",
45
+ "required": true,
46
+ "description": "要在页面内容中搜索的关键词"
47
+ },
48
+ {
49
+ "name": "spaces",
50
+ "type": "array<string>",
51
+ "required": false,
52
+ "description": "按空间标识过滤"
53
+ },
54
+ {
55
+ "name": "contributors",
56
+ "type": "array<string>",
57
+ "required": false,
58
+ "description": "按贡献者用户名过滤"
59
+ },
60
+ {
61
+ "name": "limit",
62
+ "type": "number",
63
+ "required": false,
64
+ "default": 10,
65
+ "description": "最大返回结果数"
66
+ }
67
+ ]
68
+ },
69
+ {
70
+ "name": "get_children",
71
+ "description": "获取指定页面的子页面",
72
+ "parameters": [
73
+ {
74
+ "name": "pageId",
75
+ "type": "string",
76
+ "required": true,
77
+ "description": "父页面 ID"
78
+ }
79
+ ]
80
+ },
81
+ {
82
+ "name": "get_page_tree",
83
+ "description": "递归获取页面树",
84
+ "parameters": [
85
+ {
86
+ "name": "rootPageId",
87
+ "type": "string",
88
+ "required": true,
89
+ "description": "根页面 ID"
90
+ },
91
+ {
92
+ "name": "depth",
93
+ "type": "number",
94
+ "required": false,
95
+ "default": 2,
96
+ "description": "递归深度"
97
+ }
98
+ ]
99
+ },
100
+ {
101
+ "name": "ask_confluence",
102
+ "description": "使用混合搜索 + BM25 重排序检索 Confluence 相关内容",
103
+ "parameters": [
104
+ {
105
+ "name": "question",
106
+ "type": "string",
107
+ "required": true,
108
+ "description": "自然语言问题,用于搜索 Confluence"
109
+ },
110
+ {
111
+ "name": "spaces",
112
+ "type": "array<string>",
113
+ "required": false,
114
+ "description": "按空间标识过滤"
115
+ },
116
+ {
117
+ "name": "contributors",
118
+ "type": "array<string>",
119
+ "required": false,
120
+ "description": "按贡献者用户名过滤"
121
+ },
122
+ {
123
+ "name": "maxPages",
124
+ "type": "number",
125
+ "required": false,
126
+ "default": 10,
127
+ "description": "在分块前最多获取的页面数"
128
+ },
129
+ {
130
+ "name": "maxChunks",
131
+ "type": "number",
132
+ "required": false,
133
+ "default": 15,
134
+ "description": "BM25 重排序后返回的最大文本块数"
135
+ }
136
+ ]
137
+ },
138
+ {
139
+ "name": "explore_confluence",
140
+ "description": "探索与主题相关的页面及其子页面",
141
+ "parameters": [
142
+ {
143
+ "name": "topic",
144
+ "type": "string",
145
+ "required": true,
146
+ "description": "要探索的主题关键词"
147
+ }
148
+ ]
149
+ }
150
+ ]
151
+ }