内容
███╗ ███╗ ██████╗██████╗ ██╗ ██████╗ ██████╗ ███╗ ███╗
████╗ ████║██╔════╝██╔══██╗ ██║ ██╔═══██╗██╔═══██╗████╗ ████║
██╔████╔██║██║ ██████╔╝█████╗██║ ██║ ██║██║ ██║██╔████╔██║
██║╚██╔╝██║██║ ██╔═══╝ ╚════╝██║ ██║ ██║██║ ██║██║╚██╔╝██║
██║ ╚═╝ ██║╚██████╗██║ ███████╗╚██████╔╝╚██████╔╝██║ ╚═╝ ██║
╚═╝ ╚═╝ ╚═════╝╚═╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝
用于MCP的上下文织布工具。
在N台服务器前面只有一个代理。超大工具结果被编织成可查询的数据集 — 48k个标记输入,2k个标记输出。
warp: 结果流进入 weft: 织布工具在数据集上穿梭
══════════════════════ ═════════════════════════════════════
│ │ │ │
│ │ │ │ 48,377 个标记 ┄┄┄┄┄┄┄┄┄> ✂ 拦截
●━━━━┿━━━━┿━━━━┿━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
│ ●━━━━┿━━━━┿━━━━━━━ 摄入 → DuckDB ━━━━━━┫
│ │ ●━━━━┿━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
│ │ │ ●━━━━ 模式 · 统计 · 连接 ━━┛
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
[ 原始数据洪流,被丢弃 ] [ 2,104-标记信封 ]
引用 · 示例 · SQL



## 功能描述
MCP客户端向工具提出问题,答案以一万行JSON数据的形式返回 — 其中大部分数据模型并不需要,但仍需要将其保存在上下文中。织布工具(Loom)就位于两者之间。
当结果很小的时候,它会直接通过。当结果很大且为表格形式时,Loom会将其摄入嵌入的DuckDB中,并为模型提供一个紧凑的**信封**:数据集引用、模式、列统计、样本行、连接提示和出处。模型然后精确查询它需要的数据 — 包括跨不同服务器的数据进行连接,而单个服务器无法回答这些问题。
经济效益:一个48k标记的工具结果变成一个约2k标记的信封。模型将上下文花费在答案上,而不是原始数据转储上。
## 适用场景
Loom在三种情况下发挥作用,涵盖了大多数MCP使用的普通情况:
- **没有Shell的客户端**。Claude Desktop、移动设备和代理平台 — 最大的客户端界面 — 没有CLI可供回落。MCP工具是模型通往数据的唯一途径,因此拦截功能在此处启用。
- **服务器上没有模型可达的CLI**。内部工具、SaaS连接器和凭证数据库,其密钥仅存在于服务器的`env`块中 — 大多数实际连接器。没有`gh`等效命令可供模型路由。
- **可审计的持久数据集分析**。Loom将“模型审阅数据转储”转换为“模型在仍可检查的数据上运行可验证的查询” — 带有出处戳记的精确SQL数据集,这些数据集在事后仍可重新打开和重新查询。此场景适用于任何地方,无论有无Shell。
跨服务器连接贯穿这三种情况:没有单个CLI能够回答跨两个服务数据的问题。
有两个界限:
- **具有Shell的代理将跳过MCP层以直接访问服务**。给模型一个`bash`和一个具有已知CLI和环境凭证的服务 — `gh`针对GitHub是最明显的情况 — 然后它直接访问CLI,速度更快,成本更低,跳过Loom和MCP。这是因为代理更喜欢CLI而不是MCP,而不是Loom缺陷;Loom是MCP层产品,不会比`gh`更像`gh`。这也是少数情况:大多数服务器用没有模型可达的CLI包装,而最大的客户端界面没有Shell。
- **信封保真度受下游返回数据的限制**。Loom忠实地转换下游服务器发送的数据 — 但它不能添加服务器从未发送的字段。GitHub MCP服务器例如返回一个精简的17字段存储库对象(没有`stargazersCount`、`language`或`forks`);Loom忠实地捕获了所有17个字段,但仍无法回答从这些数据中提出的星级排名问题。界限是下游的,而不是Loom的。
## 工作原理
Loom本身是一个stdio MCP服务器。它位于一个上游MCP客户端(Claude Desktop、Claude Code、Cursor等)和N个下游MCP服务器之间,您可以配置这些服务器:
- 启动并监督每个配置的下游服务器(握手健康检查、重启和回退、注销工具并发出`list_changed`如果服务器耗尽重启预算)。
- 每个下游工具都被命名为`<server>_<tool>`,并将聚合的工具列表向上重新公开,保持其活力,通过`list_changed`。
- 将`call_tool`路由到正确的下游子服务器。小型结果返回未经修改 — 直通是字节相同的。对于大型表格结果,Loom将其拦截到DuckDB中,并用`loom_dataset_ref`信封替换;任何拦截失败都会降级为原始数据。
- 暴露基于缓存数据集的合成查询接口:`loom_query`(受保护的只读SQL,包括跨服务器连接)、`loom_list_datasets`、`loom_describe`(完整模式、统计和新鲜的连接提示)和`loom_materialize`(固定派生查询结果为新数据集)和`loom_export`(密封的csv/json导出)。拦截深度上限为每个上游调用1;`loom_materialize`不会消耗此预算。
- 在信号或上游stdin EOF时干净地关闭,没有孤立的子进程,溢出目录被删除。
Loom拦截结构化结果和格式化文本:当结果带有`structuredContent`通道时,它从中摄入;否则,它解析JSON-in-text,如果失败,则从格式化文本中提取重复记录,严格遵循从不撒谎的原则(它从不发出它不确定的表格 — 任何不确定性都会降级为未触及的直通)。有关完整规范,请参见[SPEC.md](./SPEC.md)。
## 用法
```bash
npx mcp-loom --config loom.config.json
```
将任何MCP客户端指向该命令。配置也可以通过`LOOM_CONFIG`环境变量提供,而不是`--config`:
```bash
LOOM_CONFIG=/path/to/loom.config.json npx mcp-loom
```
(如果两者都设置,则`--config`优先。)请参阅[`loom.config.example.json`](./loom.config.example.json)以获取最小工作示例。
## 客户端配方
Loom由MCP客户端启动,就像启动任何stdio服务器一样 — 将客户端指向`npx -y mcp-loom --config <绝对路径到loom.config.json>`。
**Claude Desktop** (`claude_desktop_config.json`):
```json
{
"mcpServers": {
"loom": {
"command": "npx",
"args": ["-y", "mcp-loom", "--config", "/绝对路径/to/loom.config.json"]
}
}
}
```
**Cursor** (`.cursor/mcp.json`): 与上述相同的`mcpServers`块。
**Claude Code** - 从CLI添加:
```bash
claude mcp add loom -- npx -y mcp-loom --config /绝对路径/to/loom.config.json
```
使用**绝对路径**到`loom.config.json` — MCP客户端在未指定的工作目录中生成服务器。然后,Loom启动并命名下游服务器列表,这些服务器在该配置中列出。
## 配置参考
配置是一个单独的JSON文件,在启动时使用zod进行验证;无效的配置将快速失败,并显示一个可读的错误列表,列出每个违反的字段。
| 字段 | 类型 | 默认值 | 备注 |
|---|---|---|---|
| `servers` | 数组(最小1) | — (必填) | 要代理的下游服务器。参见下文。 |
| `servers[].name` | 字符串 | — (必填) | 必须匹配`^[a-z][a-z0-9_]{0,31}$`,在所有服务器中唯一。`loom`是保留的(专用于合成工具),将被拒绝。 |
| `servers[].command` | 字符串 | — (必填) | 要生成的可执行文件(例如`npx`)。非空。 |
| `servers[].args` | 字符串数组 | `[]` | 传递给`command`的参数。 |
| `servers[].env` | 对象(字符串→字符串) | `{}` | 仅适用于**此服务器的子进程**的额外环境变量。参见下面的“子环境”。 |
| `servers[].envPassthrough` | 字符串数组 | `[]` | 从Loom自己的环境中传递的额外**非机密**变量,在此服务器的子进程之上,基于默认拒绝 — 启动shell的环境不会全部复制。参见下面的“子环境”。 |
| `servers[].provenanceDenylist` | 字符串数组 | `[]` | 此服务器的工具名称,其`provenance.args`被完全省略(`"[omitted]"`)来自信封、`loom_describe`和`loom_list_datasets` — 用于工具的参数只是凭证。其他工具保留启发式密钥为基础的减少。 |
| `tokenThreshold` | ≥100的整数 | `2000` | 拦截触发器,当表格文本结果超过此值时;粗略的字符/4阶数估算,不精确(少算CJK)。 |
| `memoryBudgetBytes` | ≥1048576的整数 | `268435456`(256 MiB) | 跟踪数据集字节的软驱逐预算;隐含查询结果数据集先被逐出,然后是下游摄入。 |
| `duckdbMemoryLimit` | 字符串, `/^\d+(\.\d+)?(KB\|MB\|GB\|TB)$/` | `"512MB"` | 硬上限(`SET memory_limit`);运算溢出到磁盘在计算线程边界下,而不是OOM。 |
| `exportDir` | 字符串 | `"./exports"` | `loom_export`写入csv和json文件的目录。 |
| `queryTimeoutMs` | ≥1的整数 | `30000` | 约束`loom_query`(中断()-定时器)。 |
| `restart.maxAttempts` | ≥0的整数 | `5` | 每个下游服务器连续重启尝试的最大次数,在被注销(工具删除,发出`list_changed`)之前。 `0`禁用重启。 |
| `restart.baseDelayMs` | ≥1的整数 | `1000` | 重启回退的基准延迟。 |
如果**所有**配置的服务器都无法启动,Loom本身也会启动失败(致命)。如果只有某些服务器启动失败,Loom会启动并显示警告,列出可用的服务器工具。
## 拦截和查询
大型表格工具结果被拦截并摄入到嵌入的DuckDB存储中作为可查询的数据集。超过`tokenThreshold`并携带结构化数据的工具结果 — 从协议的`structuredContent`通道读取(如果存在),否则从文本渲染中解析 — 变成`loom_dataset_ref`信封:引用ID、推断的模式、列统计、样本行和连接提示 — 保持标记成本低。使用合成的`loom_query`工具查询这些数据集:它接受DuckDB方言的只读SQL(单个SELECT或WITH语句),由引擎解析器分类为只读。如果丢失数据集引用,`loom_list_datasets`恢复它或列出当前会话中所有可用数据集。
数据集引用是**会话范围**:它们仅在对话期间存在,并且在内存压力下自动被逐出(隐含查询结果优先被逐出,显式摄入的数据保留更长)。信封中的样本行是**任意**的 — DuckDB默认情况下不保留插入顺序 — 因此请始终在需要时使用显式`ORDER BY`子句。令牌估计器是粗略的数量级拨盘,并且少算包含CJK字符的文本。监控实际令牌使用情况,如果您经常超过`tokenThreshold`。
### 使用`loom_describe`检查数据集
在数据集引用上调用`loom_describe`以详细检查其结构和内容:完整的模式、每列统计数据、10行样本、新鲜的连接提示到其他数据集(带有值重叠分数,将实际连接排在名称匹配之上)、出处和创建时间戳。在需要了解数据集的形状之前查询它,或在多个中间操作后重新发现数据集的模式。
### 使用`loom_materialize`物化查询结果
查询结果会隐含缓存,但如果要跨会话边界保留派生结果或确保它永远不会被逐出,请使用`loom_materialize`:传递只读SELECT语句并获取新的固定`loom_dataset_ref`信封。物化数据集永远不会在内存压力下被逐出。 **重要:** 物化数据集存储在它们的JSON文本表示中,因此日期和十进制列变成文本字符串。在SELECT中添加CAST,如果需要对物化结果进行日期算术或数值运算 — 例如,`SELECT CAST(created_at AS DATE) FROM ...`以物化可查询的日期列。
### 使用`loom_export`导出数据集(密封边界)
使用`loom_export`将缓存的数据集导出到文件中:它在配置的`exportDir`下以csv或json(仅支持这些格式)写入数据集并返回写入文件的绝对路径。这是将结果移出会话的方式。
**密封导出边界:** Loom引擎永远不会触及文件系统。导出是从Node进程中的只读车道SELECT语句序列化为文本 — 引擎永远不会写入文件(`COPY TO`保持禁用)。安全边界是引擎的不可变语句分类,而不是运行时过滤;这确保没有查询可以被错误分类或提升为文件系统访问在运行时。
## 命名空间
每个下游工具都以`<server>_<tool>`的形式重新公开,其中`<server>`是配置中服务器的名称。路由通过内部双向映射(公开名称⇄服务器+原始工具名称) — 绝不会通过在`_`上拆分公开名称 — 因此包含下划线的服务器名称可以正常工作。名称超过常见客户端限制(64个字符)的名称会确定性地截断哈希后缀;冲突后置数后缀。 `loom`是一个保留的服务器名称,以便`loom_-prefixed`合成工具永远不会与实际的下游工具冲突。
## 子环境(默认拒绝 - 请先阅读配置机密)
每个下游子进程都会获得**精选的非机密基数**(`PATH`、`HOME`、`TMPDIR`,以及Windows等效项) **加上该服务器自己的`env`块** — **而不是** Loom启动shell的副本。因此,如果在Loom的启动shell中导出机密(`GITHUB_TOKEN`、`AWS_SECRET_ACCESS_KEY`,...),**不会**将其转发到下游服务器,并且受损的`npx -y`包无法读取用于**不同**服务器的机密。
将每个服务器的机密放在服务器自己的`env`块中:
```json
{ "name": "repo", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "env": { "GITHUB_TOKEN": "ghp_..." } }
```
如果一个服务器需要来自shell的额外**非机密**变量,请使用`envPassthrough`(默认为`[]`)显式选择:
```json
{ "name": "svc", "command": "...", "envPassthrough": ["HTTPS_PROXY", "NODE_EXTRA_CA_CERTS"] }
```
**边界 - 权限最小化,而不是零泄漏。** 这关闭了**环境**向量,而不是每个向量。您传递的变量本身可以携带机密(`HTTPS_PROXY=http://user:pass@host`,CA路径),而恶意子进程仍然可以在其上访问文件系统和网络。OS级每个子进程隔离(容器,命名空间)是上限,并且超出范围的零配置stdio代理 — 而且没有“继承所有”标志。