@abraca/mcp 2.6.0 → 2.7.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 +10134 -10006
- package/dist/abracadabra-mcp.cjs.map +1 -1
- package/dist/abracadabra-mcp.esm.js +10206 -10078
- package/dist/abracadabra-mcp.esm.js.map +1 -1
- package/dist/index.d.ts +29 -2
- package/package.json +2 -2
- package/src/hook-bridge.ts +305 -197
- package/src/index.ts +150 -136
- package/src/server.ts +1160 -940
- package/src/tools/channel.ts +138 -98
- package/src/tools/hooks.ts +44 -35
package/src/tools/channel.ts
CHANGED
|
@@ -1,115 +1,155 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Channel tools — reply to channel events and send chat messages.
|
|
3
3
|
*/
|
|
4
|
-
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
5
|
-
import { z } from 'zod'
|
|
6
|
-
import type { AbracadabraMCPServer } from '../server.ts'
|
|
7
|
-
import { makeEntryMap } from '@abraca/dabra'
|
|
8
|
-
import { populateYDocFromMarkdown } from '../converters/markdownToYjs.ts'
|
|
9
4
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
doc_id: z.string().describe('Document ID to create the reply under.'),
|
|
16
|
-
text: z.string().describe('Markdown content for the reply.'),
|
|
17
|
-
task_id: z.string().optional().describe('If replying to an ai:task, the task ID to clear from awareness.'),
|
|
18
|
-
},
|
|
19
|
-
async ({ doc_id, text, task_id }) => {
|
|
20
|
-
try {
|
|
21
|
-
server.setAutoStatus('writing', doc_id)
|
|
22
|
-
server.setActiveToolCall({ name: 'reply', target: doc_id })
|
|
5
|
+
import { makeEntryMap } from "@abraca/dabra";
|
|
6
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { populateYDocFromMarkdown } from "../converters/markdownToYjs.ts";
|
|
9
|
+
import type { AbracadabraMCPServer } from "../server.ts";
|
|
23
10
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
11
|
+
export function registerChannelTools(
|
|
12
|
+
mcp: McpServer,
|
|
13
|
+
server: AbracadabraMCPServer,
|
|
14
|
+
) {
|
|
15
|
+
mcp.tool(
|
|
16
|
+
"reply",
|
|
17
|
+
"Create a document reply. Creates a child document under doc_id with the reply content as a rich-text document. Use this for structured, persistent responses. For conversational chat, use send_chat_message instead.",
|
|
18
|
+
{
|
|
19
|
+
doc_id: z.string().describe("Document ID to create the reply under."),
|
|
20
|
+
text: z.string().describe("Markdown content for the reply."),
|
|
21
|
+
task_id: z
|
|
22
|
+
.string()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe(
|
|
25
|
+
"If replying to an ai:task, the task ID to clear from awareness.",
|
|
26
|
+
),
|
|
27
|
+
},
|
|
28
|
+
async ({ doc_id, text, task_id }) => {
|
|
29
|
+
try {
|
|
30
|
+
server.setAutoStatus("writing", doc_id);
|
|
31
|
+
server.setActiveToolCall({ name: "reply", target: doc_id });
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
const treeMap = server.getTreeMap();
|
|
34
|
+
const rootDoc = server.rootDocument;
|
|
35
|
+
if (!treeMap || !rootDoc) {
|
|
36
|
+
return {
|
|
37
|
+
content: [{ type: "text" as const, text: "Not connected" }],
|
|
38
|
+
isError: true,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
35
41
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}))
|
|
45
|
-
})
|
|
42
|
+
const timestamp = new Date()
|
|
43
|
+
.toISOString()
|
|
44
|
+
.replace("T", " ")
|
|
45
|
+
.slice(0, 19);
|
|
46
|
+
const snippet = text.slice(0, 40).replace(/\n/g, " ");
|
|
47
|
+
const label = `AI Reply — ${timestamp}: ${snippet}`;
|
|
48
|
+
const replyId = crypto.randomUUID();
|
|
49
|
+
const now = Date.now();
|
|
46
50
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
rootDoc.transact(() => {
|
|
52
|
+
treeMap.set(
|
|
53
|
+
replyId,
|
|
54
|
+
makeEntryMap({
|
|
55
|
+
label,
|
|
56
|
+
parentId: doc_id,
|
|
57
|
+
order: now,
|
|
58
|
+
type: "doc",
|
|
59
|
+
createdAt: now,
|
|
60
|
+
updatedAt: now,
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
});
|
|
50
64
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
65
|
+
const replyProvider = await server.getChildProvider(replyId);
|
|
66
|
+
const fragment = replyProvider.document.getXmlFragment("default");
|
|
67
|
+
populateYDocFromMarkdown(fragment, text);
|
|
54
68
|
|
|
69
|
+
if (task_id) {
|
|
70
|
+
server.clearAiTask(task_id);
|
|
71
|
+
}
|
|
55
72
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
return {
|
|
74
|
+
content: [
|
|
75
|
+
{
|
|
76
|
+
type: "text" as const,
|
|
77
|
+
text: JSON.stringify({ replyDocId: replyId, parentId: doc_id }),
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
} catch (error: any) {
|
|
82
|
+
return {
|
|
83
|
+
content: [{ type: "text" as const, text: `Error: ${error.message}` }],
|
|
84
|
+
isError: true,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
);
|
|
67
89
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
90
|
+
mcp.tool(
|
|
91
|
+
"send_chat_message",
|
|
92
|
+
'Send a chat message visible in the dashboard chat UI. Pass channel_doc_id — the UUID of a doc with kind "channel" (group chat under a Space) or "dm" (direct message). Use list_connected_users + the doc tree to discover channels.',
|
|
93
|
+
{
|
|
94
|
+
channel_doc_id: z
|
|
95
|
+
.string()
|
|
96
|
+
.describe('Channel doc UUID (kind = "channel" or "dm")'),
|
|
97
|
+
text: z.string().describe("Message text"),
|
|
98
|
+
},
|
|
99
|
+
async ({ channel_doc_id, text }) => {
|
|
100
|
+
try {
|
|
101
|
+
const rootProvider = server.rootYProvider;
|
|
102
|
+
if (!rootProvider) {
|
|
103
|
+
return {
|
|
104
|
+
content: [{ type: "text" as const, text: "Not connected" }],
|
|
105
|
+
isError: true,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
81
108
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
109
|
+
// Normalize literal escape sequences. Some LLM outputs emit the
|
|
110
|
+
// 2-char `\n` / `\t` / `\r` instead of the real control chars, which
|
|
111
|
+
// then render literally in the chat (markdown renderer can't see a
|
|
112
|
+
// newline where there is just "\n"). Convert them back to real chars.
|
|
113
|
+
const normalized = text
|
|
114
|
+
.replace(/\\r\\n/g, "\n")
|
|
115
|
+
.replace(/\\n/g, "\n")
|
|
116
|
+
.replace(/\\t/g, "\t")
|
|
117
|
+
.replace(/\\r/g, "\n");
|
|
91
118
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
119
|
+
// Show a brief "writing" phase so the dashboard renders typing dots
|
|
120
|
+
// just before the message lands (the reasoning phase showed the
|
|
121
|
+
// incantation; this is the distinct compose/send phase). Clear any
|
|
122
|
+
// tool pill, mark status 'writing' (the dashboard maps this to typing
|
|
123
|
+
// dots, not the phrase), emit a typing frame, and pause briefly so
|
|
124
|
+
// the dots are actually visible before the bubble appears.
|
|
125
|
+
server.setActiveToolCall(null);
|
|
126
|
+
server.setWritingStatus(channel_doc_id);
|
|
127
|
+
await new Promise((r) => setTimeout(r, 650));
|
|
98
128
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
129
|
+
rootProvider.sendStateless(
|
|
130
|
+
JSON.stringify({
|
|
131
|
+
type: "messages:send",
|
|
132
|
+
channel_doc_id,
|
|
133
|
+
content: normalized,
|
|
134
|
+
mentions: [],
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
105
137
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
138
|
+
// End the turn AFTER the send: clears status, tool pill, turn id and
|
|
139
|
+
// stops the typing heartbeat so nothing lingers after the reply.
|
|
140
|
+
server.setAutoStatus(null);
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
content: [
|
|
144
|
+
{ type: "text" as const, text: `Sent to ${channel_doc_id}` },
|
|
145
|
+
],
|
|
146
|
+
};
|
|
147
|
+
} catch (error: any) {
|
|
148
|
+
return {
|
|
149
|
+
content: [{ type: "text" as const, text: `Error: ${error.message}` }],
|
|
150
|
+
isError: true,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
);
|
|
115
155
|
}
|
package/src/tools/hooks.ts
CHANGED
|
@@ -1,42 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Hook tools — provides Claude Code hook configuration for activity bridging.
|
|
3
3
|
*/
|
|
4
|
-
import type { McpServer } from
|
|
5
|
-
import type { HookBridge } from
|
|
4
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import type { HookBridge } from "../hook-bridge.ts";
|
|
6
6
|
|
|
7
7
|
export function registerHookTools(mcp: McpServer, hookBridge: HookBridge) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
hooks: {
|
|
26
|
-
PreToolUse: [{ hooks: [hookEntry] }],
|
|
27
|
-
PostToolUse: [{ hooks: [hookEntry] }],
|
|
28
|
-
SubagentStart: [{ hooks: [hookEntry] }],
|
|
29
|
-
SubagentStop: [{ hooks: [hookEntry] }],
|
|
30
|
-
Stop: [{ hooks: [hookEntry] }],
|
|
31
|
-
},
|
|
32
|
-
}
|
|
8
|
+
mcp.tool(
|
|
9
|
+
"get_hook_config",
|
|
10
|
+
'Returns Claude Code hook configuration JSON for bridging activity to the Abracadabra dashboard. Copy the "hooks" object into your .claude/settings.json (or settings.local.json) to enable real-time tool-call activity cards in the chat, visible to all connected users.',
|
|
11
|
+
{},
|
|
12
|
+
async () => {
|
|
13
|
+
const config = { hooks: buildHookConfig(hookBridge.portFile) };
|
|
14
|
+
return {
|
|
15
|
+
content: [
|
|
16
|
+
{
|
|
17
|
+
type: "text",
|
|
18
|
+
text: JSON.stringify(config, null, 2),
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
);
|
|
24
|
+
}
|
|
33
25
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Build the Claude Code `hooks` object. We use a `command` hook (not `http`)
|
|
28
|
+
* that reads the bridge's port file at execution time and POSTs the event
|
|
29
|
+
* JSON (received on stdin) to it. This survives MCP restarts that pick a new
|
|
30
|
+
* random port — the URL is resolved fresh each call instead of being baked in.
|
|
31
|
+
*
|
|
32
|
+
* The command is a no-op (`exit 0`) when the port file is missing or curl
|
|
33
|
+
* fails, so a stopped bridge never blocks a tool call.
|
|
34
|
+
*/
|
|
35
|
+
export function buildHookConfig(portFile: string): Record<string, unknown> {
|
|
36
|
+
const cmd = `P=$(cat ${JSON.stringify(portFile)} 2>/dev/null); [ -n "$P" ] && curl -s -m 3 -X POST "http://127.0.0.1:$P/hook" --data-binary @- >/dev/null 2>&1; exit 0`;
|
|
37
|
+
const hookEntry = { type: "command", command: cmd, timeout: 5 };
|
|
38
|
+
const events = [
|
|
39
|
+
"UserPromptSubmit",
|
|
40
|
+
"PreToolUse",
|
|
41
|
+
"PostToolUse",
|
|
42
|
+
"SubagentStart",
|
|
43
|
+
"SubagentStop",
|
|
44
|
+
"Stop",
|
|
45
|
+
];
|
|
46
|
+
const hooks: Record<string, unknown> = {};
|
|
47
|
+
for (const ev of events) {
|
|
48
|
+
hooks[ev] = [{ hooks: [hookEntry] }];
|
|
49
|
+
}
|
|
50
|
+
return hooks;
|
|
42
51
|
}
|