@memstack/mcp 0.7.3 → 0.8.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/README.md CHANGED
@@ -11,11 +11,16 @@ npm install -g @memstack/mcp
11
11
  Install a database driver only when selecting that storage backend:
12
12
 
13
13
  ```bash
14
- npm install @memstack/mcp better-sqlite3 # SQLite
15
- npm install @memstack/mcp ioredis # Redis
16
- npm install @memstack/mcp postgres # Postgres (or pg)
14
+ npm install @memstack/mcp better-sqlite3@^11.10.0 # SQLite
15
+ npm install @memstack/mcp ioredis@^5.11.1 # Redis
16
+ npm install @memstack/mcp postgres@^3.4.9 # Postgres (or pg)
17
17
  ```
18
18
 
19
+ With `npx`, add the driver with `-p` and name the command, for example
20
+ `npx -y -p @memstack/mcp -p postgres@^3.4.9 memstack-mcp`. Copy-paste client
21
+ configs for each backend are in
22
+ [MCP Setup: Database backends](../../docs/MCP_SETUP.md#database-backends-sqlite-postgres-redis).
23
+
19
24
  Memory, disk, and Markdown storage need only `@memstack/mcp`. SQLite requires
20
25
  a writable database path and package lifecycle scripts. The Glama deployment
21
26
  image is `packages/mcp/Dockerfile`; it runs the stdio MCP command directly,
package/dist/cli.js CHANGED
@@ -1,63 +1,213 @@
1
1
  #!/usr/bin/env node
2
2
  import { parseArgs } from 'util';
3
3
  import { createServer as createServer$1 } from 'http';
4
- import { MemStack, AnthropicLLMAdapter, OpenAILLMAdapter, OpenAIEmbeddingAdapter, RedisStorageAdapter, SQLiteStorageAdapter, PostgresStorageAdapter, DiskStorageAdapter, MarkdownStorageAdapter, InMemoryStorageAdapter } from '@memstack/core';
5
- import { readFileSync } from 'fs';
4
+ import { MemStack, InMemoryStorageAdapter, HarnessMemory, defaultRecallNamespaces, projectNamespace, GLOBAL_NAMESPACE, MemStackError, AnthropicLLMAdapter, OpenAILLMAdapter, OpenAIEmbeddingAdapter, RedisStorageAdapter, SQLiteStorageAdapter, PostgresStorageAdapter, DiskStorageAdapter, MarkdownStorageAdapter } from '@memstack/core';
5
+ import { readFileSync, realpathSync, existsSync } from 'fs';
6
+ import { homedir } from 'os';
7
+ import { resolve, join, dirname } from 'path';
8
+ import { execFileSync } from 'child_process';
9
+ import { createHash } from 'crypto';
6
10
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
7
11
  import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema } from '@modelcontextprotocol/sdk/types.js';
12
+ import { z } from 'zod';
8
13
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
9
14
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
10
15
 
11
- function buildLLMAdapter() {
12
- if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY) {
16
+ var STORAGE_TYPES = ["memory", "markdown", "disk", "postgres", "sqlite", "redis"];
17
+ function memstackHome() {
18
+ return process.env.MEMSTACK_HOME ?? join(homedir(), ".memstack");
19
+ }
20
+ function configFilePath(home = memstackHome()) {
21
+ return join(home, "config.json");
22
+ }
23
+ function readConfigFile(path = configFilePath()) {
24
+ if (!existsSync(path)) return void 0;
25
+ let parsed;
26
+ try {
27
+ parsed = JSON.parse(readFileSync(path, "utf8"));
28
+ } catch (error) {
29
+ throw new Error(`${path} is not valid JSON: ${error.message}`);
30
+ }
31
+ return validateConfigFile(parsed, path);
32
+ }
33
+ function validateConfigFile(value, path) {
34
+ const fail = (message) => {
35
+ throw new Error(`Invalid ${path}: ${message}`);
36
+ };
37
+ if (!isObject(value)) fail("expected a JSON object");
38
+ const config = value;
39
+ if (config.version !== 1) fail(`unsupported version ${JSON.stringify(config.version)}, expected 1`);
40
+ if (config.llm !== void 0) {
41
+ if (!isObject(config.llm)) fail("llm must be an object");
42
+ const llm = config.llm;
43
+ if (llm.provider !== "openai-compatible" && llm.provider !== "anthropic") {
44
+ fail('llm.provider must be "openai-compatible" or "anthropic"');
45
+ }
46
+ if (typeof llm.apiKey !== "string" || !llm.apiKey) fail("llm.apiKey must be a non-empty string");
47
+ for (const key of ["baseURL", "model"]) {
48
+ if (llm[key] !== void 0 && typeof llm[key] !== "string") fail(`llm.${key} must be a string`);
49
+ }
50
+ }
51
+ if (config.storage !== void 0) {
52
+ if (!isObject(config.storage)) fail("storage must be an object");
53
+ const storage = config.storage;
54
+ if (!STORAGE_TYPES.includes(storage.type)) {
55
+ fail(`storage.type must be one of ${STORAGE_TYPES.join(", ")}`);
56
+ }
57
+ for (const key of ["path", "url"]) {
58
+ if (storage[key] !== void 0 && typeof storage[key] !== "string") fail(`storage.${key} must be a string`);
59
+ }
60
+ }
61
+ return config;
62
+ }
63
+ function isObject(value) {
64
+ return typeof value === "object" && value !== null && !Array.isArray(value);
65
+ }
66
+ var PIN_FILE = ".memstack.json";
67
+ var PIN_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
68
+ function resolveProject(cwd = process.cwd()) {
69
+ const dir = realpathSync(resolve(cwd));
70
+ const root = git(dir, ["rev-parse", "--show-toplevel"]);
71
+ const base = root ?? dir;
72
+ const pinned = readPin(base);
73
+ const fallbacks = transitionalIds(dir, root);
74
+ if (pinned) return { id: pinned, source: "pin", key: pinned, root: base, previousIds: [] };
75
+ if (!root) return { ...identity("directory", dir), root: dir, previousIds: [] };
76
+ const shallow = git(root, ["rev-parse", "--is-shallow-repository"]) === "true";
77
+ const rootCommit = shallow ? null : firstCommit(root);
78
+ if (rootCommit) return { ...identity("root-commit", rootCommit), root, previousIds: fallbacks };
79
+ const remote = git(root, ["config", "--get", "remote.origin.url"]);
80
+ const normalized = shallow && remote ? normalizeRemote(remote) : null;
81
+ if (normalized) return { ...identity("remote", normalized), root, previousIds: fallbacks };
82
+ const gitId = identity("git", gitRepoKey(root));
83
+ return { ...gitId, root, previousIds: fallbacks.filter((id) => id !== gitId.id) };
84
+ }
85
+ function normalizeRemote(url) {
86
+ const value = url.trim();
87
+ if (!value) return null;
88
+ let host;
89
+ let path;
90
+ const scp = value.match(/^(?:[^@/]+@)?([^:/]+):(?!\/)(.+)$/);
91
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(value)) {
92
+ let parsed;
93
+ try {
94
+ parsed = new URL(value);
95
+ } catch {
96
+ return null;
97
+ }
98
+ if (parsed.protocol === "file:" || !parsed.hostname) return null;
99
+ host = parsed.hostname;
100
+ path = decodeURIComponent(parsed.pathname);
101
+ } else if (scp) {
102
+ host = scp[1];
103
+ path = scp[2];
104
+ } else {
105
+ return null;
106
+ }
107
+ path = path.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "");
108
+ if (!path) return null;
109
+ return `${host.toLowerCase()}/${path}`;
110
+ }
111
+ function readPin(base) {
112
+ const path = join(base, PIN_FILE);
113
+ if (!existsSync(path)) return null;
114
+ let project;
115
+ try {
116
+ project = JSON.parse(readFileSync(path, "utf8")).project;
117
+ } catch (error) {
118
+ throw new Error(`${path} is not valid JSON: ${error.message}`);
119
+ }
120
+ if (project === void 0) return null;
121
+ if (typeof project !== "string" || !PIN_PATTERN.test(project)) {
122
+ throw new Error(`${path}: "project" must use letters, digits, '.', '_', or '-' and be at most 64 characters.`);
123
+ }
124
+ return project;
125
+ }
126
+ function firstCommit(root) {
127
+ const out = git(root, ["rev-list", "--max-parents=0", "--first-parent", "HEAD"]);
128
+ return out ? out.split("\n").pop().trim() : null;
129
+ }
130
+ function gitRepoKey(root) {
131
+ const commonDir = git(root, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
132
+ return commonDir ? dirname(realpathSync(commonDir)) : root;
133
+ }
134
+ function transitionalIds(dir, root) {
135
+ const ids = /* @__PURE__ */ new Set();
136
+ if (root) ids.add(identity("git", gitRepoKey(root)).id);
137
+ ids.add(identity("directory", dir).id);
138
+ return [...ids];
139
+ }
140
+ function identity(source, key) {
141
+ const id = createHash("sha256").update(`${source}:${key}`).digest("hex").slice(0, 16);
142
+ return { id, source, key };
143
+ }
144
+ function git(cwd, args) {
145
+ try {
146
+ const out = execFileSync("git", ["-C", cwd, ...args], {
147
+ encoding: "utf8",
148
+ stdio: ["ignore", "pipe", "ignore"],
149
+ timeout: 5e3
150
+ }).trim();
151
+ return out || null;
152
+ } catch {
153
+ return null;
154
+ }
155
+ }
156
+ function isMissingModule(error) {
157
+ const code = error?.code;
158
+ return code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND";
159
+ }
160
+ function buildLLMAdapter(env) {
161
+ if (!env.ANTHROPIC_API_KEY && !env.OPENAI_API_KEY) {
13
162
  throw new Error("At least one of OPENAI_API_KEY or ANTHROPIC_API_KEY must be set");
14
163
  }
15
- if (process.env.ANTHROPIC_API_KEY) {
16
- return new AnthropicLLMAdapter({ apiKey: process.env.ANTHROPIC_API_KEY });
164
+ if (env.ANTHROPIC_API_KEY) {
165
+ return new AnthropicLLMAdapter({ apiKey: env.ANTHROPIC_API_KEY, defaultModel: env.MEMSTACK_LLM_MODEL });
17
166
  }
18
167
  return new OpenAILLMAdapter({
19
- apiKey: process.env.OPENAI_API_KEY ?? "",
20
- baseURL: process.env.MEMSTACK_OPENAI_BASE_URL,
21
- defaultModel: process.env.MEMSTACK_LLM_MODEL
168
+ apiKey: env.OPENAI_API_KEY ?? "",
169
+ baseURL: env.MEMSTACK_OPENAI_BASE_URL,
170
+ defaultModel: env.MEMSTACK_LLM_MODEL
22
171
  });
23
172
  }
24
- function buildEmbeddingAdapter() {
25
- if (process.env.MEMSTACK_OPENAI_BASE_URL) return void 0;
26
- if (process.env.OPENAI_API_KEY) {
27
- return new OpenAIEmbeddingAdapter({ apiKey: process.env.OPENAI_API_KEY });
173
+ function buildEmbeddingAdapter(env) {
174
+ if (env.MEMSTACK_OPENAI_BASE_URL) return void 0;
175
+ if (env.OPENAI_API_KEY) {
176
+ return new OpenAIEmbeddingAdapter({ apiKey: env.OPENAI_API_KEY });
28
177
  }
29
178
  return void 0;
30
179
  }
31
- async function buildStorageAdapter() {
32
- const storage = process.env.MEMSTACK_STORAGE ?? "memory";
180
+ async function buildStorageAdapter(env) {
181
+ const storage = env.MEMSTACK_STORAGE ?? "memory";
33
182
  switch (storage) {
34
183
  case "memory":
35
184
  return new InMemoryStorageAdapter();
36
185
  case "markdown":
37
186
  return new MarkdownStorageAdapter({
38
- dir: process.env.MEMSTACK_DIR ?? "./memories"
187
+ dir: env.MEMSTACK_DIR ?? "./memories"
39
188
  });
40
189
  case "disk":
41
190
  return new DiskStorageAdapter({
42
- storageDir: process.env.MEMSTACK_DIR ?? "./memstack-data"
191
+ storageDir: env.MEMSTACK_DIR ?? "./memstack-data"
43
192
  });
44
193
  case "postgres":
45
- if (!process.env.DATABASE_URL) {
194
+ if (!env.DATABASE_URL) {
46
195
  throw new Error("postgres storage requires DATABASE_URL");
47
196
  }
48
197
  return new PostgresStorageAdapter({
49
- connectionString: process.env.DATABASE_URL
198
+ connectionString: env.DATABASE_URL
50
199
  });
51
200
  case "sqlite": {
52
201
  try {
53
202
  const BetterSqlite3 = await import('better-sqlite3');
54
203
  const db = new BetterSqlite3.default(
55
- process.env.SQLITE_PATH ?? "./memstack.db"
204
+ env.SQLITE_PATH ?? "./memstack.db"
56
205
  );
57
206
  return new SQLiteStorageAdapter({ db });
58
- } catch {
207
+ } catch (error) {
208
+ if (!isMissingModule(error)) throw error;
59
209
  throw new Error(
60
- "SQLite requires better-sqlite3. Install: npm install better-sqlite3"
210
+ "SQLite requires better-sqlite3. Install: npm install better-sqlite3@^11.10.0"
61
211
  );
62
212
  }
63
213
  }
@@ -65,12 +215,13 @@ async function buildStorageAdapter() {
65
215
  try {
66
216
  const Redis = await import('ioredis');
67
217
  const client = new Redis.default(
68
- process.env.REDIS_URL ?? "redis://localhost:6379"
218
+ env.REDIS_URL ?? "redis://localhost:6379"
69
219
  );
70
220
  return new RedisStorageAdapter({ redis: client });
71
- } catch {
221
+ } catch (error) {
222
+ if (!isMissingModule(error)) throw error;
72
223
  throw new Error(
73
- "Redis requires ioredis. Install: npm install ioredis"
224
+ "Redis requires ioredis. Install: npm install ioredis@^5.11.1"
74
225
  );
75
226
  }
76
227
  }
@@ -79,11 +230,39 @@ async function buildStorageAdapter() {
79
230
  }
80
231
  }
81
232
  async function loadConfigFromEnv() {
82
- const llm = buildLLMAdapter();
83
- const embedding = buildEmbeddingAdapter();
84
- const storage = await buildStorageAdapter();
85
- const embedOnStore = process.env.MEMSTACK_EMBED_ON_STORE !== "false";
86
- const defaultActorId = process.env.MEMSTACK_ACTOR ?? "default";
233
+ return buildConfig(process.env);
234
+ }
235
+ async function loadConfig(options = {}) {
236
+ return buildConfig(mergeConfigFile(readConfigFile(options.configPath), process.env));
237
+ }
238
+ function mergeConfigFile(file, env) {
239
+ const merged = { ...env };
240
+ const envHasLLM = LLM_ENV.some((key) => env[key] !== void 0);
241
+ if (file?.llm && !envHasLLM) {
242
+ if (file.llm.provider === "anthropic") merged.ANTHROPIC_API_KEY = file.llm.apiKey;
243
+ else {
244
+ merged.OPENAI_API_KEY = file.llm.apiKey;
245
+ merged.MEMSTACK_OPENAI_BASE_URL = file.llm.baseURL;
246
+ }
247
+ merged.MEMSTACK_LLM_MODEL = file.llm.model;
248
+ }
249
+ if (file?.storage && env.MEMSTACK_STORAGE === void 0) {
250
+ const { type, path, url } = file.storage;
251
+ merged.MEMSTACK_STORAGE = type;
252
+ if (type === "sqlite") merged.SQLITE_PATH = path;
253
+ if (type === "disk" || type === "markdown") merged.MEMSTACK_DIR = path;
254
+ if (type === "postgres") merged.DATABASE_URL = url;
255
+ if (type === "redis") merged.REDIS_URL = url;
256
+ }
257
+ return merged;
258
+ }
259
+ var LLM_ENV = ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "MEMSTACK_OPENAI_BASE_URL", "MEMSTACK_LLM_MODEL"];
260
+ async function buildConfig(env) {
261
+ const llm = buildLLMAdapter(env);
262
+ const embedding = buildEmbeddingAdapter(env);
263
+ const storage = await buildStorageAdapter(env);
264
+ const embedOnStore = env.MEMSTACK_EMBED_ON_STORE !== "false";
265
+ const defaultActorId = env.MEMSTACK_ACTOR ?? "default";
87
266
  return {
88
267
  config: { llm, embedding, storage, defaults: { embedOnStore } },
89
268
  defaultActorId
@@ -91,9 +270,12 @@ async function loadConfigFromEnv() {
91
270
  }
92
271
 
93
272
  // src/config.ts
94
- async function loadConfig() {
273
+ async function loadConfig2() {
95
274
  return loadConfigFromEnv();
96
275
  }
276
+ async function loadHarnessConfig() {
277
+ return loadConfig();
278
+ }
97
279
  var pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
98
280
  var TOOLS = [
99
281
  {
@@ -106,7 +288,7 @@ var TOOLS = [
106
288
  actorId: { type: "string", description: "Actor ID. Defaults to the current session actor." },
107
289
  importance: { type: "number", description: "Importance score (0.0-1.0). Auto-scored if omitted." },
108
290
  tags: { type: "array", items: { type: "string" }, description: "Tags for categorization. Auto-extracted if omitted." },
109
- memoryType: { type: "string", description: "Memory type (interaction, summary, observation, fact, reflection). Default: interaction." },
291
+ memoryType: { type: "string", description: "Memory type (interaction, summary, observation, fact, reflection, preference, decision, instruction). Default: interaction." },
110
292
  metadata: { type: "object", description: "Additional metadata key-value pairs" }
111
293
  },
112
294
  required: ["content"]
@@ -122,7 +304,7 @@ var TOOLS = [
122
304
  actorId: { type: "string", description: "Actor ID. Defaults to the current session actor." },
123
305
  importance: { type: "number", description: "Importance score 0.0-1.0. Default: 0.5." },
124
306
  tags: { type: "array", items: { type: "string" }, description: "Tags for categorization" },
125
- memoryType: { type: "string", description: "Memory type (interaction, summary, observation, fact, reflection). Default: interaction." },
307
+ memoryType: { type: "string", description: "Memory type (interaction, summary, observation, fact, reflection, preference, decision, instruction). Default: interaction." },
126
308
  metadata: { type: "object", description: "Additional metadata key-value pairs" }
127
309
  },
128
310
  required: ["content"]
@@ -144,7 +326,7 @@ var TOOLS = [
144
326
  actorId: { type: "string", description: "Actor ID. Defaults to the current session actor." },
145
327
  importance: { type: "number", description: "Importance score 0.0-1.0. Default: 0.5." },
146
328
  tags: { type: "array", items: { type: "string" }, description: "Tags for categorization" },
147
- memoryType: { type: "string", description: "Memory type (interaction, summary, observation, fact, reflection). Default: interaction." },
329
+ memoryType: { type: "string", description: "Memory type (interaction, summary, observation, fact, reflection, preference, decision, instruction). Default: interaction." },
148
330
  metadata: { type: "object", description: "Additional metadata key-value pairs" }
149
331
  },
150
332
  required: ["content"]
@@ -714,21 +896,293 @@ function createServer({
714
896
  });
715
897
  return server;
716
898
  }
899
+ var pkg2 = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
900
+ var KINDS = [
901
+ "fact",
902
+ "preference",
903
+ "decision",
904
+ "instruction",
905
+ "observation",
906
+ "interaction",
907
+ "summary",
908
+ "reflection"
909
+ ];
910
+ var HARNESS_INSTRUCTIONS = [
911
+ "MemStack is this project's persistent memory, shared with other coding agents.",
912
+ "When the user asks you to remember something, call memory_store. Do not write it into README, AGENTS.md, or other project files unless the user asks for a file change.",
913
+ "Also call memory_store when you learn a durable fact, decision, preference, or rule.",
914
+ "Call memory_retrieve at the start of a task and before decisions that depend on project history, conventions, or preferences.",
915
+ "Never store secrets, credentials, or personal data.",
916
+ "",
917
+ 'Store one self-contained statement per memory, e.g. "This project uses Hono for the API". Memories are project-scoped; use scope "global" only for preferences that apply to every project. memory_retrieve takes natural-language questions. Cite memory IDs when you rely on them, and delete memories that turn out to be wrong with memory_delete.'
918
+ ].join("\n");
919
+ var StoreArgs = z.object({
920
+ content: z.string().trim().min(1).max(8e3).describe("One self-contained statement to remember."),
921
+ kind: z.enum(KINDS).default("fact").describe("What kind of memory this is."),
922
+ importance: z.number().min(0).max(1).optional().describe("0 to 1. Default 0.5; use 0.8+ for rules that must not be missed."),
923
+ tags: z.array(z.string().trim().min(1).max(40)).max(10).optional().describe("Optional topic tags. MemStack adds its own."),
924
+ scope: z.enum(["project", "global"]).default("project").describe('"global" only for preferences that apply to every project.')
925
+ }).strict();
926
+ var RetrieveArgs = z.object({
927
+ query: z.string().max(1e3).optional().describe("Natural-language question or keywords. Omit to get the most important memories."),
928
+ limit: z.number().int().min(1).max(25).default(10).describe("Maximum memories to return."),
929
+ kinds: z.array(z.enum(KINDS)).optional().describe("Only return these kinds.")
930
+ }).strict();
931
+ var IdArgs = z.object({ id: z.string().min(1).max(200) }).strict();
932
+ var NoArgs = z.object({}).strict();
933
+ var TOOLS2 = [
934
+ {
935
+ name: "memory_store",
936
+ description: "Save to project memory. Use this whenever the user asks you to remember something, instead of editing project files, and for durable facts, decisions, preferences, or rules. Later sessions and other agents can recall it.",
937
+ schema: StoreArgs
938
+ },
939
+ {
940
+ name: "memory_retrieve",
941
+ description: "Recall memories for this project (and global preferences) relevant to a question. Fast and local; call it freely.",
942
+ schema: RetrieveArgs
943
+ },
944
+ { name: "memory_get", description: "Get one memory by ID.", schema: IdArgs },
945
+ { name: "memory_delete", description: "Delete a memory that is wrong or outdated, by ID.", schema: IdArgs },
946
+ { name: "memory_stats", description: "Show the current project and how many memories it has.", schema: NoArgs }
947
+ ];
948
+ var RECALL_MAX_CHARS = 8e3;
949
+ function createHarnessServer({ memory, projectId, harness, cwd }) {
950
+ const server = new Server(
951
+ { name: "memstack", version: pkg2.version },
952
+ { capabilities: { tools: {} }, instructions: HARNESS_INSTRUCTIONS }
953
+ );
954
+ const project = projectNamespace(projectId);
955
+ const namespaces = defaultRecallNamespaces(projectId);
956
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
957
+ tools: TOOLS2.map(({ name, description, schema }) => ({
958
+ name,
959
+ description,
960
+ inputSchema: z.toJSONSchema(schema, { io: "input" })
961
+ }))
962
+ }));
963
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
964
+ const tool = TOOLS2.find((t) => t.name === request.params.name);
965
+ if (!tool) return failure(`Unknown tool: ${request.params.name}`);
966
+ const parsed = tool.schema.safeParse(request.params.arguments ?? {});
967
+ if (!parsed.success) {
968
+ return failure(`Invalid arguments for ${tool.name}: ${z.prettifyError(parsed.error)}`);
969
+ }
970
+ try {
971
+ switch (tool.name) {
972
+ case "memory_store": {
973
+ const args = parsed.data;
974
+ const stored = await memory.remember({
975
+ namespace: args.scope === "global" ? GLOBAL_NAMESPACE : project,
976
+ content: args.content,
977
+ kind: args.kind,
978
+ importance: args.importance,
979
+ tags: args.tags,
980
+ source: {
981
+ harness: harness ?? server.getClientVersion()?.name ?? "unknown",
982
+ project: projectId,
983
+ cwd
984
+ }
985
+ });
986
+ return text(`Stored ${stored.id} (${args.scope}, ${stored.memoryType}). Tags: ${stored.tags.join(", ") || "none"}.`);
987
+ }
988
+ case "memory_retrieve": {
989
+ const args = parsed.data;
990
+ const { hits, fallback } = await memory.recall({
991
+ namespaces,
992
+ query: args.query,
993
+ kinds: args.kinds,
994
+ limit: args.limit,
995
+ maxChars: RECALL_MAX_CHARS
996
+ });
997
+ if (hits.length === 0) return text("No memories yet for this project.");
998
+ const header = fallback && args.query ? `Nothing matched "${args.query}". The most important memories instead:` : `${hits.length} ${hits.length === 1 ? "memory" : "memories"}:`;
999
+ return text([header, ...hits.map(({ memory: m }) => formatMemory(m, projectId))].join("\n"));
1000
+ }
1001
+ case "memory_get": {
1002
+ const { id } = parsed.data;
1003
+ const found = await memory.get(id, namespaces);
1004
+ return found ? text(formatMemory(found, projectId, true)) : failure(`Memory not found in this project: ${id}`);
1005
+ }
1006
+ case "memory_delete": {
1007
+ const { id } = parsed.data;
1008
+ await memory.forget(id, namespaces);
1009
+ return text(`Deleted ${id}.`);
1010
+ }
1011
+ case "memory_stats": {
1012
+ const counts = await memory.stats(namespaces);
1013
+ return text(
1014
+ `Project ${projectId}${cwd ? ` (${cwd})` : ""}: ${counts[project] ?? 0} memories. Global: ${counts[GLOBAL_NAMESPACE] ?? 0}.`
1015
+ );
1016
+ }
1017
+ }
1018
+ } catch (error) {
1019
+ if (error instanceof MemStackError && error.code === "NOT_FOUND") {
1020
+ return failure(`Memory not found in this project: ${parsed.data.id ?? ""}`);
1021
+ }
1022
+ return failure(error instanceof Error ? error.message : String(error));
1023
+ }
1024
+ });
1025
+ return server;
1026
+ }
1027
+ function formatMemory(m, projectId, detailed = false) {
1028
+ const scope = m.actorId === GLOBAL_NAMESPACE ? "global" : "project";
1029
+ const source = m.metadata?.source?.harness;
1030
+ const meta = [m.memoryType, scope, source && `from ${source}`, m.createdAt.toISOString().slice(0, 10)].filter(Boolean);
1031
+ const line = `- [${m.id}] ${m.content} (${meta.join(", ")})`;
1032
+ if (!detailed) return line;
1033
+ return `${line}
1034
+ importance: ${m.importance}
1035
+ tags: ${m.tags.join(", ") || "none"}
1036
+ project: ${scope === "project" ? projectId : "all"}`;
1037
+ }
1038
+ function text(message) {
1039
+ return { content: [{ type: "text", text: message }] };
1040
+ }
1041
+ function failure(message) {
1042
+ return { content: [{ type: "text", text: message }], isError: true };
1043
+ }
1044
+ var HOOK_MAX_CHARS = 6e3;
1045
+ var HOOK_LIMIT = 15;
1046
+ async function sessionStartContext(input, env = process.env) {
1047
+ const cwd = env.CLAUDE_PROJECT_DIR ?? input.cwd ?? process.cwd();
1048
+ const project = resolveProject(cwd);
1049
+ const { config } = await loadConfig();
1050
+ const storage = config.storage ?? new InMemoryStorageAdapter();
1051
+ try {
1052
+ const memory = new HarnessMemory({ storage, llm: config.llm, autoTags: false });
1053
+ await memory.adoptProjects(project.previousIds, project.id);
1054
+ const { hits } = await memory.recall({
1055
+ namespaces: defaultRecallNamespaces(project.id),
1056
+ limit: HOOK_LIMIT,
1057
+ maxChars: HOOK_MAX_CHARS
1058
+ });
1059
+ if (hits.length === 0) return "";
1060
+ return [
1061
+ `MemStack project memory (project ${project.id}), most important first. Follow these unless the user says otherwise; use memory_retrieve for more and memory_store to save new facts.`,
1062
+ ...hits.map(({ memory: m }) => formatLine(m))
1063
+ ].join("\n");
1064
+ } finally {
1065
+ await storage.close();
1066
+ }
1067
+ }
1068
+ async function runSessionStartHook() {
1069
+ try {
1070
+ const input = parseInput(await readStdin());
1071
+ const context = await sessionStartContext(input);
1072
+ if (context) process.stdout.write(`${context}
1073
+ `);
1074
+ } catch (error) {
1075
+ process.stderr.write(`memstack hook: ${error instanceof Error ? error.message : String(error)}
1076
+ `);
1077
+ }
1078
+ }
1079
+ function formatLine(m) {
1080
+ const scope = m.actorId === GLOBAL_NAMESPACE ? "global" : "project";
1081
+ return `- [${m.id}] ${m.content} (${m.memoryType}, ${scope})`;
1082
+ }
1083
+ function parseInput(raw) {
1084
+ if (!raw.trim()) return {};
1085
+ try {
1086
+ const parsed = JSON.parse(raw);
1087
+ return parsed && typeof parsed === "object" ? parsed : {};
1088
+ } catch {
1089
+ return {};
1090
+ }
1091
+ }
1092
+ function readStdin(timeoutMs = 2e3) {
1093
+ if (process.stdin.isTTY) return Promise.resolve("");
1094
+ return new Promise((resolve2) => {
1095
+ let data = "";
1096
+ const timer = setTimeout(() => resolve2(data), timeoutMs);
1097
+ process.stdin.setEncoding("utf8");
1098
+ process.stdin.on("data", (chunk) => data += chunk);
1099
+ process.stdin.on("end", () => {
1100
+ clearTimeout(timer);
1101
+ resolve2(data);
1102
+ });
1103
+ process.stdin.on("error", () => {
1104
+ clearTimeout(timer);
1105
+ resolve2(data);
1106
+ });
1107
+ });
1108
+ }
717
1109
  async function main() {
1110
+ if (process.argv[2] === "hook") {
1111
+ if (process.argv[3] !== "session-start") {
1112
+ console.error(`Unknown hook "${process.argv[3] ?? ""}". Supported: session-start`);
1113
+ return;
1114
+ }
1115
+ await runSessionStartHook();
1116
+ process.exit(0);
1117
+ }
718
1118
  const { values } = parseArgs({
719
1119
  options: {
720
1120
  http: { type: "boolean", default: false },
721
- port: { type: "string", default: "3939" }
1121
+ port: { type: "string", default: "3939" },
1122
+ profile: { type: "string", default: "default" },
1123
+ harness: { type: "string" }
722
1124
  }
723
1125
  });
724
- const { config, defaultActorId } = await loadConfig();
1126
+ if (values.profile === "harness") {
1127
+ if (values.http) throw new Error("--profile harness supports stdio only: the project comes from the harness's working directory");
1128
+ await runHarness(values.harness);
1129
+ return;
1130
+ }
1131
+ if (values.profile !== "default") throw new Error(`Unknown --profile "${values.profile}". Use "default" or "harness".`);
1132
+ const { config, defaultActorId } = await loadConfig2();
725
1133
  if (values.http) {
726
1134
  await runHttp({ config, defaultActorId, port: Number(values.port) });
727
1135
  return;
728
1136
  }
729
- const server = createServer({ config, defaultActorId });
1137
+ const ms = new MemStack(config);
1138
+ const server = createServer({ config, defaultActorId, ms });
1139
+ const transport = new StdioServerTransport();
1140
+ await server.connect(transport);
1141
+ exitWhenStdinCloses(transport, () => ms.close());
1142
+ }
1143
+ async function runHarness(harness) {
1144
+ const cwd = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
1145
+ const project = resolveProject(cwd);
1146
+ const { config } = await loadHarnessConfig();
1147
+ const storage = config.storage ?? new InMemoryStorageAdapter();
1148
+ if (storage instanceof InMemoryStorageAdapter) {
1149
+ console.error("memstack-mcp: storage is in-memory, so memories are lost when this session ends. Run `memstack init` to choose a store.");
1150
+ }
1151
+ const memory = new HarnessMemory({
1152
+ storage,
1153
+ llm: config.llm,
1154
+ onError: (error, context) => console.error(`memstack-mcp: ${context}: ${error.message}`)
1155
+ });
1156
+ const adopted = await memory.adoptProjects(project.previousIds, project.id);
1157
+ if (adopted > 0) console.error(`memstack-mcp: moved ${adopted} memories from this project's previous ID to ${project.id}.`);
1158
+ const server = createHarnessServer({ memory, projectId: project.id, harness, cwd: project.root });
730
1159
  const transport = new StdioServerTransport();
731
1160
  await server.connect(transport);
1161
+ exitWhenStdinCloses(transport, () => storage.close());
1162
+ }
1163
+ function exitWhenStdinCloses(transport, close) {
1164
+ let pending = 0;
1165
+ let ended = false;
1166
+ const finish = () => {
1167
+ if (ended && pending === 0) void close().finally(() => process.exit(0));
1168
+ };
1169
+ const onmessage = transport.onmessage;
1170
+ transport.onmessage = (message) => {
1171
+ if ("method" in message && "id" in message) pending++;
1172
+ onmessage?.(message);
1173
+ };
1174
+ const send = transport.send.bind(transport);
1175
+ transport.send = async (message) => {
1176
+ await send(message);
1177
+ if (!("method" in message) && "id" in message) {
1178
+ pending--;
1179
+ finish();
1180
+ }
1181
+ };
1182
+ process.stdin.once("end", () => {
1183
+ ended = true;
1184
+ finish();
1185
+ });
732
1186
  }
733
1187
  async function runHttp({ config, defaultActorId, port }) {
734
1188
  const ms = new MemStack(config);