retrocode-mcp 1.3.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/mcp-server.js +572 -0
- package/package.json +18 -0
package/mcp-server.js
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// RETROCODE_MCP_VERSION=1.3.0
|
|
3
|
+
/**
|
|
4
|
+
* RetroCode MCP Server + Channel
|
|
5
|
+
*
|
|
6
|
+
* Connects Claude to the RetroCode editor via the local HTTP API.
|
|
7
|
+
* Supports both MCP tools (23 tools) AND Channel push notifications.
|
|
8
|
+
*
|
|
9
|
+
* As MCP server (tools only):
|
|
10
|
+
* claude mcp add retrocode node ~/mcp-server.js
|
|
11
|
+
*
|
|
12
|
+
* As Channel (push + tools — recommended):
|
|
13
|
+
* claude --dangerously-load-development-channels server:retrocode
|
|
14
|
+
* (or add to channels config once published)
|
|
15
|
+
*
|
|
16
|
+
* When running as a Channel, RetroCode chat messages are pushed directly
|
|
17
|
+
* into Claude's session — no polling needed, instant response.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const http = require('http');
|
|
21
|
+
|
|
22
|
+
const API_BASE = 'http://127.0.0.1:21580';
|
|
23
|
+
|
|
24
|
+
// ── HTTP helpers ──────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
// Track last content written via set_editor so save_file can use it
|
|
27
|
+
let lastWrittenContent = '';
|
|
28
|
+
let lastWrittenLanguage = '';
|
|
29
|
+
|
|
30
|
+
function apiGet(path) {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
http.get(`${API_BASE}${path}`, (res) => {
|
|
33
|
+
let data = '';
|
|
34
|
+
res.on('data', (chunk) => data += chunk);
|
|
35
|
+
res.on('end', () => {
|
|
36
|
+
try { resolve(JSON.parse(data)); }
|
|
37
|
+
catch { resolve(data); }
|
|
38
|
+
});
|
|
39
|
+
}).on('error', reject);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function apiPost(path, body) {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
const data = JSON.stringify(body);
|
|
46
|
+
const req = http.request(`${API_BASE}${path}`, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) },
|
|
49
|
+
}, (res) => {
|
|
50
|
+
let result = '';
|
|
51
|
+
res.on('data', (chunk) => result += chunk);
|
|
52
|
+
res.on('end', () => {
|
|
53
|
+
try { resolve(JSON.parse(result)); }
|
|
54
|
+
catch { resolve(result); }
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
req.on('error', reject);
|
|
58
|
+
req.write(data);
|
|
59
|
+
req.end();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function apiPut(path, body) {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const data = JSON.stringify(body);
|
|
66
|
+
const req = http.request(`${API_BASE}${path}`, {
|
|
67
|
+
method: 'PUT',
|
|
68
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) },
|
|
69
|
+
}, (res) => {
|
|
70
|
+
let result = '';
|
|
71
|
+
res.on('data', (chunk) => result += chunk);
|
|
72
|
+
res.on('end', () => {
|
|
73
|
+
try { resolve(JSON.parse(result)); }
|
|
74
|
+
catch { resolve(result); }
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
req.on('error', reject);
|
|
78
|
+
req.write(data);
|
|
79
|
+
req.end();
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── MCP Protocol (JSON-RPC over stdio) ────────────────
|
|
84
|
+
|
|
85
|
+
const tools = [
|
|
86
|
+
{
|
|
87
|
+
name: 'retrocode_get_editor',
|
|
88
|
+
description: 'Get the current content of the RetroCode editor, including the mode (asm/basic/text), language, file path, and full editor text.',
|
|
89
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'retrocode_set_editor',
|
|
93
|
+
description: 'Set the content of the RetroCode editor. Always specify the language so syntax highlighting and file saving work correctly. This replaces the current editor content.',
|
|
94
|
+
inputSchema: {
|
|
95
|
+
type: 'object',
|
|
96
|
+
properties: {
|
|
97
|
+
content: { type: 'string', description: 'The new editor content' },
|
|
98
|
+
language: { type: 'string', description: 'Programming language (required for correct highlighting and save). Use: python, javascript, typescript, bash, powershell, c, cpp, csharp, rust, go, ruby, swift, kotlin, java, lua, perl, r, html, css, yaml, json, sql, toml, markdown, dockerfile' },
|
|
99
|
+
mode: { type: 'string', description: 'Optional: switch mode (asm, basic, text). Defaults to text for code.', enum: ['asm', 'basic', 'text'] },
|
|
100
|
+
},
|
|
101
|
+
required: ['content', 'language'],
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'retrocode_get_mode',
|
|
106
|
+
description: 'Get the current RetroCode mode: asm (assembly), basic (BASIC programming), or text (code editor).',
|
|
107
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'retrocode_append_editor',
|
|
111
|
+
description: 'Append text to the current editor content (adds to the end instead of replacing).',
|
|
112
|
+
inputSchema: {
|
|
113
|
+
type: 'object',
|
|
114
|
+
properties: {
|
|
115
|
+
content: { type: 'string', description: 'Text to append to the editor' },
|
|
116
|
+
},
|
|
117
|
+
required: ['content'],
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: 'retrocode_read_file',
|
|
122
|
+
description: 'Read a file from disk into the RetroCode editor. Specify the full path.',
|
|
123
|
+
inputSchema: {
|
|
124
|
+
type: 'object',
|
|
125
|
+
properties: {
|
|
126
|
+
path: { type: 'string', description: 'Full file path to read' },
|
|
127
|
+
},
|
|
128
|
+
required: ['path'],
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'retrocode_save_file',
|
|
133
|
+
description: 'Save content to a file on disk. If content is not provided, saves the last content that was written via retrocode_set_editor.',
|
|
134
|
+
inputSchema: {
|
|
135
|
+
type: 'object',
|
|
136
|
+
properties: {
|
|
137
|
+
path: { type: 'string', description: 'Full file path to save to' },
|
|
138
|
+
content: { type: 'string', description: 'Content to save. If omitted, uses the last content from retrocode_set_editor.' },
|
|
139
|
+
},
|
|
140
|
+
required: ['path'],
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'retrocode_list_tabs',
|
|
145
|
+
description: 'List all open editor tabs in the current mode. Returns tab index, filename, language, and modified status.',
|
|
146
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: 'retrocode_switch_tab',
|
|
150
|
+
description: 'Switch to a specific editor tab by index (0-based) in the current mode.',
|
|
151
|
+
inputSchema: {
|
|
152
|
+
type: 'object',
|
|
153
|
+
properties: {
|
|
154
|
+
index: { type: 'number', description: 'Tab index (0-based)' },
|
|
155
|
+
},
|
|
156
|
+
required: ['index'],
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'retrocode_new_tab',
|
|
161
|
+
description: 'Open a new editor tab in the current mode (max 5). Optionally provide content and language.',
|
|
162
|
+
inputSchema: {
|
|
163
|
+
type: 'object',
|
|
164
|
+
properties: {
|
|
165
|
+
content: { type: 'string', description: 'Initial content for the new tab' },
|
|
166
|
+
language: { type: 'string', description: 'Language for syntax highlighting' },
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: 'retrocode_list_languages',
|
|
172
|
+
description: 'List all supported programming languages with their file extensions.',
|
|
173
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: 'retrocode_switch_mode',
|
|
177
|
+
description: 'Switch RetroCode mode. Modes: asm (Assembly), basic (BASIC), text (Code editor).',
|
|
178
|
+
inputSchema: {
|
|
179
|
+
type: 'object',
|
|
180
|
+
properties: {
|
|
181
|
+
mode: { type: 'string', description: 'Mode to switch to', enum: ['asm', 'basic', 'text'] },
|
|
182
|
+
},
|
|
183
|
+
required: ['mode'],
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: 'retrocode_run',
|
|
188
|
+
description: 'Execute the current program. In ASM mode: assembles and runs. In BASIC mode: runs the program. Returns status.',
|
|
189
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: 'retrocode_stop',
|
|
193
|
+
description: 'Stop the currently running program (ASM or BASIC).',
|
|
194
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: 'retrocode_chat',
|
|
198
|
+
description: 'Send a message to the user via the RetroCode chat panel. Use this to explain what you are doing, ask questions, provide guidance, or coach the user. Messages appear in a chat panel inside RetroCode.',
|
|
199
|
+
inputSchema: {
|
|
200
|
+
type: 'object',
|
|
201
|
+
properties: {
|
|
202
|
+
text: { type: 'string', description: 'Message to display to the user' },
|
|
203
|
+
},
|
|
204
|
+
required: ['text'],
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: 'retrocode_open_terminal',
|
|
209
|
+
description: 'Open the RetroCode terminal with the appropriate shell for the platform.',
|
|
210
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: 'retrocode_close_terminal',
|
|
214
|
+
description: 'Close the RetroCode terminal.',
|
|
215
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: 'retrocode_get_chat',
|
|
219
|
+
description: 'Read all chat messages from the RetroCode chat panel. Use this to check if the user has responded to your messages.',
|
|
220
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: 'retrocode_terminal',
|
|
224
|
+
description: 'Send a command to a RetroCode terminal. The terminal must be open. Use this to run scripts, execute commands, show output to the user. The command runs in the user\'s actual shell (zsh/bash/PowerShell).',
|
|
225
|
+
inputSchema: {
|
|
226
|
+
type: 'object',
|
|
227
|
+
properties: {
|
|
228
|
+
command: { type: 'string', description: 'Shell command to execute (e.g., "python3 script.py", "ls -la", "echo hello")' },
|
|
229
|
+
terminal_id: { type: 'string', description: 'Terminal ID (e.g., "term-1"). Defaults to active terminal.' },
|
|
230
|
+
},
|
|
231
|
+
required: ['command'],
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: 'retrocode_terminal_output',
|
|
236
|
+
description: 'Read the last 5KB of terminal output. Use this to see the result of commands you sent to the terminal.',
|
|
237
|
+
inputSchema: {
|
|
238
|
+
type: 'object',
|
|
239
|
+
properties: {
|
|
240
|
+
terminal_id: { type: 'string', description: 'Terminal ID (e.g., "term-1"). Defaults to active terminal.' },
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
name: 'retrocode_heartbeat',
|
|
246
|
+
description: 'Send a heartbeat to RetroCode to keep the connection status green. Call this periodically (every 30s) during long operations to show the user you are still working.',
|
|
247
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: 'retrocode_asm_reference',
|
|
251
|
+
description: 'Get the complete RetroCode assembly language reference — all 36 opcodes, registers, memory map, label syntax, and graphics commands. Call this before writing assembly code.',
|
|
252
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: 'retrocode_basic_reference',
|
|
256
|
+
description: 'Get the complete RetroCode BASIC V2.0 reference — all commands, math/string functions, graphics commands, and syntax. Call this before writing BASIC code.',
|
|
257
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
name: 'retrocode_health',
|
|
261
|
+
description: 'Check if RetroCode is running and the MCP API is available. RetroCode must be running for all tools to work. If not running, tell the user to launch RetroCode first.',
|
|
262
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
263
|
+
},
|
|
264
|
+
];
|
|
265
|
+
|
|
266
|
+
async function handleToolCall(name, args) {
|
|
267
|
+
switch (name) {
|
|
268
|
+
case 'retrocode_get_editor': {
|
|
269
|
+
const state = await apiGet('/api/editor');
|
|
270
|
+
return { content: [{ type: 'text', text: JSON.stringify(state, null, 2) }] };
|
|
271
|
+
}
|
|
272
|
+
case 'retrocode_set_editor': {
|
|
273
|
+
lastWrittenContent = args.content || '';
|
|
274
|
+
lastWrittenLanguage = args.language || '';
|
|
275
|
+
const body = {
|
|
276
|
+
content: lastWrittenContent,
|
|
277
|
+
language: lastWrittenLanguage,
|
|
278
|
+
mode: args.mode || 'text',
|
|
279
|
+
};
|
|
280
|
+
await apiPut('/api/editor', body);
|
|
281
|
+
return { content: [{ type: 'text', text: `Editor updated (${args.language || 'text'}).` }] };
|
|
282
|
+
}
|
|
283
|
+
case 'retrocode_append_editor': {
|
|
284
|
+
await apiPost('/api/editor/append', { content: args.content || '' });
|
|
285
|
+
return { content: [{ type: 'text', text: 'Content appended to editor.' }] };
|
|
286
|
+
}
|
|
287
|
+
case 'retrocode_get_mode': {
|
|
288
|
+
const state = await apiGet('/api/editor');
|
|
289
|
+
return { content: [{ type: 'text', text: `Current mode: ${state.mode || 'unknown'}` }] };
|
|
290
|
+
}
|
|
291
|
+
case 'retrocode_read_file': {
|
|
292
|
+
const http2 = require('http');
|
|
293
|
+
const readData = JSON.stringify({ path: args.path });
|
|
294
|
+
// Use the Tauri backend to read the file
|
|
295
|
+
const content = await new Promise((resolve, reject) => {
|
|
296
|
+
const req = http2.request(`${API_BASE}/api/readfile?path=${encodeURIComponent(args.path)}`, (res) => {
|
|
297
|
+
let body = '';
|
|
298
|
+
res.on('data', c => body += c);
|
|
299
|
+
res.on('end', () => resolve(body));
|
|
300
|
+
});
|
|
301
|
+
req.on('error', reject);
|
|
302
|
+
req.end();
|
|
303
|
+
});
|
|
304
|
+
return { content: [{ type: 'text', text: content }] };
|
|
305
|
+
}
|
|
306
|
+
case 'retrocode_save_file': {
|
|
307
|
+
// Use provided content, or last written content, or try API state as fallback
|
|
308
|
+
let fileContent = args.content || lastWrittenContent;
|
|
309
|
+
if (!fileContent) {
|
|
310
|
+
const editor = await apiGet('/api/editor');
|
|
311
|
+
fileContent = editor.content || '';
|
|
312
|
+
}
|
|
313
|
+
if (!fileContent) {
|
|
314
|
+
return { content: [{ type: 'text', text: 'Error: No content to save. Write to the editor first with retrocode_set_editor.' }], isError: true };
|
|
315
|
+
}
|
|
316
|
+
const saveData = JSON.stringify({ path: args.path, content: fileContent });
|
|
317
|
+
await new Promise((resolve, reject) => {
|
|
318
|
+
const req = http.request(`${API_BASE}/api/savefile`, {
|
|
319
|
+
method: 'POST',
|
|
320
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(saveData) },
|
|
321
|
+
}, (res) => {
|
|
322
|
+
let body = '';
|
|
323
|
+
res.on('data', c => body += c);
|
|
324
|
+
res.on('end', () => resolve(body));
|
|
325
|
+
});
|
|
326
|
+
req.on('error', reject);
|
|
327
|
+
req.write(saveData);
|
|
328
|
+
req.end();
|
|
329
|
+
});
|
|
330
|
+
return { content: [{ type: 'text', text: `File saved to: ${args.path}` }] };
|
|
331
|
+
}
|
|
332
|
+
case 'retrocode_list_tabs': {
|
|
333
|
+
const state = await apiGet('/api/editor');
|
|
334
|
+
const tabs = state.tabs || [{ index: 0, name: state.file_path || 'untitled', language: state.language, modified: false }];
|
|
335
|
+
return { content: [{ type: 'text', text: JSON.stringify(tabs, null, 2) }] };
|
|
336
|
+
}
|
|
337
|
+
case 'retrocode_switch_tab': {
|
|
338
|
+
await apiPut('/api/editor', { tab_index: args.index });
|
|
339
|
+
return { content: [{ type: 'text', text: `Switched to tab ${args.index}` }] };
|
|
340
|
+
}
|
|
341
|
+
case 'retrocode_new_tab': {
|
|
342
|
+
await apiPost('/api/editor/newtab', { content: args.content || '', language: args.language || '' });
|
|
343
|
+
return { content: [{ type: 'text', text: 'New tab opened.' }] };
|
|
344
|
+
}
|
|
345
|
+
case 'retrocode_list_languages': {
|
|
346
|
+
const langs = [
|
|
347
|
+
'bash (.sh)', 'c (.c)', 'cpp (.cpp)', 'csharp (.cs)', 'css (.css)',
|
|
348
|
+
'dockerfile (Dockerfile)', 'go (.go)', 'html (.html)', 'json (.json)',
|
|
349
|
+
'javascript (.js)', 'kotlin (.kt)', 'lua (.lua)', 'markdown (.md)',
|
|
350
|
+
'perl (.pl)', 'powershell (.ps1)', 'python (.py)', 'r (.r)',
|
|
351
|
+
'ruby (.rb)', 'rust (.rs)', 'sql (.sql)', 'swift (.swift)',
|
|
352
|
+
'toml (.toml)', 'typescript (.ts)', 'yaml (.yml)'
|
|
353
|
+
];
|
|
354
|
+
return { content: [{ type: 'text', text: 'Supported languages:\n' + langs.join('\n') }] };
|
|
355
|
+
}
|
|
356
|
+
case 'retrocode_switch_mode': {
|
|
357
|
+
await apiPut('/api/editor', { mode: args.mode });
|
|
358
|
+
return { content: [{ type: 'text', text: `Switched to ${args.mode} mode.` }] };
|
|
359
|
+
}
|
|
360
|
+
case 'retrocode_run': {
|
|
361
|
+
await apiPost('/api/action', { action: 'run' });
|
|
362
|
+
return { content: [{ type: 'text', text: 'Program running.' }] };
|
|
363
|
+
}
|
|
364
|
+
case 'retrocode_stop': {
|
|
365
|
+
await apiPost('/api/action', { action: 'stop' });
|
|
366
|
+
return { content: [{ type: 'text', text: 'Program stopped.' }] };
|
|
367
|
+
}
|
|
368
|
+
case 'retrocode_chat': {
|
|
369
|
+
const ts = Math.floor(Date.now() / 1000);
|
|
370
|
+
await apiPost('/api/chat', { role: 'claude', text: args.text, timestamp: ts });
|
|
371
|
+
return { content: [{ type: 'text', text: 'Message sent to chat panel.' }] };
|
|
372
|
+
}
|
|
373
|
+
case 'retrocode_open_terminal': {
|
|
374
|
+
await apiPost('/api/action', { action: 'open_terminal' });
|
|
375
|
+
return { content: [{ type: 'text', text: 'Terminal opened.' }] };
|
|
376
|
+
}
|
|
377
|
+
case 'retrocode_close_terminal': {
|
|
378
|
+
await apiPost('/api/action', { action: 'close_terminal' });
|
|
379
|
+
return { content: [{ type: 'text', text: 'Terminal closed.' }] };
|
|
380
|
+
}
|
|
381
|
+
case 'retrocode_get_chat': {
|
|
382
|
+
const msgs = await apiGet('/api/chat');
|
|
383
|
+
return { content: [{ type: 'text', text: JSON.stringify(msgs, null, 2) }] };
|
|
384
|
+
}
|
|
385
|
+
case 'retrocode_terminal': {
|
|
386
|
+
const body = { command: args.command };
|
|
387
|
+
if (args.terminal_id) body.terminal_id = args.terminal_id;
|
|
388
|
+
await apiPost('/api/terminal', body);
|
|
389
|
+
return { content: [{ type: 'text', text: `Terminal command sent: ${args.command}` }] };
|
|
390
|
+
}
|
|
391
|
+
case 'retrocode_terminal_output': {
|
|
392
|
+
// Try dedicated endpoint first, fall back to editor state
|
|
393
|
+
let output = '';
|
|
394
|
+
try { output = await apiGet('/api/terminal/output'); } catch {}
|
|
395
|
+
if (!output || output === '{}') {
|
|
396
|
+
const state = await apiGet('/api/editor');
|
|
397
|
+
output = state.terminal_output || '';
|
|
398
|
+
}
|
|
399
|
+
// Strip ANSI codes for readability
|
|
400
|
+
output = String(output).replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '').replace(/[\x00-\x08\x0e-\x1f]/g, '');
|
|
401
|
+
return { content: [{ type: 'text', text: output || '(no output yet — terminal may still be running)' }] };
|
|
402
|
+
}
|
|
403
|
+
case 'retrocode_heartbeat': {
|
|
404
|
+
await apiPost('/api/heartbeat', {});
|
|
405
|
+
return { content: [{ type: 'text', text: 'Heartbeat sent.' }] };
|
|
406
|
+
}
|
|
407
|
+
case 'retrocode_asm_reference': {
|
|
408
|
+
const ref = await apiGet('/api/reference/asm');
|
|
409
|
+
return { content: [{ type: 'text', text: JSON.stringify(ref, null, 2) }] };
|
|
410
|
+
}
|
|
411
|
+
case 'retrocode_basic_reference': {
|
|
412
|
+
const ref = await apiGet('/api/reference/basic');
|
|
413
|
+
return { content: [{ type: 'text', text: JSON.stringify(ref, null, 2) }] };
|
|
414
|
+
}
|
|
415
|
+
case 'retrocode_health': {
|
|
416
|
+
const health = await apiGet('/api/health');
|
|
417
|
+
return { content: [{ type: 'text', text: JSON.stringify(health) }] };
|
|
418
|
+
}
|
|
419
|
+
default:
|
|
420
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ── JSON-RPC message handling ─────────────────────────
|
|
425
|
+
|
|
426
|
+
let buffer = '';
|
|
427
|
+
|
|
428
|
+
process.stdin.setEncoding('utf8');
|
|
429
|
+
process.stdin.on('data', (chunk) => {
|
|
430
|
+
buffer += chunk;
|
|
431
|
+
|
|
432
|
+
// Process complete messages (newline-delimited JSON)
|
|
433
|
+
const lines = buffer.split('\n');
|
|
434
|
+
buffer = lines.pop() || '';
|
|
435
|
+
|
|
436
|
+
for (const line of lines) {
|
|
437
|
+
if (!line.trim()) continue;
|
|
438
|
+
try {
|
|
439
|
+
const msg = JSON.parse(line);
|
|
440
|
+
handleMessage(msg);
|
|
441
|
+
} catch (e) {
|
|
442
|
+
// Skip malformed messages
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
function sendResponse(id, result) {
|
|
448
|
+
const response = { jsonrpc: '2.0', id, result };
|
|
449
|
+
process.stdout.write(JSON.stringify(response) + '\n');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function sendError(id, code, message) {
|
|
453
|
+
const response = { jsonrpc: '2.0', id, error: { code, message } };
|
|
454
|
+
process.stdout.write(JSON.stringify(response) + '\n');
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ── Channel push notifications ───────────────────────
|
|
458
|
+
|
|
459
|
+
let channelEnabled = false;
|
|
460
|
+
let lastSeenUserMsgCount = 0;
|
|
461
|
+
let channelPollTimer = null;
|
|
462
|
+
|
|
463
|
+
function sendNotification(method, params) {
|
|
464
|
+
const msg = { jsonrpc: '2.0', method, params };
|
|
465
|
+
process.stdout.write(JSON.stringify(msg) + '\n');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function pushChannelMessage(content, meta) {
|
|
469
|
+
if (!channelEnabled) return;
|
|
470
|
+
sendNotification('notifications/claude/channel', { content, meta: meta || {} });
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async function startChannelPolling() {
|
|
474
|
+
// Initialize message count
|
|
475
|
+
try {
|
|
476
|
+
const msgs = await apiGet('/api/chat');
|
|
477
|
+
const userMsgs = Array.isArray(msgs) ? msgs.filter(m => m.role === 'user') : [];
|
|
478
|
+
lastSeenUserMsgCount = userMsgs.length;
|
|
479
|
+
} catch {}
|
|
480
|
+
|
|
481
|
+
// Send heartbeat to RetroCode
|
|
482
|
+
apiPost('/api/heartbeat', {}).catch(() => {});
|
|
483
|
+
|
|
484
|
+
// Poll for new messages every 1 second
|
|
485
|
+
channelPollTimer = setInterval(async () => {
|
|
486
|
+
try {
|
|
487
|
+
// Heartbeat
|
|
488
|
+
apiPost('/api/heartbeat', {}).catch(() => {});
|
|
489
|
+
|
|
490
|
+
// Check for new user messages
|
|
491
|
+
const msgs = await apiGet('/api/chat');
|
|
492
|
+
const userMsgs = Array.isArray(msgs) ? msgs.filter(m => m.role === 'user') : [];
|
|
493
|
+
|
|
494
|
+
if (userMsgs.length > lastSeenUserMsgCount) {
|
|
495
|
+
const newMsgs = userMsgs.slice(lastSeenUserMsgCount);
|
|
496
|
+
lastSeenUserMsgCount = userMsgs.length;
|
|
497
|
+
|
|
498
|
+
for (const msg of newMsgs) {
|
|
499
|
+
// Get editor context for richer push
|
|
500
|
+
let context = '';
|
|
501
|
+
try {
|
|
502
|
+
const editor = await apiGet('/api/editor');
|
|
503
|
+
if (editor.mode) context = ` [mode: ${editor.mode}]`;
|
|
504
|
+
if (editor.language) context += ` [lang: ${editor.language}]`;
|
|
505
|
+
} catch {}
|
|
506
|
+
|
|
507
|
+
process.stderr.write(`[channel] New message: ${msg.text}\n`);
|
|
508
|
+
pushChannelMessage(
|
|
509
|
+
`RetroCode chat message from user${context}: ${msg.text}`,
|
|
510
|
+
{ source: 'retrocode-chat', mode: 'user-message' }
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
} catch (e) {
|
|
515
|
+
// Connection lost — keep trying
|
|
516
|
+
}
|
|
517
|
+
}, 1000);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// ── JSON-RPC message handling ─────────────────────────
|
|
521
|
+
|
|
522
|
+
async function handleMessage(msg) {
|
|
523
|
+
const { id, method, params } = msg;
|
|
524
|
+
|
|
525
|
+
switch (method) {
|
|
526
|
+
case 'initialize':
|
|
527
|
+
sendResponse(id, {
|
|
528
|
+
protocolVersion: '2024-11-05',
|
|
529
|
+
capabilities: {
|
|
530
|
+
tools: {},
|
|
531
|
+
experimental: { 'claude/channel': {} },
|
|
532
|
+
},
|
|
533
|
+
serverInfo: { name: 'retrocode', version: '1.3.0' },
|
|
534
|
+
});
|
|
535
|
+
break;
|
|
536
|
+
|
|
537
|
+
case 'notifications/initialized':
|
|
538
|
+
// Start channel polling after initialization
|
|
539
|
+
channelEnabled = true;
|
|
540
|
+
process.stderr.write('[channel] Channel mode active — pushing RetroCode events to Claude\n');
|
|
541
|
+
startChannelPolling();
|
|
542
|
+
break;
|
|
543
|
+
|
|
544
|
+
case 'tools/list':
|
|
545
|
+
sendResponse(id, { tools });
|
|
546
|
+
break;
|
|
547
|
+
|
|
548
|
+
case 'tools/call':
|
|
549
|
+
try {
|
|
550
|
+
const result = await handleToolCall(params.name, params.arguments || {});
|
|
551
|
+
sendResponse(id, result);
|
|
552
|
+
} catch (e) {
|
|
553
|
+
sendResponse(id, {
|
|
554
|
+
content: [{ type: 'text', text: `Error: ${e.message}. Is RetroCode running?` }],
|
|
555
|
+
isError: true,
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
break;
|
|
559
|
+
|
|
560
|
+
case 'ping':
|
|
561
|
+
sendResponse(id, {});
|
|
562
|
+
break;
|
|
563
|
+
|
|
564
|
+
default:
|
|
565
|
+
if (id) sendError(id, -32601, `Method not found: ${method}`);
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Keep the process alive
|
|
571
|
+
process.stdin.resume();
|
|
572
|
+
process.stderr.write('RetroCode MCP server started (with Channel support). Waiting for Claude...\n');
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "retrocode-mcp",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "RetroCode MCP server — connects Claude to the RetroCode editor with 23 tools and Channel push notifications",
|
|
5
|
+
"main": "mcp-server.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"retrocode-mcp": "mcp-server.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node mcp-server.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": ["mcp", "retrocode", "claude", "anthropic", "assembly", "basic"],
|
|
13
|
+
"author": "senzall",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
}
|
|
18
|
+
}
|