@inerrata/channel 0.3.2 → 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 +47 -17
- 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 {
|
|
@@ -173,10 +198,7 @@ async function pushWelcome() {
|
|
|
173
198
|
const onlineMcpOnly = connections.filter(c => c.online && c.notifyReachable === false).map(c => c.handle);
|
|
174
199
|
const unreadCount = Array.isArray(inboxMessages) ? inboxMessages.filter(m => !m.read).length : 0;
|
|
175
200
|
if (!me) {
|
|
176
|
-
await
|
|
177
|
-
method: 'notifications/claude/channel',
|
|
178
|
-
params: { content: `✦ Connected to inErrata.`, meta: { type: 'welcome' } },
|
|
179
|
-
});
|
|
201
|
+
await pushWelcomeNotif(`✦ Connected to inErrata.`, { type: 'welcome' });
|
|
180
202
|
return;
|
|
181
203
|
}
|
|
182
204
|
const level = me.level ?? 1;
|
|
@@ -188,10 +210,7 @@ async function pushWelcome() {
|
|
|
188
210
|
if (onlineMcpOnly.length)
|
|
189
211
|
lines.push(`┃ 🔵 ${onlineMcpOnly.join(', ')}`);
|
|
190
212
|
lines.push(`✦`);
|
|
191
|
-
await
|
|
192
|
-
method: 'notifications/claude/channel',
|
|
193
|
-
params: { content: lines.join('\n'), meta: { type: 'welcome' } },
|
|
194
|
-
});
|
|
213
|
+
await pushWelcomeNotif(lines.join('\n'), { type: 'welcome' });
|
|
195
214
|
}
|
|
196
215
|
catch {
|
|
197
216
|
// Best-effort
|
|
@@ -251,6 +270,10 @@ async function connectAnnouncementChannel(retryDelay = 1000) {
|
|
|
251
270
|
console.error('[inerrata-channel] Connected to announcement channel (session:', sessionId, ')');
|
|
252
271
|
// Reset backoff on successful connection
|
|
253
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();
|
|
254
277
|
// Send notifications/initialized (MCP protocol requirement) and push
|
|
255
278
|
// welcome in a single fire-and-forget POST — the initialized notification
|
|
256
279
|
// tells the server the session is ready, and the welcome greets the user.
|
|
@@ -320,6 +343,14 @@ async function connectAnnouncementChannel(retryDelay = 1000) {
|
|
|
320
343
|
catch (err) {
|
|
321
344
|
console.error('[inerrata-channel] Stream error:', err, '— reconnecting in', retryDelay, 'ms');
|
|
322
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
|
+
}
|
|
323
354
|
setTimeout(() => connectAnnouncementChannel(Math.min(retryDelay * 2, 30_000)), retryDelay);
|
|
324
355
|
}
|
|
325
356
|
// ---------------------------------------------------------------------------
|
|
@@ -349,7 +380,6 @@ function sendOffline() {
|
|
|
349
380
|
async function main() {
|
|
350
381
|
const transport = new StdioServerTransport();
|
|
351
382
|
await server.connect(transport);
|
|
352
|
-
startHeartbeat();
|
|
353
383
|
connectAnnouncementChannel().catch(console.error);
|
|
354
384
|
}
|
|
355
385
|
// Graceful shutdown — tell the server we're going offline
|