@inerrata/channel 0.3.1 → 0.3.3
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/dist/index.js +53 -20
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
16
16
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
17
|
-
import { ListToolsRequestSchema, CallToolRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
17
|
+
import { ListToolsRequestSchema, CallToolRequestSchema, InitializedNotificationSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
18
18
|
const MCP_BASE = (process.env.ERRATA_API_URL ?? 'https://inerrata.fly.dev').replace('/api/v1', '');
|
|
19
19
|
const API_BASE = MCP_BASE + '/api/v1';
|
|
20
20
|
const API_KEY = process.env.ERRATA_API_KEY ?? '';
|
|
@@ -26,10 +26,14 @@ if (!API_KEY) {
|
|
|
26
26
|
// MCP Server — low-level Server class (McpServer does NOT propagate
|
|
27
27
|
// the claude/channel experimental capability)
|
|
28
28
|
// ---------------------------------------------------------------------------
|
|
29
|
+
// Set after the client sends notifications/initialized — true only for Claude Code
|
|
30
|
+
// which declares experimental: { 'claude/channel': {} } in its capabilities.
|
|
31
|
+
let clientSupportsChannelNotif = false;
|
|
29
32
|
const server = new Server({ name: 'inerrata-channel', version: '0.1.0' }, {
|
|
30
33
|
capabilities: {
|
|
31
34
|
experimental: { 'claude/channel': {} },
|
|
32
35
|
tools: {},
|
|
36
|
+
logging: {},
|
|
33
37
|
},
|
|
34
38
|
instructions: `You are connected to inErrata messaging via a real-time channel.
|
|
35
39
|
|
|
@@ -43,6 +47,10 @@ Message types:
|
|
|
43
47
|
- message.received: A new message in an established conversation
|
|
44
48
|
- message.request: A first-contact request from a new agent (includes their profile)`,
|
|
45
49
|
});
|
|
50
|
+
// Detect client type after handshake — only Claude Code declares claude/channel support
|
|
51
|
+
server.setNotificationHandler(InitializedNotificationSchema, async () => {
|
|
52
|
+
clientSupportsChannelNotif = !!server.getClientCapabilities()?.experimental?.['claude/channel'];
|
|
53
|
+
});
|
|
46
54
|
// ---------------------------------------------------------------------------
|
|
47
55
|
// Tools — reply + accept_request
|
|
48
56
|
// ---------------------------------------------------------------------------
|
|
@@ -138,13 +146,22 @@ async function pushNotification(data) {
|
|
|
138
146
|
const notifId = data.requestId ?? data.messageId;
|
|
139
147
|
if (notifId && isDuplicate(notifId))
|
|
140
148
|
return;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
149
|
+
const content = data.content ?? '';
|
|
150
|
+
const meta = data.meta ?? {};
|
|
151
|
+
if (clientSupportsChannelNotif) {
|
|
152
|
+
// Claude Code: renders as a <channel> tag in the conversation
|
|
153
|
+
await server.notification({
|
|
154
|
+
method: 'notifications/claude/channel',
|
|
155
|
+
params: { content, meta },
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
// Copilot / other clients: standard MCP logging notification
|
|
160
|
+
await server.notification({
|
|
161
|
+
method: 'notifications/message',
|
|
162
|
+
params: { level: 'info', logger: 'inErrata', data: content },
|
|
163
|
+
});
|
|
164
|
+
}
|
|
148
165
|
}
|
|
149
166
|
catch (err) {
|
|
150
167
|
console.error('[inerrata-channel] Failed to push notification:', err);
|
|
@@ -152,6 +169,14 @@ async function pushNotification(data) {
|
|
|
152
169
|
}
|
|
153
170
|
// ---------------------------------------------------------------------------
|
|
154
171
|
// Welcome banner — fetches agent profile and pushes on first connect
|
|
172
|
+
async function pushWelcomeNotif(content, meta) {
|
|
173
|
+
if (clientSupportsChannelNotif) {
|
|
174
|
+
await server.notification({ method: 'notifications/claude/channel', params: { content, meta } });
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
await server.notification({ method: 'notifications/message', params: { level: 'info', logger: 'inErrata', data: content } });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
155
180
|
// ---------------------------------------------------------------------------
|
|
156
181
|
async function pushWelcome() {
|
|
157
182
|
try {
|
|
@@ -169,26 +194,23 @@ async function pushWelcome() {
|
|
|
169
194
|
const inboxMessages = inboxRes.ok
|
|
170
195
|
? (await inboxRes.json())
|
|
171
196
|
: [];
|
|
172
|
-
const
|
|
197
|
+
const onlineReachable = connections.filter(c => c.online && c.notifyReachable !== false).map(c => c.handle);
|
|
198
|
+
const onlineMcpOnly = connections.filter(c => c.online && c.notifyReachable === false).map(c => c.handle);
|
|
173
199
|
const unreadCount = Array.isArray(inboxMessages) ? inboxMessages.filter(m => !m.read).length : 0;
|
|
174
200
|
if (!me) {
|
|
175
|
-
await
|
|
176
|
-
method: 'notifications/claude/channel',
|
|
177
|
-
params: { content: `✦ Connected to inErrata.`, meta: { type: 'welcome' } },
|
|
178
|
-
});
|
|
201
|
+
await pushWelcomeNotif(`✦ Connected to inErrata.`, { type: 'welcome' });
|
|
179
202
|
return;
|
|
180
203
|
}
|
|
181
204
|
const level = me.level ?? 1;
|
|
182
205
|
const xp = me.xp ?? 0;
|
|
183
206
|
const unreadLabel = unreadCount > 0 ? ` · 💬${unreadCount}` : '';
|
|
184
207
|
const lines = [`✦ inErrata · @${me.handle} · Lv.${level} · ✨${xp}xp${unreadLabel}`];
|
|
185
|
-
if (
|
|
186
|
-
lines.push(`┃ 🟢 ${
|
|
208
|
+
if (onlineReachable.length)
|
|
209
|
+
lines.push(`┃ 🟢 ${onlineReachable.join(', ')}`);
|
|
210
|
+
if (onlineMcpOnly.length)
|
|
211
|
+
lines.push(`┃ 🔵 ${onlineMcpOnly.join(', ')}`);
|
|
187
212
|
lines.push(`✦`);
|
|
188
|
-
await
|
|
189
|
-
method: 'notifications/claude/channel',
|
|
190
|
-
params: { content: lines.join('\n'), meta: { type: 'welcome' } },
|
|
191
|
-
});
|
|
213
|
+
await pushWelcomeNotif(lines.join('\n'), { type: 'welcome' });
|
|
192
214
|
}
|
|
193
215
|
catch {
|
|
194
216
|
// Best-effort
|
|
@@ -248,6 +270,10 @@ async function connectAnnouncementChannel(retryDelay = 1000) {
|
|
|
248
270
|
console.error('[inerrata-channel] Connected to announcement channel (session:', sessionId, ')');
|
|
249
271
|
// Reset backoff on successful connection
|
|
250
272
|
retryDelay = 1000;
|
|
273
|
+
// Start heartbeat now that we have a live session — keeps channelOnlineAt
|
|
274
|
+
// in sync with actual push-path availability. Stopped in the finally block.
|
|
275
|
+
if (!heartbeatTimer)
|
|
276
|
+
startHeartbeat();
|
|
251
277
|
// Send notifications/initialized (MCP protocol requirement) and push
|
|
252
278
|
// welcome in a single fire-and-forget POST — the initialized notification
|
|
253
279
|
// tells the server the session is ready, and the welcome greets the user.
|
|
@@ -317,6 +343,14 @@ async function connectAnnouncementChannel(retryDelay = 1000) {
|
|
|
317
343
|
catch (err) {
|
|
318
344
|
console.error('[inerrata-channel] Stream error:', err, '— reconnecting in', retryDelay, 'ms');
|
|
319
345
|
}
|
|
346
|
+
finally {
|
|
347
|
+
// Stop heartbeat when session ends — channelOnlineAt will go stale naturally.
|
|
348
|
+
// This ensures notifyReachable accurately reflects push-path availability.
|
|
349
|
+
if (heartbeatTimer) {
|
|
350
|
+
clearInterval(heartbeatTimer);
|
|
351
|
+
heartbeatTimer = undefined;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
320
354
|
setTimeout(() => connectAnnouncementChannel(Math.min(retryDelay * 2, 30_000)), retryDelay);
|
|
321
355
|
}
|
|
322
356
|
// ---------------------------------------------------------------------------
|
|
@@ -346,7 +380,6 @@ function sendOffline() {
|
|
|
346
380
|
async function main() {
|
|
347
381
|
const transport = new StdioServerTransport();
|
|
348
382
|
await server.connect(transport);
|
|
349
|
-
startHeartbeat();
|
|
350
383
|
connectAnnouncementChannel().catch(console.error);
|
|
351
384
|
}
|
|
352
385
|
// Graceful shutdown — tell the server we're going offline
|