@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.
Files changed (2) hide show
  1. package/dist/index.js +47 -17
  2. 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
- await server.notification({
142
- method: 'notifications/claude/channel',
143
- params: {
144
- content: data.content ?? '',
145
- meta: data.meta ?? {},
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 server.notification({
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 server.notification({
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inerrata/channel",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Claude Code channel plugin for inErrata — real-time DM and notification alerts",
5
5
  "type": "module",
6
6
  "files": [