@markmnl/fmsg-mcp 0.2.7 → 0.2.9
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/CHANGELOG.md +59 -0
- package/README.md +18 -9
- package/SECURITY.md +2 -1
- package/dist/address.d.ts +1 -1
- package/dist/address.js +33 -4
- package/dist/client/client.d.ts +2 -2
- package/dist/client/client.js +3 -2
- package/dist/client/redact.d.ts +3 -1
- package/dist/client/redact.js +32 -6
- package/dist/client/types.d.ts +8 -1
- package/dist/instructions.d.ts +0 -2
- package/dist/instructions.js +19 -23
- package/dist/prompts.js +2 -1
- package/dist/render.d.ts +20 -2
- package/dist/render.js +39 -11
- package/dist/server.js +0 -2
- package/dist/thread.d.ts +15 -2
- package/dist/thread.js +37 -13
- package/dist/tools/common.d.ts +13 -0
- package/dist/tools/common.js +38 -5
- package/dist/tools/download.js +2 -3
- package/dist/tools/identity.js +9 -9
- package/dist/tools/list.js +2 -2
- package/dist/tools/read.d.ts +7 -0
- package/dist/tools/read.js +60 -25
- package/dist/tools/save.js +16 -4
- package/dist/tools/send.js +88 -41
- package/dist/tools/wait.js +42 -24
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/tools/send.js
CHANGED
|
@@ -1,17 +1,46 @@
|
|
|
1
1
|
import * as z from "zod/v4";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveAddress, sameAddress } from "../address.js";
|
|
3
3
|
import { toolError } from "../errors.js";
|
|
4
|
-
import { attachmentType, isoTime, participantsOf } from "../render.js";
|
|
4
|
+
import { addressList, addressText, attachmentType, headerValue, isoTime, participantsOf, sortAttachments } from "../render.js";
|
|
5
5
|
import { SENDS, attachmentItem, idSchema, ok, outputObject, resolverFor, withCaller } from "./common.js";
|
|
6
|
+
import { rootTopic } from "./read.js";
|
|
6
7
|
const IMMUTABLE = "fmsg messages are immutable: once sent they cannot be edited or recalled. Send within the user's requested task or authorized automation.";
|
|
7
|
-
const attachmentInput = z.
|
|
8
|
-
filename: z.string().regex(/^[A-Za-z0-9._-]+$/u, "letters, digits, dot, underscore, hyphen only"),
|
|
9
|
-
data_base64: z.string().min(1),
|
|
10
|
-
content_type: z.string().optional(),
|
|
8
|
+
const attachmentInput = z.strictObject({
|
|
9
|
+
filename: z.string().regex(/^[A-Za-z0-9._-]+$/u, "letters, digits, dot, underscore, hyphen only").describe("file name"),
|
|
10
|
+
data_base64: z.string().min(1).describe("base64 contents; the host sets the size limit"),
|
|
11
|
+
content_type: z.string().optional().describe("inferred from the filename when omitted"),
|
|
11
12
|
});
|
|
12
|
-
|
|
13
|
+
const IMPORTANT = "mark as important for recipients";
|
|
14
|
+
const NO_REPLY = "ask recipients (and their agents) not to reply";
|
|
15
|
+
/**
|
|
16
|
+
* Resolve recipients, dropping duplicates, and report each name that was not already a full address, so the model
|
|
17
|
+
* can confirm a guessed address with the user.
|
|
18
|
+
*/
|
|
19
|
+
function resolveRecipients(names, resolver) {
|
|
20
|
+
const to = [];
|
|
21
|
+
const resolved = [];
|
|
22
|
+
for (const name of names) {
|
|
23
|
+
const r = resolveAddress(name, resolver);
|
|
24
|
+
if (r.resolution !== "literal")
|
|
25
|
+
resolved.push({ input: name, address: r.address });
|
|
26
|
+
if (!to.some((existing) => sameAddress(existing, r.address)))
|
|
27
|
+
to.push(r.address);
|
|
28
|
+
}
|
|
29
|
+
return { to, resolved };
|
|
30
|
+
}
|
|
31
|
+
const resolvedOutput = z.array(outputObject({ input: z.string().optional(), address: z.string().optional() })).optional().describe("short names and how they resolved, when any were given; confirm with the user if an address looks wrong");
|
|
32
|
+
/** Warnings for the model to pass on: what was redacted, and short names that were expanded. */
|
|
33
|
+
function sendWarnings(redactions, kinds, resolved) {
|
|
34
|
+
const warnings = [];
|
|
35
|
+
if (redactions)
|
|
36
|
+
warnings.push(`Tell the user ${redactions} secret(s) (${kinds.join(", ")}) were replaced with placeholders before sending.`);
|
|
37
|
+
if (resolved.length)
|
|
38
|
+
warnings.push(`Short names were resolved: ${resolved.map((r) => `${r.input} → ${r.address}`).join(", ")}.`);
|
|
39
|
+
return warnings;
|
|
40
|
+
}
|
|
41
|
+
/** Sent attachments with the type each was given, or the one its filename implies, in the order every tool lists them. */
|
|
13
42
|
function sentAttachments(sent, outbound) {
|
|
14
|
-
return sent.map((a, i) => ({ ...a, type: attachmentType(a.filename, outbound[i]?.contentType) }));
|
|
43
|
+
return sortAttachments(sent.map((a, i) => ({ ...a, type: attachmentType(a.filename, outbound[i]?.contentType) })));
|
|
15
44
|
}
|
|
16
45
|
function decodeAttachments(items) {
|
|
17
46
|
return (items ?? []).map((a) => {
|
|
@@ -28,30 +57,39 @@ const sentOutput = outputObject({
|
|
|
28
57
|
to: z.array(z.string()),
|
|
29
58
|
topic: z.string(),
|
|
30
59
|
parent_id: z.string().nullable(),
|
|
31
|
-
attachments: z.array(attachmentItem),
|
|
60
|
+
attachments: z.array(attachmentItem).describe("sorted by filename, the same order in every tool"),
|
|
32
61
|
redactions: z.number().describe("secrets replaced with placeholders before sending"),
|
|
33
|
-
|
|
62
|
+
redacted: z.array(z.string()).optional().describe("the kinds of secret replaced, such as github_token; empty when none"),
|
|
63
|
+
resolved: resolvedOutput,
|
|
64
|
+
warnings: z.array(z.string()).describe("things to tell the user, such as redacted secrets or resolved short names"),
|
|
65
|
+
thread_topic: z.string().nullable().optional().describe("the thread's topic: the topic sent for a new message; for a reply, the thread root's topic, or null when the root is not visible"),
|
|
34
66
|
});
|
|
67
|
+
function redactionNote(count, kinds) {
|
|
68
|
+
return count ? `. ${count} secret(s) were redacted before sending (${kinds.join(", ")}).` : ".";
|
|
69
|
+
}
|
|
70
|
+
function resolvedNote(resolved) {
|
|
71
|
+
return resolved.length ? ` Short names resolved: ${resolved.map((r) => `${r.input} → ${addressText(r.address)}`).join(", ")}.` : "";
|
|
72
|
+
}
|
|
35
73
|
export const registerSendTools = (server, deps) => {
|
|
36
74
|
server.registerTool("send_message", {
|
|
37
75
|
title: "Send new fmsg message",
|
|
38
76
|
description: `Send a new message immediately, starting a new thread. ${IMMUTABLE} ` +
|
|
39
77
|
"Recipients may be full @user@domain addresses or resolvable short names. The body is Markdown by default. " +
|
|
40
|
-
"Secrets
|
|
41
|
-
"
|
|
42
|
-
inputSchema: z.
|
|
78
|
+
"Secrets are redacted. If the host rejects the message its reason is returned verbatim. " +
|
|
79
|
+
"To continue an existing conversation use reply instead.",
|
|
80
|
+
inputSchema: z.strictObject({
|
|
43
81
|
to: z.array(z.string()).min(1).describe("recipient addresses (@user@domain) or short names"),
|
|
44
82
|
topic: z.string().max(256).describe("thread topic (subject) — immutable once sent"),
|
|
45
83
|
body: z.string().describe("message body; Markdown unless type says otherwise"),
|
|
46
84
|
type: z.string().default("text/markdown; charset=utf-8").describe("body media type"),
|
|
47
|
-
important: z.boolean().default(false),
|
|
48
|
-
no_reply: z.boolean().default(false).describe(
|
|
49
|
-
attachments: z.array(attachmentInput).optional(),
|
|
85
|
+
important: z.boolean().default(false).describe(IMPORTANT),
|
|
86
|
+
no_reply: z.boolean().default(false).describe(NO_REPLY),
|
|
87
|
+
attachments: z.array(attachmentInput).optional().describe("files"),
|
|
50
88
|
}),
|
|
51
89
|
outputSchema: sentOutput,
|
|
52
90
|
annotations: SENDS,
|
|
53
91
|
}, async ({ to, topic, body, type, important, no_reply, attachments }, ctx) => withCaller(deps, ctx, async (caller, signal) => {
|
|
54
|
-
const recipients =
|
|
92
|
+
const { to: recipients, resolved } = resolveRecipients(to, resolverFor(deps, caller));
|
|
55
93
|
const outbound = decodeAttachments(attachments);
|
|
56
94
|
const sent = await caller.client.send({
|
|
57
95
|
to: recipients,
|
|
@@ -72,28 +110,31 @@ export const registerSendTools = (server, deps) => {
|
|
|
72
110
|
parent_id: null,
|
|
73
111
|
attachments: sentAttachments(sent.attachments, outbound),
|
|
74
112
|
redactions: sent.redactions,
|
|
75
|
-
|
|
113
|
+
redacted: sent.redacted,
|
|
114
|
+
...(resolved.length ? { resolved } : {}),
|
|
115
|
+
warnings: sendWarnings(sent.redactions, sent.redacted, resolved),
|
|
116
|
+
thread_topic: sent.topic,
|
|
76
117
|
};
|
|
77
|
-
const text = `Sent message ${sent.id} "${sent.topic}" to ${recipients
|
|
78
|
-
(
|
|
79
|
-
(structured.redactions
|
|
118
|
+
const text = `Sent message ${sent.id} "${sent.topic}" to ${addressList(recipients)} at ${structured.time ?? "?"}` +
|
|
119
|
+
(structured.attachments.length ? ` with ${structured.attachments.map((a) => a.filename).join(", ")}` : "") +
|
|
120
|
+
redactionNote(structured.redactions, structured.redacted) + resolvedNote(resolved);
|
|
80
121
|
return ok(text, structured);
|
|
81
122
|
}));
|
|
82
123
|
server.registerTool("reply", {
|
|
83
124
|
title: "Reply in fmsg thread",
|
|
84
125
|
description: `Send an immediate reply to a message (linking it into that thread). ${IMMUTABLE} ` +
|
|
85
126
|
"By default the reply goes to everyone on the parent message — its sender, recipients and anyone added later — " +
|
|
86
|
-
"except you; pass recipients to narrow or widen that. Fails if the parent is terminal
|
|
87
|
-
"is refused unless allow_no_reply is true. Secrets are redacted
|
|
88
|
-
inputSchema: z.
|
|
127
|
+
"except you; pass recipients to narrow or widen that. Fails if the parent is terminal (no replies possible); " +
|
|
128
|
+
"a parent marked no-reply is refused unless allow_no_reply is true. Secrets are redacted.",
|
|
129
|
+
inputSchema: z.strictObject({
|
|
89
130
|
id: idSchema.describe("message to reply to"),
|
|
90
|
-
body: z.string(),
|
|
131
|
+
body: z.string().describe("reply body; Markdown unless type says otherwise"),
|
|
91
132
|
recipients: z.array(z.string()).optional().describe("override the reply-all recipient set"),
|
|
92
|
-
type: z.string().default("text/markdown; charset=utf-8"),
|
|
93
|
-
important: z.boolean().default(false),
|
|
94
|
-
no_reply: z.boolean().default(false),
|
|
133
|
+
type: z.string().default("text/markdown; charset=utf-8").describe("body media type"),
|
|
134
|
+
important: z.boolean().default(false).describe(IMPORTANT),
|
|
135
|
+
no_reply: z.boolean().default(false).describe(NO_REPLY),
|
|
95
136
|
allow_no_reply: z.boolean().default(false).describe("reply even though the parent asked for no replies"),
|
|
96
|
-
attachments: z.array(attachmentInput).optional(),
|
|
137
|
+
attachments: z.array(attachmentInput).optional().describe("files"),
|
|
97
138
|
}),
|
|
98
139
|
outputSchema: sentOutput,
|
|
99
140
|
annotations: SENDS,
|
|
@@ -104,10 +145,9 @@ export const registerSendTools = (server, deps) => {
|
|
|
104
145
|
if (parent.no_reply && !allow_no_reply) {
|
|
105
146
|
return toolError(`message ${id} is marked no-reply; pass allow_no_reply: true only if the user explicitly wants to reply anyway`);
|
|
106
147
|
}
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
: participantsOf(parent).filter((a) => !sameAddress(a, caller.address));
|
|
148
|
+
const { to, resolved } = recipients?.length
|
|
149
|
+
? resolveRecipients(recipients, resolverFor(deps, caller))
|
|
150
|
+
: { to: participantsOf(parent).filter((a) => !sameAddress(a, caller.address)), resolved: [] };
|
|
111
151
|
if (to.length === 0)
|
|
112
152
|
return toolError(`message ${id} has no other participants to reply to; pass recipients`);
|
|
113
153
|
const outbound = decodeAttachments(attachments);
|
|
@@ -130,17 +170,23 @@ export const registerSendTools = (server, deps) => {
|
|
|
130
170
|
parent_id: parent.id,
|
|
131
171
|
attachments: sentAttachments(sent.attachments, outbound),
|
|
132
172
|
redactions: sent.redactions,
|
|
133
|
-
|
|
173
|
+
redacted: sent.redacted,
|
|
174
|
+
...(resolved.length ? { resolved } : {}),
|
|
175
|
+
warnings: sendWarnings(sent.redactions, sent.redacted, resolved),
|
|
176
|
+
// The reply is already sent: a failed topic lookup only leaves the topic unknown.
|
|
177
|
+
thread_topic: await rootTopic(caller.client, parent, signal).catch(() => null),
|
|
134
178
|
};
|
|
135
|
-
const
|
|
136
|
-
|
|
179
|
+
const topic = structured.thread_topic ? ` in "${headerValue(structured.thread_topic)}"` : "";
|
|
180
|
+
const text = `Sent reply ${sent.id} to message ${parent.id}${topic} for ${addressList(to)} at ${structured.time ?? "?"}` +
|
|
181
|
+
redactionNote(structured.redactions, structured.redacted) + resolvedNote(resolved);
|
|
137
182
|
return ok(text, structured);
|
|
138
183
|
}));
|
|
139
184
|
server.registerTool("add_recipients", {
|
|
140
185
|
title: "Add fmsg recipients",
|
|
141
186
|
description: "Add recipients to a message that was already sent (one you sent or received as a primary recipient). " +
|
|
142
|
-
"They receive the message and become participants of its thread.
|
|
143
|
-
|
|
187
|
+
"They receive the message and its attachments, can read it and become participants of its thread. " +
|
|
188
|
+
"This cannot be undone. Fails on terminal messages.",
|
|
189
|
+
inputSchema: z.strictObject({
|
|
144
190
|
id: idSchema.describe("the sent message to add recipients to"),
|
|
145
191
|
recipients: z.array(z.string()).min(1).optional().describe("addresses or short names to add"),
|
|
146
192
|
add_to: z.array(z.string()).min(1).optional().describe("deprecated alias of recipients; pass one or the other"),
|
|
@@ -149,6 +195,7 @@ export const registerSendTools = (server, deps) => {
|
|
|
149
195
|
id: z.string(),
|
|
150
196
|
added: z.number(),
|
|
151
197
|
recipients: z.array(z.string()).optional().describe("the resolved addresses that were added"),
|
|
198
|
+
resolved: resolvedOutput,
|
|
152
199
|
add_to: z.array(z.string()).describe("deprecated copy of recipients"),
|
|
153
200
|
}),
|
|
154
201
|
annotations: { ...SENDS, idempotentHint: true },
|
|
@@ -156,16 +203,16 @@ export const registerSendTools = (server, deps) => {
|
|
|
156
203
|
if ((recipients === undefined) === (add_to === undefined))
|
|
157
204
|
return toolError("pass recipients: the addresses to add (add_to is its deprecated alias; not both)");
|
|
158
205
|
return withCaller(deps, ctx, async (caller, signal) => {
|
|
159
|
-
const addresses =
|
|
206
|
+
const { to: addresses, resolved } = resolveRecipients((recipients ?? add_to), resolverFor(deps, caller));
|
|
160
207
|
const result = await caller.client.addRecipients(id, addresses, signal);
|
|
161
|
-
return ok(`Added ${result.added} recipient(s) to message ${id}: ${addresses
|
|
208
|
+
return ok(`Added ${result.added} recipient(s) to message ${id}: ${addressList(addresses)}`, { ...result, recipients: addresses, ...(resolved.length ? { resolved } : {}), add_to: addresses });
|
|
162
209
|
});
|
|
163
210
|
});
|
|
164
211
|
server.registerTool("react", {
|
|
165
212
|
title: "React to fmsg message",
|
|
166
213
|
description: "Set or clear your emoji reaction on a message (one reaction per person; a new emoji replaces the previous). " +
|
|
167
214
|
"Sends a small reaction message to the other participants. Fails on drafts and terminal messages.",
|
|
168
|
-
inputSchema: z.
|
|
215
|
+
inputSchema: z.strictObject({
|
|
169
216
|
id: idSchema,
|
|
170
217
|
emoji: z.string().max(32).nullable().describe("a single emoji; null or empty clears your reaction"),
|
|
171
218
|
}),
|
package/dist/tools/wait.js
CHANGED
|
@@ -1,34 +1,50 @@
|
|
|
1
1
|
import * as z from "zod/v4";
|
|
2
|
+
import { compareMessageIds } from "../client/message-id.js";
|
|
2
3
|
import { resolveAddress } from "../address.js";
|
|
3
4
|
import { assembleThread, renderThread } from "../thread.js";
|
|
4
|
-
import { DATA_NOT_INSTRUCTIONS, fence, headerValue, messageData, messageLine } from "../render.js";
|
|
5
|
+
import { DATA_NOT_INSTRUCTIONS, addressText, fence, headerValue, messageData, messageLine } from "../render.js";
|
|
5
6
|
import { waitForMessage } from "../wait.js";
|
|
6
7
|
import { READ_ONLY, UNTRUSTED, idSchema, messageItem, ok, openEnum, outputObject, resolverFor, toItem, untrustedNotice, withCaller, } from "./common.js";
|
|
7
8
|
/** Skipped reactions as one line each, for the data block. */
|
|
8
9
|
function reactionLines(skipped) {
|
|
9
|
-
return skipped.filter((s) => s.emoji !== undefined).map((s) => `- ${s.id} from ${
|
|
10
|
+
return skipped.filter((s) => s.emoji !== undefined).map((s) => `- ${s.id} from ${addressText(s.from)}: ${s.emoji ? `reacted ${headerValue(s.emoji)}` : "cleared a reaction"}${s.reaction_to ? ` on message ${s.reaction_to}` : ""}`);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Messages that arrived on other threads (or beyond the batch limit) during the settle window. They are not
|
|
14
|
+
* returned and after_id moves past them, so the next step must name each one; otherwise a model following `next`
|
|
15
|
+
* never sees them.
|
|
16
|
+
*/
|
|
17
|
+
function pendingStep(pending, batchRoot) {
|
|
18
|
+
if (!pending.length)
|
|
19
|
+
return "";
|
|
20
|
+
const described = pending.map((p) => {
|
|
21
|
+
const where = p.root_id === null ? "" : p.root_id === batchRoot ? " in this thread (beyond the batch limit)" : ` in another thread (root ${p.root_id})`;
|
|
22
|
+
return `message ${p.id}${where}`;
|
|
23
|
+
});
|
|
24
|
+
const many = pending.length > 1;
|
|
25
|
+
return `Also new and not included here: ${described.join(", ")}. after_id has moved past ${many ? "them" : "it"}, so ` +
|
|
26
|
+
`the next wait will not return ${many ? "them" : "it"}: read ${many ? "each" : "it"} with get_thread (or get_message) ` +
|
|
27
|
+
`${pending.map((p) => `"${p.id}"`).join(", ")} and reply if appropriate before waiting again.`;
|
|
10
28
|
}
|
|
11
29
|
export const registerWaitTools = (server, deps) => {
|
|
12
30
|
const maxWait = deps.config.waitMaxSeconds;
|
|
13
31
|
server.registerTool("wait_for_message", {
|
|
14
32
|
title: "Wait for next fmsg message",
|
|
15
|
-
description: "Block until the next inbound message arrives
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
`at most timeout_seconds (max ${maxWait})
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
"returned after_id; no messages are lost.",
|
|
27
|
-
inputSchema: z.object({
|
|
33
|
+
description: "Block until the next inbound message arrives and return it with its thread context, so you can answer with reply.\n" +
|
|
34
|
+
"When: the user asks you to chat, keep replying or respond to the next message.\n" +
|
|
35
|
+
"Loop: wait → reply → wait again, passing each result's after_id. On \"timeout\" or \"interrupted\" (server " +
|
|
36
|
+
"restart) call again with the returned after_id; nothing is replayed or lost. Same-thread messages arriving " +
|
|
37
|
+
"within settle_seconds come as ONE result: reply once, to reply_target_id; next says what to do.\n" +
|
|
38
|
+
"Skipped: your own messages, reactions and no-reply messages (listed in skipped). Messages on other threads " +
|
|
39
|
+
"are not returned: after_id moves past them and pending_ids lists them; read them with get_thread before " +
|
|
40
|
+
"waiting again.\n" +
|
|
41
|
+
`Limits: each call blocks at most timeout_seconds (max ${maxWait}). In chat hosts each wait is a model turn: ` +
|
|
42
|
+
"tell the user you are listening and for how long, and stop when they interrupt or their limits are reached.",
|
|
43
|
+
inputSchema: z.strictObject({
|
|
28
44
|
after_id: idSchema.optional().describe("only messages with a greater id qualify; pass the after_id from the previous result. Omit on the first call to wait for messages arriving from now on"),
|
|
29
45
|
thread_of: idSchema.optional().describe("only accept messages in this message's thread"),
|
|
30
46
|
from: z.string().optional().describe("only accept messages from this address or short name"),
|
|
31
|
-
timeout_seconds: z.number().int().min(1).max(maxWait).default(Math.min(90, maxWait)),
|
|
47
|
+
timeout_seconds: z.number().int().min(1).max(maxWait).default(Math.min(90, maxWait)).describe("longest this call blocks"),
|
|
32
48
|
settle_seconds: z.number().int().min(0).max(30).default(3).describe("after the first message, keep collecting same-thread messages for this long"),
|
|
33
49
|
include_thread: z.boolean().default(true).describe("include the assembled thread context of the newest message"),
|
|
34
50
|
}),
|
|
@@ -39,7 +55,8 @@ export const registerWaitTools = (server, deps) => {
|
|
|
39
55
|
thread_topic: z.string().nullable().optional().describe("the thread root's topic (replies carry none); null when unknown"),
|
|
40
56
|
reply_target_id: z.string().nullable().describe("newest message of the batch; reply to this one"),
|
|
41
57
|
messages: z.array(messageItem.extend({ body: z.string().nullable() })),
|
|
42
|
-
pending_other_threads: z.array(outputObject({ id: z.string(), from: z.string(), root_id: z.string().nullable() })),
|
|
58
|
+
pending_other_threads: z.array(outputObject({ id: z.string(), from: z.string(), root_id: z.string().nullable() })).describe("new messages not returned here (other threads, or beyond the batch limit); after_id has moved past them, so read each with get_thread or get_message"),
|
|
59
|
+
pending_ids: z.array(z.string()).optional().describe("ids of pending_other_threads, oldest first: read these before waiting again"),
|
|
43
60
|
skipped: z.array(outputObject({
|
|
44
61
|
id: z.string(),
|
|
45
62
|
reason: openEnum(["own", "reaction", "no_reply", "from_mismatch", "other_thread"]),
|
|
@@ -77,13 +94,16 @@ export const registerWaitTools = (server, deps) => {
|
|
|
77
94
|
const messages = await Promise.all(result.messages.map(async (m) => ({ ...toItem(m, caller.address), body: await caller.client.getText(m, signal) })));
|
|
78
95
|
const newest = result.messages[result.messages.length - 1];
|
|
79
96
|
const keepWaiting = `call wait_for_message with after_id "${result.after_id}"${thread_of !== undefined ? ` and thread_of "${thread_of}"` : ""} to keep listening`;
|
|
97
|
+
const pending = [...result.pending_other_threads].sort((a, b) => compareMessageIds(a.id, b.id));
|
|
98
|
+
const alsoNew = pendingStep(pending, result.thread_root_id);
|
|
99
|
+
const thenWait = alsoNew ? `${alsoNew} Then ${keepWaiting}.` : "";
|
|
80
100
|
const next = result.status === "interrupted"
|
|
81
101
|
? `The server is restarting; ${keepWaiting}. No messages are lost.`
|
|
82
102
|
: !newest
|
|
83
103
|
? `No new message yet; ${keepWaiting}, unless the user's time limit is reached.`
|
|
84
104
|
: newest.terminal
|
|
85
|
-
? `Message ${newest.id} is terminal and cannot be replied to
|
|
86
|
-
: `Reply to message ${newest.id} with the reply tool
|
|
105
|
+
? `Message ${newest.id} is terminal and cannot be replied to.${thenWait ? ` ${thenWait}` : ` Next, ${keepWaiting}.`}`
|
|
106
|
+
: `Reply to message ${newest.id} with the reply tool.${thenWait ? ` ${thenWait}` : ` Then ${keepWaiting}.`}`;
|
|
87
107
|
const structured = {
|
|
88
108
|
status: result.status,
|
|
89
109
|
after_id: result.after_id,
|
|
@@ -91,7 +111,8 @@ export const registerWaitTools = (server, deps) => {
|
|
|
91
111
|
thread_topic: result.thread_topic,
|
|
92
112
|
reply_target_id: newest?.id ?? null,
|
|
93
113
|
messages,
|
|
94
|
-
pending_other_threads:
|
|
114
|
+
pending_other_threads: pending,
|
|
115
|
+
pending_ids: pending.map((p) => p.id),
|
|
95
116
|
skipped: result.skipped,
|
|
96
117
|
unclassified: result.unclassified,
|
|
97
118
|
transport: result.transport,
|
|
@@ -112,9 +133,6 @@ export const registerWaitTools = (server, deps) => {
|
|
|
112
133
|
];
|
|
113
134
|
if (reactions.length)
|
|
114
135
|
lines.push(`Skipped reactions:\n${messageData(reactions.join("\n"))}`);
|
|
115
|
-
if (result.pending_other_threads.length) {
|
|
116
|
-
lines.push(`Also waiting on other threads: ${result.pending_other_threads.map((p) => p.id).join(", ")}`);
|
|
117
|
-
}
|
|
118
136
|
if (result.unclassified.length) {
|
|
119
137
|
lines.push(`Could not classify ${result.unclassified.map((u) => u.id).join(", ")}; after_id is held before them, call again to retry.`);
|
|
120
138
|
}
|
|
@@ -132,7 +150,7 @@ export const registerWaitTools = (server, deps) => {
|
|
|
132
150
|
lines.push("", DATA_NOT_INSTRUCTIONS);
|
|
133
151
|
for (const m of messages)
|
|
134
152
|
if (m.body !== null)
|
|
135
|
-
lines.push("", `--- message ${m.id} from ${
|
|
153
|
+
lines.push("", `--- message ${m.id} from ${addressText(m.from)} ---`, fence(m.body));
|
|
136
154
|
lines.push("", "End of message data.");
|
|
137
155
|
lines.push("", next);
|
|
138
156
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markmnl/fmsg-mcp",
|
|
3
3
|
"mcpName": "io.github.markmnl/fmsg-mcp",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.9",
|
|
5
5
|
"description": "MCP server for fmsg: send and receive federated messages from any AI agent via a deployed fmsg Web API",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
package/server.json
CHANGED
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
"url": "https://github.com/markmnl/fmsg-mcp",
|
|
9
9
|
"source": "github"
|
|
10
10
|
},
|
|
11
|
-
"version": "0.2.
|
|
11
|
+
"version": "0.2.9",
|
|
12
12
|
"packages": [
|
|
13
13
|
{
|
|
14
14
|
"registryType": "npm",
|
|
15
15
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
16
16
|
"identifier": "@markmnl/fmsg-mcp",
|
|
17
|
-
"version": "0.2.
|
|
17
|
+
"version": "0.2.9",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|
|
20
20
|
},
|