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.
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "mcpServers": {
3
+ "cntx-ui": {
4
+ "command": "cntx-ui",
5
+ "args": ["mcp"],
6
+ "cwd": "{{projectDir}}"
7
+ }
8
+ }
9
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cntx-ui",
3
3
  "type": "module",
4
- "version": "2.0.3",
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"