@afterxleep/doc-bot 1.0.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/src/index.js ADDED
@@ -0,0 +1,371 @@
1
+ const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
2
+ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
3
+ const { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } = require('@modelcontextprotocol/sdk/types.js');
4
+ const { DocumentationService } = require('./services/DocumentationService.js');
5
+ const { InferenceEngine } = require('./services/InferenceEngine.js');
6
+ const { ManifestLoader } = require('./services/ManifestLoader.js');
7
+ const chokidar = require('chokidar');
8
+ const path = require('path');
9
+
10
+ class DocsServer {
11
+ constructor(options = {}) {
12
+ this.options = {
13
+ docsPath: options.docsPath || './docs.ai',
14
+ configPath: options.configPath || './docs.ai/manifest.json',
15
+ verbose: options.verbose || false,
16
+ watch: options.watch || false,
17
+ ...options
18
+ };
19
+
20
+ this.server = new Server({
21
+ name: 'doc-bot',
22
+ version: '1.0.0',
23
+ description: 'Generic MCP server for intelligent documentation access'
24
+ }, {
25
+ capabilities: {
26
+ resources: {},
27
+ tools: {}
28
+ }
29
+ });
30
+
31
+ this.manifestLoader = new ManifestLoader(this.options.configPath);
32
+ this.docService = new DocumentationService(this.options.docsPath, this.manifestLoader);
33
+ this.inferenceEngine = new InferenceEngine(this.docService, this.manifestLoader);
34
+
35
+ this.setupHandlers();
36
+
37
+ if (this.options.watch) {
38
+ this.setupWatcher();
39
+ }
40
+ }
41
+
42
+ setupHandlers() {
43
+ // List available resources
44
+ this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
45
+ const manifest = await this.manifestLoader.load();
46
+ return {
47
+ resources: [
48
+ {
49
+ uri: 'docs://search',
50
+ name: 'Search Documentation',
51
+ description: 'Search all documentation files',
52
+ mimeType: 'application/json'
53
+ },
54
+ {
55
+ uri: 'docs://global-rules',
56
+ name: 'Global Rules',
57
+ description: 'Get always-apply documentation rules',
58
+ mimeType: 'application/json'
59
+ },
60
+ {
61
+ uri: 'docs://contextual',
62
+ name: 'Contextual Documentation',
63
+ description: 'Get context-aware documentation',
64
+ mimeType: 'application/json'
65
+ },
66
+ {
67
+ uri: 'docs://manifest',
68
+ name: 'Documentation Manifest',
69
+ description: 'Project documentation configuration',
70
+ mimeType: 'application/json'
71
+ }
72
+ ]
73
+ };
74
+ });
75
+
76
+ // Read resource content
77
+ this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
78
+ const { uri } = request.params;
79
+
80
+ switch (uri) {
81
+ case 'docs://search':
82
+ const allDocs = await this.docService.getAllDocuments();
83
+ return {
84
+ contents: [{
85
+ uri,
86
+ mimeType: 'application/json',
87
+ text: JSON.stringify(allDocs, null, 2)
88
+ }]
89
+ };
90
+
91
+ case 'docs://global-rules':
92
+ const globalRules = await this.docService.getGlobalRules();
93
+ return {
94
+ contents: [{
95
+ uri,
96
+ mimeType: 'application/json',
97
+ text: JSON.stringify(globalRules, null, 2)
98
+ }]
99
+ };
100
+
101
+ case 'docs://manifest':
102
+ const manifest = await this.manifestLoader.load();
103
+ return {
104
+ contents: [{
105
+ uri,
106
+ mimeType: 'application/json',
107
+ text: JSON.stringify(manifest, null, 2)
108
+ }]
109
+ };
110
+
111
+ default:
112
+ throw new Error(`Unknown resource: ${uri}`);
113
+ }
114
+ });
115
+
116
+ // List available tools
117
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => {
118
+ return {
119
+ tools: [
120
+ {
121
+ name: 'search_documentation',
122
+ description: 'Search documentation by query',
123
+ inputSchema: {
124
+ type: 'object',
125
+ properties: {
126
+ query: {
127
+ type: 'string',
128
+ description: 'Search query'
129
+ }
130
+ },
131
+ required: ['query']
132
+ }
133
+ },
134
+ {
135
+ name: 'get_relevant_docs',
136
+ description: 'Get context-aware documentation suggestions',
137
+ inputSchema: {
138
+ type: 'object',
139
+ properties: {
140
+ context: {
141
+ type: 'object',
142
+ description: 'Context for inference (query, filePath, codeSnippet)',
143
+ properties: {
144
+ query: { type: 'string' },
145
+ filePath: { type: 'string' },
146
+ codeSnippet: { type: 'string' }
147
+ }
148
+ }
149
+ },
150
+ required: ['context']
151
+ }
152
+ },
153
+ {
154
+ name: 'get_global_rules',
155
+ description: 'Get always-apply documentation rules',
156
+ inputSchema: {
157
+ type: 'object',
158
+ properties: {}
159
+ }
160
+ },
161
+ {
162
+ name: 'get_file_docs',
163
+ description: 'Get documentation specific to a file path',
164
+ inputSchema: {
165
+ type: 'object',
166
+ properties: {
167
+ filePath: {
168
+ type: 'string',
169
+ description: 'File path to get documentation for'
170
+ }
171
+ },
172
+ required: ['filePath']
173
+ }
174
+ }
175
+ ]
176
+ };
177
+ });
178
+
179
+ // Handle tool calls
180
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
181
+ const { name, arguments: args } = request.params;
182
+
183
+ try {
184
+ switch (name) {
185
+ case 'search_documentation':
186
+ const query = args?.query;
187
+ if (!query) {
188
+ throw new Error('Query parameter is required');
189
+ }
190
+ const results = await this.docService.searchDocuments(query);
191
+ return {
192
+ content: [{
193
+ type: 'text',
194
+ text: this.formatSearchResults(results, query)
195
+ }]
196
+ };
197
+
198
+ case 'get_relevant_docs':
199
+ const context = args?.context;
200
+ if (!context) {
201
+ throw new Error('Context parameter is required');
202
+ }
203
+ const relevant = await this.inferenceEngine.getRelevantDocumentation(context);
204
+ return {
205
+ content: [{
206
+ type: 'text',
207
+ text: this.formatRelevantDocs(relevant)
208
+ }]
209
+ };
210
+
211
+ case 'get_global_rules':
212
+ const globalRules = await this.docService.getGlobalRules();
213
+ return {
214
+ content: [{
215
+ type: 'text',
216
+ text: this.formatGlobalRules(globalRules)
217
+ }]
218
+ };
219
+
220
+ case 'get_file_docs':
221
+ const filePath = args?.filePath;
222
+ if (!filePath) {
223
+ throw new Error('FilePath parameter is required');
224
+ }
225
+ const fileDocs = await this.docService.getContextualDocs(filePath);
226
+ return {
227
+ content: [{
228
+ type: 'text',
229
+ text: this.formatFileDocs(fileDocs, filePath)
230
+ }]
231
+ };
232
+
233
+ default:
234
+ throw new Error(`Unknown tool: ${name}`);
235
+ }
236
+ } catch (error) {
237
+ return {
238
+ content: [{
239
+ type: 'text',
240
+ text: `Error: ${error.message}`
241
+ }]
242
+ };
243
+ }
244
+ });
245
+ }
246
+
247
+ setupWatcher() {
248
+ const watcher = chokidar.watch([this.options.docsPath, this.options.configPath], {
249
+ ignored: /(^|[\/\\])\../, // ignore dotfiles
250
+ persistent: true
251
+ });
252
+
253
+ watcher.on('change', async (filePath) => {
254
+ if (this.options.verbose) {
255
+ console.log(`📄 Documentation updated: ${path.relative(process.cwd(), filePath)}`);
256
+ }
257
+
258
+ // Reload manifest if config changed
259
+ if (filePath === this.options.configPath) {
260
+ await this.manifestLoader.reload();
261
+ }
262
+
263
+ // Reload docs if documentation changed
264
+ if (filePath.startsWith(this.options.docsPath)) {
265
+ await this.docService.reload();
266
+ }
267
+ });
268
+ }
269
+
270
+ formatSearchResults(results, query) {
271
+ if (!results || results.length === 0) {
272
+ return `No documentation found for query: "${query}"`;
273
+ }
274
+
275
+ let output = `# Search Results for "${query}"\n\n`;
276
+ output += `Found ${results.length} relevant document(s):\n\n`;
277
+
278
+ results.forEach((doc, index) => {
279
+ output += `## ${index + 1}. ${doc.metadata?.title || doc.fileName}\n`;
280
+ output += `**File:** ${doc.fileName}\n`;
281
+ if (doc.metadata?.description) {
282
+ output += `**Description:** ${doc.metadata.description}\n`;
283
+ }
284
+ output += `\n${doc.content}\n\n---\n\n`;
285
+ });
286
+
287
+ return output;
288
+ }
289
+
290
+ formatRelevantDocs(relevant) {
291
+ let output = '# Relevant Documentation\n\n';
292
+
293
+ if (relevant.globalRules?.length > 0) {
294
+ output += '## 🌟 Global Rules (Always Apply)\n\n';
295
+ relevant.globalRules.forEach(rule => {
296
+ output += `### ${rule.metadata?.title || rule.fileName}\n`;
297
+ output += `${rule.content}\n\n`;
298
+ });
299
+ }
300
+
301
+ if (relevant.contextualDocs?.length > 0) {
302
+ output += '## 📂 Contextual Documentation\n\n';
303
+ relevant.contextualDocs.forEach(doc => {
304
+ output += `### ${doc.metadata?.title || doc.fileName}\n`;
305
+ output += `${doc.content}\n\n`;
306
+ });
307
+ }
308
+
309
+ if (relevant.inferredDocs?.length > 0) {
310
+ output += '## 🧠 Inferred Documentation\n\n';
311
+ relevant.inferredDocs.forEach(doc => {
312
+ output += `### ${doc.metadata?.title || doc.fileName}\n`;
313
+ output += `${doc.content}\n\n`;
314
+ });
315
+ }
316
+
317
+ if (relevant.confidence !== undefined) {
318
+ output += `**Confidence:** ${relevant.confidence.toFixed(2)}\n\n`;
319
+ }
320
+
321
+ return output;
322
+ }
323
+
324
+ formatGlobalRules(globalRules) {
325
+ if (!globalRules || globalRules.length === 0) {
326
+ return 'No global rules defined.';
327
+ }
328
+
329
+ let output = '# Global Rules (Always Apply)\n\n';
330
+ output += 'These rules should be applied to all interactions:\n\n';
331
+
332
+ globalRules.forEach(rule => {
333
+ output += `## ${rule.metadata?.title || rule.fileName}\n`;
334
+ output += `${rule.content}\n\n`;
335
+ });
336
+
337
+ return output;
338
+ }
339
+
340
+ formatFileDocs(fileDocs, filePath) {
341
+ if (!fileDocs || fileDocs.length === 0) {
342
+ return `No specific documentation found for file: ${filePath}`;
343
+ }
344
+
345
+ let output = `# Documentation for ${filePath}\n\n`;
346
+
347
+ fileDocs.forEach(doc => {
348
+ output += `## ${doc.metadata?.title || doc.fileName}\n`;
349
+ output += `${doc.content}\n\n`;
350
+ });
351
+
352
+ return output;
353
+ }
354
+
355
+ async start() {
356
+ // Initialize services
357
+ await this.manifestLoader.load();
358
+ await this.docService.initialize();
359
+ await this.inferenceEngine.initialize();
360
+
361
+ // Start server
362
+ const transport = new StdioServerTransport();
363
+ await this.server.connect(transport);
364
+
365
+ if (this.options.verbose) {
366
+ console.log('🔧 Server initialized with MCP transport');
367
+ }
368
+ }
369
+ }
370
+
371
+ module.exports = { DocsServer };
@@ -0,0 +1,247 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const glob = require('glob');
4
+ const yaml = require('yaml');
5
+
6
+ class DocumentationService {
7
+ constructor(docsPath, manifestLoader = null) {
8
+ this.docsPath = docsPath;
9
+ this.manifestLoader = manifestLoader;
10
+ this.documents = new Map();
11
+ this.lastScanned = null;
12
+ }
13
+
14
+ async initialize() {
15
+ await this.loadDocuments();
16
+ }
17
+
18
+ async reload() {
19
+ this.documents.clear();
20
+ this.lastScanned = null;
21
+ await this.loadDocuments();
22
+ }
23
+
24
+ async loadDocuments() {
25
+ try {
26
+ if (!await fs.pathExists(this.docsPath)) {
27
+ console.warn(`Documentation path does not exist: ${this.docsPath}`);
28
+ return;
29
+ }
30
+
31
+ const pattern = path.join(this.docsPath, '**/*.{md,mdx,mdc}');
32
+ const files = glob.sync(pattern);
33
+
34
+ for (const filePath of files) {
35
+ await this.loadDocument(filePath);
36
+ }
37
+
38
+ this.lastScanned = new Date();
39
+ } catch (error) {
40
+ console.error('Error loading documents:', error);
41
+ }
42
+ }
43
+
44
+ async loadDocument(filePath) {
45
+ try {
46
+ const content = await fs.readFile(filePath, 'utf8');
47
+ const relativePath = path.relative(this.docsPath, filePath);
48
+
49
+ // Parse frontmatter if present
50
+ const { metadata, content: documentContent } = this.parseFrontmatter(content);
51
+
52
+ const document = {
53
+ fileName: relativePath,
54
+ filePath: filePath,
55
+ content: documentContent,
56
+ metadata: metadata || {},
57
+ lastModified: (await fs.stat(filePath)).mtime
58
+ };
59
+
60
+ this.documents.set(relativePath, document);
61
+ } catch (error) {
62
+ console.error(`Error loading document ${filePath}:`, error);
63
+ }
64
+ }
65
+
66
+ parseFrontmatter(content) {
67
+ const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
68
+ const match = content.match(frontmatterRegex);
69
+
70
+ if (match) {
71
+ try {
72
+ const metadata = yaml.parse(match[1]);
73
+ return {
74
+ metadata,
75
+ content: match[2]
76
+ };
77
+ } catch (error) {
78
+ console.warn('Failed to parse frontmatter:', error);
79
+ }
80
+ }
81
+
82
+ return {
83
+ metadata: {},
84
+ content: content
85
+ };
86
+ }
87
+
88
+ async getAllDocuments() {
89
+ return Array.from(this.documents.values());
90
+ }
91
+
92
+ async searchDocuments(query) {
93
+ if (!query || query.trim() === '') {
94
+ return [];
95
+ }
96
+
97
+ const searchTerm = query.toLowerCase();
98
+ const results = [];
99
+
100
+ for (const doc of this.documents.values()) {
101
+ const score = this.calculateRelevanceScore(doc, searchTerm);
102
+ if (score > 0) {
103
+ results.push({
104
+ ...doc,
105
+ relevanceScore: score
106
+ });
107
+ }
108
+ }
109
+
110
+ // Sort by relevance score
111
+ return results.sort((a, b) => b.relevanceScore - a.relevanceScore);
112
+ }
113
+
114
+ calculateRelevanceScore(doc, searchTerm) {
115
+ let score = 0;
116
+ const content = doc.content.toLowerCase();
117
+ const title = (doc.metadata?.title || doc.fileName).toLowerCase();
118
+
119
+ // Title matches get highest score
120
+ if (title.includes(searchTerm)) {
121
+ score += 10;
122
+ }
123
+
124
+ // Content matches
125
+ const contentMatches = (content.match(new RegExp(searchTerm, 'g')) || []).length;
126
+ score += contentMatches * 2;
127
+
128
+ // Keyword matches in metadata
129
+ if (doc.metadata?.keywords) {
130
+ const keywords = Array.isArray(doc.metadata.keywords)
131
+ ? doc.metadata.keywords
132
+ : [doc.metadata.keywords];
133
+
134
+ for (const keyword of keywords) {
135
+ if (keyword.toLowerCase().includes(searchTerm)) {
136
+ score += 5;
137
+ }
138
+ }
139
+ }
140
+
141
+ // Category/tag matches
142
+ if (doc.metadata?.category?.toLowerCase().includes(searchTerm)) {
143
+ score += 3;
144
+ }
145
+
146
+ if (doc.metadata?.tags) {
147
+ const tags = Array.isArray(doc.metadata.tags)
148
+ ? doc.metadata.tags
149
+ : [doc.metadata.tags];
150
+
151
+ for (const tag of tags) {
152
+ if (tag.toLowerCase().includes(searchTerm)) {
153
+ score += 2;
154
+ }
155
+ }
156
+ }
157
+
158
+ return score;
159
+ }
160
+
161
+ async getGlobalRules() {
162
+ if (!this.manifestLoader) {
163
+ return [];
164
+ }
165
+
166
+ const manifest = await this.manifestLoader.load();
167
+ const globalRulePaths = manifest.globalRules || [];
168
+
169
+ const globalRules = [];
170
+ for (const rulePath of globalRulePaths) {
171
+ const doc = this.documents.get(rulePath);
172
+ if (doc) {
173
+ globalRules.push(doc);
174
+ }
175
+ }
176
+
177
+ return globalRules;
178
+ }
179
+
180
+ async getContextualDocs(filePath) {
181
+ if (!this.manifestLoader) {
182
+ return [];
183
+ }
184
+
185
+ const manifest = await this.manifestLoader.load();
186
+ const contextualRules = manifest.contextualRules || {};
187
+
188
+ const matchingDocs = [];
189
+
190
+ for (const [pattern, docPaths] of Object.entries(contextualRules)) {
191
+ if (this.matchesPattern(filePath, pattern)) {
192
+ for (const docPath of docPaths) {
193
+ const doc = this.documents.get(docPath);
194
+ if (doc) {
195
+ matchingDocs.push(doc);
196
+ }
197
+ }
198
+ }
199
+ }
200
+
201
+ return matchingDocs;
202
+ }
203
+
204
+ matchesPattern(filePath, pattern) {
205
+ // Simple glob-like pattern matching
206
+ const regexPattern = pattern
207
+ .replace(/\*/g, '.*')
208
+ .replace(/\?/g, '.')
209
+ .replace(/\[([^\]]+)\]/g, '($1)');
210
+
211
+ const regex = new RegExp(regexPattern, 'i');
212
+ return regex.test(filePath);
213
+ }
214
+
215
+ getDocument(fileName) {
216
+ return this.documents.get(fileName);
217
+ }
218
+
219
+ getDocumentsByCategory(category) {
220
+ const results = [];
221
+
222
+ for (const doc of this.documents.values()) {
223
+ if (doc.metadata?.category === category) {
224
+ results.push(doc);
225
+ }
226
+ }
227
+
228
+ return results;
229
+ }
230
+
231
+ getDocumentsByTag(tag) {
232
+ const results = [];
233
+
234
+ for (const doc of this.documents.values()) {
235
+ const tags = doc.metadata?.tags || [];
236
+ const tagArray = Array.isArray(tags) ? tags : [tags];
237
+
238
+ if (tagArray.includes(tag)) {
239
+ results.push(doc);
240
+ }
241
+ }
242
+
243
+ return results;
244
+ }
245
+ }
246
+
247
+ module.exports = { DocumentationService };