@mediagato/modelreins-channel 1.0.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/modelreins-channel.mjs +261 -0
- package/package.json +19 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ModelReins MCP Channel — bridges a Claude Code session to ModelReins.
|
|
4
|
+
*
|
|
5
|
+
* When installed in .mcp.json, this turns any Claude Code session into a
|
|
6
|
+
* ModelReins worker with full local access (files, git, SSH, env vars).
|
|
7
|
+
*
|
|
8
|
+
* The channel:
|
|
9
|
+
* 1. Registers with ModelReins as a worker (PUT /presence)
|
|
10
|
+
* 2. Polls for assigned jobs (GET /jobs?assigned_to=<name>&status=pending)
|
|
11
|
+
* 3. Pushes job prompts into Claude's session via MCP notifications
|
|
12
|
+
* 4. Captures Claude's responses and posts them back (POST /jobs/{id}/output)
|
|
13
|
+
*
|
|
14
|
+
* Config (env vars or passed via args):
|
|
15
|
+
* MODELREINS_URL — ModelReins server URL (default: http://localhost:8484)
|
|
16
|
+
* MODELREINS_TOKEN — Auth token
|
|
17
|
+
* MODELREINS_WORKER — Worker name (default: channel-<hostname>)
|
|
18
|
+
*
|
|
19
|
+
* .mcp.json entry:
|
|
20
|
+
* {
|
|
21
|
+
* "mcpServers": {
|
|
22
|
+
* "modelreins": {
|
|
23
|
+
* "command": "node",
|
|
24
|
+
* "args": ["path/to/modelreins-channel.mjs"],
|
|
25
|
+
* "env": {
|
|
26
|
+
* "MODELREINS_URL": "https://app.modelreins.com",
|
|
27
|
+
* "MODELREINS_TOKEN": "your-token",
|
|
28
|
+
* "MODELREINS_WORKER": "my-laptop"
|
|
29
|
+
* }
|
|
30
|
+
* }
|
|
31
|
+
* }
|
|
32
|
+
* }
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
36
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
37
|
+
import {
|
|
38
|
+
ListToolsRequestSchema,
|
|
39
|
+
CallToolRequestSchema,
|
|
40
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
41
|
+
import { hostname } from 'os';
|
|
42
|
+
|
|
43
|
+
// ── Config ──────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
const BASE_URL = process.env.MODELREINS_URL || 'http://localhost:8484';
|
|
46
|
+
const TOKEN = process.env.MODELREINS_TOKEN || '';
|
|
47
|
+
const WORKER_NAME = process.env.MODELREINS_WORKER || `channel-${hostname().toLowerCase()}`;
|
|
48
|
+
const POLL_MS = parseInt(process.env.MODELREINS_POLL_MS || '5000', 10);
|
|
49
|
+
const WORKER_MODEL = process.env.MODELREINS_WORKER_MODEL || 'claude-session';
|
|
50
|
+
const WORKER_TAGS = process.env.MODELREINS_WORKER_TAGS || 'local,interactive,full-access';
|
|
51
|
+
|
|
52
|
+
// ── HTTP helpers ────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
async function api(method, path, body = null) {
|
|
55
|
+
const url = `${BASE_URL}${path}`;
|
|
56
|
+
const opts = {
|
|
57
|
+
method,
|
|
58
|
+
headers: {
|
|
59
|
+
'Authorization': `Bearer ${TOKEN}`,
|
|
60
|
+
'Content-Type': 'application/json',
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
if (body) opts.body = JSON.stringify(body);
|
|
64
|
+
try {
|
|
65
|
+
const res = await fetch(url, opts);
|
|
66
|
+
const data = await res.json();
|
|
67
|
+
return { status: res.status, data };
|
|
68
|
+
} catch (e) {
|
|
69
|
+
return { status: 0, data: { error: e.message } };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── State ───────────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
let currentJobId = null;
|
|
76
|
+
let mcp = null;
|
|
77
|
+
|
|
78
|
+
// ── MCP Server ──────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
mcp = new Server(
|
|
81
|
+
{ name: 'modelreins', version: '1.0.0' },
|
|
82
|
+
{
|
|
83
|
+
capabilities: {
|
|
84
|
+
experimental: { 'claude/channel': {} },
|
|
85
|
+
tools: {},
|
|
86
|
+
},
|
|
87
|
+
instructions: [
|
|
88
|
+
`You are connected to ModelReins (${BASE_URL}) as worker "${WORKER_NAME}".`,
|
|
89
|
+
'Jobs from the ModelReins dashboard arrive as <channel source="modelreins" job_id="..." priority="..."> tags.',
|
|
90
|
+
'When you receive a job, complete the task described in the prompt using your full local capabilities.',
|
|
91
|
+
'When done, use the modelreins_complete tool to report the result back.',
|
|
92
|
+
'If you cannot complete a job, use modelreins_complete with success=false and explain why.',
|
|
93
|
+
'Between jobs, you remain a normal Claude Code session — the user can still interact with you directly.',
|
|
94
|
+
].join('\n'),
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// Tool: report job completion back to ModelReins
|
|
99
|
+
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
100
|
+
tools: [
|
|
101
|
+
{
|
|
102
|
+
name: 'modelreins_complete',
|
|
103
|
+
description: 'Report job completion back to ModelReins dashboard. Call this when you finish a dispatched task.',
|
|
104
|
+
inputSchema: {
|
|
105
|
+
type: 'object',
|
|
106
|
+
properties: {
|
|
107
|
+
job_id: { type: 'string', description: 'The job ID from the <channel> tag' },
|
|
108
|
+
summary: { type: 'string', description: 'Brief summary of what was done' },
|
|
109
|
+
success: { type: 'boolean', description: 'Whether the task succeeded' },
|
|
110
|
+
},
|
|
111
|
+
required: ['job_id', 'summary', 'success'],
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: 'modelreins_dispatch',
|
|
116
|
+
description: 'Dispatch a new job to another ModelReins worker from this session.',
|
|
117
|
+
inputSchema: {
|
|
118
|
+
type: 'object',
|
|
119
|
+
properties: {
|
|
120
|
+
prompt: { type: 'string', description: 'The task prompt' },
|
|
121
|
+
assigned_to: { type: 'string', description: 'Worker name or "auto"' },
|
|
122
|
+
model_preference: { type: 'string', description: 'Model: auto/haiku/sonnet/opus' },
|
|
123
|
+
},
|
|
124
|
+
required: ['prompt'],
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'modelreins_status',
|
|
129
|
+
description: 'Check ModelReins fleet status — see active workers, running jobs, and schedules.',
|
|
130
|
+
inputSchema: {
|
|
131
|
+
type: 'object',
|
|
132
|
+
properties: {},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
}));
|
|
137
|
+
|
|
138
|
+
mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
139
|
+
const { name, arguments: args } = req.params;
|
|
140
|
+
|
|
141
|
+
if (name === 'modelreins_complete') {
|
|
142
|
+
const { job_id, summary, success } = args;
|
|
143
|
+
// Post completion output
|
|
144
|
+
await api('POST', `/jobs/${job_id}/output`, {
|
|
145
|
+
stream: 'stdout',
|
|
146
|
+
content: summary,
|
|
147
|
+
});
|
|
148
|
+
// Mark job done or failed
|
|
149
|
+
await api('PUT', `/jobs/${job_id}`, {
|
|
150
|
+
status: success ? 'done' : 'failed',
|
|
151
|
+
exit_code: success ? 0 : 1,
|
|
152
|
+
error: success ? null : summary,
|
|
153
|
+
});
|
|
154
|
+
currentJobId = null;
|
|
155
|
+
return { content: [{ type: 'text', text: `Job #${job_id} ${success ? 'completed' : 'failed'}. Result posted to ModelReins.` }] };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (name === 'modelreins_dispatch') {
|
|
159
|
+
const { prompt, assigned_to, model_preference } = args;
|
|
160
|
+
const res = await api('POST', '/dispatch', {
|
|
161
|
+
prompt,
|
|
162
|
+
assigned_to: assigned_to || 'auto',
|
|
163
|
+
model_preference: model_preference || null,
|
|
164
|
+
});
|
|
165
|
+
if (res.data?.success) {
|
|
166
|
+
return { content: [{ type: 'text', text: `Dispatched job #${res.data.id} to ${res.data.assigned_to}` }] };
|
|
167
|
+
}
|
|
168
|
+
return { content: [{ type: 'text', text: `Dispatch failed: ${JSON.stringify(res.data)}` }] };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (name === 'modelreins_status') {
|
|
172
|
+
const [workers, jobs, schedules] = await Promise.all([
|
|
173
|
+
api('GET', '/workers'),
|
|
174
|
+
api('GET', '/jobs?limit=5'),
|
|
175
|
+
api('GET', '/schedules'),
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
const lines = ['## ModelReins Fleet Status\n'];
|
|
179
|
+
|
|
180
|
+
// Workers
|
|
181
|
+
const wList = workers.data?.workers || [];
|
|
182
|
+
lines.push(`### Workers (${wList.length})`);
|
|
183
|
+
for (const w of wList) {
|
|
184
|
+
lines.push(`- ${w.instance}: ${w.status} (${w.model || '?'})`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Recent jobs
|
|
188
|
+
const jList = workers.data?.jobs || jobs.data?.jobs || [];
|
|
189
|
+
lines.push(`\n### Recent Jobs`);
|
|
190
|
+
for (const j of jList.slice(0, 5)) {
|
|
191
|
+
lines.push(`- #${j.id} ${j.status} → ${j.assigned_to} (${j.prompt?.substring(0, 60)}...)`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Schedules
|
|
195
|
+
const sList = schedules.data?.schedules || [];
|
|
196
|
+
if (sList.length > 0) {
|
|
197
|
+
lines.push(`\n### Schedules (${sList.length})`);
|
|
198
|
+
for (const s of sList) {
|
|
199
|
+
lines.push(`- ${s.name}: ${s.human_schedule || s.cron_expr} → ${s.assigned_to || 'auto'} (runs: ${s.run_count})`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// ── Connect ─────────────────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
await mcp.connect(new StdioServerTransport());
|
|
212
|
+
|
|
213
|
+
// ── Heartbeat + Job Polling Loop ────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
async function heartbeat() {
|
|
216
|
+
await api('PUT', '/presence', {
|
|
217
|
+
instance: WORKER_NAME,
|
|
218
|
+
status: currentJobId ? 'busy' : 'active',
|
|
219
|
+
project: 'channel',
|
|
220
|
+
worker_type: 'channel',
|
|
221
|
+
model: WORKER_MODEL,
|
|
222
|
+
details: currentJobId ? `working on #${currentJobId}` : 'idle, polling',
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function pollJobs() {
|
|
227
|
+
if (currentJobId) return; // already working on something
|
|
228
|
+
|
|
229
|
+
const res = await api('GET', `/jobs?status=pending&assigned_to=${WORKER_NAME}&limit=1`);
|
|
230
|
+
const jobs = res.data?.jobs || (Array.isArray(res.data) ? res.data : []);
|
|
231
|
+
|
|
232
|
+
if (jobs.length > 0) {
|
|
233
|
+
const job = jobs[0];
|
|
234
|
+
currentJobId = job.id;
|
|
235
|
+
|
|
236
|
+
// Claim the job
|
|
237
|
+
await api('PUT', `/jobs/${job.id}`, { status: 'running' });
|
|
238
|
+
|
|
239
|
+
// Push into Claude's session
|
|
240
|
+
await mcp.notification({
|
|
241
|
+
method: 'notifications/claude/channel',
|
|
242
|
+
params: {
|
|
243
|
+
content: job.prompt,
|
|
244
|
+
meta: {
|
|
245
|
+
job_id: String(job.id),
|
|
246
|
+
priority: String(job.priority || 0),
|
|
247
|
+
model_preference: job.model_preference || '',
|
|
248
|
+
working_dir: job.working_dir || '',
|
|
249
|
+
source: 'modelreins-dashboard',
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Main loop
|
|
257
|
+
setInterval(heartbeat, 30000);
|
|
258
|
+
setInterval(pollJobs, POLL_MS);
|
|
259
|
+
|
|
260
|
+
// Initial heartbeat
|
|
261
|
+
heartbeat();
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mediagato/modelreins-channel",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "ModelReins MCP Channel — turn any Claude Code session into a ModelReins worker",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "modelreins-channel.mjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"modelreins-channel": "./modelreins-channel.mjs"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@modelcontextprotocol/sdk": "^1.12.0"
|
|
12
|
+
},
|
|
13
|
+
"keywords": ["modelreins", "mcp", "claude-code", "channel", "ai-orchestration"],
|
|
14
|
+
"license": "BSL-1.1",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/mediagato/modelreins"
|
|
18
|
+
}
|
|
19
|
+
}
|