@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.
- package/.turbo/turbo-build.log +12 -0
- package/README.md +121 -0
- package/dist/index.js +741 -0
- package/package.json +34 -0
- package/src/definitions.ts +166 -0
- package/src/index.ts +253 -0
- package/src/provider.ts +458 -0
- package/tsconfig.json +20 -0
package/src/provider.ts
ADDED
|
@@ -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
|
+
}
|