Metadata-Version: 2.4
Name: n1mem-mcp
Version: 0.4.0
Summary: N1Mem MCP server (BYOK, zero dependencies) — long-term memory for Claude / Cursor / OpenClaw, including one-call import of your existing agent memory and local value telemetry (~/.n1mem/events.jsonl)
Author: N1Mem (powered by T1Mem engine)
License: Proprietary
Project-URL: Homepage, https://www.n1mem.com
Keywords: mcp,memory,llm,agent,n1mem,t1mem,claude,cursor,openclaw
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Description-Content-Type: text/markdown

# n1mem-mcp · N1Mem 记忆 MCP Server

> powered by T1Mem engine · BYOK（自带 Key）· 零依赖（仅标准库）

让 Claude / Cursor / OpenClaw 之类支持 MCP 的客户端，直接获得 N1Mem 的**长期记忆**能力。

## 九个工具

| 工具 | 作用 |
|---|---|
| `memory_write` | 写入一段记忆（持久化 + 建向量，**写入即可召回**）；可选 `cat` 标「踩坑」、`project` 标项目归属 |
| `memory_recall` | 按自然语言问题召回；`answer` 已基于命中记忆接地，`retrieved` 给出命中的明文记忆 |
| `memory_import` | **把本机已有的 Agent 记忆批量迁进来**（身份层 + 技能层），可 `dry_run` 先预览 |
| `memory_skill_lookup` | 任务开始前查「有没有现成经验可复用」，返回匹配的 skill 与要点 |
| `memory_forget` | 遗忘指定记忆（按 id，RLS 隔离，只能删自己的） |
| `memory_health` | 查看服务健康与 provider 可用性 |
| `memory_skill_search` | 在**公共库**里搜别人分享的经验说明（跨租户可见；只有说明文本） |
| `memory_skill_publish` | 把自己的一条经验作为**公开名片**分享出去（只发说明文本，不带脚本） |
| `memory_skill_unpublish` | 下架自己发布过的名片（下架后同一内容不可重发，防重传） |

典型用法是让模型**自己**用这三步：
`memory_skill_lookup`（先查经验）→ `memory_recall`（再取上下文）→ `memory_write`（把结论存回去）。

本 Server 是**薄层透传**：不含任何记忆逻辑，只把工具调用转发到 N1Mem API
（`https://api.n1mem.com`）。因此零依赖、可塞进任意环境。

## 安装

```bash
pip install n1mem-mcp
```

用到 `memory_import` 时还需本机有导入器：

```bash
pip install -U "n1mem>=0.2.0"
```

（不装也能用其它 8 个工具 —— `memory_import` 会在调用时给出这条安装提示，
而不是甩一个 ImportError 堆栈。）

## 实例身份（设备授权）

MCP Server 支持把请求绑定到具体设备：设置环境变量 `N1MEM_INSTANCE=<local_id>:<secret>`
即可让通过该 Server 的记忆写入带设备级标识，配合服务端「实例强制」模式可实现设备级隔离与撤销。
默认不设置，行为不变（0.3.1 新增）。

## 获取 Key

访问 **<https://api.n1mem.com/register>** 自助注册，邮箱即拿到形如 `tk_xxx` 的 Key。

## 配置（Cursor / Claude / OpenClaw）

把下面片段加进你的 MCP 配置（`~/.cursor/mcp.json`、Claude 的 `claude_desktop_config.json`、
或 OpenClaw 的 MCP 配置）：

```json
{
  "mcpServers": {
    "n1mem": {
      "command": "n1mem-mcp",
      "env": { "N1MEM_API_KEY": "tk_xxx" }
    }
  }
}
```

或用模块方式启动（无需 `pip install` 也可，只要能 import）：

```json
{
  "mcpServers": {
    "n1mem": {
      "command": "python",
      "args": ["-m", "n1mem_mcp"],
      "env": { "N1MEM_API_KEY": "tk_xxx" }
    }
  }
}
```

## 环境变量

| 变量 | 说明 | 默认 |
|---|---|---|
| `N1MEM_API_KEY` | 你的 Key（必填） | 空 |
| `N1MEM_SUBJECT` | **主体名**（谁在调用，如 `workbuddy-pc`）—— 服务端据此反查成员表；不设 ⇒ 不发该头 | 空 |
| `N1MEM_BASE_URL` | API 地址 | `https://api.n1mem.com` |
| `N1MEM_MCP_TIMEOUT` | 单条调用超时（秒） | `90` |
| `N1MEM_KEEPALIVE` | 设为 `0` 关闭连接复用（默认持久连接：同一条 TCP + TLS 连接上连续发请求，省掉二次建连的两个 RTT；配了代理时复用同一条 CONNECT 隧道）。只在"中间设备对 keep-alive 处理异常"时才需要关 | `1` |

### 主体声明 `N1MEM_SUBJECT`（0.3.0 新增）

**Key 回答「哪个组织」，`N1MEM_SUBJECT` 回答「谁」。** 同一台机器上跑多个 Agent 时，
它们**共用一个主体** —— 别把它和 `N1MEM_AGENT_ID` 混起来：后者是「哪个 Agent」，
只用于本地埋点（跨 Agent 命中率统计），不是身份。

在你的 MCP 配置里加一个键即可：

```json
"env": { "N1MEM_API_KEY": "tk_xxx", "N1MEM_SUBJECT": "workbuddy-pc" }
```

* 须与组织里已登记的主体名**逐字一致**；拼错不会报错，服务端只会认为「未登记」。
* **不设 ⇒ 不发该头**，行为与 0.3.0 之前完全一致（可随时去掉该键回退）。
* 🔴 **不要把它设成 `N1MEM_AGENT_ID` 的值**（那是 Agent 类型名，如 `workbuddy`）——
  登记名与声明名对不上时，接入会一直停在「未登记」，而且**没有任何报错**。

## `memory_import` 用法

```
memory_import(dry_run=true)     # 先看会导入什么（条数 / 字节 / 分层），不写任何数据
memory_import()                 # 确认后真导（默认 identity + skill 两层）
memory_import(path="…", layers=["identity","skill","note"])
```

导入是**在本机**读你磁盘上的记忆资产，服务端只接收结构化条目。
想撤销：用服务端按批次 rollback（返回值里带 `source_id`），或对单条用 `memory_forget`。

## `memory_write` 的两个可选标记：`cat` / `project`（0.3.6 新增）

写入时可以额外声明两件事。**两个都不填 ⇒ 行为与之前完全一致**（不发字段、服务端不写键）。

### `cat`：合集标记（闭集）

| 取值 | 含义 |
|---|---|
| `""`（或不传） | 普通记忆 |
| `"pitfall"` | 踩坑经验 —— 进「踩坑合集」，**全队 Agent 都能召回** |
| 其它任何值 | **400 `cat_malformed`，且不落库** |

`cat` 是**闭集不是自由文本**：合集是产品概念，一旦放开自由值，「踩坑合集」会逐渐混进
「临时 / 想法 / 待办」这类杂项，最后没人敢用它查东西。所以写错值不是"静默忽略"，
而是**当场报错**——让你在写的时候就知道自己在写什么。

`pitfall` 最有价值的写法是写满三段：**现象 / 真因 / 下次怎么防**。

### `project`：项目归属（只清洗，不归并）

填了算该项目的「产品知识库」，不填算用户级「团队知识库」（跨项目通用）。

服务端对项目名**只做清洗**：控制字符（含换行/制表符）→ 空格、压缩连续空白、截断到 64 字。

🔴 **不做相似度归并、不做"猜测纠正"**：`T1mem` 不会被自动纠成 `T1Mem`，`n1mem-mcp` 和
`n1mem_mcp` 也不会合并。项目名是**你的事实**——纠正了，你按自己写的名字反而查不到那条记忆。

注意清洗的细节：`"T1Mem\n2026"` → `"T1Mem 2026"`（换行**变成空格**，不是直接删除；
直接删会粘成 `T1Mem2026`，把项目名悄悄改写掉）。

### 三个可复制的例子

```
# ① 普通记忆（不填任何标记，与旧版行为一致）
memory_write(text="我在 3 楼靠窗的工位")

# ② 踩坑经验：进「踩坑合集」，全队可召回
memory_write(
    text="部署包漏了大对象：清单对了但货没到。真因是打包时只按 manifest 走、"
         "没校验对象存储的实际存在性。下次发版前先跑一次对象存在性核对。",
    cat="pitfall",
    project="T1Mem",
)

# ③ 只标项目、不标踩坑（进该项目的产品知识库）
memory_write(text="T1Mem 的迁移脚本统一用 --plan 先 dryrun，不要直接 apply。",
             project="T1Mem")
```

## 连接级故障转移（0.3.7 新增）

**为什么要做**：`api.n1mem.com` 的 DNS 返回多个 A 记录，其中个别是 **L4 黑洞**
（SYN 无回应、也不 RST），而 Python 建连是**串行**的 —— 单个黑洞实测 **21.03s**，
排在响应列表第 k 位就等 ≈k×21s（实测 22s / 43s / 106s）。MCP 是 Agent 的入口，
**一次工具调用挂 21s+ 会被宿主判成卡死**，比慢更糟的是"看起来没反应"。

**做法**：调用上游前先**并发探测**（每地址 3s 上限）挑一个连得上的地址，再在这个
请求的作用域内把解析钉到它。工具名、参数、返回结构**一律不变**，你不需要改配置。

```jsonc
// 配置完全不用动
{ "mcpServers": { "n1mem": { "command": "n1mem-mcp",
                             "env": { "N1MEM_API_KEY": "tk_xxx" } } } }
```

**边界（诚实说明）**：

* 只在解析出 **≥2 个地址**时探测；单地址不探，行为零变化。
* 全部不可达 / 解析失败 ⇒ **原样回落** `urlopen`，错误类型与语义跟改动前一致。
* URL 仍是域名（**不写死 IP、不手拼 Host 头**）⇒ SNI 与证书校验不受影响。
* 解析覆盖是**线程本地**且**请求结束必还原**（含异常路径与并发场景）。
* 探测失败不会让调用失败：最坏情况退化成"和以前一样慢"，不会新增报错。
* 离线 / CI 环境设 `T1MEM_OFFLINE=1` 整段关闭（探测=真实建连）。

## 连接复用与代理隧道（0.4.0 新增）

`urlopen` 每个请求都重付「TCP 建连 + TLS 握手」（现场读数：首次 697ms / 后续 336ms）。
MCP 是 Agent 的**长驻入口**，工具调用一次接一次，这两个 RTT 是纯浪费。本版把上游请求
换成标准库 `http.client` 的持久连接 + 进程级连接池：

* 空闲连接还在 ⇒ 直接复用（零探测、零握手）；
* 不在 ⇒ 仍按上一节的故障转移挑地址建连，用完留在池里；
* 对端回收了空闲连接 ⇒ 自动重建并**只重试一次**（不会变成你看到的报错）；
* 遇到 3xx ⇒ 交回 `urlopen`（不实现重定向，也不偷发非幂等请求）。

**挂代理也能复用**：此前检测到 `http_proxy` / `https_proxy` 就整段退回 `urlopen` ——
出口对，但一点收益没有。现在改成**在代理上开 CONNECT 隧道并复用隧道**：

* 出口读 `urllib.request.getproxies()`（环境变量优先，其次系统配置），与 urllib 同口径；
* `no_proxy` 命中 ⇒ 直连，不隧穿；
* 代理 URL 的 `user:pass@` ⇒ 自动带 `Proxy-Authorization: Basic …`；
* `https` 目标在隧道内**对目标域名**做 TLS（SNI 与证书主机名都是目标，不是代理），
  证书校验与直连完全相同；
* 代理拒绝 CONNECT（407 / 403）⇒ 回落 `urlopen`（**还是同一个代理**，只是不复用），
  **不会**改成直连 —— 你的出口策略不会被替我们改掉。

工具名、参数、返回结构一律不变，配置也不用动。想退回"每个请求一条连接"：
`N1MEM_KEEPALIVE=0`。

## 已知限制（诚实告知）

- 暂无 `update` 工具（服务端更新能力未就绪，故**不暴露**，避免客户端以为支持）。
  删除是支持的（`memory_forget`）—— 0.1.x 的说明曾写"暂无 delete"，与实现不符，0.2.0 已更正。
- `memory_health` 如实反映服务健康：**未配置的上游不会拉低整体状态**；只有当"已配置的上游"
  或存储真的不可用时才返回 `degraded`（0.2.1 修正：此前会把未配置的 provider 误算成 down，
  导致核心链路全绿却报 degraded）。
- `memory_import` 目前支持 WorkBuddy（`~/.workbuddy`）与关键文档目录两种来源。
- **Agent 间沟通通道不暴露为工具**（2026-10 起平台内提供，工具清单保持 9 个不变）：
  线程 / 消息 / 接力装配 / 要点沉淀属平台内能力，需直接对接 C-2 API
  （`/v1/comm/*`、`/v1/console/comm/*`）。本 Server 的对外工具契约不受影响，
  评估纳入须另行走契约变更。

## 与 `n1mem` 的区别

| 包 | 形态 | 用途 |
|---|---|---|
| `n1mem` | Python SDK（函数调用） | 代码里直接 `m.ingest() / m.recall() / m.ingest_batch()` |
| `n1mem-mcp` | MCP Server（stdio） | 让 MCP 客户端（Claude / Cursor / OpenClaw）以工具方式调用记忆 |

两者都遵守 BYOK：Key 由你提供，SDK 与 Server 都不存储、不上传你的凭据。
