@templmf/temp-solf-lmf 0.0.139 → 0.0.140

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 (2) hide show
  1. package/confMCP.ts +639 -0
  2. package/package.json +1 -1
package/confMCP.ts ADDED
@@ -0,0 +1,639 @@
1
+ import axios, { AxiosInstance } 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
+ interface ConfluencePage {
9
+ id: string;
10
+ title: string;
11
+ space?: string;
12
+ version?: number;
13
+ url: string;
14
+ content: string;
15
+ }
16
+
17
+ interface PageResult {
18
+ id: string;
19
+ title: string;
20
+ url: string;
21
+ }
22
+
23
+ interface SpaceInfo {
24
+ key: string;
25
+ name: string;
26
+ }
27
+
28
+ interface SearchResult {
29
+ pageId: string;
30
+ title: string;
31
+ url: string;
32
+ }
33
+
34
+ interface SearchParams {
35
+ keywords: string[];
36
+ spaces?: string[];
37
+ contributors?: string[];
38
+ limit?: number;
39
+ }
40
+
41
+ interface HybridSearchParams {
42
+ question: string;
43
+ spaces?: string[];
44
+ contributors?: string[];
45
+ maxPages?: number;
46
+ }
47
+
48
+ interface AskConfluenceParams {
49
+ question: string;
50
+ spaces?: string[];
51
+ contributors?: string[];
52
+ maxPages?: number;
53
+ maxChunks?: number;
54
+ }
55
+
56
+ interface TreePage {
57
+ id: string;
58
+ title: string;
59
+ children?: TreePage[];
60
+ }
61
+
62
+ interface ScoredPage extends SearchResult {
63
+ score: number;
64
+ }
65
+
66
+ interface Document {
67
+ pageId: string;
68
+ title: string;
69
+ url: string;
70
+ chunk: string;
71
+ }
72
+
73
+ interface SearchResultWithScore {
74
+ title: string;
75
+ url: string;
76
+ score: number;
77
+ content: string;
78
+ }
79
+
80
+ interface ExploreResult {
81
+ page: SearchResult;
82
+ children: { id: string; title: string }[];
83
+ }
84
+
85
+ interface ChildPage {
86
+ id: string;
87
+ title: string;
88
+ }
89
+
90
+ const server = new Server({
91
+ name: "confluence-7.4.7",
92
+ version: "1.0.0"
93
+ });
94
+
95
+ const baseURL = process.env.CONFLUENCE_BASE_URL;
96
+ const authorization = process.env.CONFLUENCE_AUTHORIZATION;
97
+
98
+ if (!baseURL || !authorization) {
99
+ throw new Error(
100
+ "CONFLUENCE_BASE_URL and CONFLUENCE_AUTHORIZATION are required"
101
+ );
102
+ }
103
+
104
+ const client: AxiosInstance = axios.create({
105
+ baseURL,
106
+ timeout: 30000,
107
+ headers: {
108
+ Authorization: authorization,
109
+ Accept: "application/json"
110
+ }
111
+ });
112
+
113
+ const pageCache = new LRUCache<string, ConfluencePage>({
114
+ max: 100,
115
+ ttl: 1000 * 60 * 10
116
+ });
117
+
118
+ function stripHtml(html = ""): string {
119
+ return html
120
+ .replace(/<[^>]+>/g, " ")
121
+ .replace(/\s+/g, " ")
122
+ .trim();
123
+ }
124
+
125
+ interface BuildCqlParams {
126
+ keywords?: string[];
127
+ spaces?: string[];
128
+ contributors?: string[];
129
+ }
130
+
131
+ function buildCql({
132
+ keywords = [],
133
+ spaces = [],
134
+ contributors = []
135
+ }: BuildCqlParams): string {
136
+ const clauses: string[] = [];
137
+
138
+ if (spaces.length) {
139
+ clauses.push(
140
+ "(" +
141
+ spaces
142
+ .map((v) => `space="${v}"`)
143
+ .join(" OR ") +
144
+ ")"
145
+ );
146
+ }
147
+
148
+ if (contributors.length) {
149
+ clauses.push(
150
+ "(" +
151
+ contributors
152
+ .map((v) => `contributor="${v}"`)
153
+ .join(" OR ") +
154
+ ")"
155
+ );
156
+ }
157
+
158
+ if (keywords.length) {
159
+ clauses.push(
160
+ "(" +
161
+ keywords
162
+ .map((v) => `text~"${v}"`)
163
+ .join(" OR ") +
164
+ ")"
165
+ );
166
+ }
167
+
168
+ return clauses.join(" AND ");
169
+ }
170
+
171
+ async function getPageById(pageId: string): Promise<ConfluencePage> {
172
+ const cached = pageCache.get(pageId);
173
+
174
+ if (cached) {
175
+ return cached;
176
+ }
177
+
178
+ const { data } = await client.get(
179
+ `/rest/api/content/${pageId}`,
180
+ {
181
+ params: {
182
+ expand: "body.storage,version,space"
183
+ }
184
+ }
185
+ );
186
+
187
+ const result: ConfluencePage = {
188
+ id: data.id,
189
+ title: data.title,
190
+ space: data.space?.key,
191
+ version: data.version?.number,
192
+ url: `${baseURL}${data._links?.webui}`,
193
+ content: data.body?.storage?.value || ""
194
+ };
195
+
196
+ pageCache.set(pageId, result);
197
+
198
+ return result;
199
+ }
200
+
201
+ async function getPageByTitle(
202
+ title: string,
203
+ space?: string
204
+ ): Promise<PageResult | null> {
205
+ const params: Record<string, string> = {
206
+ title,
207
+ expand: "body.storage"
208
+ };
209
+
210
+ if (space) {
211
+ params.spaceKey = space;
212
+ }
213
+
214
+ const { data } = await client.get(
215
+ "/rest/api/content",
216
+ {
217
+ params
218
+ }
219
+ );
220
+
221
+ const page = data.results?.[0];
222
+
223
+ if (!page) {
224
+ return null;
225
+ }
226
+
227
+ return {
228
+ id: page.id,
229
+ title: page.title,
230
+ url: `${baseURL}${page._links?.webui}`
231
+ };
232
+ }
233
+
234
+ async function getSpaces(): Promise<SpaceInfo[]> {
235
+ const { data } = await client.get(
236
+ "/rest/api/space"
237
+ );
238
+
239
+ return data.results.map((v: any) => ({
240
+ key: v.key,
241
+ name: v.name
242
+ }));
243
+ }
244
+
245
+ async function searchPages({
246
+ keywords,
247
+ spaces = [],
248
+ contributors = [],
249
+ limit = 10
250
+ }: SearchParams): Promise<SearchResult[]> {
251
+ const cql = buildCql({
252
+ keywords,
253
+ spaces,
254
+ contributors
255
+ });
256
+
257
+ const { data } = await client.get(
258
+ "/rest/api/search",
259
+ {
260
+ params: {
261
+ cql,
262
+ limit
263
+ }
264
+ }
265
+ );
266
+
267
+ return data.results.map((item: any) => ({
268
+ pageId: item.content?.id,
269
+ title: item.title,
270
+ url: `${baseURL}${item.url}`
271
+ }));
272
+ }
273
+
274
+ server.registerTool(
275
+ "get_page_by_id",
276
+ {
277
+ title: "Get Page By Id",
278
+ description:
279
+ "Get Confluence page content by page id",
280
+ inputSchema: {
281
+ pageId: z.string()
282
+ }
283
+ },
284
+ async ({ pageId }: { pageId: string }) => {
285
+ return await getPageById(pageId);
286
+ }
287
+ );
288
+
289
+ server.registerTool(
290
+ "get_page_by_title",
291
+ {
292
+ title: "Get Page By Title",
293
+ description:
294
+ "Find page by title",
295
+ inputSchema: {
296
+ title: z.string(),
297
+ space: z.string().optional()
298
+ }
299
+ },
300
+ async ({ title, space }: { title: string; space?: string }) => {
301
+ return await getPageByTitle(
302
+ title,
303
+ space
304
+ );
305
+ }
306
+ );
307
+
308
+ server.registerTool(
309
+ "get_spaces",
310
+ {
311
+ title: "Get Spaces",
312
+ description:
313
+ "List all spaces"
314
+ },
315
+ async () => {
316
+ return await getSpaces();
317
+ }
318
+ );
319
+
320
+ server.registerTool(
321
+ "search_pages",
322
+ {
323
+ title: "Search Pages",
324
+ description:
325
+ "Search pages by keywords, spaces and contributors",
326
+ inputSchema: {
327
+ keywords: z
328
+ .array(z.string())
329
+ .min(1),
330
+ spaces: z
331
+ .array(z.string())
332
+ .optional(),
333
+ contributors: z
334
+ .array(z.string())
335
+ .optional(),
336
+ limit: z.number().default(10)
337
+ }
338
+ },
339
+ async (args: SearchParams) => {
340
+ return await searchPages(args);
341
+ }
342
+ );
343
+
344
+ async function getChildren(pageId: string): Promise<ChildPage[]> {
345
+ const { data } = await client.get(
346
+ `/rest/api/content/${pageId}/child/page`
347
+ );
348
+
349
+ return data.results.map((v: any) => ({
350
+ id: v.id,
351
+ title: v.title
352
+ }));
353
+ }
354
+
355
+ async function buildTree(pageId: string, depth: number): Promise<TreePage> {
356
+ const page = await getPageById(pageId);
357
+
358
+ if (depth <= 0) {
359
+ return {
360
+ id: page.id,
361
+ title: page.title
362
+ };
363
+ }
364
+
365
+ const children = await getChildren(pageId);
366
+
367
+ return {
368
+ id: page.id,
369
+ title: page.title,
370
+ children: await Promise.all(
371
+ children.map((v) =>
372
+ buildTree(v.id, depth - 1)
373
+ )
374
+ )
375
+ };
376
+ }
377
+
378
+ function chunkText(
379
+ text: string,
380
+ size = 1200,
381
+ overlap = 200
382
+ ): string[] {
383
+ const chunks: string[] = [];
384
+
385
+ let start = 0;
386
+
387
+ while (start < text.length) {
388
+ chunks.push(
389
+ text.slice(start, start + size)
390
+ );
391
+
392
+ start += size - overlap;
393
+ }
394
+
395
+ return chunks;
396
+ }
397
+
398
+ function buildQueries(question: string): string[] {
399
+ const queries = [question];
400
+
401
+ queries.push(
402
+ ...question
403
+ .split(/[ ,,、]/)
404
+ .map((v) => v.trim())
405
+ .filter(Boolean)
406
+ );
407
+
408
+ return [...new Set(queries)];
409
+ }
410
+
411
+ async function hybridSearch({
412
+ question,
413
+ spaces,
414
+ contributors,
415
+ maxPages = 10
416
+ }: HybridSearchParams): Promise<ScoredPage[]> {
417
+ const queries = buildQueries(question);
418
+
419
+ const weights = [
420
+ 1,
421
+ 0.8,
422
+ 0.6,
423
+ 0.4,
424
+ 0.2
425
+ ];
426
+
427
+ const scoreMap = new Map<string, ScoredPage>();
428
+
429
+ for (let i = 0; i < queries.length; i++) {
430
+ const pages = await searchPages({
431
+ keywords: [queries[i]],
432
+ spaces,
433
+ contributors,
434
+ limit: 20
435
+ });
436
+
437
+ for (const page of pages) {
438
+ const old =
439
+ scoreMap.get(page.pageId) || {
440
+ ...page,
441
+ score: 0
442
+ };
443
+
444
+ old.score += weights[i] || 0.1;
445
+
446
+ scoreMap.set(page.pageId, old);
447
+ }
448
+ }
449
+
450
+ return [...scoreMap.values()]
451
+ .sort((a, b) => b.score - a.score)
452
+ .slice(0, maxPages);
453
+ }
454
+
455
+ async function askConfluence({
456
+ question,
457
+ spaces,
458
+ contributors,
459
+ maxPages = 10,
460
+ maxChunks = 15
461
+ }: AskConfluenceParams): Promise<SearchResultWithScore[]> {
462
+ const pages = await hybridSearch({
463
+ question,
464
+ spaces,
465
+ contributors,
466
+ maxPages
467
+ });
468
+
469
+ const documents: Document[] = [];
470
+
471
+ for (const page of pages) {
472
+ const detail = await getPageById(
473
+ page.pageId
474
+ );
475
+
476
+ const text = htmlToText(
477
+ detail.content,
478
+ {
479
+ wordwrap: false
480
+ }
481
+ );
482
+
483
+ const chunks = chunkText(text);
484
+
485
+ chunks.forEach((chunk) => {
486
+ documents.push({
487
+ pageId: detail.id,
488
+ title: detail.title,
489
+ url: detail.url,
490
+ chunk
491
+ });
492
+ });
493
+ }
494
+
495
+ const engine = BM25();
496
+
497
+ engine.defineConfig({
498
+ fldWeights: {
499
+ chunk: 1
500
+ }
501
+ });
502
+
503
+ engine.definePrepTasks([]);
504
+
505
+ engine.defineField("chunk");
506
+
507
+ engine.defineRef("id");
508
+
509
+ engine.configure();
510
+
511
+ documents.forEach((doc, index) => {
512
+ engine.addDoc(index, {
513
+ chunk: doc.chunk
514
+ });
515
+ });
516
+
517
+ engine.consolidate();
518
+
519
+ const results = engine.search(
520
+ question,
521
+ maxChunks
522
+ );
523
+
524
+ return results.map((v: [number, number]) => {
525
+ const doc = documents[v[0]];
526
+
527
+ return {
528
+ title: doc.title,
529
+ url: doc.url,
530
+ score: v[1],
531
+ content: doc.chunk
532
+ };
533
+ });
534
+ }
535
+
536
+ async function exploreConfluence(
537
+ topic: string
538
+ ): Promise<ExploreResult[]> {
539
+ const pages = await searchPages({
540
+ keywords: [topic],
541
+ limit: 5
542
+ });
543
+
544
+ const result: ExploreResult[] = [];
545
+
546
+ for (const page of pages) {
547
+ const children = await getChildren(
548
+ page.pageId
549
+ );
550
+
551
+ result.push({
552
+ page,
553
+ children
554
+ });
555
+ }
556
+
557
+ return result;
558
+ }
559
+
560
+ server.registerTool(
561
+ "get_children",
562
+ {
563
+ title: "Get Children",
564
+ description:
565
+ "Get child pages",
566
+ inputSchema: {
567
+ pageId: z.string()
568
+ }
569
+ },
570
+ async ({ pageId }: { pageId: string }) =>
571
+ getChildren(pageId)
572
+ );
573
+
574
+ server.registerTool(
575
+ "get_page_tree",
576
+ {
577
+ title: "Get Page Tree",
578
+ description:
579
+ "Get page tree recursively",
580
+ inputSchema: {
581
+ rootPageId: z.string(),
582
+ depth: z.number().default(2)
583
+ }
584
+ },
585
+ async ({
586
+ rootPageId,
587
+ depth
588
+ }: {
589
+ rootPageId: string;
590
+ depth: number;
591
+ }) =>
592
+ buildTree(rootPageId, depth)
593
+ );
594
+
595
+ server.registerTool(
596
+ "ask_confluence",
597
+ {
598
+ title: "Ask Confluence",
599
+ description:
600
+ "Retrieve relevant contexts from Confluence",
601
+
602
+ inputSchema: {
603
+ question: z.string(),
604
+
605
+ spaces: z
606
+ .array(z.string())
607
+ .optional(),
608
+
609
+ contributors: z
610
+ .array(z.string())
611
+ .optional(),
612
+
613
+ maxPages: z
614
+ .number()
615
+ .default(10),
616
+
617
+ maxChunks: z
618
+ .number()
619
+ .default(15)
620
+ }
621
+ },
622
+ async (args: AskConfluenceParams) =>
623
+ askConfluence(args)
624
+ );
625
+
626
+ server.registerTool(
627
+ "explore_confluence",
628
+ {
629
+ title: "Explore Topic",
630
+ description:
631
+ "Explore topic related pages",
632
+
633
+ inputSchema: {
634
+ topic: z.string()
635
+ }
636
+ },
637
+ async ({ topic }: { topic: string }) =>
638
+ exploreConfluence(topic)
639
+ );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@templmf/temp-solf-lmf",
3
- "version": "0.0.139",
3
+ "version": "0.0.140",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {