oh-memos-mcp 3.1.0 → 3.1.2

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
@@ -6,7 +6,7 @@
6
6
 
7
7
  MCP Server for **oh-memos** — Intelligent Persistent Memory for AI Assistants.
8
8
 
9
- Pure Node.js. No Python required. Works with `npx` out of the box.
9
+ Pure Node.js. No Python required.
10
10
 
11
11
  > **3.0:** requires Node.js 20+, serves legacy 2025-era and MCP `2026-07-28`
12
12
  > clients from the same stdio package, and requires no data migration. Still on
@@ -19,8 +19,8 @@ Pure Node.js. No Python required. Works with `npx` out of the box.
19
19
  - **Node.js 20 or newer**
20
20
 
21
21
  ```bash
22
- npx -y oh-memos-mcp # 3.x, npm latest — needs Node.js 20+
23
- npx -y oh-memos-mcp@2 # 2.x maintenance line — still runs on Node.js 18
22
+ npm i -g oh-memos-mcp # 3.x, npm latest — needs Node.js 20+
23
+ npm i -g oh-memos-mcp@2 # 2.x maintenance line — still runs on Node.js 18
24
24
  ```
25
25
 
26
26
  oh-memos backend API must be running before connecting this MCP server:
@@ -35,50 +35,96 @@ oh-memos backend API must be running before connecting this MCP server:
35
35
 
36
36
  ## Quick Start
37
37
 
38
- Add to your Claude Code `~/.claude/settings.json`:
38
+ Install once, then point the client at the installed entry point:
39
+
40
+ ```bash
41
+ npm install -g oh-memos-mcp
42
+ npm root -g # prints the directory used in "args" below
43
+ ```
39
44
 
40
45
  ```json
41
46
  {
42
47
  "mcpServers": {
43
48
  "oh-memos": {
44
- "command": "npx",
45
- "args": ["-y", "oh-memos-mcp"],
49
+ "command": "node",
50
+ "args": ["<npm root -g>/oh-memos-mcp/dist/index.js"],
46
51
  "env": {
47
52
  "MEMOS_URL": "http://localhost:18000",
48
53
  "MEMOS_USER": "dev_user",
49
54
  "MEMOS_DEFAULT_CUBE": "dev_cube",
50
- "MEMOS_CUBES_DIR": "G:/test/oh-memos/data/oh-memos_cubes"
55
+ "MEMOS_CUBES_DIR": "/path/to/oh-memos/data/oh-memos_cubes",
56
+ "MEMOS_ENV_FILE": "/path/to/oh-memos/.env"
51
57
  }
52
58
  }
53
59
  }
54
60
  }
55
61
  ```
56
62
 
57
- Or use a `.env` file in your working directory (the server auto-discovers it):
63
+ To upgrade, reinstall globally — the config needs no edit:
64
+
65
+ ```bash
66
+ npm install -g oh-memos-mcp@latest
67
+ ```
68
+
69
+ Then restart the client. A running MCP server is never swapped mid-session: the
70
+ stdio pipe is bound when the client starts, so an upgrade takes effect only after
71
+ the client restarts.
72
+
73
+ ### Why not `npx`
74
+
75
+ `npx -y oh-memos-mcp@<version>` also works and is fine for a one-off trial, but it
76
+ costs two extra processes per client — `cmd`/`sh` → `npx-cli` (node) → `cmd`/`sh` →
77
+ server (node), versus a direct launch's two. With several MCP clients open at once
78
+ that overhead is measurable: on one Windows machine running seven clients, the npx
79
+ wrappers alone held ~360 MB. `npx` also pins the version inside every client
80
+ config, so upgrading means editing each one.
81
+
82
+ | | `npx -y oh-memos-mcp@x.y.z` | `node <npm root -g>/oh-memos-mcp/dist/index.js` |
83
+ |---|---|---|
84
+ | Processes per client | 4 | 2 |
85
+ | Upgrade | edit every config | `npm i -g oh-memos-mcp@latest` |
86
+ | Version source | the config | the global install |
87
+
88
+ A third option is to point `args` at a checkout's `dist/index.js`. That is the
89
+ lowest-overhead choice for working *on* oh-memos — `npm run build` takes effect on
90
+ the next client restart, with no publish — but the client then breaks if the
91
+ checkout moves or its build output is stale.
92
+
93
+ Or use a `.env` file in your working directory (the server auto-discovers it when
94
+ the working directory is the project root):
58
95
 
59
96
  ```bash
60
97
  cp node_modules/oh-memos-mcp/.env.example .env
61
98
  # Edit .env with your paths
62
- npx -y oh-memos-mcp
99
+ node "$(npm root -g)/oh-memos-mcp/dist/index.js"
63
100
  ```
64
101
 
65
102
  ---
66
103
 
67
104
  ## Configuration Examples
68
105
 
106
+ The `args` path below assumes npm's **default** global prefix for each platform.
107
+ A custom prefix (nvm, fnm, Volta, a hand-set `prefix`) puts the package
108
+ elsewhere, so confirm with `npm root -g` and use what it prints:
109
+
110
+ ```bash
111
+ node -e "console.log(require('path').join(process.argv[1],'oh-memos-mcp/dist/index.js'))" "$(npm root -g)"
112
+ ```
113
+
69
114
  ### Linux / macOS
70
115
 
71
116
  ```json
72
117
  {
73
118
  "mcpServers": {
74
119
  "oh-memos": {
75
- "command": "npx",
76
- "args": ["-y", "oh-memos-mcp"],
120
+ "command": "node",
121
+ "args": ["/usr/local/lib/node_modules/oh-memos-mcp/dist/index.js"],
77
122
  "env": {
78
123
  "MEMOS_URL": "http://localhost:18000",
79
124
  "MEMOS_USER": "dev_user",
80
125
  "MEMOS_DEFAULT_CUBE": "dev_cube",
81
- "MEMOS_CUBES_DIR": "/home/user/oh-memos/data/oh-memos_cubes"
126
+ "MEMOS_CUBES_DIR": "/home/user/oh-memos/data/oh-memos_cubes",
127
+ "MEMOS_ENV_FILE": "/home/user/oh-memos/.env"
82
128
  }
83
129
  }
84
130
  }
@@ -91,13 +137,14 @@ npx -y oh-memos-mcp
91
137
  {
92
138
  "mcpServers": {
93
139
  "oh-memos": {
94
- "command": "npx",
95
- "args": ["-y", "oh-memos-mcp"],
140
+ "command": "node",
141
+ "args": ["C:/Users/you/AppData/Roaming/npm/node_modules/oh-memos-mcp/dist/index.js"],
96
142
  "env": {
97
143
  "MEMOS_URL": "http://localhost:18000",
98
144
  "MEMOS_USER": "dev_user",
99
145
  "MEMOS_DEFAULT_CUBE": "dev_cube",
100
- "MEMOS_CUBES_DIR": "G:/test/oh-memos/data/oh-memos_cubes"
146
+ "MEMOS_CUBES_DIR": "G:/test/oh-memos/data/oh-memos_cubes",
147
+ "MEMOS_ENV_FILE": "G:/test/oh-memos/.env"
101
148
  }
102
149
  }
103
150
  }
@@ -110,13 +157,14 @@ npx -y oh-memos-mcp
110
157
  {
111
158
  "mcpServers": {
112
159
  "oh-memos": {
113
- "command": "npx",
114
- "args": ["-y", "oh-memos-mcp"],
160
+ "command": "node",
161
+ "args": ["/usr/lib/node_modules/oh-memos-mcp/dist/index.js"],
115
162
  "env": {
116
163
  "MEMOS_URL": "http://localhost:18000",
117
164
  "MEMOS_USER": "dev_user",
118
165
  "MEMOS_DEFAULT_CUBE": "dev_cube",
119
- "MEMOS_CUBES_DIR": "/mnt/g/test/oh-memos/data/oh-memos_cubes"
166
+ "MEMOS_CUBES_DIR": "/mnt/g/test/oh-memos/data/oh-memos_cubes",
167
+ "MEMOS_ENV_FILE": "/mnt/g/test/oh-memos/.env"
120
168
  }
121
169
  }
122
170
  }
@@ -137,13 +185,14 @@ be deleted without a prompt; add it only if you have decided you want that.
137
185
  "mcpServers": {
138
186
  "oh-memos": {
139
187
  "type": "stdio",
140
- "command": "npx",
141
- "args": ["-y", "oh-memos-mcp"],
188
+ "command": "node",
189
+ "args": ["<npm root -g>/oh-memos-mcp/dist/index.js"],
142
190
  "env": {
143
191
  "MEMOS_URL": "http://localhost:18000",
144
192
  "MEMOS_USER": "dev_user",
145
193
  "MEMOS_DEFAULT_CUBE": "dev_cube",
146
- "MEMOS_CUBES_DIR": "/path/to/oh-memos/data/oh-memos_cubes"
194
+ "MEMOS_CUBES_DIR": "/path/to/oh-memos/data/oh-memos_cubes",
195
+ "MEMOS_ENV_FILE": "/path/to/oh-memos/.env"
147
196
  },
148
197
  "alwaysAllow": [
149
198
  "memos_context_resume",
@@ -183,16 +232,80 @@ be deleted without a prompt; add it only if you have decided you want that.
183
232
  | `NEO4J_PASSWORD` | No | — | Neo4j password |
184
233
  | `MEMOS_ENV_FILE` | No | — | Explicit path to a `.env` file. Highest priority — see below |
185
234
 
235
+ ### Mode and provider
236
+
237
+ | Variable | Required | Default | Description |
238
+ |----------|----------|---------|-------------|
239
+ | `MEMOS_MODE` | No | `full` | `lite` runs the local JSONL provider with no API, Python, Neo4j, or Qdrant. `full` uses the HTTP backend |
240
+ | `MEMOS_PROVIDER` | No | `local` when `MEMOS_MODE=lite`, else `api` | Storage backend. Set explicitly only to override the mode-derived default |
241
+ | `MEMOS_LOG_LEVEL` | No | `info` | `debug` / `info` / `warning` / `error`. Logs go to stderr so they never corrupt the stdio JSON-RPC stream |
242
+
243
+ `MEMOS_URL` is required for Full mode and unused in Lite.
244
+
245
+ ### Retrieval behaviour (3.1.x)
246
+
247
+ These change what search returns. All are off or conservative by default, so
248
+ upgrading never silently changes ranking.
249
+
250
+ | Variable | Required | Default | Description |
251
+ |----------|----------|---------|-------------|
252
+ | `MEMOS_SPREAD_ACTIVATION` | No | `false` | One-hop graph spreading activation. When on, a search also returns memories reachable from its direct hits over `CAUSE` / `CONDITION` / `RELATE` edges, annotated ` · via CAUSE from <first 8 chars of the source id>`. Full mode only — it queries Neo4j directly, so `NEO4J_HTTP_URL`, `NEO4J_USER`, and `NEO4J_PASSWORD` must all resolve |
253
+ | `MEMOS_SHOW_WORKING_MEMORY` | No | `false` | Show the scheduler's `WorkingMemory` tier. The backend writes each memory twice — one short-term copy plus one long-term graph node with identical content — so leaving this off is what stops every memory appearing in pairs. Debugging only |
254
+ | `MEMOS_AUTO_CAPTURE` | No | `false` | Accept auto-captured memories. Auto-captured records are also ranked below explicit saves |
255
+
256
+ > **Spreading activation degrades silently without Neo4j credentials.** Retrieval
257
+ > still returns memories and logs nothing unusual — you simply get no ` via `
258
+ > annotations. If you enabled the switch and see no annotations, check that the
259
+ > Neo4j variables actually reach the server (under `npx`, that means
260
+ > `MEMOS_ENV_FILE`), not that the feature is missing.
261
+
262
+ Ranking that needs no configuration: per-type exponential decay (a `PROGRESS`
263
+ note ages out in weeks, a `DECISION` stays relevant for years), access
264
+ reinforcement, and near-duplicate folding with per-type thresholds. Folded
265
+ duplicates are reported as ` · folded N: <ids>` rather than dropped.
266
+
267
+ ### Lite embeddings
268
+
269
+ | Variable | Required | Default | Description |
270
+ |----------|----------|---------|-------------|
271
+ | `MEMOS_LITE_EMBED` | No | `auto` | `off` forces lexical search. `auto` uses Ollama embeddings when reachable and falls back to lexical |
272
+ | `MEMOS_LITE_EMBED_URL` | No | `http://localhost:11434` | Ollama endpoint |
273
+ | `MEMOS_LITE_EMBED_MODEL` | No | `nomic-embed-text` | Embedding model |
274
+
275
+ ### Read from `.env`, not from the client config
276
+
277
+ The server reads these when building a cube, and they are conventionally kept in
278
+ `.env` rather than repeated in every client config. `MOS_CHAT_MODEL` is
279
+ **required** — cube registration dies on it first, surfacing as a cube that
280
+ "registers" and immediately reports itself unregistered.
281
+
282
+ | Variable | Description |
283
+ |----------|-------------|
284
+ | `MOS_CHAT_MODEL` | Chat model for memory extraction. Required |
285
+ | `MOS_EMBEDDER_BACKEND` / `_PROVIDER` / `_MODEL` / `_API_BASE` | Embedder wiring |
286
+ | `MOS_ENABLE_REORGANIZE` | Background memory reorganization |
287
+ | `NEO4J_BACKEND` / `NEO4J_URI` / `NEO4J_DB_NAME` | Graph DB connection for cube construction |
288
+ | `NEO4J_AUTO_CREATE` / `NEO4J_USE_MULTI_DB` | Cube creation policy |
289
+
186
290
  > **On locating `.env`**: without `MEMOS_ENV_FILE`, the file is found by guessing
187
291
  > from position (working directory, two levels above the package, then dotenv's
188
- > upward search). That works from a checkout and **never works under `npx`**,
189
- > where the package root sits in the npm cache and every candidate misses — so no
190
- > variable loads at all. Set `MEMOS_ENV_FILE` (or pass `--memos-env-file`) when the
191
- > client's working directory and the install location are both outside the project.
192
- > A path that does not exist warns on stderr rather than failing silently.
292
+ > upward search). That works from a checkout but **not from a global install or
293
+ > `npx`**, where the package sits outside the project and every candidate misses —
294
+ > so no variable loads at all. Set `MEMOS_ENV_FILE` (or pass `--memos-env-file`)
295
+ > when the client's working directory and the install location are both outside the
296
+ > project. A path that does not exist warns on stderr rather than failing silently.
193
297
  >
194
298
  > Copy `.env.example` to get started.
195
299
 
300
+ > **`MEMOS_ENV_FILE` wins over the client config.** It is loaded with
301
+ > `override: true`, so a value in that file beats the same key in the MCP client's
302
+ > `env` block. The positional fallbacks are the opposite — they only fill gaps and
303
+ > never override the launcher. This is deliberate: an explicitly named env file is
304
+ > meant to be authoritative, while a `.env` that merely happens to sit nearby is
305
+ > not. The practical consequence is that switches like
306
+ > `MEMOS_SHOW_WORKING_MEMORY` cannot be flipped from the client config once
307
+ > `.env` sets them — edit the env file instead.
308
+
196
309
  ---
197
310
 
198
311
  ## Tools (17)
@@ -11,6 +11,7 @@ import { ensureCubeRegistered } from "../cube-manager.js";
11
11
  import { parseMemoryWriteResponse } from "../memory-write-response.js";
12
12
  import { recordAccess } from "../access-tracker.js";
13
13
  import { filterEphemeralTier } from "../memory-tier.js";
14
+ import { findSiblings, renderVerbatimSections, verbatimOf, } from "../verbatim-source.js";
14
15
  // formatMemoriesForDisplay intentionally not imported here — list output is built
15
16
  // from the truncated allMemories in handleMemosList (avoids printing the full cube).
16
17
  import { PREVIEW_COUNT, shouldCompact, compactedResultToText, toMinimal, toFull, } from "../models.js";
@@ -223,6 +224,26 @@ export async function handleMemosList(arguments_) {
223
224
  // ============================================================================
224
225
  // memos_get
225
226
  // ============================================================================
227
+ /**
228
+ * 取同源碎片。只在当前记忆确实带原文时才发这次请求 —— fast-path 写入不会付这个代价。
229
+ *
230
+ * 失败即返回空数组:同源列表是附加信息,取不到时 `memos_get` 仍应给出原文。
231
+ * 拉取量上限 200,避免大 cube 上把整库拉回来(碎片来自同一次写入,时间相邻,
232
+ * 后端按 updated_at 倒序返回,200 条足以覆盖)。
233
+ */
234
+ async function fetchSiblings(cubeId, target) {
235
+ try {
236
+ const result = await apiCallWithRetry("GET", `${MEMOS_URL}/memories`, cubeId, { params: { user_id: MEMOS_USER, mem_cube_id: cubeId, limit: 200 } }, ensureCubeRegistered);
237
+ if (!result.success || !result.data)
238
+ return [];
239
+ const data = result.data.data;
240
+ return findSiblings(target, extractMemoriesFromData(data ?? {}));
241
+ }
242
+ catch (err) {
243
+ logger.debug(`sibling lookup failed: ${String(err)}`);
244
+ return [];
245
+ }
246
+ }
226
247
  export async function handleMemosGet(arguments_) {
227
248
  const cubeId = getCubeIdFromArgs(arguments_);
228
249
  const memoryId = String(arguments_.memory_id ?? "");
@@ -251,10 +272,11 @@ export async function handleMemosGet(arguments_) {
251
272
  `**Cube**: ${full.cubeId} (local)`,
252
273
  full.tags.length ? `**Tags**: ${full.tags.join(", ")}` : "",
253
274
  full.createdAt ? `**Created**: ${full.createdAt}` : "",
254
- "",
255
- "### Content",
256
- "",
257
- full.content,
275
+ // Lite 的 JSONL 不写 sources,verbatimOf 返回 null → 保持原有输出。
276
+ // 保留这个分支是为了后端哪天开始写该字段时自动生效。
277
+ ...(verbatimOf(node) === null
278
+ ? ["", "### Content", "", full.content]
279
+ : renderVerbatimSections(verbatimOf(node), [], full.content)),
258
280
  ]
259
281
  .filter(Boolean)
260
282
  .join("\n"),
@@ -272,6 +294,7 @@ export async function handleMemosGet(arguments_) {
272
294
  // Full 模式同样记在本机侧车 —— 使用度是本机数据,不需要后端端点。
273
295
  recordAccess(MEMOS_CUBES_DIR, cubeId, [memoryId]);
274
296
  const fullMem = toFull(resultData, cubeId, MEMOS_USER);
297
+ const verbatim = verbatimOf(resultData);
275
298
  const lines = [
276
299
  "## 📝 Memory Details",
277
300
  "",
@@ -285,7 +308,14 @@ export async function handleMemosGet(arguments_) {
285
308
  lines.push(`**Tags**: ${fullMem.tags.join(", ")}`);
286
309
  if (fullMem.createdAt)
287
310
  lines.push(`**Created**: ${fullMem.createdAt}`);
288
- lines.push("", "### Content", "", fullMem.content);
311
+ // 有原文时原文即正文,`memory` 字段(LLM 概括)降级到下方的「抽取概括」区块。
312
+ // 无原文(fast-path 写入)时保持原有行为。
313
+ if (verbatim === null) {
314
+ lines.push("", "### Content", "", fullMem.content);
315
+ }
316
+ else {
317
+ lines.push(...renderVerbatimSections(verbatim, await fetchSiblings(cubeId, resultData), fullMem.content));
318
+ }
289
319
  if (fullMem.background) {
290
320
  lines.push("", "### Background", "", fullMem.background);
291
321
  }
@@ -4,9 +4,52 @@
4
4
  * memos_search, memos_search, memos_suggest, memos_context_resume
5
5
  */
6
6
  import type { TextContent, MemoryNode } from "../types.js";
7
+ /**
8
+ * Cypher for the recency query behind `memos_context_resume` and the
9
+ * `temporal` search intent.
10
+ *
11
+ * ## 为什么必须在 Cypher 里排除 WorkingMemory,而不是在 handler 层
12
+ *
13
+ * 这个查询原先不 RETURN `memory_type`,构造出的 metadata 只有
14
+ * `relativity`/`temporal_rank`/`source`。于是 `isEphemeralTier()` 永远读到
15
+ * undefined 并判为可见 —— **下游任何分层过滤对 temporal 记忆都是空转**。
16
+ *
17
+ * 受害面是四个调用点,不只是 context_resume:
18
+ * - `memos_context_resume`(成对显示,用户实测发现)
19
+ * - `memos_search` 的 temporal intent 两处(经 applyMemoryQualityPolicy,
20
+ * 过滤存在但读不到字段)
21
+ * - `memos_think`(同上)
22
+ *
23
+ * 在 Cypher 里滤还顺带避开超额取数:`LIMIT` 在过滤之后,要 10 条就得 10 条真
24
+ * 记忆。若放到 handler 层,limit 会被随即隐藏的副本吃掉(这正是 §9.6 在 list
25
+ * 路径踩过的顺序缺陷)。
26
+ *
27
+ * 同时 RETURN `memory_type`,让下游过滤从空转变成真的有落点 —— 双保险,
28
+ * 且 `mergeTemporalResults` 之后的 quality policy 也能正确判层。
29
+ *
30
+ * 逃生开关 `MEMOS_SHOW_WORKING_MEMORY=true` 时不加这个条件,与
31
+ * `filterEphemeralTier` 的语义保持一致。
32
+ */
33
+ export declare function buildTemporalCypher(timeWindowHours?: number, env?: NodeJS.ProcessEnv): string;
7
34
  export declare function getTemporalMemories(cubeId: string, topK?: number, timeWindowHours?: number): Promise<MemoryNode[]>;
8
35
  export declare function handleMemosSearch(arguments_: Record<string, unknown>): Promise<TextContent[]>;
9
36
  export declare function handleMemosSearchContext(arguments_: Record<string, unknown>): Promise<TextContent[]>;
10
37
  export declare function handleMemosSuggest(arguments_: Record<string, unknown>): Promise<TextContent[]>;
38
+ /**
39
+ * 分层过滤 + 截断,供 `memos_context_resume` 两条路径共用。
40
+ *
41
+ * ## 顺序要紧:滤层级必须在 slice 之前
42
+ *
43
+ * 放到之后,`limit` 会被随即隐藏的 WorkingMemory 副本吃掉 —— 要 10 条只拿到
44
+ * 5 条(用户实测就是 10 条里 5 对)。与 `prepareListMemories` 同形,
45
+ * 那处已经因为同样的原因栽过一次。
46
+ *
47
+ * ## 为什么必须是导出的函数
48
+ *
49
+ * 原先这段逻辑内联在 handler 里,单测触达不到 —— §9.6 与 P1.5 已两次证明:
50
+ * 切断 handler 内联逻辑时全部单测仍然通过。在测试里自行组合只能证明测试文件,
51
+ * 证明不了 handler。所以 handler 与测试必须调用**同一个**函数。
52
+ */
53
+ export declare function prepareResumeMemories(memories: readonly MemoryNode[], limit: number, env?: NodeJS.ProcessEnv): MemoryNode[];
11
54
  export declare function handleMemosContextResume(arguments_: Record<string, unknown>): Promise<TextContent[]>;
12
55
  //# sourceMappingURL=search.d.ts.map
@@ -15,6 +15,7 @@ import { summarizeActiveCanvases } from "./canvas.js";
15
15
  import { applyMemoryQualityPolicy } from "../memory-quality.js";
16
16
  import { readAccessStats } from "../access-tracker.js";
17
17
  import { buildSpreadCypher, mergeSpread, pickSeeds, rankNeighbours, rowToNeighbour, spreadEnabled, spreadRelativity, toSpreadNode, } from "../spreading-activation.js";
18
+ import { filterEphemeralTier, showsEphemeralTier } from "../memory-tier.js";
18
19
  import { apiErrorResponse, cubeRegistrationError, getCubeIdFromArgs, } from "./utils.js";
19
20
  /**
20
21
  * 检索排序策略的选项。
@@ -35,26 +36,59 @@ function qualityOptions(cubeId, query) {
35
36
  // ============================================================================
36
37
  // Temporal Graph Query (Neo4j)
37
38
  // ============================================================================
39
+ /**
40
+ * Cypher for the recency query behind `memos_context_resume` and the
41
+ * `temporal` search intent.
42
+ *
43
+ * ## 为什么必须在 Cypher 里排除 WorkingMemory,而不是在 handler 层
44
+ *
45
+ * 这个查询原先不 RETURN `memory_type`,构造出的 metadata 只有
46
+ * `relativity`/`temporal_rank`/`source`。于是 `isEphemeralTier()` 永远读到
47
+ * undefined 并判为可见 —— **下游任何分层过滤对 temporal 记忆都是空转**。
48
+ *
49
+ * 受害面是四个调用点,不只是 context_resume:
50
+ * - `memos_context_resume`(成对显示,用户实测发现)
51
+ * - `memos_search` 的 temporal intent 两处(经 applyMemoryQualityPolicy,
52
+ * 过滤存在但读不到字段)
53
+ * - `memos_think`(同上)
54
+ *
55
+ * 在 Cypher 里滤还顺带避开超额取数:`LIMIT` 在过滤之后,要 10 条就得 10 条真
56
+ * 记忆。若放到 handler 层,limit 会被随即隐藏的副本吃掉(这正是 §9.6 在 list
57
+ * 路径踩过的顺序缺陷)。
58
+ *
59
+ * 同时 RETURN `memory_type`,让下游过滤从空转变成真的有落点 —— 双保险,
60
+ * 且 `mergeTemporalResults` 之后的 quality policy 也能正确判层。
61
+ *
62
+ * 逃生开关 `MEMOS_SHOW_WORKING_MEMORY=true` 时不加这个条件,与
63
+ * `filterEphemeralTier` 的语义保持一致。
64
+ */
65
+ export function buildTemporalCypher(timeWindowHours, env = process.env) {
66
+ const timeFilter = timeWindowHours
67
+ ? `AND n.updated_at >= datetime() - duration({hours: ${timeWindowHours}})`
68
+ : "";
69
+ const tierFilter = showsEphemeralTier(env)
70
+ ? ""
71
+ : "AND coalesce(n.memory_type, '') <> 'WorkingMemory'";
72
+ return `
73
+ MATCH (n:Memory)
74
+ WHERE n.user_name = $user_name
75
+ AND n.status = 'activated'
76
+ ${timeFilter}
77
+ ${tierFilter}
78
+ RETURN n.id AS id, n.memory AS memory, n.key AS key,
79
+ n.updated_at AS updated_at, n.background AS background,
80
+ n.tags AS tags, n.memory_type AS memory_type
81
+ ORDER BY n.updated_at DESC
82
+ LIMIT $top_k
83
+ `;
84
+ }
38
85
  export async function getTemporalMemories(cubeId, topK = 10, timeWindowHours) {
39
86
  if (!NEO4J_HTTP_URL || !NEO4J_USER || !NEO4J_PASSWORD) {
40
87
  logger.debug("Neo4j config missing, skipping temporal query");
41
88
  return [];
42
89
  }
43
90
  const auth = Buffer.from(`${NEO4J_USER}:${NEO4J_PASSWORD}`).toString("base64");
44
- const timeFilter = timeWindowHours
45
- ? `AND n.updated_at >= datetime() - duration({hours: ${timeWindowHours}})`
46
- : "";
47
- const cypher = `
48
- MATCH (n:Memory)
49
- WHERE n.user_name = $user_name
50
- AND n.status = 'activated'
51
- ${timeFilter}
52
- RETURN n.id AS id, n.memory AS memory, n.key AS key,
53
- n.updated_at AS updated_at, n.background AS background,
54
- n.tags AS tags
55
- ORDER BY n.updated_at DESC
56
- LIMIT $top_k
57
- `;
91
+ const cypher = buildTemporalCypher(timeWindowHours);
58
92
  try {
59
93
  const response = await fetchWithTimeout(NEO4J_HTTP_URL, {
60
94
  method: "POST",
@@ -96,6 +130,13 @@ export async function getTemporalMemories(cubeId, topK = 10, timeWindowHours) {
96
130
  relativity: 0.8 - i * 0.05,
97
131
  temporal_rank: i + 1,
98
132
  source: "temporal_query",
133
+ // 带上层级,否则下游 filterEphemeralTier 读到 undefined 而空转。
134
+ // Cypher 已排除 WorkingMemory,这里是双保险 + 让 quality policy
135
+ // 能正确判层。r[6] 可能缺失(旧节点没写该属性),留 undefined 即
136
+ // 按可见处理,与 isEphemeralTier 的缺失语义一致。
137
+ ...(r.length > 6 && r[6] != null
138
+ ? { memory_type: String(r[6]) }
139
+ : {}),
99
140
  },
100
141
  });
101
142
  }
@@ -467,7 +508,7 @@ function renderContextResume(cubeId, recentMemories) {
467
508
  lines.push(...canvasLines);
468
509
  if (recentMemories.length > 0) {
469
510
  lines.push(`**Recent memories** (${recentMemories.length} items, last 24h):`, "");
470
- for (let i = 0; i < Math.min(recentMemories.length, 10); i++)
511
+ for (let i = 0; i < Math.min(recentMemories.length, RESUME_LIMIT); i++)
471
512
  lines.push(`${i + 1}. ${(recentMemories[i].memory ?? "").slice(0, 120).split("\n")[0]}`);
472
513
  }
473
514
  else
@@ -475,25 +516,68 @@ function renderContextResume(cubeId, recentMemories) {
475
516
  lines.push("", "---", "**REMINDER**: Use MCP memos tools for ALL memory operations.", "NEVER use `mkdir` or `Write` to create memory files.");
476
517
  return [{ type: "text", text: lines.join("\n") }];
477
518
  }
519
+ /**
520
+ * 分层过滤 + 截断,供 `memos_context_resume` 两条路径共用。
521
+ *
522
+ * ## 顺序要紧:滤层级必须在 slice 之前
523
+ *
524
+ * 放到之后,`limit` 会被随即隐藏的 WorkingMemory 副本吃掉 —— 要 10 条只拿到
525
+ * 5 条(用户实测就是 10 条里 5 对)。与 `prepareListMemories` 同形,
526
+ * 那处已经因为同样的原因栽过一次。
527
+ *
528
+ * ## 为什么必须是导出的函数
529
+ *
530
+ * 原先这段逻辑内联在 handler 里,单测触达不到 —— §9.6 与 P1.5 已两次证明:
531
+ * 切断 handler 内联逻辑时全部单测仍然通过。在测试里自行组合只能证明测试文件,
532
+ * 证明不了 handler。所以 handler 与测试必须调用**同一个**函数。
533
+ */
534
+ export function prepareResumeMemories(memories, limit, env = process.env) {
535
+ const visible = filterEphemeralTier(memories, (m) => m.metadata, env);
536
+ return [...visible].slice(0, limit);
537
+ }
538
+ /** `memos_context_resume` 显示的条数上限。 */
539
+ const RESUME_LIMIT = 10;
540
+ /**
541
+ * 取数时的超额系数。
542
+ *
543
+ * API 回退路径的 limit 由服务端施加,过滤只能在取回之后做,所以必须多取 ——
544
+ * 每条记忆恰好有一个 WorkingMemory 副本,2 倍即可覆盖,取 3 倍留余量
545
+ * (旧节点可能没有副本,多取的部分会被 slice 丢掉,无副作用)。
546
+ *
547
+ * temporal 路径不需要这个:Cypher 的 LIMIT 在过滤之后。
548
+ */
549
+ const RESUME_OVERFETCH = 3;
478
550
  export async function handleMemosContextResume(arguments_) {
479
551
  const cubeId = getCubeIdFromArgs(arguments_);
480
552
  const localProvider = getMemoryProvider(MEMOS_CUBES_DIR);
481
553
  if (localProvider) {
482
- const recentMemories = await localProvider.recent(cubeId, 24, 10);
483
- return renderContextResume(cubeId, recentMemories);
554
+ // Lite 的 JSONL 不写 memory_type,filterEphemeralTier 按缺失即可见处理,
555
+ // 所以这里过滤是空操作 —— 保留它是为了后端哪天开始写该字段时自动生效。
556
+ const recentMemories = await localProvider.recent(cubeId, 24, RESUME_LIMIT);
557
+ return renderContextResume(cubeId, prepareResumeMemories(recentMemories, RESUME_LIMIT));
484
558
  }
485
559
  const [regSuccess, regError] = await ensureCubeRegistered(cubeId);
486
560
  if (!regSuccess)
487
561
  return cubeRegistrationError(cubeId, regError);
488
- // Try temporal query first
489
- let recentMemories = await getTemporalMemories(cubeId, 10, 24);
562
+ // Try temporal query first. Cypher 已排除 WorkingMemory 且 LIMIT 在过滤之后,
563
+ // 所以按原样取 RESUME_LIMIT 条就是 RESUME_LIMIT 条真记忆。
564
+ let recentMemories = await getTemporalMemories(cubeId, RESUME_LIMIT, 24);
490
565
  // Fallback to API list
491
566
  if (recentMemories.length === 0) {
492
- const result = await apiCallWithRetry("GET", `${MEMOS_URL}/memories`, cubeId, { params: { user_id: MEMOS_USER, mem_cube_id: cubeId, limit: 10 } }, ensureCubeRegistered);
567
+ const result = await apiCallWithRetry("GET", `${MEMOS_URL}/memories`, cubeId, {
568
+ params: {
569
+ user_id: MEMOS_USER,
570
+ mem_cube_id: cubeId,
571
+ // 超额取:服务端施加 limit,过滤只能在取回后做,不多取则 limit 被
572
+ // 随即隐藏的副本吃掉。
573
+ limit: RESUME_LIMIT * RESUME_OVERFETCH,
574
+ },
575
+ }, ensureCubeRegistered);
493
576
  if (result.success && result.data) {
494
577
  recentMemories = extractSearchMemories(result.data.data ?? {});
495
578
  }
496
579
  }
580
+ recentMemories = prepareResumeMemories(recentMemories, RESUME_LIMIT);
497
581
  const lines = ["## Context Resumed", ""];
498
582
  // Unfinished task canvases come first: after a compaction, "where was I" is
499
583
  // more urgent than "what do we know". Headlines only — the model opens what it
@@ -503,7 +587,7 @@ export async function handleMemosContextResume(arguments_) {
503
587
  lines.push(...canvasLines);
504
588
  if (recentMemories.length > 0) {
505
589
  lines.push(`**Recent memories** (${recentMemories.length} items, last 24h):`, "");
506
- for (let i = 0; i < Math.min(recentMemories.length, 10); i++) {
590
+ for (let i = 0; i < Math.min(recentMemories.length, RESUME_LIMIT); i++) {
507
591
  const mem = recentMemories[i];
508
592
  const content = mem.memory ?? "";
509
593
  const summary = content.slice(0, 120).split("\n")[0];
@@ -32,20 +32,20 @@ export const SPREAD_FROM_KEY = "spread_from";
32
32
  */
33
33
  export function buildSpreadCypher() {
34
34
  const types = SPREAD_EDGE_TYPES.map((t) => `'${t}'`).join(", ");
35
- return `
36
- MATCH (n:Memory)-[r]-(m:Memory)
37
- WHERE n.id IN $seeds
38
- AND type(r) IN [${types}]
39
- AND NOT m.id IN $seeds
40
- AND m.user_name = $user_name
41
- AND coalesce(m.status, 'activated') = 'activated'
42
- AND coalesce(m.memory_type, '') <> 'WorkingMemory'
43
- AND coalesce(m.type, '') <> 'reasoning'
44
- WITH m, n, type(r) AS via, size([(m)--() | 1]) AS degree
45
- WITH m, collect({via: via, from: n.id})[0] AS edge, degree
46
- RETURN m.id AS id, m.memory AS memory, m.key AS key, m.tags AS tags,
47
- m.updated_at AS updated_at, edge.via AS via, edge.from AS from,
48
- degree
35
+ return `
36
+ MATCH (n:Memory)-[r]-(m:Memory)
37
+ WHERE n.id IN $seeds
38
+ AND type(r) IN [${types}]
39
+ AND NOT m.id IN $seeds
40
+ AND m.user_name = $user_name
41
+ AND coalesce(m.status, 'activated') = 'activated'
42
+ AND coalesce(m.memory_type, '') <> 'WorkingMemory'
43
+ AND coalesce(m.type, '') <> 'reasoning'
44
+ WITH m, n, type(r) AS via, size([(m)--() | 1]) AS degree
45
+ WITH m, collect({via: via, from: n.id})[0] AS edge, degree
46
+ RETURN m.id AS id, m.memory AS memory, m.key AS key, m.tags AS tags,
47
+ m.updated_at AS updated_at, edge.via AS via, edge.from AS from,
48
+ degree
49
49
  `;
50
50
  }
51
51
  /**
@@ -0,0 +1,76 @@
1
+ /**
2
+ * 原文取回(verbatim source)。
3
+ *
4
+ * ## 为什么需要它
5
+ *
6
+ * `MOS_TYPED_SAVE_FAST=false` 时后端走 LLM 抽取:一次写入被切成多条记忆,每条的
7
+ * `memory` 字段是 LLM 概括(约 200-250 字),而**逐字原文保存在
8
+ * `metadata.sources[].content` 里**(实测:一条 877 字原文被切成 5 条,5 条的
9
+ * sources 内容 sha 完全相同)。
10
+ *
11
+ * 于是同一份数据有两层,用途不同:
12
+ *
13
+ * | 层 | 内容 | 服务于 |
14
+ * |---|---|---|
15
+ * | `memory` | LLM 概括,细粒度 | 向量化与建边 —— 图谱节点质量、扩散联想 |
16
+ * | `metadata.sources[].content` | 逐字原文 | `memos_get` —— 完整上下文 |
17
+ *
18
+ * 两者不冲突:抽取让图谱拿到细粒度节点(`memos_search` 的 ` via CAUSE from` 联想
19
+ * 依赖它),而 `memos_get` 读 sources 就不丢原文。维护者的决定是「保证图谱节点
20
+ * 编写分析,同时让 memos_get 拉原文」。
21
+ *
22
+ * ## 同源判定用内容指纹,不用 session_id
23
+ *
24
+ * 实测 5 条碎片的 `session_id` 相同 —— 但那是**整个 MCP 会话**的 id,一个会话里
25
+ * 的多次无关写入会共享它,用它分组会把无关记忆归到一起。`created_at` 也不行:
26
+ * 5 条各不相同(微秒级递增)。
27
+ *
28
+ * 只有 sources 内容本身是精确的分组键。
29
+ *
30
+ * ## fast-path 下本模块是无操作
31
+ *
32
+ * `MOS_TYPED_SAVE_FAST=true`(默认)时原样存储,`memory` 就是原文、`sources` 为空
33
+ * → `verbatimOf()` 返回 null,渲染层保持原有行为。所以这套改动对两种模式都安全。
34
+ *
35
+ * 设计讨论见 docs/design/memory-retrieval-optimization.md。
36
+ */
37
+ import type { MemoryNode } from "./types.js";
38
+ /**
39
+ * 取出逐字原文。
40
+ *
41
+ * 没有 sources、sources 为空、或第一条没有非空 content 时返回 null —— 调用方据此
42
+ * 判断「这条是 fast-path 存的,无需原文层」。
43
+ *
44
+ * 多条 sources 时用第一条:LLM 抽取路径下一次写入只产生一个 source(实测
45
+ * `src_n=1`)。多条的情况来自 messages 数组写入,此时第一条仍是用户输入。
46
+ */
47
+ export declare function verbatimOf(node: MemoryNode | undefined | null): string | null;
48
+ /**
49
+ * 同源指纹。同一次写入切出的碎片共享它。
50
+ *
51
+ * 直接用原文本身,不做哈希:这是进程内的分组键,不持久化也不跨机器比较,
52
+ * 字符串相等就是精确判定。
53
+ *
54
+ * 初版加了 `${text.length}:` 前缀,理由是「让不同原文在第一个字符就分开」——
55
+ * 变异验证时发现构造不出它能防住的碰撞(去掉前缀后 `abc` 与 `abcdef` 指纹依然
56
+ * 不同),且 V8 的字符串相等本就先比长度。没有测试能支撑的防御代码比没有更糟,
57
+ * 所以删掉。
58
+ *
59
+ * 无原文返回 null —— null 不参与分组,避免把所有 fast-path 记忆归成一组。
60
+ */
61
+ export declare function sourceFingerprint(node: MemoryNode | undefined | null): string | null;
62
+ /** 同源碎片的展示条目。 */
63
+ export interface SiblingRef {
64
+ id: string;
65
+ key: string;
66
+ }
67
+ /** 渲染原文区块与同源碎片列表,供 `memos_get` 两条路径共用。 */
68
+ export declare function renderVerbatimSections(verbatim: string | null, siblings: readonly SiblingRef[], summary: string): string[];
69
+ /**
70
+ * 在候选集里找与 target 同源的其他碎片。
71
+ *
72
+ * 排除 target 自身,也排除没有原文的节点。顺序按候选集原顺序,由调用方决定
73
+ * 候选集怎么排(后端返回顺序通常已按 created_at)。
74
+ */
75
+ export declare function findSiblings(target: MemoryNode | undefined | null, candidates: readonly (MemoryNode | undefined | null)[], limit?: number): SiblingRef[];
76
+ //# sourceMappingURL=verbatim-source.d.ts.map
@@ -0,0 +1,118 @@
1
+ /**
2
+ * 原文取回(verbatim source)。
3
+ *
4
+ * ## 为什么需要它
5
+ *
6
+ * `MOS_TYPED_SAVE_FAST=false` 时后端走 LLM 抽取:一次写入被切成多条记忆,每条的
7
+ * `memory` 字段是 LLM 概括(约 200-250 字),而**逐字原文保存在
8
+ * `metadata.sources[].content` 里**(实测:一条 877 字原文被切成 5 条,5 条的
9
+ * sources 内容 sha 完全相同)。
10
+ *
11
+ * 于是同一份数据有两层,用途不同:
12
+ *
13
+ * | 层 | 内容 | 服务于 |
14
+ * |---|---|---|
15
+ * | `memory` | LLM 概括,细粒度 | 向量化与建边 —— 图谱节点质量、扩散联想 |
16
+ * | `metadata.sources[].content` | 逐字原文 | `memos_get` —— 完整上下文 |
17
+ *
18
+ * 两者不冲突:抽取让图谱拿到细粒度节点(`memos_search` 的 ` via CAUSE from` 联想
19
+ * 依赖它),而 `memos_get` 读 sources 就不丢原文。维护者的决定是「保证图谱节点
20
+ * 编写分析,同时让 memos_get 拉原文」。
21
+ *
22
+ * ## 同源判定用内容指纹,不用 session_id
23
+ *
24
+ * 实测 5 条碎片的 `session_id` 相同 —— 但那是**整个 MCP 会话**的 id,一个会话里
25
+ * 的多次无关写入会共享它,用它分组会把无关记忆归到一起。`created_at` 也不行:
26
+ * 5 条各不相同(微秒级递增)。
27
+ *
28
+ * 只有 sources 内容本身是精确的分组键。
29
+ *
30
+ * ## fast-path 下本模块是无操作
31
+ *
32
+ * `MOS_TYPED_SAVE_FAST=true`(默认)时原样存储,`memory` 就是原文、`sources` 为空
33
+ * → `verbatimOf()` 返回 null,渲染层保持原有行为。所以这套改动对两种模式都安全。
34
+ *
35
+ * 设计讨论见 docs/design/memory-retrieval-optimization.md。
36
+ */
37
+ /**
38
+ * 取出逐字原文。
39
+ *
40
+ * 没有 sources、sources 为空、或第一条没有非空 content 时返回 null —— 调用方据此
41
+ * 判断「这条是 fast-path 存的,无需原文层」。
42
+ *
43
+ * 多条 sources 时用第一条:LLM 抽取路径下一次写入只产生一个 source(实测
44
+ * `src_n=1`)。多条的情况来自 messages 数组写入,此时第一条仍是用户输入。
45
+ */
46
+ export function verbatimOf(node) {
47
+ const sources = node?.metadata
48
+ ?.sources;
49
+ if (!Array.isArray(sources) || sources.length === 0)
50
+ return null;
51
+ const first = sources[0];
52
+ const content = first?.content;
53
+ if (typeof content !== "string" || content.trim() === "")
54
+ return null;
55
+ return content;
56
+ }
57
+ /**
58
+ * 同源指纹。同一次写入切出的碎片共享它。
59
+ *
60
+ * 直接用原文本身,不做哈希:这是进程内的分组键,不持久化也不跨机器比较,
61
+ * 字符串相等就是精确判定。
62
+ *
63
+ * 初版加了 `${text.length}:` 前缀,理由是「让不同原文在第一个字符就分开」——
64
+ * 变异验证时发现构造不出它能防住的碰撞(去掉前缀后 `abc` 与 `abcdef` 指纹依然
65
+ * 不同),且 V8 的字符串相等本就先比长度。没有测试能支撑的防御代码比没有更糟,
66
+ * 所以删掉。
67
+ *
68
+ * 无原文返回 null —— null 不参与分组,避免把所有 fast-path 记忆归成一组。
69
+ */
70
+ export function sourceFingerprint(node) {
71
+ return verbatimOf(node);
72
+ }
73
+ /** 渲染原文区块与同源碎片列表,供 `memos_get` 两条路径共用。 */
74
+ export function renderVerbatimSections(verbatim, siblings, summary) {
75
+ if (verbatim === null)
76
+ return [];
77
+ const lines = ["", "### 原文 | Verbatim source", "", verbatim];
78
+ // 概括也一并给出:它是向量化与建边的实际输入,看得见才能判断图谱节点切得对不对。
79
+ if (summary.trim() !== "" && summary.trim() !== verbatim.trim()) {
80
+ lines.push("", "### 本节点的抽取概括 | Extracted summary (this node)", "", summary);
81
+ }
82
+ if (siblings.length > 0) {
83
+ lines.push("", `### 同源节点 | Sibling nodes (${siblings.length})`, "", "同一份原文被 LLM 抽取切成的其他节点。原文在每条上都完整,无需逐条取回;", "这里列出是为了看清图谱节点的切分方式。", "Other nodes the same source was split into. Each carries the full source;", "listed so the graph-node segmentation is visible.", "");
84
+ for (const s of siblings) {
85
+ lines.push(`- \`${s.id}\`${s.key ? ` — ${s.key}` : ""}`);
86
+ }
87
+ }
88
+ return lines;
89
+ }
90
+ /**
91
+ * 在候选集里找与 target 同源的其他碎片。
92
+ *
93
+ * 排除 target 自身,也排除没有原文的节点。顺序按候选集原顺序,由调用方决定
94
+ * 候选集怎么排(后端返回顺序通常已按 created_at)。
95
+ */
96
+ export function findSiblings(target, candidates, limit = 12) {
97
+ const fp = sourceFingerprint(target);
98
+ if (fp === null)
99
+ return [];
100
+ const targetId = String(target?.id ?? "");
101
+ const out = [];
102
+ const seen = new Set();
103
+ for (const node of candidates) {
104
+ if (out.length >= limit)
105
+ break;
106
+ const id = String(node?.id ?? "");
107
+ if (!id || id === targetId || seen.has(id))
108
+ continue;
109
+ if (sourceFingerprint(node) !== fp)
110
+ continue;
111
+ seen.add(id);
112
+ const meta = (node?.metadata ?? {});
113
+ const key = String(meta.key ?? "").trim();
114
+ out.push({ id, key });
115
+ }
116
+ return out;
117
+ }
118
+ //# sourceMappingURL=verbatim-source.js.map
package/package.json CHANGED
@@ -1,67 +1,68 @@
1
- {
2
- "name": "oh-memos-mcp",
3
- "version": "3.1.0",
4
- "description": "MCP Server for oh-memos — Intelligent Memory Management (Node.js, no Python required)",
5
- "type": "module",
6
- "main": "dist/index.js",
7
- "bin": {
8
- "oh-memos-mcp": "dist/index.js"
9
- },
10
- "files": [
11
- "dist/**/*.js",
12
- "dist/**/*.d.ts",
13
- "README.md",
14
- "CHANGELOG.md",
15
- ".env.example"
16
- ],
17
- "scripts": {
18
- "build": "tsc",
19
- "dev": "tsx src/index.ts",
20
- "test": "vitest run",
21
- "test:protocol": "npm run build && node scripts/protocol-smoke.mjs && node scripts/protocol-v2-smoke.mjs",
22
- "test:protocol:v2": "npm run build && node scripts/protocol-v2-smoke.mjs",
23
- "test:lite-smoke": "npm run build && node scripts/lite-smoke-test.mjs",
24
- "test:spread-smoke": "npm run build && node scripts/spread-smoke-test.mjs",
25
- "schema:semantic": "npm run build && node scripts/schema-semantic-snapshot.mjs --check",
26
- "schema:semantic:freeze": "npm run build && node scripts/schema-semantic-snapshot.mjs --write",
27
- "test:pack": "node scripts/pack-contract.mjs",
28
- "test:watch": "vitest",
29
- "schema:budget": "npm run build && node scripts/schema-budget.mjs --check",
30
- "schema:budget:freeze": "npm run build && node scripts/schema-budget.mjs --write",
31
- "prepublishOnly": "npm run build"
32
- },
33
- "engines": {
34
- "node": ">=20.0.0"
35
- },
36
- "keywords": [
37
- "mcp",
38
- "memory",
39
- "memos",
40
- "claude",
41
- "ai",
42
- "knowledge-graph"
43
- ],
44
- "author": "xigou",
45
- "license": "MIT",
46
- "repository": {
47
- "type": "git",
48
- "url": "https://github.com/lsg1103275794/oh-memos.git",
49
- "directory": "mcp-server-node"
50
- },
51
- "bugs": {
52
- "url": "https://github.com/lsg1103275794/oh-memos/issues"
53
- },
54
- "homepage": "https://github.com/lsg1103275794/oh-memos/tree/main/mcp-server-node",
55
- "dependencies": {
56
- "@modelcontextprotocol/server": "^2.0.0",
57
- "dotenv": "^16.0.0",
58
- "zod": "^4.2.0"
59
- },
60
- "devDependencies": {
61
- "@modelcontextprotocol/client": "^2.0.0",
62
- "@types/node": "^22.0.0",
63
- "tsx": "^4.23.12",
64
- "typescript": "^5.5.0",
65
- "vitest": "3.2.7"
66
- }
67
- }
1
+ {
2
+ "name": "oh-memos-mcp",
3
+ "version": "3.1.2",
4
+ "description": "MCP Server for oh-memos — Intelligent Memory Management (Node.js, no Python required)",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "oh-memos-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist/**/*.js",
12
+ "dist/**/*.d.ts",
13
+ "README.md",
14
+ "CHANGELOG.md",
15
+ ".env.example"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "dev": "tsx src/index.ts",
20
+ "test": "vitest run",
21
+ "test:protocol": "npm run build && node scripts/protocol-smoke.mjs && node scripts/protocol-v2-smoke.mjs",
22
+ "test:protocol:v2": "npm run build && node scripts/protocol-v2-smoke.mjs",
23
+ "test:lite-smoke": "npm run build && node scripts/lite-smoke-test.mjs",
24
+ "test:spread-smoke": "npm run build && node scripts/spread-smoke-test.mjs",
25
+ "test:host-env-smoke": "npm run build && node scripts/host-env-smoke.mjs",
26
+ "schema:semantic": "npm run build && node scripts/schema-semantic-snapshot.mjs --check",
27
+ "schema:semantic:freeze": "npm run build && node scripts/schema-semantic-snapshot.mjs --write",
28
+ "test:pack": "node scripts/pack-contract.mjs",
29
+ "test:watch": "vitest",
30
+ "schema:budget": "npm run build && node scripts/schema-budget.mjs --check",
31
+ "schema:budget:freeze": "npm run build && node scripts/schema-budget.mjs --write",
32
+ "prepublishOnly": "npm run build"
33
+ },
34
+ "engines": {
35
+ "node": ">=20.0.0"
36
+ },
37
+ "keywords": [
38
+ "mcp",
39
+ "memory",
40
+ "memos",
41
+ "claude",
42
+ "ai",
43
+ "knowledge-graph"
44
+ ],
45
+ "author": "xigou",
46
+ "license": "MIT",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "https://github.com/lsg1103275794/oh-memos.git",
50
+ "directory": "mcp-server-node"
51
+ },
52
+ "bugs": {
53
+ "url": "https://github.com/lsg1103275794/oh-memos/issues"
54
+ },
55
+ "homepage": "https://github.com/lsg1103275794/oh-memos/tree/main/mcp-server-node",
56
+ "dependencies": {
57
+ "@modelcontextprotocol/server": "^2.0.0",
58
+ "dotenv": "^16.0.0",
59
+ "zod": "^4.2.0"
60
+ },
61
+ "devDependencies": {
62
+ "@modelcontextprotocol/client": "^2.0.0",
63
+ "@types/node": "^22.0.0",
64
+ "tsx": "^4.23.12",
65
+ "typescript": "^5.5.0",
66
+ "vitest": "3.2.7"
67
+ }
68
+ }