cntx-ui 2.0.3 → 2.0.4
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/README.md +312 -1
- package/bin/cntx-ui.js +17 -4
- package/lib/mcp-server.js +387 -0
- package/lib/mcp-transport.js +97 -0
- package/mcp-config-example.json +9 -0
- package/package.json +4 -2
- package/server.js +398 -13
- package/web/dist/assets/index-DZnz-iQT.js +526 -0
- package/web/dist/assets/index-dtGilZT4.css +1 -0
- package/web/dist/index.html +6 -3
- package/web/dist/assets/index-DfyThajP.js +0 -505
- package/web/dist/assets/index-vqmctNU6.css +0 -1
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { join, relative } from 'path';
|
|
3
|
+
|
|
4
|
+
export class MCPServer {
|
|
5
|
+
constructor(cntxServer) {
|
|
6
|
+
this.cntxServer = cntxServer;
|
|
7
|
+
this.clientCapabilities = null;
|
|
8
|
+
this.serverInfo = {
|
|
9
|
+
name: 'cntx-ui',
|
|
10
|
+
version: '2.0.4'
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// JSON-RPC 2.0 message handler
|
|
15
|
+
async handleMessage(message) {
|
|
16
|
+
try {
|
|
17
|
+
const request = typeof message === 'string' ? JSON.parse(message) : message;
|
|
18
|
+
|
|
19
|
+
// Handle JSON-RPC 2.0 format
|
|
20
|
+
if (!request.jsonrpc || request.jsonrpc !== '2.0') {
|
|
21
|
+
return this.createErrorResponse(null, -32600, 'Invalid Request');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const response = await this.routeRequest(request);
|
|
25
|
+
return response;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return this.createErrorResponse(null, -32700, 'Parse error');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async routeRequest(request) {
|
|
32
|
+
const { method, params, id } = request;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
switch (method) {
|
|
36
|
+
case 'initialize':
|
|
37
|
+
return this.handleInitialize(params, id);
|
|
38
|
+
|
|
39
|
+
case 'initialized':
|
|
40
|
+
return null; // No response needed for notification
|
|
41
|
+
|
|
42
|
+
case 'resources/list':
|
|
43
|
+
return this.handleListResources(id);
|
|
44
|
+
|
|
45
|
+
case 'resources/read':
|
|
46
|
+
return this.handleReadResource(params, id);
|
|
47
|
+
|
|
48
|
+
case 'tools/list':
|
|
49
|
+
return this.handleListTools(id);
|
|
50
|
+
|
|
51
|
+
case 'tools/call':
|
|
52
|
+
return this.handleCallTool(params, id);
|
|
53
|
+
|
|
54
|
+
default:
|
|
55
|
+
return this.createErrorResponse(id, -32601, 'Method not found');
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return this.createErrorResponse(id, -32603, 'Internal error', error.message);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Initialize MCP session
|
|
63
|
+
handleInitialize(params, id) {
|
|
64
|
+
this.clientCapabilities = params?.capabilities || {};
|
|
65
|
+
|
|
66
|
+
return this.createSuccessResponse(id, {
|
|
67
|
+
protocolVersion: '2024-11-05',
|
|
68
|
+
capabilities: {
|
|
69
|
+
resources: {
|
|
70
|
+
subscribe: true,
|
|
71
|
+
listChanged: true
|
|
72
|
+
},
|
|
73
|
+
tools: {}
|
|
74
|
+
},
|
|
75
|
+
serverInfo: this.serverInfo
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// List available resources (bundles)
|
|
80
|
+
handleListResources(id) {
|
|
81
|
+
const resources = [];
|
|
82
|
+
|
|
83
|
+
this.cntxServer.bundles.forEach((bundle, name) => {
|
|
84
|
+
resources.push({
|
|
85
|
+
uri: `cntx://bundle/${name}`,
|
|
86
|
+
name: `Bundle: ${name}`,
|
|
87
|
+
description: `File bundle containing ${bundle.files.length} files`,
|
|
88
|
+
mimeType: 'application/xml'
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Add individual file resources
|
|
93
|
+
const allFiles = this.cntxServer.getAllFiles();
|
|
94
|
+
allFiles.slice(0, 100).forEach((filePath) => { // Limit to first 100 files
|
|
95
|
+
resources.push({
|
|
96
|
+
uri: `cntx://file/${filePath}`,
|
|
97
|
+
name: `File: ${filePath}`,
|
|
98
|
+
description: `Individual file: ${filePath}`,
|
|
99
|
+
mimeType: this.getMimeType(filePath)
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return this.createSuccessResponse(id, {
|
|
104
|
+
resources
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Read a specific resource
|
|
109
|
+
handleReadResource(params, id) {
|
|
110
|
+
const { uri } = params;
|
|
111
|
+
|
|
112
|
+
if (!uri || !uri.startsWith('cntx://')) {
|
|
113
|
+
return this.createErrorResponse(id, -32602, 'Invalid URI');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
if (uri.startsWith('cntx://bundle/')) {
|
|
118
|
+
const bundleName = uri.replace('cntx://bundle/', '');
|
|
119
|
+
const bundle = this.cntxServer.bundles.get(bundleName);
|
|
120
|
+
|
|
121
|
+
if (!bundle) {
|
|
122
|
+
return this.createErrorResponse(id, -32602, 'Bundle not found');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return this.createSuccessResponse(id, {
|
|
126
|
+
contents: [{
|
|
127
|
+
uri,
|
|
128
|
+
mimeType: 'application/xml',
|
|
129
|
+
text: bundle.content
|
|
130
|
+
}]
|
|
131
|
+
});
|
|
132
|
+
} else if (uri.startsWith('cntx://file/')) {
|
|
133
|
+
const filePath = uri.replace('cntx://file/', '');
|
|
134
|
+
const fullPath = join(this.cntxServer.CWD, filePath);
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const content = readFileSync(fullPath, 'utf8');
|
|
138
|
+
return this.createSuccessResponse(id, {
|
|
139
|
+
contents: [{
|
|
140
|
+
uri,
|
|
141
|
+
mimeType: this.getMimeType(filePath),
|
|
142
|
+
text: content
|
|
143
|
+
}]
|
|
144
|
+
});
|
|
145
|
+
} catch (error) {
|
|
146
|
+
return this.createErrorResponse(id, -32602, 'File not found');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return this.createErrorResponse(id, -32602, 'Invalid resource URI');
|
|
151
|
+
} catch (error) {
|
|
152
|
+
return this.createErrorResponse(id, -32603, 'Internal error reading resource');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// List available tools
|
|
157
|
+
handleListTools(id) {
|
|
158
|
+
const tools = [
|
|
159
|
+
{
|
|
160
|
+
name: 'list_bundles',
|
|
161
|
+
description: 'List all available file bundles',
|
|
162
|
+
inputSchema: {
|
|
163
|
+
type: 'object',
|
|
164
|
+
properties: {},
|
|
165
|
+
required: []
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: 'get_bundle',
|
|
170
|
+
description: 'Get the content of a specific bundle',
|
|
171
|
+
inputSchema: {
|
|
172
|
+
type: 'object',
|
|
173
|
+
properties: {
|
|
174
|
+
name: {
|
|
175
|
+
type: 'string',
|
|
176
|
+
description: 'Name of the bundle to retrieve'
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
required: ['name']
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: 'generate_bundle',
|
|
184
|
+
description: 'Regenerate a specific bundle',
|
|
185
|
+
inputSchema: {
|
|
186
|
+
type: 'object',
|
|
187
|
+
properties: {
|
|
188
|
+
name: {
|
|
189
|
+
type: 'string',
|
|
190
|
+
description: 'Name of the bundle to regenerate'
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
required: ['name']
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: 'get_file_tree',
|
|
198
|
+
description: 'Get the project file tree',
|
|
199
|
+
inputSchema: {
|
|
200
|
+
type: 'object',
|
|
201
|
+
properties: {},
|
|
202
|
+
required: []
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: 'get_project_status',
|
|
207
|
+
description: 'Get current project status and bundle information',
|
|
208
|
+
inputSchema: {
|
|
209
|
+
type: 'object',
|
|
210
|
+
properties: {},
|
|
211
|
+
required: []
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
return this.createSuccessResponse(id, { tools });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Handle tool execution
|
|
220
|
+
async handleCallTool(params, id) {
|
|
221
|
+
const { name, arguments: args = {} } = params;
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
switch (name) {
|
|
225
|
+
case 'list_bundles':
|
|
226
|
+
return this.toolListBundles(id);
|
|
227
|
+
|
|
228
|
+
case 'get_bundle':
|
|
229
|
+
return this.toolGetBundle(args, id);
|
|
230
|
+
|
|
231
|
+
case 'generate_bundle':
|
|
232
|
+
return this.toolGenerateBundle(args, id);
|
|
233
|
+
|
|
234
|
+
case 'get_file_tree':
|
|
235
|
+
return this.toolGetFileTree(id);
|
|
236
|
+
|
|
237
|
+
case 'get_project_status':
|
|
238
|
+
return this.toolGetProjectStatus(id);
|
|
239
|
+
|
|
240
|
+
default:
|
|
241
|
+
return this.createErrorResponse(id, -32602, 'Unknown tool');
|
|
242
|
+
}
|
|
243
|
+
} catch (error) {
|
|
244
|
+
return this.createErrorResponse(id, -32603, 'Tool execution failed', error.message);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Tool implementations
|
|
249
|
+
toolListBundles(id) {
|
|
250
|
+
const bundles = [];
|
|
251
|
+
this.cntxServer.bundles.forEach((bundle, name) => {
|
|
252
|
+
bundles.push({
|
|
253
|
+
name,
|
|
254
|
+
fileCount: bundle.files.length,
|
|
255
|
+
size: bundle.size,
|
|
256
|
+
lastGenerated: bundle.lastGenerated,
|
|
257
|
+
changed: bundle.changed,
|
|
258
|
+
patterns: bundle.patterns
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
return this.createSuccessResponse(id, {
|
|
263
|
+
content: [{
|
|
264
|
+
type: 'text',
|
|
265
|
+
text: `Available bundles:\n${bundles.map(b =>
|
|
266
|
+
`• ${b.name}: ${b.fileCount} files (${(b.size / 1024).toFixed(1)}KB) ${b.changed ? '[CHANGED]' : '[SYNCED]'}`
|
|
267
|
+
).join('\n')}`
|
|
268
|
+
}]
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
toolGetBundle(args, id) {
|
|
273
|
+
const { name } = args;
|
|
274
|
+
const bundle = this.cntxServer.bundles.get(name);
|
|
275
|
+
|
|
276
|
+
if (!bundle) {
|
|
277
|
+
return this.createErrorResponse(id, -32602, `Bundle '${name}' not found`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return this.createSuccessResponse(id, {
|
|
281
|
+
content: [{
|
|
282
|
+
type: 'text',
|
|
283
|
+
text: bundle.content
|
|
284
|
+
}]
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
toolGenerateBundle(args, id) {
|
|
289
|
+
const { name } = args;
|
|
290
|
+
|
|
291
|
+
if (!this.cntxServer.bundles.has(name)) {
|
|
292
|
+
return this.createErrorResponse(id, -32602, `Bundle '${name}' not found`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
this.cntxServer.generateBundle(name);
|
|
296
|
+
this.cntxServer.saveBundleStates();
|
|
297
|
+
|
|
298
|
+
const bundle = this.cntxServer.bundles.get(name);
|
|
299
|
+
|
|
300
|
+
return this.createSuccessResponse(id, {
|
|
301
|
+
content: [{
|
|
302
|
+
type: 'text',
|
|
303
|
+
text: `Bundle '${name}' regenerated successfully. Contains ${bundle.files.length} files (${(bundle.size / 1024).toFixed(1)}KB).`
|
|
304
|
+
}]
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
toolGetFileTree(id) {
|
|
309
|
+
const fileTree = this.cntxServer.getFileTree();
|
|
310
|
+
const treeText = fileTree.map(file =>
|
|
311
|
+
`${file.path} (${(file.size / 1024).toFixed(1)}KB)`
|
|
312
|
+
).join('\n');
|
|
313
|
+
|
|
314
|
+
return this.createSuccessResponse(id, {
|
|
315
|
+
content: [{
|
|
316
|
+
type: 'text',
|
|
317
|
+
text: `Project file tree:\n${treeText}`
|
|
318
|
+
}]
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
toolGetProjectStatus(id) {
|
|
323
|
+
const bundleCount = this.cntxServer.bundles.size;
|
|
324
|
+
const changedBundles = Array.from(this.cntxServer.bundles.entries())
|
|
325
|
+
.filter(([_, bundle]) => bundle.changed)
|
|
326
|
+
.map(([name, _]) => name);
|
|
327
|
+
|
|
328
|
+
const statusText = `Project Status:
|
|
329
|
+
Working Directory: ${relative(process.cwd(), this.cntxServer.CWD)}
|
|
330
|
+
Total Bundles: ${bundleCount}
|
|
331
|
+
Changed Bundles: ${changedBundles.length > 0 ? changedBundles.join(', ') : 'None'}
|
|
332
|
+
|
|
333
|
+
Bundle Details:
|
|
334
|
+
${Array.from(this.cntxServer.bundles.entries()).map(([name, bundle]) =>
|
|
335
|
+
`• ${name}: ${bundle.files.length} files, ${(bundle.size / 1024).toFixed(1)}KB ${bundle.changed ? '[CHANGED]' : '[SYNCED]'}`
|
|
336
|
+
).join('\n')}`;
|
|
337
|
+
|
|
338
|
+
return this.createSuccessResponse(id, {
|
|
339
|
+
content: [{
|
|
340
|
+
type: 'text',
|
|
341
|
+
text: statusText
|
|
342
|
+
}]
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Helper methods
|
|
347
|
+
getMimeType(filePath) {
|
|
348
|
+
const ext = filePath.split('.').pop()?.toLowerCase();
|
|
349
|
+
const mimeTypes = {
|
|
350
|
+
'js': 'application/javascript',
|
|
351
|
+
'jsx': 'application/javascript',
|
|
352
|
+
'ts': 'application/typescript',
|
|
353
|
+
'tsx': 'application/typescript',
|
|
354
|
+
'json': 'application/json',
|
|
355
|
+
'xml': 'application/xml',
|
|
356
|
+
'html': 'text/html',
|
|
357
|
+
'css': 'text/css',
|
|
358
|
+
'md': 'text/markdown',
|
|
359
|
+
'txt': 'text/plain',
|
|
360
|
+
'py': 'text/x-python',
|
|
361
|
+
'java': 'text/x-java',
|
|
362
|
+
'c': 'text/x-c',
|
|
363
|
+
'cpp': 'text/x-c++',
|
|
364
|
+
'php': 'text/x-php'
|
|
365
|
+
};
|
|
366
|
+
return mimeTypes[ext] || 'text/plain';
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
createSuccessResponse(id, result) {
|
|
370
|
+
return {
|
|
371
|
+
jsonrpc: '2.0',
|
|
372
|
+
id,
|
|
373
|
+
result
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
createErrorResponse(id, code, message, data = null) {
|
|
378
|
+
const error = { code, message };
|
|
379
|
+
if (data) error.data = data;
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
jsonrpc: '2.0',
|
|
383
|
+
id,
|
|
384
|
+
error
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { MCPServer } from './mcp-server.js';
|
|
2
|
+
|
|
3
|
+
export class MCPTransport {
|
|
4
|
+
constructor(cntxServer) {
|
|
5
|
+
this.mcpServer = new MCPServer(cntxServer);
|
|
6
|
+
this.buffer = '';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Start stdio transport
|
|
10
|
+
start() {
|
|
11
|
+
console.error('🚀 MCP server starting on stdio transport');
|
|
12
|
+
|
|
13
|
+
// Handle incoming messages from stdin
|
|
14
|
+
process.stdin.on('data', (data) => {
|
|
15
|
+
this.handleIncomingData(data.toString());
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Handle process cleanup
|
|
19
|
+
process.on('SIGINT', () => {
|
|
20
|
+
console.error('📡 MCP server shutting down');
|
|
21
|
+
process.exit(0);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
process.on('SIGTERM', () => {
|
|
25
|
+
console.error('📡 MCP server shutting down');
|
|
26
|
+
process.exit(0);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Set stdin to raw mode for proper JSON-RPC communication
|
|
30
|
+
process.stdin.setEncoding('utf8');
|
|
31
|
+
|
|
32
|
+
console.error('✅ MCP server ready for JSON-RPC messages');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Handle incoming data and parse JSON-RPC messages
|
|
36
|
+
async handleIncomingData(data) {
|
|
37
|
+
this.buffer += data;
|
|
38
|
+
|
|
39
|
+
// Split by newlines to handle multiple messages
|
|
40
|
+
const lines = this.buffer.split('\n');
|
|
41
|
+
this.buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
|
42
|
+
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
if (line.trim()) {
|
|
45
|
+
try {
|
|
46
|
+
const message = JSON.parse(line.trim());
|
|
47
|
+
await this.processMessage(message);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error('❌ Failed to parse JSON-RPC message:', error.message);
|
|
50
|
+
this.sendError(null, -32700, 'Parse error');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Process a single JSON-RPC message
|
|
57
|
+
async processMessage(message) {
|
|
58
|
+
try {
|
|
59
|
+
const response = await this.mcpServer.handleMessage(message);
|
|
60
|
+
|
|
61
|
+
// Only send response if not null (notifications don't need responses)
|
|
62
|
+
if (response !== null) {
|
|
63
|
+
this.sendMessage(response);
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error('❌ Error processing message:', error.message);
|
|
67
|
+
this.sendError(message.id || null, -32603, 'Internal error');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Send a message via stdout
|
|
72
|
+
sendMessage(message) {
|
|
73
|
+
const messageStr = JSON.stringify(message);
|
|
74
|
+
process.stdout.write(messageStr + '\n');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Send an error response
|
|
78
|
+
sendError(id, code, message, data = null) {
|
|
79
|
+
const error = { code, message };
|
|
80
|
+
if (data) error.data = data;
|
|
81
|
+
|
|
82
|
+
const response = {
|
|
83
|
+
jsonrpc: '2.0',
|
|
84
|
+
id,
|
|
85
|
+
error
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
this.sendMessage(response);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Factory function to start MCP transport
|
|
93
|
+
export function startMCPTransport(cntxServer) {
|
|
94
|
+
const transport = new MCPTransport(cntxServer);
|
|
95
|
+
transport.start();
|
|
96
|
+
return transport;
|
|
97
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cntx-ui",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.4",
|
|
5
5
|
"description": "Minimal file bundling and tagging tool for AI development",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"ai",
|
|
@@ -22,8 +22,10 @@
|
|
|
22
22
|
"files": [
|
|
23
23
|
"bin/cntx-ui.js",
|
|
24
24
|
"server.js",
|
|
25
|
+
"lib/",
|
|
25
26
|
"README.md",
|
|
26
|
-
"web/dist"
|
|
27
|
+
"web/dist",
|
|
28
|
+
"mcp-config-example.json"
|
|
27
29
|
],
|
|
28
30
|
"engines": {
|
|
29
31
|
"node": ">=18.0.0"
|