@markmnl/fmsg-mcp 0.1.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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +155 -0
  3. package/dist/address.d.ts +18 -0
  4. package/dist/address.js +50 -0
  5. package/dist/auth.d.ts +21 -0
  6. package/dist/auth.js +84 -0
  7. package/dist/client/client.d.ts +83 -0
  8. package/dist/client/client.js +310 -0
  9. package/dist/client/index.d.ts +6 -0
  10. package/dist/client/index.js +5 -0
  11. package/dist/client/message-id.d.ts +19 -0
  12. package/dist/client/message-id.js +70 -0
  13. package/dist/client/redact.d.ts +8 -0
  14. package/dist/client/redact.js +25 -0
  15. package/dist/client/types.d.ts +126 -0
  16. package/dist/client/types.js +2 -0
  17. package/dist/client/ws.d.ts +6 -0
  18. package/dist/client/ws.js +25 -0
  19. package/dist/config.d.ts +31 -0
  20. package/dist/config.js +74 -0
  21. package/dist/context.d.ts +20 -0
  22. package/dist/context.js +17 -0
  23. package/dist/errors.d.ts +5 -0
  24. package/dist/errors.js +39 -0
  25. package/dist/http.d.ts +14 -0
  26. package/dist/http.js +112 -0
  27. package/dist/index.d.ts +8 -0
  28. package/dist/index.js +137 -0
  29. package/dist/prompts.d.ts +2 -0
  30. package/dist/prompts.js +44 -0
  31. package/dist/public.d.ts +10 -0
  32. package/dist/public.js +10 -0
  33. package/dist/render.d.ts +27 -0
  34. package/dist/render.js +109 -0
  35. package/dist/resources.d.ts +3 -0
  36. package/dist/resources.js +37 -0
  37. package/dist/server.d.ts +9 -0
  38. package/dist/server.js +26 -0
  39. package/dist/thread.d.ts +42 -0
  40. package/dist/thread.js +176 -0
  41. package/dist/tools/common.d.ts +62 -0
  42. package/dist/tools/common.js +88 -0
  43. package/dist/tools/identity.d.ts +2 -0
  44. package/dist/tools/identity.js +58 -0
  45. package/dist/tools/list.d.ts +2 -0
  46. package/dist/tools/list.js +72 -0
  47. package/dist/tools/read.d.ts +2 -0
  48. package/dist/tools/read.js +202 -0
  49. package/dist/tools/send.d.ts +2 -0
  50. package/dist/tools/send.js +170 -0
  51. package/dist/tools/wait.d.ts +2 -0
  52. package/dist/tools/wait.js +96 -0
  53. package/dist/version.d.ts +2 -0
  54. package/dist/version.js +5 -0
  55. package/dist/wait.d.ts +41 -0
  56. package/dist/wait.js +210 -0
  57. package/package.json +74 -0
  58. package/server.json +24 -0
package/dist/wait.js ADDED
@@ -0,0 +1,210 @@
1
+ import { compareMessageIds, maxMessageId } from "./client/message-id.js";
2
+ import { openFmsgWebSocket, parseWsEvent } from "./client/ws.js";
3
+ /**
4
+ * Block until the next qualifying inbound message (plus any that arrive on the
5
+ * same thread within the settle window), or until the deadline.
6
+ */
7
+ export async function waitForMessage(client, self, options, signal, deps = {}) {
8
+ const start = Date.now();
9
+ const deadline = start + options.timeoutMs;
10
+ const maxBatch = options.maxBatch ?? 20;
11
+ const pollIntervalMs = options.pollIntervalMs ?? 2000;
12
+ const me = self.toLowerCase();
13
+ const wantFrom = options.from?.toLowerCase();
14
+ // Floor: the newest inbox id at call time unless the caller supplied a cursor.
15
+ let floor = options.afterId;
16
+ if (floor === undefined) {
17
+ const [newest] = await client.listInbox(1, 0, signal);
18
+ floor = newest?.id ?? "0";
19
+ }
20
+ let skippedMax = floor;
21
+ const rootCache = new Map();
22
+ const rootOf = async (id) => {
23
+ if (rootCache.has(id))
24
+ return rootCache.get(id);
25
+ let root = null;
26
+ try {
27
+ root = (await client.getThreadMessages(id, signal)).root_id;
28
+ }
29
+ catch {
30
+ // Fall back to a bounded pid walk.
31
+ try {
32
+ let cur = id;
33
+ for (let i = 0; i < 100; i++) {
34
+ const m = await client.getMessage(cur, signal);
35
+ if (!m.pid) {
36
+ root = m.id;
37
+ break;
38
+ }
39
+ cur = m.pid;
40
+ }
41
+ }
42
+ catch {
43
+ root = null;
44
+ }
45
+ }
46
+ rootCache.set(id, root);
47
+ return root;
48
+ };
49
+ const targetRoot = options.threadOf ? await rootOf(options.threadOf) : undefined;
50
+ if (options.threadOf && targetRoot === null)
51
+ throw new Error(`could not determine the thread of message ${options.threadOf}`);
52
+ const seen = new Set();
53
+ const batch = [];
54
+ const pending = [];
55
+ let batchRoot = null;
56
+ let transport = "websocket";
57
+ let note = null;
58
+ let socket;
59
+ let pollTimer;
60
+ let settleTimer;
61
+ let finished = false;
62
+ return new Promise((resolve, reject) => {
63
+ const cleanup = () => {
64
+ finished = true;
65
+ clearTimeout(deadlineTimer);
66
+ clearTimeout(settleTimer);
67
+ clearInterval(pollTimer);
68
+ clearInterval(tickTimer);
69
+ signal?.removeEventListener("abort", onAbort);
70
+ if (socket) {
71
+ socket.removeAllListeners();
72
+ try {
73
+ socket.close();
74
+ }
75
+ catch {
76
+ /* ignore */
77
+ }
78
+ }
79
+ };
80
+ const finish = () => {
81
+ if (finished)
82
+ return;
83
+ cleanup();
84
+ const ids = batch.map((m) => m.id);
85
+ resolve({
86
+ status: batch.length ? "message" : "timeout",
87
+ after_id: batch.length ? maxMessageId(ids) : skippedMax,
88
+ thread_root_id: batchRoot,
89
+ messages: [...batch].sort((a, b) => compareMessageIds(a.id, b.id)),
90
+ pending_other_threads: pending,
91
+ transport,
92
+ note,
93
+ });
94
+ };
95
+ const fail = (error) => {
96
+ if (finished)
97
+ return;
98
+ cleanup();
99
+ reject(error);
100
+ };
101
+ const onAbort = () => {
102
+ note = "cancelled";
103
+ finish();
104
+ };
105
+ signal?.addEventListener("abort", onAbort, { once: true });
106
+ if (signal?.aborted)
107
+ return onAbort();
108
+ const deadlineTimer = setTimeout(() => {
109
+ if (batch.length && settleTimer)
110
+ note = "the time limit cut the settle window short";
111
+ finish();
112
+ }, Math.max(0, deadline - Date.now()));
113
+ const tickTimer = setInterval(() => options.onTick?.(Date.now() - start), 20_000);
114
+ const consider = async (m) => {
115
+ if (finished || seen.has(m.id))
116
+ return;
117
+ seen.add(m.id);
118
+ if (compareMessageIds(m.id, floor) <= 0)
119
+ return;
120
+ const disqualified = m.from.toLowerCase() === me ||
121
+ (m.reaction !== null && m.reaction !== undefined) ||
122
+ m.no_reply === true ||
123
+ (wantFrom !== undefined && m.from.toLowerCase() !== wantFrom);
124
+ if (disqualified) {
125
+ if (compareMessageIds(m.id, skippedMax) > 0)
126
+ skippedMax = m.id;
127
+ return;
128
+ }
129
+ const root = await rootOf(m.id);
130
+ if (finished)
131
+ return;
132
+ if (targetRoot !== undefined && root !== targetRoot) {
133
+ if (compareMessageIds(m.id, skippedMax) > 0)
134
+ skippedMax = m.id;
135
+ return;
136
+ }
137
+ if (batch.length === 0) {
138
+ batchRoot = root;
139
+ batch.push(m);
140
+ const settle = Math.min(options.settleMs, Math.max(0, deadline - Date.now()));
141
+ settleTimer = setTimeout(finish, settle);
142
+ if (settle === 0)
143
+ finish();
144
+ return;
145
+ }
146
+ if (root === batchRoot && batch.length < maxBatch) {
147
+ batch.push(m);
148
+ return;
149
+ }
150
+ pending.push({ id: m.id, from: m.from, root_id: root });
151
+ };
152
+ const catchUp = async () => {
153
+ try {
154
+ const page = await client.listInbox(100, 0, signal);
155
+ for (const m of [...page].reverse())
156
+ await consider(m);
157
+ }
158
+ catch (error) {
159
+ if (!finished)
160
+ fail(error);
161
+ }
162
+ };
163
+ const startPolling = (why) => {
164
+ if (finished || pollTimer)
165
+ return;
166
+ transport = "poll";
167
+ note = note ?? why;
168
+ pollTimer = setInterval(() => void catchUp(), pollIntervalMs);
169
+ void catchUp();
170
+ };
171
+ const open = deps.openSocket ?? openFmsgWebSocket;
172
+ open(client)
173
+ .then((ws) => {
174
+ if (finished) {
175
+ ws.close();
176
+ return;
177
+ }
178
+ socket = ws;
179
+ const openTimer = setTimeout(() => {
180
+ if (ws.readyState !== ws.OPEN) {
181
+ ws.removeAllListeners();
182
+ ws.terminate();
183
+ socket = undefined;
184
+ startPolling("WebSocket did not open; polling instead");
185
+ }
186
+ }, options.wsOpenTimeoutMs ?? 10_000);
187
+ ws.on("open", () => {
188
+ clearTimeout(openTimer);
189
+ void catchUp();
190
+ });
191
+ ws.on("message", (raw) => {
192
+ const event = parseWsEvent(raw);
193
+ if (event?.type === "new_msg" && event.data)
194
+ void consider(event.data);
195
+ });
196
+ ws.on("error", () => {
197
+ clearTimeout(openTimer);
198
+ socket = undefined;
199
+ startPolling("WebSocket failed; polling instead");
200
+ });
201
+ ws.on("close", () => {
202
+ clearTimeout(openTimer);
203
+ socket = undefined;
204
+ if (!finished)
205
+ startPolling("WebSocket closed; polling instead");
206
+ });
207
+ })
208
+ .catch(() => startPolling("WebSocket unavailable; polling instead"));
209
+ });
210
+ }
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@markmnl/fmsg-mcp",
3
+ "mcpName": "io.github.markmnl/fmsg-mcp",
4
+ "version": "0.1.0",
5
+ "description": "MCP server for fmsg: send and receive federated messages from any AI agent via a deployed fmsg Web API",
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "author": "Mark Mennell",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/markmnl/fmsg-mcp.git"
12
+ },
13
+ "homepage": "https://github.com/markmnl/fmsg-mcp#readme",
14
+ "bugs": "https://github.com/markmnl/fmsg-mcp/issues",
15
+ "keywords": [
16
+ "mcp",
17
+ "model-context-protocol",
18
+ "mcp-server",
19
+ "fmsg",
20
+ "federated-messaging",
21
+ "agent"
22
+ ],
23
+ "engines": {
24
+ "node": ">=22"
25
+ },
26
+ "bin": {
27
+ "fmsg-mcp": "dist/index.js"
28
+ },
29
+ "main": "./dist/public.js",
30
+ "types": "./dist/public.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/public.d.ts",
34
+ "default": "./dist/public.js"
35
+ },
36
+ "./client": {
37
+ "types": "./dist/client/index.d.ts",
38
+ "default": "./dist/client/index.js"
39
+ }
40
+ },
41
+ "files": [
42
+ "dist/**/*.js",
43
+ "dist/**/*.d.ts",
44
+ "README.md",
45
+ "LICENSE",
46
+ "server.json"
47
+ ],
48
+ "scripts": {
49
+ "build": "tsc -p tsconfig.build.json",
50
+ "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
51
+ "typecheck": "tsc -p tsconfig.json --noEmit",
52
+ "test": "vitest run",
53
+ "test:e2e": "FMSG_E2E=1 vitest run test/fmsg-docker.e2e.test.ts",
54
+ "start": "node dist/index.js",
55
+ "start:http": "node dist/index.js --http",
56
+ "inspect": "npx @modelcontextprotocol/inspector node dist/index.js",
57
+ "prepack": "npm run clean && npm run build && npm test"
58
+ },
59
+ "publishConfig": {
60
+ "access": "public"
61
+ },
62
+ "dependencies": {
63
+ "@modelcontextprotocol/server": "^2.0.0",
64
+ "ws": "^8.21.3",
65
+ "zod": "^4.5.4"
66
+ },
67
+ "devDependencies": {
68
+ "@modelcontextprotocol/client": "^2.0.0",
69
+ "@types/node": "^24.13.3",
70
+ "@types/ws": "^8.18.1",
71
+ "typescript": "^5.9.3",
72
+ "vitest": "^3.2.7"
73
+ }
74
+ }
package/server.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.markmnl/fmsg-mcp",
4
+ "description": "Send and receive fmsg federated messages from any AI agent via a deployed fmsg Web API",
5
+ "repository": {
6
+ "url": "https://github.com/markmnl/fmsg-mcp",
7
+ "source": "github"
8
+ },
9
+ "version": "0.1.0",
10
+ "packages": [
11
+ {
12
+ "registryType": "npm",
13
+ "registryBaseUrl": "https://registry.npmjs.org",
14
+ "identifier": "@markmnl/fmsg-mcp",
15
+ "version": "0.1.0",
16
+ "transport": { "type": "stdio" },
17
+ "environmentVariables": [
18
+ { "name": "FMSG_API_URL", "description": "Base URL of the fmsg Web API", "isRequired": true },
19
+ { "name": "FMSG_API_KEY", "description": "fmsg API key (fmsgk_...) for the address to send as", "isRequired": true, "isSecret": true },
20
+ { "name": "FMSG_DEFAULT_DOMAIN", "description": "Optional: lets short names resolve to @name@<domain>", "isRequired": false }
21
+ ]
22
+ }
23
+ ]
24
+ }