@madezmedia/acmi-mcp 1.3.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mikey Shaw / Mad EZ Media Partners
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # @madezmedia/acmi-mcp
2
+
3
+ > **Persistent agent memory in any MCP host.** A drop-in Model Context Protocol server that gives Claude Desktop, Cursor, Cline, Windsurf — or any MCP-compatible AI tool — direct read/write access to **ACMI** (Agentic Context Management Infrastructure) on Upstash Redis.
4
+
5
+ [![npm](https://img.shields.io/npm/v/@madezmedia/acmi-mcp.svg)](https://www.npmjs.com/package/@madezmedia/acmi-mcp)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](./LICENSE)
7
+ [![Node.js](https://img.shields.io/badge/Node.js-18+-339933?logo=node.js&logoColor=white)](https://nodejs.org)
8
+
9
+ ACMI is the **three-key protocol for agent memory**: every entity has a Profile (who/what), Signals (current state), and a Timeline (event log). This MCP package exposes 16 tools that wrap that model so any LLM tool can persist, retrieve, and coordinate across sessions and agents — no SQL joins, no schema migrations, no token bloat.
10
+
11
+ ---
12
+
13
+ ## Why this exists
14
+
15
+ LLM tools are stateless by default. Every conversation starts cold; agents can't remember decisions, share context with siblings, or pick up where they left off. ACMI fixes that with a single Redis-backed primitive (Profile + Signals + Timeline per entity), and this package makes it accessible to any MCP host with one config line.
16
+
17
+ **You get:**
18
+ - Cross-session memory that survives restarts, model swaps, and tool changes.
19
+ - Multi-agent coordination — Claude, Cursor, Cline, your custom agents all reading/writing the same store.
20
+ - Real-time event timelines with correlation tracking, so you can answer "what did Agent X decide about Project Y last Tuesday?" in one query.
21
+ - 16 tools covering profiles, signals, events, work items, multi-stream views, agent bootstrap, thread engagement, deletion, and rollups.
22
+
23
+ ---
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ npm install -g @madezmedia/acmi-mcp
29
+ ```
30
+
31
+ Set Upstash credentials:
32
+
33
+ ```bash
34
+ export UPSTASH_REDIS_REST_URL="https://your-instance.upstash.io"
35
+ export UPSTASH_REDIS_REST_TOKEN="your-token"
36
+ ```
37
+
38
+ (Don't have Upstash yet? Sign up free at [upstash.com](https://upstash.com) — REST API only, no Redis client needed.)
39
+
40
+ ---
41
+
42
+ ## Configure your MCP host
43
+
44
+ ### Claude Desktop
45
+
46
+ Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
47
+
48
+ ```json
49
+ {
50
+ "mcpServers": {
51
+ "acmi": {
52
+ "command": "acmi-mcp",
53
+ "env": {
54
+ "UPSTASH_REDIS_REST_URL": "https://your-instance.upstash.io",
55
+ "UPSTASH_REDIS_REST_TOKEN": "your-token"
56
+ }
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ Restart Claude Desktop. You'll see 16 ACMI tools in the tools list.
63
+
64
+ ### Cursor
65
+
66
+ In Cursor settings, MCP section, add:
67
+
68
+ ```json
69
+ {
70
+ "acmi": {
71
+ "command": "acmi-mcp",
72
+ "env": {
73
+ "UPSTASH_REDIS_REST_URL": "...",
74
+ "UPSTASH_REDIS_REST_TOKEN": "..."
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ ### Cline / Windsurf
81
+
82
+ Same shape — `command: "acmi-mcp"` plus the two env vars. The package installs `acmi-mcp` globally so it's on PATH.
83
+
84
+ ---
85
+
86
+ ## The 16 tools
87
+
88
+ | Tool | What it does |
89
+ |---|---|
90
+ | `acmi_profile` | Create or update an entity profile (who/what — stable identity & metadata) |
91
+ | `acmi_signal` | Update an entity's signals (current AI state — mutable, frequent) |
92
+ | `acmi_event` | Log a timeline event (the workhorse — timestamped, correlation-tracked) |
93
+ | `acmi_get` | Fetch an entity's full context bundle (profile + signals + recent timeline) |
94
+ | `acmi_list` | List all entities in a namespace |
95
+ | `acmi_work_create` | Create a work item (cross-session project, task, or idea) |
96
+ | `acmi_work_event` | Log progress on a work item |
97
+ | `acmi_work_signal` | Update signals on a work item (progress, blockers, metrics) |
98
+ | `acmi_work_get` | Read a work item's full context (profile + signals + timeline + sessions) |
99
+ | `acmi_work_list` | List all work item IDs |
100
+ | `acmi_cat` | Multi-stream merge view — combine timelines from multiple entities, sorted by time |
101
+ | `acmi_spawn` | Log an agent session spawn (when an agent starts a new run) |
102
+ | `acmi_bootstrap` | One-shot agent context bundle — everything a fresh session needs |
103
+ | `acmi_active` | Track agent-thread engagement (add/remove/list active threads) |
104
+ | `acmi_rollup_set` | Set a rollup snapshot for an agent (paired with `acmi_bootstrap`) |
105
+ | `acmi_delete` | Delete an ACMI key — protected paths refused, dry-run by default, requires `confirm=true` |
106
+
107
+ ---
108
+
109
+ ## Built-in safety
110
+
111
+ - **`validateKeySegments`** — every tool that builds a key validates input segments aren't `undefined`/`null`/empty/colon-containing/oversize. Prevents the entire bug class where unsubstituted variables or status messages leak into key names.
112
+ - **`validateJson`** — every tool that takes a JSON string argument validates it's parseable before storing. Catches "I forgot to JSON.stringify()" at the boundary.
113
+ - **`isProtectedKey`** — keys under `acmi:registry:*` and `acmi:notion-sync:*` cannot be mutated or deleted via this MCP server. Read-only by policy.
114
+ - **`safeTool`** wrapper — every tool returns a structured `{ok: false, error, tool}` payload on throw, never an opaque MCP transport error. Easier to debug from the LLM side.
115
+ - **`acmi_delete` is dry-run by default** — pass `confirm: true` to actually DELETE; otherwise returns a preview (key existence, type) so you can review.
116
+
117
+ ---
118
+
119
+ ## Example: agent bootstrap in two MCP calls
120
+
121
+ ```js
122
+ // 1. Fresh agent session — pull everything you need:
123
+ acmi_bootstrap({ agentId: "claude-engineer" })
124
+ // Returns: profile, signals, active_context, rollup_latest, timeline_recent (last 20), recent_spawns (last 5)
125
+
126
+ // 2. Log that you started:
127
+ acmi_spawn({ agentId: "claude-engineer", sessionId: "2026-05-06-A", modelId: "claude-opus-4-7" })
128
+ ```
129
+
130
+ The agent now has full context and the fleet sees it spawned. That's it — no DB schema, no ORM, no joins.
131
+
132
+ ---
133
+
134
+ ## ACMI protocol
135
+
136
+ This MCP package is one entry point to ACMI. The protocol itself is documented at:
137
+
138
+ - **Spec:** [github.com/madezmedia/acmi](https://github.com/madezmedia/acmi) (`SPEC.md`, `ROADMAP.md`)
139
+ - **Main JS package:** [`@madezmedia/acmi`](https://www.npmjs.com/package/@madezmedia/acmi) — direct ACMI client (no MCP layer)
140
+ - **Product page:** [v3-ten-beta.vercel.app/acmi](https://v3-ten-beta.vercel.app/acmi)
141
+
142
+ ACMI is also exposed via:
143
+ - CLI: `npx @madezmedia/acmi <command>`
144
+ - HTTP: roadmap item — REST gateway for non-Redis hosts
145
+ - Direct Redis: REST API (Upstash) — what this MCP wraps
146
+
147
+ ---
148
+
149
+ ## License
150
+
151
+ MIT © Michael Shaw / Mad EZ Media Partners
@@ -0,0 +1,51 @@
1
+ // mcp-server-helpers.mjs — pure validation/guard helpers for ACMI MCP server.
2
+ // Extracted so they can be unit-tested without booting the stdio transport.
3
+ // v1.3 (2026-05-06).
4
+
5
+ /**
6
+ * Reject key-segment values that produce malformed ACMI keys.
7
+ * Matches the bug class drift-remediator quarantines at the read side.
8
+ */
9
+ export function validateKeySegments(...segments) {
10
+ for (const s of segments) {
11
+ if (s === undefined || s === null) {
12
+ throw new Error(`Invalid key segment: received ${s} (undefined/null)`);
13
+ }
14
+ const v = String(s);
15
+ if (!v) throw new Error(`Invalid key segment: empty string`);
16
+ if (v === "undefined" || v === "null") {
17
+ throw new Error(`Invalid key segment: literal "${v}" — likely an unsubstituted JS variable`);
18
+ }
19
+ if (v.includes(":")) {
20
+ throw new Error(`Invalid key segment: "${v}" contains ":" — would corrupt key structure`);
21
+ }
22
+ if (v.length > 200) {
23
+ throw new Error(`Invalid key segment: ${v.length} chars exceeds 200 — looks like status text or error message bleeding into key`);
24
+ }
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Validate that a string is parseable JSON before SET.
30
+ * Catches the "I forgot to JSON.stringify()" bug at the boundary.
31
+ */
32
+ export function validateJson(s, fieldName) {
33
+ if (typeof s !== "string") {
34
+ throw new Error(`${fieldName} must be a JSON string, got ${typeof s}`);
35
+ }
36
+ try {
37
+ JSON.parse(s);
38
+ } catch (e) {
39
+ throw new Error(`${fieldName} is not valid JSON: ${e.message}`);
40
+ }
41
+ }
42
+
43
+ const PROTECTED_PREFIXES = [
44
+ "acmi:registry:",
45
+ "acmi:notion-sync:",
46
+ ];
47
+
48
+ export function isProtectedKey(key) {
49
+ if (!key) return true;
50
+ return PROTECTED_PREFIXES.some(p => key.startsWith(p));
51
+ }
package/mcp-server.mjs ADDED
@@ -0,0 +1,488 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ACMI MCP Server — Model Context Protocol interface for
5
+ * Agentic Context Management Infrastructure.
6
+ *
7
+ * Transport: stdio (Claude Desktop, Cursor, Cline, Windsurf)
8
+ * Wraps the ACMI Redis KV layer directly (same logic as acmi.mjs CLI).
9
+ *
10
+ * v1.3 (2026-05-06 hackathon hardening):
11
+ * - validateKeySegments() rejects undefined/null/empty/colon-containing/oversize segments
12
+ * - validateJson() catches malformed JSON before SET
13
+ * - safeTool() wraps every handler in try/catch returning {ok:false, error}
14
+ * - isProtectedKey() refuses mutation of acmi:registry:* / acmi:notion-sync:*
15
+ * - acmi_delete tool (with dry-run + confirm + protected-path guards)
16
+ * - acmi_rollup_set tool (companion to acmi_bootstrap's rollup_latest read)
17
+ */
18
+
19
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
20
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
21
+ import { z } from "zod";
22
+ import { validateKeySegments, validateJson, isProtectedKey } from "./mcp-server-helpers.mjs";
23
+
24
+ // ─── Redis helpers (shared with acmi.mjs) ──────────────────────────
25
+
26
+ const url = process.env.UPSTASH_REDIS_REST_URL;
27
+ const token = process.env.UPSTASH_REDIS_REST_TOKEN;
28
+
29
+ if (!url || !token) {
30
+ console.error("ERROR: UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN must be set.");
31
+ process.exit(1);
32
+ }
33
+
34
+ async function redis(command, ...args) {
35
+ const endpoint = `${url.replace(/\/$/, '')}/`;
36
+ const res = await fetch(endpoint, {
37
+ method: "POST",
38
+ headers: {
39
+ Authorization: `Bearer ${token}`,
40
+ "Content-Type": "application/json",
41
+ },
42
+ body: JSON.stringify([command, ...args]),
43
+ });
44
+ const data = await res.json();
45
+ if (data.error) throw new Error(`Upstash: ${data.error}`);
46
+ return data.result;
47
+ }
48
+
49
+ function tryParse(s) {
50
+ try { return JSON.parse(s); } catch { return s; }
51
+ }
52
+
53
+ function parseZWithScores(arr) {
54
+ const out = [];
55
+ for (let i = 0; i < (arr || []).length; i += 2) {
56
+ out.push({ ts: Number(arr[i + 1]), data: tryParse(arr[i]) });
57
+ }
58
+ return out;
59
+ }
60
+
61
+ function parseHash(arr) {
62
+ const out = {};
63
+ for (let i = 0; i < (arr || []).length; i += 2) {
64
+ out[arr[i]] = tryParse(arr[i + 1]);
65
+ }
66
+ return out;
67
+ }
68
+
69
+ function parseSince(s) {
70
+ const m = String(s).match(/^(\d+)([hdm])$/);
71
+ if (!m) return 0;
72
+ const n = Number(m[1]);
73
+ return n * (m[2] === "h" ? 3600e3 : m[2] === "d" ? 86400e3 : 60e3);
74
+ }
75
+
76
+ /**
77
+ * Wrap a tool handler so any thrown error becomes a structured MCP response
78
+ * instead of an opaque transport-layer failure.
79
+ */
80
+ function safeTool(name, fn) {
81
+ return async (args) => {
82
+ try {
83
+ return await fn(args);
84
+ } catch (e) {
85
+ return jsonResult({ ok: false, error: e.message, tool: name });
86
+ }
87
+ };
88
+ }
89
+
90
+ // ─── MCP Server ─────────────────────────────────────────────────────
91
+
92
+ const server = new McpServer({
93
+ name: "acmi",
94
+ version: "1.3.0",
95
+ });
96
+
97
+ // Helper: JSON result as text content
98
+ function jsonResult(data) {
99
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
100
+ }
101
+
102
+ // ── 1. acmi_profile ────────────────────────────────────────────────
103
+
104
+ server.tool(
105
+ "acmi_profile",
106
+ "Create or update an entity profile in ACMI. Stores arbitrary JSON profile data for an entity (agent, thread, project, etc.).",
107
+ {
108
+ namespace: z.string().describe("ACMI namespace (e.g. 'agent', 'thread', 'sales')"),
109
+ id: z.string().describe("Entity ID within the namespace"),
110
+ profile: z.string().describe("JSON string of profile data to store"),
111
+ },
112
+ safeTool("acmi_profile", async ({ namespace, id, profile }) => {
113
+ validateKeySegments(namespace, id);
114
+ validateJson(profile, "profile");
115
+ const key = `acmi:${namespace}:${id}:profile`;
116
+ await redis("SET", key, profile);
117
+ await redis("SADD", `acmi:${namespace}:list`, id);
118
+ return jsonResult({ ok: true, key });
119
+ })
120
+ );
121
+
122
+ // ── 2. acmi_signal ─────────────────────────────────────────────────
123
+
124
+ server.tool(
125
+ "acmi_signal",
126
+ "Update AI signals for an entity. Signals are mutable KV state (mood, priorities, scores, etc.) that changes frequently.",
127
+ {
128
+ namespace: z.string().describe("ACMI namespace"),
129
+ id: z.string().describe("Entity ID"),
130
+ signals: z.string().describe("JSON string of signal data to store"),
131
+ },
132
+ safeTool("acmi_signal", async ({ namespace, id, signals }) => {
133
+ validateKeySegments(namespace, id);
134
+ validateJson(signals, "signals");
135
+ const key = `acmi:${namespace}:${id}:signals`;
136
+ await redis("SET", key, signals);
137
+ await redis("SADD", `acmi:${namespace}:list`, id);
138
+ return jsonResult({ ok: true, key });
139
+ })
140
+ );
141
+
142
+ // ── 3. acmi_event ──────────────────────────────────────────────────
143
+
144
+ server.tool(
145
+ "acmi_event",
146
+ "Log a timeline event for an entity. The workhorse tool — records timestamped events with source, kind, correlationId, and summary. Follows ACMI Communication Standard v1.1.",
147
+ {
148
+ namespace: z.string().describe("ACMI namespace (e.g. 'thread', 'agent', 'work')"),
149
+ id: z.string().describe("Entity ID"),
150
+ source: z.string().describe("Source of the event (agent name, system, etc.)"),
151
+ summary: z.string().describe("Human-readable event summary"),
152
+ kind: z.string().optional().describe("Event kind (e.g. 'handoff-complete', 'step-done', 'decision')"),
153
+ correlationId: z.string().optional().describe("Correlation ID for tracking across agents/sessions (camelCase)"),
154
+ },
155
+ safeTool("acmi_event", async ({ namespace, id, source, summary, kind, correlationId }) => {
156
+ validateKeySegments(namespace, id);
157
+ const key = `acmi:${namespace}:${id}:timeline`;
158
+ const ts = Date.now();
159
+ const event = { ts, source, summary };
160
+ if (kind) event.kind = kind;
161
+ if (correlationId) event.correlationId = correlationId;
162
+ await redis("ZADD", key, ts, JSON.stringify(event));
163
+ await redis("SADD", `acmi:${namespace}:list`, id);
164
+ return jsonResult({ ok: true, key, event });
165
+ })
166
+ );
167
+
168
+ // ── 4. acmi_get ────────────────────────────────────────────────────
169
+
170
+ server.tool(
171
+ "acmi_get",
172
+ "Fetch complete entity context: profile + signals + recent timeline events (last 10).",
173
+ {
174
+ namespace: z.string().describe("ACMI namespace"),
175
+ id: z.string().describe("Entity ID"),
176
+ },
177
+ safeTool("acmi_get", async ({ namespace, id }) => {
178
+ validateKeySegments(namespace, id);
179
+ const prefix = `acmi:${namespace}:${id}`;
180
+ const [profile, signals, timeline] = await Promise.all([
181
+ redis("GET", `${prefix}:profile`),
182
+ redis("GET", `${prefix}:signals`),
183
+ redis("ZREVRANGE", `${prefix}:timeline`, 0, 9),
184
+ ]);
185
+ return jsonResult({
186
+ profile: profile ? tryParse(profile) : null,
187
+ signals: signals ? tryParse(signals) : null,
188
+ timeline_recent: (timeline || []).map(tryParse),
189
+ });
190
+ })
191
+ );
192
+
193
+ // ── 5. acmi_list ───────────────────────────────────────────────────
194
+
195
+ server.tool(
196
+ "acmi_list",
197
+ "List all entity IDs in a namespace.",
198
+ {
199
+ namespace: z.string().describe("ACMI namespace to list"),
200
+ },
201
+ safeTool("acmi_list", async ({ namespace }) => {
202
+ validateKeySegments(namespace);
203
+ const arr = await redis("SMEMBERS", `acmi:${namespace}:list`);
204
+ return jsonResult(arr || []);
205
+ })
206
+ );
207
+
208
+ // ── 6. acmi_work_create ────────────────────────────────────────────
209
+
210
+ server.tool(
211
+ "acmi_work_create",
212
+ "Create a new work item (cross-session project, task, or idea).",
213
+ {
214
+ id: z.string().describe("Unique work item ID"),
215
+ profile: z.string().describe("JSON string of work item profile (title, owner, status, etc.)"),
216
+ },
217
+ safeTool("acmi_work_create", async ({ id, profile }) => {
218
+ validateKeySegments(id);
219
+ validateJson(profile, "profile");
220
+ await redis("SET", `acmi:work:${id}:profile`, profile);
221
+ await redis("SADD", "acmi:work:list", id);
222
+ return jsonResult({ ok: true, work_id: id });
223
+ })
224
+ );
225
+
226
+ // ── 7. acmi_work_event ─────────────────────────────────────────────
227
+
228
+ server.tool(
229
+ "acmi_work_event",
230
+ "Log a progress event on a work item.",
231
+ {
232
+ id: z.string().describe("Work item ID"),
233
+ source: z.string().describe("Source of the event"),
234
+ summary: z.string().describe("Event summary"),
235
+ sessionId: z.string().optional().describe("Optional session ID to associate"),
236
+ },
237
+ safeTool("acmi_work_event", async ({ id, source, summary, sessionId }) => {
238
+ validateKeySegments(id);
239
+ const ts = Date.now();
240
+ const event = { ts, source, summary };
241
+ if (sessionId) event.session_id = sessionId;
242
+ await redis("ZADD", `acmi:work:${id}:timeline`, ts, JSON.stringify(event));
243
+ if (sessionId) await redis("SADD", `acmi:work:${id}:sessions`, sessionId);
244
+ return jsonResult({ ok: true, work_id: id, event });
245
+ })
246
+ );
247
+
248
+ // ── 8. acmi_work_signal ────────────────────────────────────────────
249
+
250
+ server.tool(
251
+ "acmi_work_signal",
252
+ "Update signals for a work item (progress, blockers, metrics, etc.).",
253
+ {
254
+ id: z.string().describe("Work item ID"),
255
+ signals: z.string().describe("JSON string of signal data"),
256
+ },
257
+ safeTool("acmi_work_signal", async ({ id, signals }) => {
258
+ validateKeySegments(id);
259
+ validateJson(signals, "signals");
260
+ await redis("SET", `acmi:work:${id}:signals`, signals);
261
+ return jsonResult({ ok: true, work_id: id });
262
+ })
263
+ );
264
+
265
+ // ── 9. acmi_work_get ───────────────────────────────────────────────
266
+
267
+ server.tool(
268
+ "acmi_work_get",
269
+ "Read a work item's full context: profile, signals, timeline (last 50), and sessions.",
270
+ {
271
+ id: z.string().describe("Work item ID"),
272
+ },
273
+ safeTool("acmi_work_get", async ({ id }) => {
274
+ validateKeySegments(id);
275
+ const prefix = `acmi:work:${id}`;
276
+ const [profile, signals, timeline, sessions] = await Promise.all([
277
+ redis("GET", `${prefix}:profile`),
278
+ redis("GET", `${prefix}:signals`),
279
+ redis("ZREVRANGE", `${prefix}:timeline`, 0, 49),
280
+ redis("SMEMBERS", `${prefix}:sessions`),
281
+ ]);
282
+ return jsonResult({
283
+ work_id: id,
284
+ profile: profile ? tryParse(profile) : null,
285
+ signals: signals ? tryParse(signals) : null,
286
+ timeline: (timeline || []).map(tryParse),
287
+ sessions: sessions || [],
288
+ });
289
+ })
290
+ );
291
+
292
+ // ── 10. acmi_work_list ─────────────────────────────────────────────
293
+
294
+ server.tool(
295
+ "acmi_work_list",
296
+ "List all work item IDs.",
297
+ {},
298
+ safeTool("acmi_work_list", async () => {
299
+ const arr = await redis("SMEMBERS", "acmi:work:list");
300
+ return jsonResult(arr || []);
301
+ })
302
+ );
303
+
304
+ // ── 11. acmi_cat ───────────────────────────────────────────────────
305
+
306
+ server.tool(
307
+ "acmi_cat",
308
+ "Multi-stream event merge view. Combines timeline events from multiple entities, sorted by timestamp. Supports --since filtering.",
309
+ {
310
+ keys: z.array(z.string()).describe("Timeline keys to merge. Use 'thread:name', 'agent:name', or full 'acmi:...:timeline' keys."),
311
+ since: z.string().optional().describe("Time window filter (e.g. '24h', '7d', '30m'). Default: all time."),
312
+ limit: z.number().optional().describe("Max events to return. Default: 50."),
313
+ },
314
+ safeTool("acmi_cat", async ({ keys, since, limit }) => {
315
+ const maxResults = limit || 50;
316
+ const sinceMs = since ? parseSince(since) : 0;
317
+ const targets = keys.map((k) => {
318
+ if (k.startsWith("acmi:")) return k;
319
+ if (k.endsWith(":timeline")) return `acmi:${k}`;
320
+ return `acmi:${k}:timeline`;
321
+ });
322
+
323
+ const minScore = sinceMs ? Date.now() - sinceMs : 0;
324
+ const merged = [];
325
+ for (const k of targets) {
326
+ const r = await redis("ZRANGEBYSCORE", k, minScore, "+inf", "WITHSCORES");
327
+ for (const e of parseZWithScores(r)) merged.push({ ...e, _source: k });
328
+ }
329
+ merged.sort((a, b) => b.ts - a.ts);
330
+
331
+ const results = merged.slice(0, maxResults).map((m) => {
332
+ let ts = m.ts;
333
+ if (ts > 9999999999999) ts = Number(String(ts).slice(0, 13));
334
+ const iso = new Date(ts).toISOString().slice(0, 16).replace("T", " ") + "Z";
335
+ const src = m._source.replace(/^acmi:|:timeline$/g, "");
336
+ const d = m.data || {};
337
+ return {
338
+ timestamp: iso,
339
+ source_key: src,
340
+ kind: d.kind || d.source || "?",
341
+ summary: d.summary || d.message || JSON.stringify(d),
342
+ };
343
+ });
344
+
345
+ return jsonResult(results);
346
+ })
347
+ );
348
+
349
+ // ── 12. acmi_spawn ─────────────────────────────────────────────────
350
+
351
+ server.tool(
352
+ "acmi_spawn",
353
+ "Log an agent session spawn event. Records when an agent starts a new session.",
354
+ {
355
+ agentId: z.string().describe("Agent ID that spawned"),
356
+ sessionId: z.string().optional().describe("Session ID"),
357
+ modelId: z.string().optional().describe("Model ID used for the session"),
358
+ },
359
+ safeTool("acmi_spawn", async ({ agentId, sessionId, modelId }) => {
360
+ validateKeySegments(agentId);
361
+ const ts = Date.now();
362
+ const data = JSON.stringify({
363
+ ts,
364
+ session_id: sessionId || "unknown",
365
+ model_id: modelId || "unknown",
366
+ });
367
+ await redis("ZADD", `acmi:agent:${agentId}:spawns`, ts, data);
368
+ return jsonResult({ ok: true, agent_id: agentId });
369
+ })
370
+ );
371
+
372
+ // ── 13. acmi_bootstrap ─────────────────────────────────────────────
373
+
374
+ server.tool(
375
+ "acmi_bootstrap",
376
+ "One-shot agent context bundle. Fetches everything a fresh agent session needs: profile, signals, active threads, rollup, recent timeline, and spawns.",
377
+ {
378
+ agentId: z.string().describe("Agent ID to bootstrap"),
379
+ },
380
+ safeTool("acmi_bootstrap", async ({ agentId }) => {
381
+ validateKeySegments(agentId);
382
+ const prefix = `acmi:agent:${agentId}`;
383
+ const [profile, signals, active, rollup, timeline, spawns] = await Promise.all([
384
+ redis("GET", `${prefix}:profile`),
385
+ redis("GET", `${prefix}:signals`),
386
+ redis("HGETALL", `${prefix}:active_context`),
387
+ redis("GET", `${prefix}:rollup:latest`),
388
+ redis("ZREVRANGE", `${prefix}:timeline`, 0, 19),
389
+ redis("ZREVRANGE", `${prefix}:spawns`, 0, 4, "WITHSCORES"),
390
+ ]);
391
+
392
+ return jsonResult({
393
+ agent_id: agentId,
394
+ bootstrapped_at: new Date().toISOString(),
395
+ profile: profile ? tryParse(profile) : null,
396
+ signals: signals ? tryParse(signals) : null,
397
+ active_context: parseHash(active),
398
+ rollup_latest: rollup ? tryParse(rollup) : null,
399
+ timeline_recent: (timeline || []).map(tryParse),
400
+ recent_spawns: parseZWithScores(spawns),
401
+ });
402
+ })
403
+ );
404
+
405
+ // ── 14. acmi_active ────────────────────────────────────────────────
406
+
407
+ server.tool(
408
+ "acmi_active",
409
+ "Track agent thread engagement. Add/remove threads or list current active threads for an agent.",
410
+ {
411
+ agentId: z.string().describe("Agent ID"),
412
+ action: z.enum(["add", "remove", "list"]).describe("Action: add a thread, remove a thread, or list all active threads"),
413
+ threadKey: z.string().optional().describe("Thread key (required for add/remove)"),
414
+ role: z.string().optional().describe("Role in thread (e.g. 'participant', 'lead'). Default: 'participant'"),
415
+ },
416
+ safeTool("acmi_active", async ({ agentId, action, threadKey, role }) => {
417
+ validateKeySegments(agentId);
418
+ const key = `acmi:agent:${agentId}:active_context`;
419
+
420
+ if (action === "add") {
421
+ if (!threadKey) throw new Error("threadKey is required for 'add' action");
422
+ await redis("HSET", key, threadKey, JSON.stringify({ role: role || "participant", joined_at: Date.now() }));
423
+ return jsonResult({ ok: true, action: "add", threadKey });
424
+ }
425
+
426
+ if (action === "remove") {
427
+ if (!threadKey) throw new Error("threadKey is required for 'remove' action");
428
+ await redis("HDEL", key, threadKey);
429
+ return jsonResult({ ok: true, action: "remove", threadKey });
430
+ }
431
+
432
+ // list
433
+ const res = await redis("HGETALL", key);
434
+ return jsonResult(parseHash(res));
435
+ })
436
+ );
437
+
438
+ // ── 15. acmi_rollup_set ────────────────────────────────────────────
439
+
440
+ server.tool(
441
+ "acmi_rollup_set",
442
+ "Set the latest rollup snapshot for an agent (acmi:agent:<id>:rollup:latest). Pairs with acmi_bootstrap which reads it.",
443
+ {
444
+ agentId: z.string().describe("Agent ID"),
445
+ rollup: z.string().describe("JSON string of rollup data (cross-session summary, decisions, blockers, etc.)"),
446
+ },
447
+ safeTool("acmi_rollup_set", async ({ agentId, rollup }) => {
448
+ validateKeySegments(agentId);
449
+ validateJson(rollup, "rollup");
450
+ const key = `acmi:agent:${agentId}:rollup:latest`;
451
+ await redis("SET", key, rollup);
452
+ return jsonResult({ ok: true, key });
453
+ })
454
+ );
455
+
456
+ // ── 16. acmi_delete ────────────────────────────────────────────────
457
+
458
+ server.tool(
459
+ "acmi_delete",
460
+ "Delete an ACMI key. Refuses protected paths (acmi:registry:*, acmi:notion-sync:*) and any non-acmi:* key. Defaults to dry-run; pass confirm=true to actually delete.",
461
+ {
462
+ key: z.string().describe("Full ACMI key to delete (must start with 'acmi:')"),
463
+ confirm: z.boolean().optional().describe("Must be true to actually delete; otherwise returns dry-run preview. Default: false (dry-run)."),
464
+ },
465
+ safeTool("acmi_delete", async ({ key, confirm }) => {
466
+ if (!key || !key.startsWith("acmi:")) {
467
+ throw new Error("key must start with 'acmi:' (refusing to operate outside ACMI namespace)");
468
+ }
469
+ if (isProtectedKey(key)) {
470
+ throw new Error(`refused: ${key} is a protected path (registry/notion-sync) — protected-paths are never auto-deleted`);
471
+ }
472
+ const exists = await redis("EXISTS", key);
473
+ const type = exists === 1 ? await redis("TYPE", key) : null;
474
+ if (!confirm) {
475
+ return jsonResult({ ok: true, dry_run: true, key, exists: exists === 1, type, hint: "pass confirm=true to actually delete" });
476
+ }
477
+ if (exists !== 1) {
478
+ return jsonResult({ ok: true, dry_run: false, key, deleted: false, reason: "key did not exist" });
479
+ }
480
+ const deleted = await redis("DEL", key);
481
+ return jsonResult({ ok: true, dry_run: false, key, deleted: deleted === 1, type });
482
+ })
483
+ );
484
+
485
+ // ─── Start ──────────────────────────────────────────────────────────
486
+
487
+ const transport = new StdioServerTransport();
488
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@madezmedia/acmi-mcp",
3
+ "version": "1.3.0",
4
+ "description": "MCP Server for ACMI — Agentic Context Management Infrastructure. 16 tools for persistent agent memory, timeline events, work items, and multi-agent coordination on Upstash Redis.",
5
+ "type": "module",
6
+ "main": "mcp-server.mjs",
7
+ "bin": {
8
+ "acmi-mcp": "mcp-server.mjs"
9
+ },
10
+ "files": [
11
+ "mcp-server.mjs",
12
+ "mcp-server-helpers.mjs",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "scripts": {
17
+ "start": "node mcp-server.mjs",
18
+ "test": "node --test mcp-server.test.mjs"
19
+ },
20
+ "keywords": [
21
+ "mcp",
22
+ "model-context-protocol",
23
+ "acmi",
24
+ "agent-memory",
25
+ "ai",
26
+ "redis",
27
+ "upstash",
28
+ "multi-agent",
29
+ "claude-desktop",
30
+ "cursor",
31
+ "cline",
32
+ "windsurf"
33
+ ],
34
+ "author": "Michael Shaw <madezmediapartners@gmail.com>",
35
+ "license": "MIT",
36
+ "homepage": "https://github.com/madezmedia/acmi#readme",
37
+ "bugs": "https://github.com/madezmedia/acmi/issues",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/madezmedia/acmi.git",
41
+ "directory": "mcp"
42
+ },
43
+ "engines": {
44
+ "node": ">=18.0.0"
45
+ },
46
+ "dependencies": {
47
+ "@modelcontextprotocol/sdk": "^1.29.0",
48
+ "zod": "^3.24.0"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ }
53
+ }