@abraca/mcp 2.10.0 → 2.13.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/dist/abracadabra-mcp.cjs +62 -1
- package/dist/abracadabra-mcp.cjs.map +1 -1
- package/dist/abracadabra-mcp.esm.js +63 -2
- package/dist/abracadabra-mcp.esm.js.map +1 -1
- package/dist/index.d.ts +15 -0
- package/package.json +2 -2
- package/src/server.ts +97 -1
package/dist/index.d.ts
CHANGED
|
@@ -43,6 +43,8 @@ declare class AbracadabraMCPServer {
|
|
|
43
43
|
/** Rolling buffer of the last N tool calls in the current turn, surfaced via awareness. */
|
|
44
44
|
private _toolHistory;
|
|
45
45
|
private static readonly TOOL_HISTORY_MAX;
|
|
46
|
+
/** De-dupes concurrent connection heals across parallel tool calls. */
|
|
47
|
+
private _reconnecting;
|
|
46
48
|
private _inboxStarted;
|
|
47
49
|
private _inboxDocId;
|
|
48
50
|
private _inboxDoc;
|
|
@@ -102,6 +104,19 @@ declare class AbracadabraMCPServer {
|
|
|
102
104
|
* child-content provider cache survives — content docs are keyed by global
|
|
103
105
|
* guid, independent of which space is active.
|
|
104
106
|
*/
|
|
107
|
+
/**
|
|
108
|
+
* Heal the active connection before a tool op so a dropped/idle WebSocket or
|
|
109
|
+
* an expired JWT doesn't surface as a 15s sync timeout ("MCP not responding").
|
|
110
|
+
* Refreshes the token, lets the SDK's own auto-reconnect finish if it's mid-
|
|
111
|
+
* flight, and rebuilds the space connection from scratch if the socket is
|
|
112
|
+
* truly dead. De-duped so parallel tool calls heal once. Best-effort: never
|
|
113
|
+
* throws — a failed heal falls through to the normal (possibly erroring) op.
|
|
114
|
+
*/
|
|
115
|
+
/** Whether a provider's shared WebSocket is currently connected. Isolated in
|
|
116
|
+
* its own scope so repeated checks don't trip TS control-flow narrowing on
|
|
117
|
+
* the live `connectionStatus` getter. */
|
|
118
|
+
private _wsConnected;
|
|
119
|
+
ensureConnected(): Promise<void>;
|
|
105
120
|
ensureSpaceActive(targetId: string | null | undefined): Promise<boolean>;
|
|
106
121
|
/** Get the root doc-tree Y.Map of the active space. */
|
|
107
122
|
getTreeMap(): Y.Map<any> | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abraca/mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.0",
|
|
4
4
|
"description": "MCP server for Abracadabra — AI agent collaboration on CRDT documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"y-protocols": "^1.0.6",
|
|
37
37
|
"yjs": "^13.6.8",
|
|
38
38
|
"zod": "^4.3.6",
|
|
39
|
-
"@abraca/convert": "2.
|
|
39
|
+
"@abraca/convert": "2.13.0"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"test": "node --no-warnings --conditions=source --experimental-transform-types --test 'tests/*.test.ts'"
|
package/src/server.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
Kind,
|
|
16
16
|
recordFromYAny,
|
|
17
17
|
SERVER_ROOT_ID,
|
|
18
|
+
WebSocketStatus,
|
|
18
19
|
} from "@abraca/dabra";
|
|
19
20
|
import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
20
21
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -99,6 +100,8 @@ export class AbracadabraMCPServer {
|
|
|
99
100
|
// `messages:new_message` stateless broadcast only reaches subscribers of the
|
|
100
101
|
// channel/DM doc, and the agent never subscribes to DM docs. So we observe
|
|
101
102
|
// the inbox's `entries` Y.Array and dispatch from there.
|
|
103
|
+
/** De-dupes concurrent connection heals across parallel tool calls. */
|
|
104
|
+
private _reconnecting: Promise<void> | null = null;
|
|
102
105
|
private _inboxStarted = false;
|
|
103
106
|
private _inboxDocId: string | null = null;
|
|
104
107
|
private _inboxDoc: Y.Doc | null = null;
|
|
@@ -335,9 +338,88 @@ export class AbracadabraMCPServer {
|
|
|
335
338
|
* child-content provider cache survives — content docs are keyed by global
|
|
336
339
|
* guid, independent of which space is active.
|
|
337
340
|
*/
|
|
341
|
+
/**
|
|
342
|
+
* Heal the active connection before a tool op so a dropped/idle WebSocket or
|
|
343
|
+
* an expired JWT doesn't surface as a 15s sync timeout ("MCP not responding").
|
|
344
|
+
* Refreshes the token, lets the SDK's own auto-reconnect finish if it's mid-
|
|
345
|
+
* flight, and rebuilds the space connection from scratch if the socket is
|
|
346
|
+
* truly dead. De-duped so parallel tool calls heal once. Best-effort: never
|
|
347
|
+
* throws — a failed heal falls through to the normal (possibly erroring) op.
|
|
348
|
+
*/
|
|
349
|
+
/** Whether a provider's shared WebSocket is currently connected. Isolated in
|
|
350
|
+
* its own scope so repeated checks don't trip TS control-flow narrowing on
|
|
351
|
+
* the live `connectionStatus` getter. */
|
|
352
|
+
private _wsConnected(provider: AbracadabraProvider): boolean {
|
|
353
|
+
return provider.connectionStatus === WebSocketStatus.Connected;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async ensureConnected(): Promise<void> {
|
|
357
|
+
if (this._reconnecting) return this._reconnecting;
|
|
358
|
+
this._reconnecting = (async () => {
|
|
359
|
+
try {
|
|
360
|
+
// 1) Refresh the JWT proactively so a reconnect doesn't retry with a
|
|
361
|
+
// stale token (which the server rejects, stopping the WS retry loop).
|
|
362
|
+
if (!this.client.isTokenValid() && this._signFn && this._userId) {
|
|
363
|
+
try {
|
|
364
|
+
await this.client.loginWithKey(this._userId, this._signFn);
|
|
365
|
+
} catch (e) {
|
|
366
|
+
console.error("[abracadabra-mcp] Re-auth during heal failed:", e);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const conn = this._activeConnection;
|
|
371
|
+
if (!conn) return; // cold start — connect() handles it
|
|
372
|
+
if (this._wsConnected(conn.provider)) return;
|
|
373
|
+
|
|
374
|
+
// 2) Socket is Connecting/Disconnected — give the SDK's own reconnect a
|
|
375
|
+
// bounded window to come back before we tear anything down.
|
|
376
|
+
try {
|
|
377
|
+
await waitForSync(conn.provider, 6000);
|
|
378
|
+
} catch {
|
|
379
|
+
/* fell through to rebuild */
|
|
380
|
+
}
|
|
381
|
+
// connectionStatus is a live getter — re-check via the helper (fresh
|
|
382
|
+
// scope) since the WS may have come back during the wait.
|
|
383
|
+
if (this._wsConnected(conn.provider)) return;
|
|
384
|
+
|
|
385
|
+
// 3) Still dead → rebuild the active space from scratch. A fresh
|
|
386
|
+
// provider re-auths and reconnects cleanly; cached child providers
|
|
387
|
+
// rode the old (now-destroyed) socket, so drop them too.
|
|
388
|
+
console.error(
|
|
389
|
+
"[abracadabra-mcp] Active connection dead — rebuilding space provider…",
|
|
390
|
+
);
|
|
391
|
+
const docId = conn.docId;
|
|
392
|
+
this._spaceConnections.delete(docId);
|
|
393
|
+
try {
|
|
394
|
+
conn.provider.destroy();
|
|
395
|
+
} catch {
|
|
396
|
+
/* already gone */
|
|
397
|
+
}
|
|
398
|
+
for (const [, cached] of this.childCache) {
|
|
399
|
+
try {
|
|
400
|
+
cached.provider.destroy();
|
|
401
|
+
} catch {
|
|
402
|
+
/* already gone */
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
this.childCache.clear();
|
|
406
|
+
try {
|
|
407
|
+
await this._connectToSpace(docId);
|
|
408
|
+
console.error("[abracadabra-mcp] Space provider rebuilt + synced");
|
|
409
|
+
} catch (e) {
|
|
410
|
+
console.error("[abracadabra-mcp] Connection rebuild failed:", e);
|
|
411
|
+
}
|
|
412
|
+
} finally {
|
|
413
|
+
this._reconnecting = null;
|
|
414
|
+
}
|
|
415
|
+
})();
|
|
416
|
+
return this._reconnecting;
|
|
417
|
+
}
|
|
418
|
+
|
|
338
419
|
async ensureSpaceActive(
|
|
339
420
|
targetId: string | null | undefined,
|
|
340
421
|
): Promise<boolean> {
|
|
422
|
+
await this.ensureConnected();
|
|
341
423
|
if (!targetId || targetId === this._rootDocId) return true;
|
|
342
424
|
|
|
343
425
|
// A top-level Space id — activate it directly (handles the common
|
|
@@ -411,11 +493,25 @@ export class AbracadabraMCPServer {
|
|
|
411
493
|
* Caches providers and waits for sync before returning.
|
|
412
494
|
*/
|
|
413
495
|
async getChildProvider(docId: string): Promise<AbracadabraProvider> {
|
|
496
|
+
// Heal a dropped socket / stale JWT first; this may rebuild the active
|
|
497
|
+
// provider (and clear the child cache), so run it before the cache check.
|
|
498
|
+
await this.ensureConnected();
|
|
499
|
+
|
|
414
500
|
const cached = this.childCache.get(docId);
|
|
415
|
-
if (cached) {
|
|
501
|
+
if (cached && cached.provider.connectionStatus !== WebSocketStatus.Disconnected) {
|
|
416
502
|
cached.lastAccessed = Date.now();
|
|
417
503
|
return cached.provider;
|
|
418
504
|
}
|
|
505
|
+
if (cached) {
|
|
506
|
+
// Cached provider rode a socket that's since dropped — discard and reload
|
|
507
|
+
// so we never hand back a dead provider that times out on every op.
|
|
508
|
+
try {
|
|
509
|
+
cached.provider.destroy();
|
|
510
|
+
} catch {
|
|
511
|
+
/* already gone */
|
|
512
|
+
}
|
|
513
|
+
this.childCache.delete(docId);
|
|
514
|
+
}
|
|
419
515
|
|
|
420
516
|
const activeProvider = this._activeConnection?.provider;
|
|
421
517
|
if (!activeProvider) {
|