@abraca/mcp 1.0.12 → 1.0.15

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.d.ts CHANGED
@@ -8728,6 +8728,9 @@ declare class AbracadabraMCPServer {
8728
8728
  private _serverRef;
8729
8729
  private _handledTaskIds;
8730
8730
  private _userId;
8731
+ private _statusClearTimer;
8732
+ private _typingInterval;
8733
+ private _lastChatChannel;
8731
8734
  constructor(config: MCPServerConfig);
8732
8735
  get agentName(): string;
8733
8736
  get agentColor(): string;
@@ -8778,6 +8781,25 @@ declare class AbracadabraMCPServer {
8778
8781
  clearAiTask(taskId: string): void;
8779
8782
  /** Handle incoming stateless chat messages and dispatch as channel notifications. */
8780
8783
  private _handleStatelessChat;
8784
+ /**
8785
+ * Set the agent's status in root awareness with auto-clear after idle.
8786
+ */
8787
+ setAutoStatus(status: string | null, docId?: string): void;
8788
+ /** Re-send typing indicator every 2s so dashboard keeps showing it (expires at 3s). */
8789
+ private _startTypingInterval;
8790
+ private _stopTypingInterval;
8791
+ /**
8792
+ * Broadcast which tool the agent is currently executing.
8793
+ * Dashboard renders this as a ChatTool indicator.
8794
+ */
8795
+ setActiveToolCall(toolCall: {
8796
+ name: string;
8797
+ target?: string;
8798
+ } | null): void;
8799
+ /**
8800
+ * Send a typing indicator to a chat channel.
8801
+ */
8802
+ sendTypingIndicator(channel: string): void;
8781
8803
  /** Graceful shutdown. */
8782
8804
  destroy(): Promise<void>;
8783
8805
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abraca/mcp",
3
- "version": "1.0.12",
3
+ "version": "1.0.15",
4
4
  "description": "MCP server for Abracadabra — AI agent collaboration on CRDT documents",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/server.ts CHANGED
@@ -48,6 +48,9 @@ export class AbracadabraMCPServer {
48
48
  private _serverRef: Server | null = null
49
49
  private _handledTaskIds = new Set<string>()
50
50
  private _userId: string | null = null
51
+ private _statusClearTimer: ReturnType<typeof setTimeout> | null = null
52
+ private _typingInterval: ReturnType<typeof setInterval> | null = null
53
+ private _lastChatChannel: string | null = null
51
54
 
52
55
  constructor(config: MCPServerConfig) {
53
56
  this.config = config
@@ -176,6 +179,7 @@ export class AbracadabraMCPServer {
176
179
  name: this.agentName,
177
180
  color: this.agentColor,
178
181
  publicKey: this._userId,
182
+ isAgent: true,
179
183
  })
180
184
 
181
185
  const conn: SpaceConnection = { doc, provider, docId }
@@ -240,6 +244,7 @@ export class AbracadabraMCPServer {
240
244
  name: this.agentName,
241
245
  color: this.agentColor,
242
246
  publicKey: this._userId,
247
+ isAgent: true,
243
248
  })
244
249
 
245
250
  this.childCache.set(docId, {
@@ -323,6 +328,7 @@ export class AbracadabraMCPServer {
323
328
  : 'Unknown'
324
329
 
325
330
  console.error(`[abracadabra-mcp] Handling ai:task id=${id} from ${senderName}: ${text.slice(0, 80)}`)
331
+ this.setAutoStatus('thinking')
326
332
  this._dispatchAiTask({
327
333
  id,
328
334
  text,
@@ -400,6 +406,25 @@ export class AbracadabraMCPServer {
400
406
  }
401
407
  }
402
408
 
409
+ // Auto-mark channel as read so sender sees a read receipt
410
+ if (channel) {
411
+ const rootProvider = this._activeConnection?.provider
412
+ if (rootProvider) {
413
+ rootProvider.sendStateless(JSON.stringify({
414
+ type: 'chat:mark_read',
415
+ channel,
416
+ timestamp: Math.floor(Date.now() / 1000),
417
+ }))
418
+ }
419
+ // Send immediate typing indicator and start periodic re-sends
420
+ this._lastChatChannel = channel
421
+ this.sendTypingIndicator(channel)
422
+ this._startTypingInterval(channel)
423
+ }
424
+
425
+ // Set status to "thinking" so dashboard shows AI is processing
426
+ this.setAutoStatus('thinking')
427
+
403
428
  await this._serverRef.notification({
404
429
  method: 'notifications/claude/channel',
405
430
  params: {
@@ -420,8 +445,82 @@ export class AbracadabraMCPServer {
420
445
  }
421
446
  }
422
447
 
448
+ /**
449
+ * Set the agent's status in root awareness with auto-clear after idle.
450
+ */
451
+ setAutoStatus(status: string | null, docId?: string): void {
452
+ const provider = this._activeConnection?.provider
453
+ if (!provider) return
454
+
455
+ if (this._statusClearTimer) {
456
+ clearTimeout(this._statusClearTimer)
457
+ this._statusClearTimer = null
458
+ }
459
+
460
+ provider.awareness.setLocalStateField('status', status)
461
+ if (docId !== undefined) {
462
+ provider.awareness.setLocalStateField('docId', docId)
463
+ }
464
+
465
+ // When clearing status, also stop typing interval
466
+ if (!status) {
467
+ this._stopTypingInterval()
468
+ }
469
+
470
+ // Auto-clear status after 30s of no updates
471
+ if (status) {
472
+ this._statusClearTimer = setTimeout(() => {
473
+ provider.awareness.setLocalStateField('status', null)
474
+ provider.awareness.setLocalStateField('activeToolCall', null)
475
+ this._stopTypingInterval()
476
+ }, 30_000)
477
+ }
478
+ }
479
+
480
+ /** Re-send typing indicator every 2s so dashboard keeps showing it (expires at 3s). */
481
+ private _startTypingInterval(channel: string): void {
482
+ this._stopTypingInterval()
483
+ this._typingInterval = setInterval(() => {
484
+ this.sendTypingIndicator(channel)
485
+ }, 2000)
486
+ }
487
+
488
+ private _stopTypingInterval(): void {
489
+ if (this._typingInterval) {
490
+ clearInterval(this._typingInterval)
491
+ this._typingInterval = null
492
+ }
493
+ this._lastChatChannel = null
494
+ }
495
+
496
+ /**
497
+ * Broadcast which tool the agent is currently executing.
498
+ * Dashboard renders this as a ChatTool indicator.
499
+ */
500
+ setActiveToolCall(toolCall: { name: string; target?: string } | null): void {
501
+ this._activeConnection?.provider?.awareness.setLocalStateField('activeToolCall', toolCall)
502
+ }
503
+
504
+ /**
505
+ * Send a typing indicator to a chat channel.
506
+ */
507
+ sendTypingIndicator(channel: string): void {
508
+ const rootProvider = this._activeConnection?.provider
509
+ if (!rootProvider) return
510
+ rootProvider.sendStateless(JSON.stringify({
511
+ type: 'chat:typing',
512
+ channel,
513
+ sender_name: this.agentName,
514
+ }))
515
+ }
516
+
423
517
  /** Graceful shutdown. */
424
518
  async destroy(): Promise<void> {
519
+ this._stopTypingInterval()
520
+ if (this._statusClearTimer) {
521
+ clearTimeout(this._statusClearTimer)
522
+ this._statusClearTimer = null
523
+ }
425
524
  if (this.evictionTimer) {
426
525
  clearInterval(this.evictionTimer)
427
526
  this.evictionTimer = null
@@ -17,9 +17,13 @@ export function registerChannelTools(mcp: McpServer, server: AbracadabraMCPServe
17
17
  },
18
18
  async ({ doc_id, text, task_id }) => {
19
19
  try {
20
+ server.setAutoStatus('writing', doc_id)
21
+ server.setActiveToolCall({ name: 'reply', target: doc_id })
22
+
20
23
  const treeMap = server.getTreeMap()
21
24
  const rootDoc = server.rootDocument
22
25
  if (!treeMap || !rootDoc) {
26
+ server.setActiveToolCall(null)
23
27
  return { content: [{ type: 'text' as const, text: 'Not connected' }], isError: true }
24
28
  }
25
29
 
@@ -47,10 +51,13 @@ export function registerChannelTools(mcp: McpServer, server: AbracadabraMCPServe
47
51
  server.clearAiTask(task_id)
48
52
  }
49
53
 
54
+ server.setActiveToolCall(null)
55
+
50
56
  return {
51
57
  content: [{ type: 'text' as const, text: JSON.stringify({ replyDocId: replyId, parentId: doc_id }) }],
52
58
  }
53
59
  } catch (error: any) {
60
+ server.setActiveToolCall(null)
54
61
  return {
55
62
  content: [{ type: 'text' as const, text: `Error: ${error.message}` }],
56
63
  isError: true,
@@ -80,6 +87,10 @@ export function registerChannelTools(mcp: McpServer, server: AbracadabraMCPServe
80
87
  sender_name: server.agentName,
81
88
  }))
82
89
 
90
+ // Clear thinking/typing status after sending the reply
91
+ server.setAutoStatus(null)
92
+ server.setActiveToolCall(null)
93
+
83
94
  return { content: [{ type: 'text' as const, text: `Sent to ${channel}` }] }
84
95
  } catch (error: any) {
85
96
  return {
@@ -17,6 +17,9 @@ export function registerContentTools(mcp: McpServer, server: AbracadabraMCPServe
17
17
  },
18
18
  async ({ docId }) => {
19
19
  try {
20
+ server.setAutoStatus('reading', docId)
21
+ server.setActiveToolCall({ name: 'read_document', target: docId })
22
+
20
23
  const provider = await server.getChildProvider(docId)
21
24
  const fragment = provider.document.getXmlFragment('default')
22
25
 
@@ -48,6 +51,8 @@ export function registerContentTools(mcp: McpServer, server: AbracadabraMCPServe
48
51
  children.sort((a: any, b: any) => ((treeMap.get(a.id)?.order ?? 0) - (treeMap.get(b.id)?.order ?? 0)))
49
52
  }
50
53
 
54
+ server.setActiveToolCall(null)
55
+
51
56
  const result: Record<string, unknown> = { label, type, meta, markdown, children }
52
57
  return {
53
58
  content: [{
@@ -56,6 +61,7 @@ export function registerContentTools(mcp: McpServer, server: AbracadabraMCPServe
56
61
  }],
57
62
  }
58
63
  } catch (error: any) {
64
+ server.setActiveToolCall(null)
59
65
  return {
60
66
  content: [{ type: 'text', text: `Error reading document: ${error.message}` }],
61
67
  isError: true,
@@ -74,6 +80,9 @@ export function registerContentTools(mcp: McpServer, server: AbracadabraMCPServe
74
80
  },
75
81
  async ({ docId, markdown, mode }) => {
76
82
  try {
83
+ server.setAutoStatus('writing', docId)
84
+ server.setActiveToolCall({ name: 'write_document', target: docId })
85
+
77
86
  const writeMode = mode ?? 'replace'
78
87
  const provider = await server.getChildProvider(docId)
79
88
  const doc = provider.document
@@ -119,10 +128,13 @@ export function registerContentTools(mcp: McpServer, server: AbracadabraMCPServe
119
128
  server.setFocusedDoc(docId)
120
129
  server.setDocCursor(docId, fragment.length)
121
130
 
131
+ server.setActiveToolCall(null)
132
+
122
133
  return {
123
134
  content: [{ type: 'text', text: `Document ${docId} updated (${writeMode} mode)` }],
124
135
  }
125
136
  } catch (error: any) {
137
+ server.setActiveToolCall(null)
126
138
  return {
127
139
  content: [{ type: 'text', text: `Error writing document: ${error.message}` }],
128
140
  isError: true,
package/src/tools/tree.ts CHANGED
@@ -186,9 +186,15 @@ export function registerTreeTools(mcp: McpServer, server: AbracadabraMCPServer)
186
186
  meta: z.record(z.unknown()).optional().describe('Initial metadata (PageMeta fields: color as hex string, icon as Lucide kebab-case name like "star"/"code-2"/"users" — never emoji, dateStart, dateEnd, priority 0-4, tags array, etc). Omit icon entirely to use page type default.'),
187
187
  },
188
188
  async ({ parentId, label, type, meta }) => {
189
+ server.setAutoStatus('creating')
190
+ server.setActiveToolCall({ name: 'create_document', target: label })
191
+
189
192
  const treeMap = server.getTreeMap()
190
193
  const rootDoc = server.rootDocument
191
- if (!treeMap || !rootDoc) return { content: [{ type: 'text', text: 'Not connected' }] }
194
+ if (!treeMap || !rootDoc) {
195
+ server.setActiveToolCall(null)
196
+ return { content: [{ type: 'text', text: 'Not connected' }] }
197
+ }
192
198
 
193
199
  const id = crypto.randomUUID()
194
200
  const normalizedParent = normalizeRootId(parentId, server)
@@ -205,6 +211,8 @@ export function registerTreeTools(mcp: McpServer, server: AbracadabraMCPServer)
205
211
  })
206
212
  })
207
213
 
214
+ server.setActiveToolCall(null)
215
+
208
216
  return {
209
217
  content: [{
210
218
  type: 'text',