oh-memos-mcp 3.1.7 → 3.1.8

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/CHANGELOG.md CHANGED
@@ -6,6 +6,55 @@ This project follows [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [3.1.8] - 2026-08-27
10
+
11
+ 仅 MCP server(npm `oh-memos-mcp`)。Python 包与容器镜像无改动,仍为 3.1.5。
12
+
13
+ ### 🏷️ `memos_get` 报 root 无权限,而 root 从未被传入
14
+
15
+ `memos_get` 打自己项目的 cube 时回:
16
+
17
+ ```
18
+ ❌ [API_ERROR] Get failed: User 'root' does not have access to cube
19
+ 'jincaizhaopin_cube'. Please register the cube first or request access.
20
+ ```
21
+
22
+ 但 `MEMOS_USER=dev_user`,root 不在任何调用参数里。
23
+
24
+ root 来自后端的默认回退:`start_api.py` 的 `get_memory(mem_cube_id, memory_id,
25
+ user_id=None)` 把 `user_id` 声明为可选,缺失时 `core.py` 的 `get()` 回退到
26
+ `self.user_id`(MOS 实例自己的用户,即 root)。而 `ensureCubeRegistered` 注册的是
27
+ `MEMOS_USER`,于是 root 对该 cube 无授权,`_validate_cube_access` 抛 ValueError,
28
+ 被 ValueError handler 映射成 400。
29
+
30
+ **`handleMemosGet` 是唯一漏传 `user_id` 的调用点** —— save / list / search /
31
+ stats / calendar / siblings 全都传了,admin 的 delete 路径也拼了查询串。
32
+
33
+ `apiCallWithRetry` 的 400 分支救不回来:它把 400 当「cube 未加载」,重注册后重试
34
+ —— 但重注册仍按 `MEMOS_USER` 注册,重试请求仍然不带 `user_id`,第二次照样落到 root。
35
+
36
+ 实测对照(同一个不存在的 id,`jincaizhaopin_cube`):
37
+
38
+ ```
39
+ 不带 user_id -> User 'root' does not have access to cube 'jincaizhaopin_cube'
40
+ 带 user_id=dev_user -> Memory with ID ... not found ← 正常语义
41
+ ```
42
+
43
+ 为什么以前没人踩到:cube 的**归属**决定是否触发。root 名下的 cube(`dev_cube`、
44
+ `claude_cube`、`memos_cube` 等)不受影响;只有 owner 是 `MEMOS_USER` 的项目 cube
45
+ 才会报错 —— 也就是每一个用 `project_path` 自动派生出来的 cube。
46
+
47
+ 同源缺陷也修了 `wiki-import.ts` 的 `getStoredMemory`。那里更隐蔽:该消息既不含
48
+ `not found` 也不是 404,会被当成真实故障,把整页 import 判成失败而不是「记忆不存在」。
49
+
50
+ `apiErrorResponse` 另加了对 `does not have access` 的识别。该消息同时含 `user` 和
51
+ cube 字样,却不匹配任何既有分支,此前只能给通用的「查 health / 看日志」。新提示先
52
+ 指向 `user_id`,因为最常见的成因是调用方漏传它,而不是用户或 cube 不存在。
53
+
54
+ 回归测试 `get-user-id-param.test.ts` 的 fetch stub **按 `user_id` 是否存在分流**
55
+ 应答,复刻上面的实测对照。无条件回 not-found 的 stub 守不住这个缺陷 —— 变异验证
56
+ (把 params 改回 `{}`)时三条断言必须全红,写测试时实际拿它验过。
57
+
9
58
  ## [3.1.7] - 2026-08-27
10
59
 
11
60
  仅 MCP server(npm `oh-memos-mcp`)。Python 包与容器镜像无改动,仍为 3.1.5。
@@ -50,7 +99,7 @@ export function canvasName(prefix: string, goal: string): string {
50
99
 
51
100
  ### 🧪 测试
52
101
 
53
- canvas-format 单测 41 → 50,canvas e2e 31 → 36 项。
102
+ canvas-format 单测 41 → 50,canvas e2e 34 → 40 项。
54
103
 
55
104
  核心断言是**不动点**而不是长度:`slugify(canvasName(p, g)) === canvasName(p, g)`,
56
105
  跨 3 个前缀 × 6 种 goal(超长、纯 CJK、尾部连字符诱饵、前 60 字符相同的两个变体)。
@@ -66,7 +115,7 @@ e2e 补的 5 项跨 open/list/show/delete 与磁盘:`open` 报的名字必须
66
115
  它们守的是「如果有人改用『让 `canvasPath` 对有损名字抛错』来修这个缺陷,
67
116
  这条回归会被抓到」,不是这次的不变量。
68
117
 
69
- 门禁:vitest 520 passed、tsc、canvas e2e 36 项、pack 契约 104 文件、
118
+ 门禁:vitest 520 passed、tsc、canvas e2e 40 项、pack 契约 104 文件、
70
119
  schema budget +0.8%、semantic 快照 17 工具、protocol legacy + v2、
71
120
  lite smoke、host-env smoke、spread smoke。
72
121
 
@@ -137,7 +186,7 @@ commit message 或记忆正文引用的 `001-N3` 会变得指向两个不同画
137
186
 
138
187
  ### 🧪 测试
139
188
 
140
- canvas 单测 41 → 45(format)、28 → 44(store),canvas e2e 15 → 31 项。
189
+ canvas 单测 41 → 45(format)、28 → 44(store),canvas e2e 18 → 34 项。
141
190
  两个修复都做了反向验证,还原实现后对应用例确实变红:转义 3 项、水位线 1 项(含 delete 侧那次抬升)。
142
191
 
143
192
  e2e 补了「旧 cube(无水位线)删除后不复用前缀」这条真实升级路径 ——
@@ -150,7 +199,7 @@ e2e 补了「旧 cube(无水位线)删除后不复用前缀」这条真实
150
199
  两处冻结的哈希同步重算:`schema-semantic-baseline.json` 与
151
200
  `protocol-contract.test.ts` 的 `EXPECTED_SCHEMA_HASHES`。
152
201
 
153
- 门禁:vitest 511 passed、tsc、canvas e2e 31 项、pack 契约 104 文件、
202
+ 门禁:vitest 511 passed、tsc、canvas e2e 34 项、pack 契约 104 文件、
154
203
  schema budget +0.8%(限 5%)、semantic 快照 17 工具、protocol v2、lite smoke。
155
204
 
156
205
  ---
@@ -286,7 +286,13 @@ export async function handleMemosGet(arguments_) {
286
286
  const [regSuccess, regError] = await ensureCubeRegistered(cubeId);
287
287
  if (!regSuccess)
288
288
  return cubeRegistrationError(cubeId, regError);
289
- const result = await apiCallWithRetry("GET", `${MEMOS_URL}/memories/${cubeId}/${memoryId}`, cubeId, {}, ensureCubeRegistered);
289
+ const result = await apiCallWithRetry("GET", `${MEMOS_URL}/memories/${cubeId}/${memoryId}`, cubeId,
290
+ // user_id 必须显式传:后端 `get_memory` 的 user_id 是可选参数,缺失时回退到
291
+ // MOS 实例自己的 user_id(root)。而 ensureCubeRegistered 注册的是 MEMOS_USER
292
+ // (dev_user),于是 root 对该 cube 无授权,校验抛 ValueError → 400
293
+ // "User 'root' does not have access to cube '...'"。重注册重试也救不回来:
294
+ // 它仍然按 MEMOS_USER 注册,重试请求仍然不带 user_id,第二次照样落到 root。
295
+ { params: { user_id: MEMOS_USER } }, ensureCubeRegistered);
290
296
  if (result.success && result.data) {
291
297
  const resultData = result.data
292
298
  .data;
@@ -53,10 +53,19 @@ export function apiErrorResponse(operation, statusOrMsg) {
53
53
  // Turn known API errors into actionable next steps on the hot paths
54
54
  // (save/search/list hit these far more often than register_cube does).
55
55
  const suggestions = [];
56
- if (msg.includes("user") && (msg.includes("not exist") || msg.includes("not found"))) {
56
+ // 「does not have access to cube」要在 user-missing 分支之前判:它同时含 "user"
57
+ // 和 cube 字样,但成因完全不同 —— 用户和 cube 都存在,只是该用户没被授权。
58
+ // 实测最常见的触发方式是调用方漏传 user_id,后端回退到 root 去校验(root 往往
59
+ // 没有项目 cube 的授权),所以提示要先指向 user_id,而不是让人去建用户。
60
+ if (msg.includes("does not have access")) {
61
+ suggestions.push('Cube exists but this user is not authorized — most often the caller omitted `user_id` and the API fell back to its own default user. Verify `MEMOS_USER` matches the cube owner, then re-register: `memos_admin(action="register_cube", cube_id="...")`');
62
+ }
63
+ else if (msg.includes("user") &&
64
+ (msg.includes("not exist") || msg.includes("not found"))) {
57
65
  suggestions.push('User missing — run `memos_admin(action="create_user", user_id="dev_user")`, then retry');
58
66
  }
59
- else if (msg.includes("not loaded") || (msg.includes("cube") && msg.includes("not found"))) {
67
+ else if (msg.includes("not loaded") ||
68
+ (msg.includes("cube") && msg.includes("not found"))) {
60
69
  suggestions.push('Cube not registered — run `memos_admin(action="register_cube", cube_id="...")` (find ids via `memos_admin(action="list_cubes")`), then retry');
61
70
  }
62
71
  suggestions.push("Check API health: `curl http://localhost:18000/health/detail`", "Check API logs for details");
@@ -21,8 +21,8 @@ import { MEMOS_URL, MEMOS_USER, logger } from "../config.js";
21
21
  import { apiCallWithRetry } from "../api-client.js";
22
22
  import { ensureCubeRegistered } from "../cube-manager.js";
23
23
  import { parseMemoryWriteResponse } from "../memory-write-response.js";
24
- import { inspectWikiPages, normalizeLedger } from "../wiki-import-safety.js";
25
- import { buildFileBaseIndex } from "../wiki-relations.js";
24
+ import { inspectWikiPages, normalizeLedger, } from "../wiki-import-safety.js";
25
+ import { buildFileBaseIndex, } from "../wiki-relations.js";
26
26
  import { writeRelationsForPage } from "./wiki-import-relations.js";
27
27
  import { buildMemoryContent, parseWikiPage, stripTypePrefix, } from "../wiki-import-format.js";
28
28
  import { ERR_OPERATION_FAILED, ERR_PARAM_MISSING, cubeRegistrationError, errorResponse, getCubeIdFromArgs, } from "./utils.js";
@@ -55,7 +55,10 @@ function collectMarkdownFiles(pagesDir) {
55
55
  return files;
56
56
  }
57
57
  function contentHash(page) {
58
- return crypto.createHash("sha256").update(`${page.type}\n${page.content}`).digest("hex");
58
+ return crypto
59
+ .createHash("sha256")
60
+ .update(`${page.type}\n${page.content}`)
61
+ .digest("hex");
59
62
  }
60
63
  function loadLedger(wikiDir) {
61
64
  const ledgerPath = path.join(wikiDir, LEDGER_FILE);
@@ -89,7 +92,9 @@ function saveLedger(wikiDir, ledger) {
89
92
  try {
90
93
  fs.unlinkSync(temp);
91
94
  }
92
- catch { /* best effort */ }
95
+ catch {
96
+ /* best effort */
97
+ }
93
98
  return false;
94
99
  }
95
100
  }
@@ -109,7 +114,10 @@ function acquireImportLock(wikiDir) {
109
114
  }
110
115
  }
111
116
  const fd = fs.openSync(lockPath, "wx");
112
- fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, started_at: new Date(now).toISOString() }));
117
+ fs.writeFileSync(fd, JSON.stringify({
118
+ pid: process.pid,
119
+ started_at: new Date(now).toISOString(),
120
+ }));
113
121
  fs.closeSync(fd);
114
122
  return null;
115
123
  }
@@ -121,13 +129,20 @@ function releaseImportLock(wikiDir) {
121
129
  try {
122
130
  fs.unlinkSync(path.join(wikiDir, LOCK_FILE));
123
131
  }
124
- catch { /* another process may own it */ }
132
+ catch {
133
+ /* another process may own it */
134
+ }
125
135
  }
126
136
  /** Returns the stored node, null when the memory does not exist, or an error string. */
127
137
  async function getStoredMemory(cubeId, memoryId) {
128
- const result = await apiCallWithRetry("GET", `${MEMOS_URL}/memories/${cubeId}/${memoryId}`, cubeId, {}, ensureCubeRegistered);
138
+ const result = await apiCallWithRetry("GET", `${MEMOS_URL}/memories/${cubeId}/${memoryId}`, cubeId,
139
+ // 同 handlers/memory.ts 的 handleMemosGet:缺 user_id 会让后端回退到 root,
140
+ // 对 MEMOS_USER 名下的 cube 报「does not have access」。这里更隐蔽 —— 该错误
141
+ // 既不含 "not found" 也不是 404,会被当成真实故障返回,把整页 import 判成失败。
142
+ { params: { user_id: MEMOS_USER } }, ensureCubeRegistered);
129
143
  if (result.success) {
130
- return result.data?.data ?? null;
144
+ return (result.data
145
+ ?.data ?? null);
131
146
  }
132
147
  const msg = String(result.data?.message ?? "");
133
148
  if (msg.toLowerCase().includes("not found") || result.status === 404)
@@ -152,7 +167,8 @@ async function savePageAsMemory(cubeId, page) {
152
167
  const result = await apiCallWithRetry("POST", `${MEMOS_URL}/memories`, cubeId, { body }, ensureCubeRegistered);
153
168
  if (!result.success) {
154
169
  return {
155
- error: String(result.data?.message ?? `HTTP ${result.status}`),
170
+ error: String(result.data?.message ??
171
+ `HTTP ${result.status}`),
156
172
  memoryIds: [],
157
173
  warnings: [],
158
174
  uncertain: false,
@@ -174,12 +190,12 @@ export async function handleMemosImportWiki(arguments_) {
174
190
  const projectPath = arguments_.project_path;
175
191
  const dryRun = arguments_.dry_run === true;
176
192
  const onEdit = arguments_.on_edit === "version" ? "version" : "skip";
177
- const wikiDir = arguments_.wiki_dir
178
- ?? (projectPath ? path.join(projectPath, "docs", "memory-wiki") : undefined);
193
+ const wikiDir = arguments_.wiki_dir ??
194
+ (projectPath ? path.join(projectPath, "docs", "memory-wiki") : undefined);
179
195
  if (!wikiDir) {
180
196
  return errorResponse("Need project_path (default wiki: <project_path>/docs/memory-wiki) or an explicit wiki_dir", ERR_PARAM_MISSING, [
181
197
  '`memos_import_wiki(project_path="/mnt/g/my/project")`',
182
- 'Run `memos_export_wiki` first, edit pages, then import',
198
+ "Run `memos_export_wiki` first, edit pages, then import",
183
199
  ]);
184
200
  }
185
201
  const resolvedDir = path.resolve(wikiDir);
@@ -189,7 +205,9 @@ export async function handleMemosImportWiki(arguments_) {
189
205
  return errorResponse(lockError, ERR_OPERATION_FAILED);
190
206
  try {
191
207
  if (!fs.existsSync(pagesDir)) {
192
- return errorResponse(`No pages directory under '${resolvedDir}' — export a wiki first`, ERR_OPERATION_FAILED, [`memos_export_wiki(project_path="...") writes <project_path>/docs/memory-wiki/pages/`]);
208
+ return errorResponse(`No pages directory under '${resolvedDir}' — export a wiki first`, ERR_OPERATION_FAILED, [
209
+ `memos_export_wiki(project_path="...") writes <project_path>/docs/memory-wiki/pages/`,
210
+ ]);
193
211
  }
194
212
  const [regSuccess, regError] = await ensureCubeRegistered(cubeId);
195
213
  if (!regSuccess)
@@ -209,7 +227,10 @@ export async function handleMemosImportWiki(arguments_) {
209
227
  }
210
228
  const result = parseWikiPage(raw);
211
229
  if (result.ok)
212
- parsed.push({ page: result.page, relPath: path.relative(resolvedDir, file) });
230
+ parsed.push({
231
+ page: result.page,
232
+ relPath: path.relative(resolvedDir, file),
233
+ });
213
234
  else if (result.foreign)
214
235
  foreign += 1;
215
236
  else
@@ -252,7 +273,9 @@ export async function handleMemosImportWiki(arguments_) {
252
273
  archived += 1;
253
274
  continue;
254
275
  }
255
- if (ledgerEntry && typeof ledgerEntry !== "string" && ledgerEntry.new_memory_ids.length > 0) {
276
+ if (ledgerEntry &&
277
+ typeof ledgerEntry !== "string" &&
278
+ ledgerEntry.new_memory_ids.length > 0) {
256
279
  // The old page has already produced a new version; compare the edited
257
280
  // content hash before doing another API lookup/version.
258
281
  const mappedHash = ledgerEntry.content_hash;
@@ -306,7 +329,9 @@ export async function handleMemosImportWiki(arguments_) {
306
329
  unchanged += 1;
307
330
  continue;
308
331
  }
309
- const ledgerHash = typeof ledgerEntry === "string" ? ledgerEntry : ledgerEntry?.content_hash;
332
+ const ledgerHash = typeof ledgerEntry === "string"
333
+ ? ledgerEntry
334
+ : ledgerEntry?.content_hash;
310
335
  if (ledgerHash === contentHash(page)) {
311
336
  alreadyVersioned += 1;
312
337
  continue;
@@ -370,7 +395,9 @@ export async function handleMemosImportWiki(arguments_) {
370
395
  lines.push("", `❌ 失败明细(前 ${REPORT_LIST_LIMIT} 条):`, ...failures.slice(0, REPORT_LIST_LIMIT).map((s) => `- ${s}`));
371
396
  }
372
397
  if (apiWarnings.length > 0) {
373
- lines.push("", "⚠️ API warnings:", ...apiWarnings.slice(0, REPORT_LIST_LIMIT).map((warning) => `- ${warning}`));
398
+ lines.push("", "⚠️ API warnings:", ...apiWarnings
399
+ .slice(0, REPORT_LIST_LIMIT)
400
+ .map((warning) => `- ${warning}`));
374
401
  }
375
402
  lines.push("", "ℹ️ 导入恢复 type/tags/confidence/status/时间字段;关联 wikilinks 写入 Neo4j 关系边;写入内容经过服务端凭据脱敏。");
376
403
  return [{ type: "text", text: lines.join("\n") }];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-memos-mcp",
3
- "version": "3.1.7",
3
+ "version": "3.1.8",
4
4
  "description": "MCP Server for oh-memos — Intelligent Memory Management (Node.js, no Python required)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",