@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.
@@ -1,17 +1,46 @@
1
1
  import * as z from "zod/v4";
2
- import { resolveAddresses, sameAddress } from "../address.js";
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.object({
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
- /** Sent attachments with the type each was given, or the one its filename implies. */
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
- warnings: z.array(z.string()),
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 (API keys, tokens) are redacted and the count reported. If the host rejects the message the host's " +
41
- "own reason is returned verbatim. To continue an existing conversation use reply instead.",
42
- inputSchema: z.object({
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("ask recipients (and their agents) not to reply"),
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 = resolveAddresses(to, resolverFor(deps, caller));
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
- warnings: [],
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.join(", ")} at ${structured.time ?? "?"}` +
78
- (sent.attachments.length ? ` with ${sent.attachments.map((a) => a.filename).join(", ")}` : "") +
79
- (structured.redactions ? `. ${structured.redactions} secret(s) were redacted before sending.` : ".");
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; a parent marked no-reply " +
87
- "is refused unless allow_no_reply is true. Secrets are redacted and the count reported.",
88
- inputSchema: z.object({
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 warnings = [];
108
- const to = recipients?.length
109
- ? resolveAddresses(recipients, resolverFor(deps, caller))
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
- warnings,
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 text = `Sent reply ${sent.id} to message ${parent.id} for ${to.join(", ")} at ${structured.time ?? "?"}` +
136
- (structured.redactions ? `. ${structured.redactions} secret(s) were redacted before sending.` : ".");
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. This cannot be undone. Fails on terminal messages.",
143
- inputSchema: z.object({
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 = resolveAddresses((recipients ?? add_to), resolverFor(deps, caller));
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.join(", ")}`, { ...result, recipients: addresses, add_to: 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.object({
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
  }),
@@ -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 ${headerValue(s.from)}: ${s.emoji ? `reacted ${headerValue(s.emoji)}` : "cleared a reaction"}${s.reaction_to ? ` on message ${s.reaction_to}` : ""}`);
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 (pushed over the fmsg host's WebSocket) and return it with its " +
16
- "thread context so you can answer with reply. Use this when the user asks you to chat, converse, keep replying, " +
17
- "auto-reply, or respond to the next message. Loop: wait → reply → wait again passing the after_id from the " +
18
- "previous result. On status \"timeout\" simply call again with the same arguments and the returned after_id, " +
19
- "which is never older than the newest inbox message when the call started, so nothing is replayed (\"0\" means " +
20
- "the inbox was empty and is safe to pass). Messages arriving on the same thread within settle_seconds are " +
21
- "batched into ONE result; reply once, to the newest (reply_target_id); the result's next field says what to do. " +
22
- "Your own messages, reactions and no-reply messages never qualify; they are listed in skipped. Each call blocks " +
23
- `at most timeout_seconds (max ${maxWait}); stop looping when the user interrupts or the limits they set are reached. ` +
24
- "In chat hosts each wait is a model turn: tell the user you are listening and for how long rather than " +
25
- "looping silently for long periods. Status \"interrupted\" means the server is restarting: call again with the " +
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; ${keepWaiting}.`
86
- : `Reply to message ${newest.id} with the reply tool, then ${keepWaiting}.`;
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: result.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 ${headerValue(m.from)} ---`, fence(m.body));
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.7",
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.7",
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.7",
17
+ "version": "0.2.9",
18
18
  "transport": {
19
19
  "type": "stdio"
20
20
  },