mcpField
F
footnote

footnote

本地服务
github.com/brick-byte数据与分析06 天前更新

关于 footnote

Footnote 是面向基于 ERP 和财务数据构建 AI 应用的团队的 MCP 服务,适用于需要将答案关联到源记录的场景。它采用类型化操作、验证实体、通过 SQL 聚合数据,并保存防篡改的溯源日志,而不是让模型生成数值。

主要功能

  1. 01类型化操作目录,避免模型编写 SQL
  2. 02查询前验证实体
  3. 03通过 SQL 计算聚合值
  4. 04随结果返回记录链接
  5. 05保存哈希链式溯源记录
  6. 06拒绝无匹配操作的问题

适用场景

  1. 01将财务数值追溯至源发票
  2. 02审查风险判断背后引用的事实
  3. 03导出或验证溯源日志
  4. 04拒绝缺乏支持的预测问题

MCP 相关

它通过 MCP 暴露业务数据的固定类型化操作目录。结果包含结构化内容、记录链接和溯源元数据;声明级引用需要在宿主端集成。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"com-brick-byte-footnote": {
"command": "npx",
"args": [
"-y",
"@brick-byte/footnote"
],
"env": {
"DATABASE_URL": "<DATABASE_URL>",
"PROVENANCE_DB": "<PROVENANCE_DB>",
"PROVENANCE_ACTOR": "<PROVENANCE_ACTOR>"
}
}
}
}
启动命令npx -y @brick-byte/footnote
需要配置的环境变量
DATABASE_URL密钥Postgres connection string. Omit to use the built-in SQLite sample database.
PROVENANCE_DBPath to the SQLite provenance log. Defaults to in-memory, which discards the trail on exit.
PROVENANCE_ACTORIdentity recorded against each provenance record. In production this comes from the request's auth.

本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
npm@brick-byte/footnote0.1.2

工具

还没有获取到工具列表

本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。

详细介绍

Footnote

CI

**一个 MCP 服务,可防止 AI 在回答 ERP 和财务数据相关问题时产生虚构数字。**答案中的每个数字都可追溯到生成它的数据库行:读取时可点击查看,可导出供审计人员检查,并具有防篡改性。

兼容任何 MCP 客户端:Claude Desktop、Cursor、Microsoft Agent Framework、Semantic Kernel (.NET)、LangChain,或你自己的循环。

一个 MCP 服务、一个溯源存储,以及一个展示它们组合效果的演示 UI。

最快的试用方式:

// claude_desktop_config.json
{ "mcpServers": {
    "footnote": {
      "command": "npx",
      "args": ["-y", "@brick-byte/footnote"],
      "env": {
        "PROVENANCE_DB": "/path/to/provenance.db",
        "PROVENANCE_ACTOR": "your-name",
        // Omit for the built-in SQLite sample data.
        "DATABASE_URL": "postgres://user:pass@localhost:5432/erp"
      }
    } } }

或者在本地运行演示 UI:

npm install
npm run demo      # http://localhost:4711
npm test          # 26 tests, including the invariants below

需要 Node 22.5+(使用内置的 node:sqlite,因此无需安装数据库)。

Postgres 为可选项。同一套架构、种子数据和查询集均可在 Postgres 上运行,并且两种数据库都必须通过同一套测试:

npm run db:up     # Postgres 17 in Docker, seeded from migrations/001_init.sql
npm run test:pg   # the identical 26 tests, against Postgres
npm run demo:pg   # the demo, against Postgres

演示


问题所在

语言模型生成的是看似合理的续写,而非真实内容。让它查询业务数据库,它就会信心十足地报告一个没有任何查询结果支持的数字——而这个虚构数字与真实数字难以区分,因为两者都来自同一套机制。

常见的解决办法在这里行不通。不确定性信号能捕捉模糊的情况,却抓不住流畅作答的情况。使用同一模型构建的验证器,既能轻易确认虚构内容,也能轻易发现它。而在财务、库存或采购场景中,“模型是这么说的”并不是任何人可以据此采取行动的答案。

有效的办法是从结构上解决:绝不让模型生成数字。

架构

  question
     │
     ▼
  model ──────────► picks an operation from a catalog and supplies typed args
     │                (it cannot write SQL, cannot invent an operation)
     ▼
  contract layer ─► validates params, resolves entities against the database,
     │                rejects anything that does not exist
     ▼
  deterministic ──► executes the query; SQL does all aggregation
  core                returns values + the evidence behind each one
     │
     ▼
  provenance ─────► appends a hash-chained record, server-side,
  store               where the model cannot influence it
     │
     ▼
  host UI ────────► fills slots from structured output and renders each
                      claim as a clickable citation

模型负责转换和措辞——这两件事它都擅长。数字则来自记录系统。

强制保障

这些是测试,不是指南。只要其中任何一项失效,npm test 就会失败。

**不会生成任何数字。**值会从操作的结构化输出中原样复制到模板插槽中。测试会断言,答案中的每个金额数字都有证据支持的声明作为依据,并且所述总额等于其引用记录之和。

**运行任何查询前都会验证实体。**如果客户是虚构的,就会返回可恢复错误,并列出真实的备选项,而不是静默返回空结果,让模型自行编造解释。

**聚合在 SQL 中完成。**模型选择要运行哪种聚合,但绝不决定聚合结果。

**空结果由代码处理。**这是最容易触发虚构内容的情况:在句子中途把空结果集交给模型,它就会编造内容来完成句子。操作会改为设置明确的 emptyReason。

无法路由的问题会被拒绝。“下季度的收入会是多少?”没有对应的操作,因此不会生成答案。拒绝是正式的处理结果,而不是失败。

**溯源信息由服务端写入。**模型绝不会报告自身的溯源信息。依赖模型主动选择引用的溯源信息毫无价值。

**日志具有防篡改性。**每条记录都包含其前一条记录的哈希。npm run verify 会遍历哈希链,并报告写入后被修改的第一条记录。

查询排序顺序是经过测试的契约。listCustomers 必须按记录顺序返回,因为实体提取会选择第一个匹配项。若改为按名称排序,就会导致本项目旨在防止的问题:句子流畅、数字真实、引用的行也正确,却说的是错误的公司。对此有专门的测试。

不只是数据,还有解读

更难处理的是判断。“Bergmann 有风险”是一个结论,没有可供核对的行——而缺乏依据的解读比缺乏依据的数字更危险,因为它看起来像洞见,而且经得起审查。

这里的回答先给出结论,再将每个前提列为一条单独引用的事实:

Bergmann Logistik GmbH shows two independent risk signals.
This is an interpretation — the underlying facts are:

  · 43.805,00 € overdue across 4 invoices, oldest 104 days past due
  · 2 open support tickets, 1 high severity
  · agreed payment terms of 30 days, account owned by Ana Kovač

The conclusion is the model's. Every fact above is a record you can open.

人们可以不同意其中的推理,同时仍然信任输入信息。

来源记录格式

{
  "id": "…", "sequence": 1, "timestamp": "2026-08-14T09:12:04.000Z",
  "actor": { "id": "zoran", "role": "controller" },
  "question": "How much does Bergmann owe?",
  "answer": "Bergmann Logistik GmbH is 43.805,00 € overdue across 4 invoices…",

  // Each claim indexes a span of the answer and names its evidence.
  "claims": [
    {
      "text": "43.805,00 €",
      "span": { "start": 25, "end": 36 },
      "evidenceIds": ["invoice:8812", "invoice:8834", "invoice:8901", "invoice:8977"],
      "origin": "structured"        // never "generated" for a value
    }
  ],

  // The query, so the result is reproducible.
  "operations": [
    { "name": "getOverdueInvoices",
      "arguments": { "customer": "Bergmann", "asOfDate": "2026-08-14" },
      "query": { "sql": "SELECT …", "params": ["2026-08-14", 4471, "2026-08-14"] },
      "durationMs": 0.42 }
  ],

  // Field values, not just IDs — the export stands alone if the source moves on.
  "evidence": [
    { "id": "invoice:8812", "type": "invoice", "recordId": "8812",
      "uri": "erp://invoice/8812",
      "fields": { "number": "INV-2026-8812", "due_on": "2026-05-02",
                  "outstanding": "18.450,00 €", "days_overdue": 104 } }
  ],

  "previousHash": "0000…", "hash": "9aa2ba…"
}

origin 是关键字段。structured 表示该值是从某项操作的输出中复制而来。generated 表示该值由模型生成——对于数字而言,这正是本项目要防止的情况。

MCP 详情

该服务使用协议中支持这项工作的部分:

功能用途
inputSchema模型获得的是类型明确的目录,而不是查询语言
structuredContent数值以经过验证的 JSON 与文字内容一同返回,因此数据无需经过生成的令牌往返传递
resource_link每条被访问的记录都会以 erp:// URI 的形式返回——这是可点击引用背后的深层链接基础
annotations.audience记录链接会标记为面向用户
_meta携带来源记录的 ID 和哈希,方便宿主获取完整追踪记录
isError虚构的实体会产生可恢复的工具错误,模型可以据此修正,而不是导致协议故障

MCP 不会提供的功能,以及本仓库额外实现的内容:协议中没有任何机制能将这项声明与那份证据绑定起来。协议会提供链接;声明与证据之间的映射及其呈现方式由宿主负责。这种绑定才是产品的核心。

通过标准输入/输出独立运行:

npm run mcp

claude_desktop_config.json 配置项位于本 README 的顶部。

从源代码运行(用于开发)
{ "mcpServers": {
    "footnote": {
      "command": "npx",
      "args": ["tsx", "--experimental-sqlite", "/path/to/footnote/src/mcp-server.ts"],
      "env": { "PROVENANCE_DB": "/path/to/provenance.db", "PROVENANCE_ACTOR": "your-name" }
    } } }

与普通 MCP 相比,你能获得什么,以及哪些功能需要宿主集成

任何 MCP 客户端——Claude Desktop、Microsoft Agent Framework、Semantic Kernel、LangChain,或你自己的循环——现在都能调用此服务。你能获得:

  • 固定的类型化操作目录;模型无法编写 SQL,也无法虚构操作
  • 实体验证,因此虚构的客户会被判定为错误,而不是返回空结果
  • 使用 SQL 进行聚合,而非交由模型处理
  • 当问题无法映射到任何操作时,系统会拒绝回答,而非自行发挥
  • 用于标识每条被访问记录的 resource_link 条目
  • 服务器端记录每次调用的哈希链式来源日志

你无法获得的是:可点击的声明级引用。使用普通 MCP 时,框架会接收 structuredContent 和资源链接,然后由它自己的模型组织答案——此时,具体声明与其证据之间的绑定已经消失,数字也会再次经过生成的令牌传递。

保留这种绑定是宿主端的工作,因为协议中没有任何机制能承载它。src/web.ts 和 src/public/index.html 是参考实现:声明文本区段、引用标记、记录解析。大约 200 行。将其移植到你的宿主中,就能获得另一半功能。

在演示中试试这些问题

问题展示内容
Bergmann 欠了多少?每个数字都可以点击,查看对应的发票
Bergmann Logistik 有风险吗?一种解读,并附有分别引用的前提
显示应收账款账龄分析使用 SQL 计算的整体聚合
Acme Corporation 欠了多少?虚构实体 → 拒绝回答并提供真实的替代项
下季度的收入会是多少?无法路由 → 拒绝回答,而非自行发挥

切换 “显示模型自行生成数字时的效果” 进行对比:格式完全相同,文字流畅,每个数字都错了,而且输出中没有任何标记能指出这一点。

布局

migrations/001_init.sql  schema + seed, shared by both backends and Docker
src/database.ts     the Db seam — SqliteDb and PostgresDb behind one interface
src/db.ts           opens the ERP database, applies the migration
src/operations.ts   the catalog — the model's entire callable surface
src/compose.ts      slot-filling; claims and spans are emitted here
src/provenance.ts   append-only hash-chained store, export, verification
src/mcp-server.ts   MCP server over stdio
src/web.ts          demo host application
src/verify.ts       npm run verify / npm run export
test/run.ts         26 tests covering the invariants above, on both backends
docker-compose.yml  Postgres 17, seeded from the same migration

只有 operations.ts 会以一种方言编写 ERP SQL,并使用 ? 占位符。 PostgresDb 会将它们改写为 $1..$n。两个数据库引擎之间唯一真正不同的表达式——计算两个日期之间的整天数,在 SQLite 上使用 julianday(),在 Postgres 上使用日期相减——被命名为 Db 接口上的 daysBetween,而不是直接内联。移植时,只有这个地方可能悄无声息地改变数值,因此它被单独隔离,测试套件也会断言两者得出的天数相同。

状态与限制

v0,且如实说明了范围。

  • 你无法为别人的 AI 添加来源信息。 如果答案来自 SAP Joule 或 Dynamics Copilot,你无法访问它们的检索流程或渲染方式。这种方法适用于你自己构建的、基于业务数据的 AI 层。
  • 厂商提供了引用,但功能有限。 截至 2026 年 8 月,SAP、Microsoft、NetSuite 和 Salesforce 都支持跳转到知识内容的引用,例如帮助文章、PDF 和文档。但没有一家提供通用机制,能将某个数值关联到其背后的交易记录。数字类答案在所有产品中都是最薄弱的一环。
  • 演示项目的意图路由刻意采用简单的关键词匹配,因此无需 API 密钥即可运行。在实际部署中,由模型生成工具调用,而此代码仍负责填充参数槽位。重点在于架构,而非路由。
  • 写入操作不在范围之内。在 ERP 中,编造一条读取结果令人尴尬;编造一条写入记录则会损坏账本。写入操作应置于提议 → 验证 → 确认流程之后。
  • 数据保留、访问控制,以及针对特定 ERP 的真正深度链接解析器,是最后一公里——而真正的产品价值就在最后一公里。

值得了解的先行项目

  • OpenTelemetry GenAI 语义约定 — 为 GenAI 客户端和 MCP 提供跨度。仍处于早期阶段,覆盖范围远未达到记录级来源信息。
  • OpenLineage — 数据集和作业的血缘追踪。理念相同,但位于下一层,追踪的是表,而非声明。
  • LLM 可观测性 — Langfuse、LangSmith、Arize、Datadog。为调试应用的工程师提供追踪,而非为阅读答案的人提供来源信息。

许可证

MIT。

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。