@contextos/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.
@@ -0,0 +1,458 @@
1
+ /**
2
+ * ContextOS Provider
3
+ * Interfaces with @contextos/core to provide context to MCP clients
4
+ */
5
+
6
+ import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
7
+ import { join, relative } from 'path';
8
+
9
+ // Dynamic import for @contextos/core (may not be installed in all environments)
10
+ let core: typeof import('@contextos/core') | null = null;
11
+
12
+ async function loadCore() {
13
+ if (core) return core;
14
+ try {
15
+ core = await import('@contextos/core');
16
+ return core;
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+
22
+ export class ContextOSProvider {
23
+ private projectDir: string;
24
+ private contextDir: string;
25
+ private lastContext: string = '';
26
+
27
+ constructor(projectDir?: string) {
28
+ this.projectDir = projectDir || process.cwd();
29
+ this.contextDir = join(this.projectDir, '.contextos');
30
+ }
31
+
32
+ /**
33
+ * Check if ContextOS is initialized in the current directory
34
+ */
35
+ isInitialized(): boolean {
36
+ return existsSync(this.contextDir);
37
+ }
38
+
39
+ /**
40
+ * Build optimized context for a goal
41
+ */
42
+ async buildContext(goal: string): Promise<string> {
43
+ const core = await loadCore();
44
+
45
+ if (core) {
46
+ try {
47
+ const builder = await core.getContextBuilder();
48
+ const result = await builder.build({ goal, maxTokens: 32000 });
49
+ this.lastContext = result.context;
50
+ return this.formatContext(goal, result);
51
+ } catch (error) {
52
+ // Fallback to simple context building
53
+ }
54
+ }
55
+
56
+ // Fallback: Simple context building without full core
57
+ return this.buildSimpleContext(goal);
58
+ }
59
+
60
+ /**
61
+ * Analyze codebase with RLM
62
+ */
63
+ async analyze(query: string): Promise<string> {
64
+ const core = await loadCore();
65
+
66
+ if (core) {
67
+ try {
68
+ const engine = new core.RLMEngine({
69
+ maxDepth: 3,
70
+ maxTokenBudget: 50000,
71
+ });
72
+
73
+ // Note: Requires model adapter to be configured
74
+ const result = await engine.execute(query, await this.getCurrentContext());
75
+ return result.answer;
76
+ } catch (error) {
77
+ return `Analysis requires AI API key. Error: ${error instanceof Error ? error.message : String(error)}`;
78
+ }
79
+ }
80
+
81
+ return 'Full analysis requires @contextos/core with AI API key configured.';
82
+ }
83
+
84
+ /**
85
+ * Find files matching pattern
86
+ */
87
+ async findFiles(pattern: string): Promise<string> {
88
+ const files = this.walkDirectory(this.projectDir, pattern);
89
+
90
+ if (files.length === 0) {
91
+ return `No files found matching pattern: ${pattern}`;
92
+ }
93
+
94
+ return `# Files matching "${pattern}"\n\n${files.map(f => `- ${f}`).join('\n')}`;
95
+ }
96
+
97
+ /**
98
+ * Get dependencies of a file
99
+ */
100
+ async getDependencies(file: string, depth: number = 2): Promise<string> {
101
+ const core = await loadCore();
102
+
103
+ if (core) {
104
+ try {
105
+ const fullPath = join(this.projectDir, file);
106
+ if (!existsSync(fullPath)) {
107
+ return `File not found: ${file}`;
108
+ }
109
+
110
+ const content = readFileSync(fullPath, 'utf-8');
111
+ const result = core.parseWithRegex(content, this.detectLanguage(file));
112
+
113
+ const deps = result.imports.map(i => i.source);
114
+ return `# Dependencies of ${file}\n\n${deps.map(d => `- ${d}`).join('\n') || 'No imports found'}`;
115
+ } catch (error) {
116
+ // Fallback
117
+ }
118
+ }
119
+
120
+ // Simple regex-based import extraction
121
+ return this.extractImportsSimple(file);
122
+ }
123
+
124
+ /**
125
+ * Explain a file
126
+ */
127
+ async explainFile(file: string): Promise<string> {
128
+ const fullPath = join(this.projectDir, file);
129
+
130
+ if (!existsSync(fullPath)) {
131
+ return `File not found: ${file}`;
132
+ }
133
+
134
+ const content = readFileSync(fullPath, 'utf-8');
135
+ const lines = content.split('\n').length;
136
+ const core = await loadCore();
137
+
138
+ let analysis = '';
139
+ if (core) {
140
+ try {
141
+ const result = core.parseWithRegex(content, this.detectLanguage(file));
142
+ analysis = `
143
+ ## Structure
144
+
145
+ - **Functions**: ${result.functions.join(', ') || 'None'}
146
+ - **Classes**: ${result.classes.join(', ') || 'None'}
147
+ - **Imports**: ${result.imports.length} imports
148
+ `;
149
+ } catch {
150
+ // Fallback
151
+ }
152
+ }
153
+
154
+ return `# ${file}
155
+
156
+ ## Overview
157
+
158
+ - **Lines**: ${lines}
159
+ - **Language**: ${this.detectLanguage(file)}
160
+ ${analysis}
161
+
162
+ ## Content Preview
163
+
164
+ \`\`\`${this.detectLanguage(file)}
165
+ ${content.slice(0, 2000)}${content.length > 2000 ? '\n... (truncated)' : ''}
166
+ \`\`\`
167
+ `;
168
+ }
169
+
170
+ /**
171
+ * Get current status
172
+ */
173
+ async getStatus(): Promise<string> {
174
+ const initialized = this.isInitialized();
175
+
176
+ let status = `# ContextOS Status\n\n`;
177
+ status += `- **Project Directory**: ${this.projectDir}\n`;
178
+ status += `- **Initialized**: ${initialized ? '✅ Yes' : '❌ No'}\n`;
179
+
180
+ if (initialized) {
181
+ const configPath = join(this.contextDir, 'context.yaml');
182
+ if (existsSync(configPath)) {
183
+ status += `- **Config**: context.yaml found\n`;
184
+ }
185
+ } else {
186
+ status += `\n> Run \`ctx init\` to initialize ContextOS in this project.`;
187
+ }
188
+
189
+ return status;
190
+ }
191
+
192
+ /**
193
+ * Get current context
194
+ */
195
+ async getCurrentContext(): Promise<string> {
196
+ if (this.lastContext) {
197
+ return this.lastContext;
198
+ }
199
+
200
+ // Try to load from cache
201
+ const cachePath = join(this.contextDir, 'cache', 'last-context.md');
202
+ if (existsSync(cachePath)) {
203
+ return readFileSync(cachePath, 'utf-8');
204
+ }
205
+
206
+ return 'No context built yet. Use contextos_build tool first.';
207
+ }
208
+
209
+ /**
210
+ * Get project info
211
+ */
212
+ async getProjectInfo(): Promise<string> {
213
+ const configPath = join(this.contextDir, 'context.yaml');
214
+
215
+ if (!existsSync(configPath)) {
216
+ return JSON.stringify({
217
+ error: 'ContextOS not initialized',
218
+ suggestion: 'Run ctx init',
219
+ }, null, 2);
220
+ }
221
+
222
+ const content = readFileSync(configPath, 'utf-8');
223
+
224
+ // Parse YAML (simple extraction)
225
+ const info: Record<string, string> = {};
226
+ const lines = content.split('\n');
227
+
228
+ for (const line of lines) {
229
+ const match = line.match(/^\s*(name|language|framework|description):\s*["']?([^"'\n]+)["']?/);
230
+ if (match) {
231
+ info[match[1]] = match[2].trim();
232
+ }
233
+ }
234
+
235
+ return JSON.stringify(info, null, 2);
236
+ }
237
+
238
+ /**
239
+ * Get constraints
240
+ */
241
+ async getConstraints(): Promise<string> {
242
+ const configPath = join(this.contextDir, 'context.yaml');
243
+
244
+ if (!existsSync(configPath)) {
245
+ return 'No constraints defined (ContextOS not initialized)';
246
+ }
247
+
248
+ const content = readFileSync(configPath, 'utf-8');
249
+
250
+ // Extract constraints section
251
+ const constraintsMatch = content.match(/constraints:\s*\n((?:\s+-[^\n]+\n?)+)/);
252
+
253
+ if (!constraintsMatch) {
254
+ return 'No constraints defined in context.yaml';
255
+ }
256
+
257
+ return `# Project Constraints\n\n${constraintsMatch[1]}`;
258
+ }
259
+
260
+ /**
261
+ * Get project structure
262
+ */
263
+ async getProjectStructure(): Promise<string> {
264
+ const tree = this.buildTree(this.projectDir, '', 0, 3);
265
+ return `# Project Structure\n\n\`\`\`\n${tree}\`\`\``;
266
+ }
267
+
268
+ /**
269
+ * Get file with dependencies
270
+ */
271
+ async getFileWithDeps(file: string): Promise<string> {
272
+ const content = await this.explainFile(file);
273
+ const deps = await this.getDependencies(file);
274
+ return `${content}\n\n${deps}`;
275
+ }
276
+
277
+ // ═══════════════════════════════════════════════════════════
278
+ // PRIVATE HELPERS
279
+ // ═══════════════════════════════════════════════════════════
280
+
281
+ private formatContext(goal: string, result: { context: string; files: string[]; tokens: number }): string {
282
+ return `# Context for: ${goal}
283
+
284
+ ## Statistics
285
+ - Files included: ${result.files.length}
286
+ - Total tokens: ~${result.tokens}
287
+
288
+ ## Files
289
+ ${result.files.map(f => `- ${f}`).join('\n')}
290
+
291
+ ## Content
292
+
293
+ ${result.context}
294
+ `;
295
+ }
296
+
297
+ private async buildSimpleContext(goal: string): Promise<string> {
298
+ // Simple fallback: find files related to goal keywords
299
+ const keywords = goal.toLowerCase().split(/\s+/).filter(w => w.length > 3);
300
+ const files = this.walkDirectory(this.projectDir);
301
+
302
+ const relevant = files.filter(file => {
303
+ const lower = file.toLowerCase();
304
+ return keywords.some(kw => lower.includes(kw));
305
+ }).slice(0, 10);
306
+
307
+ if (relevant.length === 0) {
308
+ return `No files found related to: ${goal}\n\nTry running \`ctx index\` first.`;
309
+ }
310
+
311
+ let context = `# Context for: ${goal}\n\n`;
312
+
313
+ for (const file of relevant) {
314
+ const fullPath = join(this.projectDir, file);
315
+ try {
316
+ const content = readFileSync(fullPath, 'utf-8');
317
+ context += `## ${file}\n\n\`\`\`\n${content.slice(0, 3000)}\n\`\`\`\n\n`;
318
+ } catch {
319
+ // Skip unreadable files
320
+ }
321
+ }
322
+
323
+ this.lastContext = context;
324
+ return context;
325
+ }
326
+
327
+ private walkDirectory(dir: string, pattern?: string, maxFiles: number = 100): string[] {
328
+ const results: string[] = [];
329
+ const ignored = ['node_modules', '.git', 'dist', 'build', '.contextos', 'coverage'];
330
+
331
+ const walk = (currentDir: string) => {
332
+ if (results.length >= maxFiles) return;
333
+
334
+ try {
335
+ const entries = readdirSync(currentDir);
336
+
337
+ for (const entry of entries) {
338
+ if (results.length >= maxFiles) break;
339
+ if (ignored.includes(entry)) continue;
340
+
341
+ const fullPath = join(currentDir, entry);
342
+ const relativePath = relative(this.projectDir, fullPath);
343
+
344
+ try {
345
+ const stat = statSync(fullPath);
346
+
347
+ if (stat.isDirectory()) {
348
+ walk(fullPath);
349
+ } else if (stat.isFile()) {
350
+ if (!pattern || this.matchPattern(relativePath, pattern)) {
351
+ results.push(relativePath);
352
+ }
353
+ }
354
+ } catch {
355
+ // Skip inaccessible
356
+ }
357
+ }
358
+ } catch {
359
+ // Skip unreadable directories
360
+ }
361
+ };
362
+
363
+ walk(dir);
364
+ return results;
365
+ }
366
+
367
+ private matchPattern(path: string, pattern: string): boolean {
368
+ // Simple glob matching
369
+ const regex = pattern
370
+ .replace(/\./g, '\\.')
371
+ .replace(/\*\*/g, '.*')
372
+ .replace(/\*/g, '[^/]*');
373
+ return new RegExp(regex).test(path);
374
+ }
375
+
376
+ private detectLanguage(file: string): string {
377
+ const ext = file.split('.').pop()?.toLowerCase() || '';
378
+ const langMap: Record<string, string> = {
379
+ ts: 'typescript',
380
+ tsx: 'typescript',
381
+ js: 'javascript',
382
+ jsx: 'javascript',
383
+ py: 'python',
384
+ go: 'go',
385
+ rs: 'rust',
386
+ java: 'java',
387
+ };
388
+ return langMap[ext] || ext;
389
+ }
390
+
391
+ private extractImportsSimple(file: string): string {
392
+ const fullPath = join(this.projectDir, file);
393
+
394
+ if (!existsSync(fullPath)) {
395
+ return `File not found: ${file}`;
396
+ }
397
+
398
+ const content = readFileSync(fullPath, 'utf-8');
399
+ const imports: string[] = [];
400
+
401
+ // Common import patterns
402
+ const patterns = [
403
+ /import\s+.*from\s+['"]([^'"]+)['"]/g,
404
+ /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
405
+ /^import\s+([\w.]+)/gm,
406
+ /^from\s+([\w.]+)\s+import/gm,
407
+ ];
408
+
409
+ for (const pattern of patterns) {
410
+ let match;
411
+ while ((match = pattern.exec(content)) !== null) {
412
+ if (match[1] && !imports.includes(match[1])) {
413
+ imports.push(match[1]);
414
+ }
415
+ }
416
+ }
417
+
418
+ return `# Dependencies of ${file}\n\n${imports.map(i => `- ${i}`).join('\n') || 'No imports found'}`;
419
+ }
420
+
421
+ private buildTree(dir: string, prefix: string, depth: number, maxDepth: number): string {
422
+ if (depth >= maxDepth) return '';
423
+
424
+ const ignored = ['node_modules', '.git', 'dist', 'build', '.contextos', 'coverage', '__pycache__'];
425
+ let result = '';
426
+
427
+ try {
428
+ const entries = readdirSync(dir).filter(e => !ignored.includes(e)).sort();
429
+
430
+ for (let i = 0; i < entries.length && i < 20; i++) {
431
+ const entry = entries[i];
432
+ const isLast = i === entries.length - 1 || i === 19;
433
+ const fullPath = join(dir, entry);
434
+
435
+ try {
436
+ const stat = statSync(fullPath);
437
+ const connector = isLast ? '└── ' : '├── ';
438
+ result += `${prefix}${connector}${entry}${stat.isDirectory() ? '/' : ''}\n`;
439
+
440
+ if (stat.isDirectory()) {
441
+ const newPrefix = prefix + (isLast ? ' ' : '│ ');
442
+ result += this.buildTree(fullPath, newPrefix, depth + 1, maxDepth);
443
+ }
444
+ } catch {
445
+ // Skip
446
+ }
447
+ }
448
+
449
+ if (entries.length > 20) {
450
+ result += `${prefix}└── ... (${entries.length - 20} more)\n`;
451
+ }
452
+ } catch {
453
+ // Skip
454
+ }
455
+
456
+ return result;
457
+ }
458
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "esModuleInterop": true,
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "outDir": "./dist",
10
+ "rootDir": "./src",
11
+ "declaration": true
12
+ },
13
+ "include": [
14
+ "src/**/*"
15
+ ],
16
+ "exclude": [
17
+ "node_modules",
18
+ "dist"
19
+ ]
20
+ }