mcpField
M
mini-app-mcp

mini-app-mcp

本地服务
github.com/ynishi数据库01 周前更新

关于 mini-app-mcp

mini-app-mcp 是面向需要结构化持久存储的 Agent 的 MCP 服务,以 schema.yaml 定义表结构,并通过 SQLite 在单个进程中管理多张表。它仅通过 MCP 提供 CRUD 与模式管理操作。

主要功能

  1. 01以 schema.yaml 定义表结构
  2. 02单个进程挂载多张表
  3. 03提供记录的 CRUD 操作
  4. 04管理表模式与查询别名
  5. 05支持批量操作和快照
  6. 06提供只读模式及文档资源

适用场景

  1. 01为 Agent 保存结构化项目数据
  2. 02按项目和用户范围管理多张表
  3. 03批量修改数据或表模式
  4. 04通过快照备份及恢复数据

MCP 相关

服务通过 MCP 工具提供数据 CRUD、模式管理、查询别名和快照操作,并提供 schema、错误码及工具文档等只读资源。支持 stdio 和 Streamable HTTP 传输。

配置

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

mcp.json · 通用配置
{
"mcpServers": {
"ynishi-mini-app": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"ghcr.io/ynishi/mini-app-mcp:0.20.0"
]
}
}
}
启动命令docker run -i --rm ghcr.io/ynishi/mini-app-mcp:0.20.0

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

安装包
ocighcr.io/ynishi/mini-app-mcp:0.20.0

工具

还没有获取到工具列表

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

详细介绍

mini-app-mcp

以 Agent 为先的 CRUD 存储 MCP 服务——由 schema.yaml 驱动,使用 SQLite 后端,可在单个守护进程中管理多张表。

功能

mini-app-mcp 是一个轻量级 MCP 服务,可在单个运行进程中管理一张或多张 SQLite 表。每张表的结构完全由 schema.yaml 文件定义;无需迁移、REST API 或 GUI。CRUD 功能仅通过 MCP 工具提供,因此它可以自然地作为需要结构化持久存储的 Agent 的后端。

设计原则

  • schema.yaml 是唯一的结构定义依据——字段名称、类型和必填约束在启动时从 YAML 读取。应用代码中没有硬编码字段。
  • 单个守护进程管理多张表——单个服务进程会发现并挂载配置的 User 和 Project 作用域目录下的所有表。专用的旧版模式(MINI_APP_SCHEMA + MINI_APP_DB)保留原有的单表行为。
  • 仅通过 MCP 入口访问——不提供 HTTP/REST/CLI CRUD 接口。所有读写操作都通过 MCP 工具完成。
  • 结构化 JSON 错误——每个错误响应都包含机器可读的 code 字段,方便 Agent 以编程方式处理故障。

schema.yaml 格式

table: issues
title: "Issue tracker"
description: "Tracks bugs and feature requests for a project."
fields:
  - name: title
    type: string
    required: true
    description: "Short one-line summary of the issue."
  - name: state
    type: string
    required: false
  - name: tags
    type: array
    required: false

表级别的可选 title 和 description 键用于提供便于人类和 AI 理解的表元数据。每个字段条目也可以包含可选的 description 字符串。这三个键均遵循 OpenAPI 3.1 / JSON Schema 2020-12 命名约定,并包含在 info 工具输出和 schema://json 资源中。

支持的类型:string、number、boolean、array、object。

配置

多表模式(推荐)
环境变量默认值说明
MINI_APP_USER_DIR~/.mini-app/User 作用域表的基础目录。每个子目录都会被视为一个表名,且必须包含 schema.yaml 和 <table>.db。
MINI_APP_PROJECT_DIR./.mini-app/Project 作用域覆盖目录。此处存在的表会完全替代同名的 User 作用域定义。
MINI_APP_BACKUP_RETENTION10每张表在 _backup/ 下保留的备份副本(YAML + DB 快照对)最大数量。每次写入备份后,超出此上限的较旧副本都会立即删除。
MINI_APP_SNAPSHOT_RETENTION10每张表在 _snapshots/ 下保留的快照代数上限。此设置与 MINI_APP_BACKUP_RETENTION 严格分开;只清理 _snapshots/ 文件,不会触及 _backup/。

启动时会扫描这两个目录以发现表。同名表的 Project 作用域定义优先于 User 作用域定义。

旧版单表模式
环境变量默认值说明
MINI_APP_SCHEMA./schema.yaml结构定义文件的路径
MINI_APP_DB(无——必须设置)SQLite 数据库文件的路径

设置 MINI_APP_SCHEMA 和 MINI_APP_DB 后,服务会以旧版模式启动,并且只挂载一张表。在此模式下,所有工具都可以省略 table 参数。

所有变量也可以放在工作目录中的 .mini-app-mcp.env 文件里。

MCP 工具

所有工具都接受可选的 table 参数,用于选择目标表。在多表模式下,此参数为必填;省略时会返回错误代码 TABLE_REQUIRED。提供未知表名时会返回错误代码 TABLE_NOT_FOUND。在旧版单表模式下,此参数可以省略。

工具描述
info以 JSON 格式返回解析后的架构(表名、字段定义)
create插入新行;根据架构验证 data 对象
get根据 id 检索单行。如果 id 少于 36 个字符,则将其视为 UUID 前缀:无匹配项时返回 NOT_FOUND;有两个或更多匹配项时返回 AMBIGUOUS_ID,并附带候选列表。完整的 36 字符 UUID 始终使用精确匹配路径。接受可选的 fields 选择器,将返回的 data 对象投影为指定的架构字段子集。
list返回行,并支持可选的 limit / offset 分页。接受可选的 fields 选择器,将返回的 data 对象投影为指定的架构字段子集。
update根据 id 更新现有行。如果 id 少于 36 个字符,则将其视为 UUID 前缀(解析规则见 get)。默认模式为 merge(RFC 7396):缺失字段将从存储的行中保留,null 值会删除可选字段,或对必填字段引发 Validation 错误。传入 "mode": "replace" 可进行完整替换(0.9 之前的行为)。
delete根据 id 删除行。如果 id 少于 36 个字符,则将其视为 UUID 前缀(解析规则见 get)。
reload重新扫描 MINI_APP_USER_DIR / MINI_APP_PROJECT_DIR,并原子性地替换表注册表。如果已设置旧版 MINI_APP_SCHEMA + MINI_APP_DB,则会重新应用。返回 { mounted, added, removed }。限制:不监视文件(仅显式调用);替换整个注册表(不支持逐表部分重新加载);不会迁移现有行的架构;并发调用 reload 时,以最后完成的调用为准。
schema_create在指定的 scope(project 或 user)下创建新的 schema.yaml,并实时注册该表。传入 dry_run: true 可预览而不写入。如果表已存在,则返回 SCHEMA_EXISTS。
schema_update用新定义替换现有表的 schema.yaml(完整覆盖)。写入前,会将旧版 YAML 和 SQLite 快照备份到 _backup/。传入 dry_run: true 可预览字段变更而不触碰磁盘。
schema_delete删除表的 schema.yaml(移至 _backup/),并将其从实时注册表中注销。不会修改或删除 SQLite 表——DDL 变更仍由操作员负责。传入 dry_run: true 可预览。
schema_batch在单个 SQLite SAVEPOINT 下原子性地执行 ops[] 数组。任何操作失败都会回滚之前的所有操作,使 YAML 和 DB 保持不变。所有操作必须针对同一张表。返回每个操作的结果,或返回带有失败操作索引的 BATCH_ABORTED 错误。
data_snapshot使用 SQLite 热备份 API,为一个或所有已挂载的表创建某一时刻的 SQLite 快照。快照写入 <scope_root>/_snapshots/<table>.<unix_secs>.db。传入 table 和/或 scope 可限定目标范围;两者均省略时,将为所有已挂载的表创建快照。传入 dry_run: true 可预览操作(目标表、行数、将清除的数量),而不创建任何文件。保留策略由 MINI_APP_SNAPSHOT_RETENTION 控制(默认值为 10),与 MINI_APP_BACKUP_RETENTION 相互独立。
alias_create为表注册具名查询别名。接受 name、filter(ListFilter 表达式)或 filter_template(MiniJinja 模板字符串;不能与 filter 同时使用)、可选的 params_schema(用于模板别名的参数名称字符串数组)、可选的 default_limit,以及可选的 description。别名名称在每张表内必须唯一;名称重复时返回 ALIAS_ALREADY_EXISTS。别名按表划分作用域,并存储在表自己的 SQLite 数据库中。
alias_list将为表注册的所有别名作为 JSON 数组返回,每个元素为 { name, filter, default_limit, description, params_schema } 对象。
alias_run按名称执行已存储的别名。接受可选的运行时 limit 和 offset,可在调用时覆盖已存储的 default_limit。对于参数化别名(通过 filter_template 创建的别名),还接受 params 对象,其键值对会注入模板。接受可选的 fields 选择器,将返回的 data 对象投影为指定的架构字段子集。如果设置了 params_schema,但省略了 params,则返回 ALIAS_PARAMS_REQUIRED。模板渲染失败时返回 ALIAS_TEMPLATE_ERROR。返回与 list 工具相同的结构。名称未知时返回 ALIAS_NOT_FOUND。
alias_delete删除表的具名别名。如果别名不存在,则返回 ALIAS_NOT_FOUND。

MCP 资源

除了上述工具外,服务还通过 URI 提供 7 个只读 资源。资源面向希望获取架构定义或参考文档、而无需调用会修改数据的工具的 Agent。

URIMIME内容
schema://yamlapplication/yaml原始 schema.yaml 文件内容(每次请求时从磁盘读取)
schema://jsonapplication/json解析后的 SchemaConfig,以 JSON 格式返回(结构与 info 工具返回的相同)
schema://json-schemaapplication/schema+json根据架构字段生成的 JSON Schema(draft-07)。调用 create / update 前,可用它验证 data 参数
docs://quickstarttext/markdownAgent 快速入门指南(模式检测 + 首次调用步骤 + 其他 docs:// 资源的指引),编译到二进制文件中。与面向人类读者的本 README 不同——请在 MCP 会话中读取此资源
docs://toolstext/markdown全部 17 个 MCP 工具及其输入结构的速查表
docs://errorstext/markdown服务返回的错误代码参考表
docs://filterstext/markdown用于 list 和 alias_create 的筛选器对象(Eq/In/Like/Or/And)构造指南

info 工具和 schema://json 资源返回的内容等效,但用途不同:info 是可调用的工具(适合在对话中进行一次性检查),而资源可通过 URI 寻址,客户端可以订阅或缓存它们。

用法

通过 --mcp(stdio 传输)或 --mcp-http(可流式传输的 HTTP 传输)启动服务。这是仅有的入口点;没有 REST/CLI-CRUD 接口。

多表模式

将每个表的 schema.yaml 和 <table>.db 放在 ~/.mini-app/<table>/(用户范围)或 ./.mini-app/<table>/(项目范围)下,然后无需设置其他环境变量即可启动:

mini-app-mcp --mcp

在 .mcp.json 中注册一次,即可提供所有已挂载的表:

{
  "mcpServers": {
    "mini-app": {
      "command": "mini-app-mcp",
      "args": ["--mcp"]
    }
  }
}
多设备模式(可流式传输的 HTTP)

运行一个管理 SQLite 文件的中央守护进程,然后从任意数量的设备连接为远程 MCP 客户端。由于所有设备都与同一个进程通信,因此可保留单写入者存储模型——不存在跨设备同步或冲突处理,也无需这些机制。

# Central host — loopback (same-machine clients, e.g. behind an SSH tunnel)
mini-app-mcp --mcp-http                          # default bind 127.0.0.1:8484

# Central host — LAN exposure requires a token (generate one once):
#   openssl rand -hex 32
MINI_APP_HTTP_TOKEN=<random-string> mini-app-mcp --mcp-http --bind 0.0.0.0:8484

在 .mcp.json 中注册客户端(网络上的任意设备均可):

{
  "mcpServers": {
    "mini-app": {
      "type": "http",
      "url": "http://central-host:8484/mcp",
      "headers": { "Authorization": "Bearer <random-string>" }
    }
  }
}

安全模型:

  • 回环绑定(默认):MINI_APP_HTTP_TOKEN 为可选项。传输内置的 Host 标头验证(仅限回环地址)可防范 DNS 重绑定攻击。
  • 非回环绑定:必须设置 MINI_APP_HTTP_TOKEN,否则将拒绝启动。此后,每个请求都必须携带 Authorization: Bearer <token>。
  • 传输加密(TLS)和更完善的授权功能不在设计范围内;如有需要,请在反向代理处终止 TLS。

中央主机的守护进程模板位于 contrib/ 下:

  • Linux:contrib/systemd/mini-app-mcp.service(systemd 用户单元;安装说明见文件头部)
  • macOS:contrib/launchd/io.github.ynishi.mini-app-mcp.plist(launchd 用户代理;安装说明见文件头部)

如需改为在 Fly.io 上托管中央守护进程,而非运行在自己的机器上(持久化卷、边缘 TLS、Bearer 身份验证、每日备份 cron),请参阅 docs/runbooks/fly-io-deploy.md 和 contrib/fly/fly.toml。

旧版单表模式
MINI_APP_SCHEMA=./schema.yaml MINI_APP_DB=./issues.db mini-app-mcp --mcp

或者通过 .mini-app-mcp.env 配置:

MINI_APP_SCHEMA=./schema.yaml
MINI_APP_DB=./issues.db

导出 / 文件物化

mini-app-mcp 可以将每条新建或更新的记录写入磁盘上的 Markdown 文件。这对从文件中读取上下文的 Agent、使用 git 对记录进行版本控制,或快速人工检查都很有用。

工作原理

每次成功调用 create 或 update 后,服务都会写入(或覆盖)一个文件:

<dump-dir>/<id>.md

文件格式如下:

# <title-field value>

<body-field value>

默认情况下,delete 不会删除导出文件(记录会作为存档保留在磁盘上)。

在 schema.yaml 中启用 dump

在 schema.yaml 中添加 dump: 部分:

table: issues
fields:
  - name: title
    type: string
    required: true
  - name: body
    type: string
    required: false
dump:
  dir: ./issues          # optional; default: <cwd>/.mini-app/<table>/
  title_field: title     # optional; default: title
  body_field: body       # optional; default: body
  sync: write-only       # optional; default: write-only
键默认值描述
dump.dir<cwd>/.mini-app/<table>/写入 <id>.md 文件的目录。相对路径以服务的工作目录为基准解析。
dump.title_fieldtitle存储的 JSON 行中用作 Markdown 标题的字段名。
dump.body_fieldbody存储的 JSON 行中用作 Markdown 正文的字段名。
dump.syncwrite-only同步方向。目前仅实现了 write-only。设置为 bidirectional 时不会报错,但会记录警告并按 write-only 处理。
在 git 中忽略 dump 文件

如果不希望版本控制跟踪 dump 文件,请将 .mini-app/(或自定义的 dump.dir)添加到 .gitignore 中:

.mini-app/

Schema 管理

mini-app-mcp 提供四个工具,可在运行时管理表 schema,而无需重启服务。

创建表
schema_create(scope="project", table="notes", title="Notes", description="Personal notes.", fields=[...])

该工具会写入 <scope_dir>/notes/schema.yaml,并立即在实时注册表中注册新表。再次调用该工具创建同名表时,会返回 SCHEMA_EXISTS。可选参数 title 和 description 会写入 YAML 文件,并立即通过 info 或 schema://json 显示。单个字段条目也可以包含 description 字符串。

更新 schema
schema_update(scope="project", table="notes", title="Notes", description="Updated notes.", fields=[...])

覆盖前,服务会将现有的 schema.yaml 和某一时刻的 SQLite 快照备份到 <scope_dir>/_backup/notes.<timestamp>.yaml 和 <scope_dir>/_backup/notes.<timestamp>.db。写入后会刷新实时注册表。不会应用 DDL 迁移——底层表结构保持不变。

删除 schema
schema_delete(scope="project", table="notes")

schema.yaml 会移至 _backup/,该表也会从实时注册表中注销。SQLite 数据库文件不会被修改。 删除或更改表仍由操作员负责。

原子批量操作
schema_batch(ops=[
  { "op": "create", "table": "notes", "scope": "project", "fields": [...] },
  { "op": "data_insert", "table": "notes", "data": { "title": "first note" } }
])

所有操作都在单个 SQLite SAVEPOINT 下执行。如果任一操作失败,整个批次都会回滚——不会写入 YAML 文件,也不会更改注册表。同一批次中的所有操作都必须针对同一张表。

dry_run 预览

所有四个 schema 工具都接受 dry_run: true。在 dry-run 模式下,工具会计算并返回影响数量(rows、fields_added、fields_removed),但不会写入任何 YAML 文件、SQLite 表或备份目录。

备份保留

备份文件会累积在 <scope_dir>/_backup/ 中。服务会自动删除超出保留上限的最旧副本(默认每张表保留 10 对)。可通过 MINI_APP_BACKUP_RETENTION 覆盖此上限。

数据快照

data_snapshot 会创建一个或多个已挂载表的独立 SQLite 快照,不会触及任何 YAML 文件或更改 schema:

data_snapshot(table="notes", scope="project")
data_snapshot()                       # snapshot all mounted tables
data_snapshot(dry_run=true)           # preview without writing

快照会使用 SQLite 热备份 API(rusqlite::Connection::backup)写入 <scope_root>/_snapshots/<table>.<unix_secs>.db,因此操作期间源数据库仍保持打开且可写。保留上限(默认 10)通过 MINI_APP_SNAPSHOT_RETENTION 独立控制,且绝不会与 _backup/ 交互。

快照上传(兼容 S3,选择启用)

启用 s3-upload 构建功能后,data_snapshot 可将每个已写入的快照推送到任意 S3 协议目标位置——AWS S3、Backblaze B2(兼容 S3 的 API)、Cloudflare R2、MinIO——仅通过端点选择:

cargo install mini-app-mcp --features s3-upload
data_snapshot(upload=true)             # snapshot all tables, then upload each
data_snapshot(table="notes", upload=true)

配置仅通过环境变量设置(从 .mini-app-mcp.env 读取);凭据绝不会通过工具调用参数传递:

环境变量必需说明
MINI_APP_S3_ENDPOINT是兼容 S3 的端点 URL。B2:https://s3.<region>.backblazeb2.com
MINI_APP_S3_BUCKET是存储桶名称
MINI_APP_S3_ACCESS_KEY_ID是访问密钥 ID(B2:应用密钥 ID)
MINI_APP_S3_SECRET_ACCESS_KEY是访问密钥(B2:应用密钥)
MINI_APP_S3_PREFIX否对象键前缀(默认值为 mini-app-snapshots/)
MINI_APP_S3_REGION否签名区域。未设置时,会从 s3.<region>.<domain> 端点自动推导(B2 / AWS 区域端点),因此 B2 用户可以省略;不包含区域信息的主机(MinIO、R2)会回退到虚拟值 us-east-1。显式设置的值始终优先——B2 会拒绝不匹配的区域,因此仅当其与端点匹配时才设置
MINI_APP_S3_VIRTUAL_HOSTED_STYLE否true = 虚拟主机寻址(bucket.endpoint/key);默认值 false = 路径样式(endpoint/bucket/key),MinIO 要求使用此样式,AWS / B2 / R2 则支持此样式
MINI_APP_S3_CHECKSUM否sha256 = 上传时发送 x-amz-checksum-sha256;默认值为 none,因为某些兼容 S3 的提供商会以 400 InvalidArgument: Unsupported header 拒绝校验和标头

语义:

  • 在写入任何快照之前验证配置——未启用此功能构建的二进制文件,或环境变量不完整时,会立即返回 UPLOAD_NOT_CONFIGURED。
  • 单个上传失败不会导致操作失败:本地快照和清理结果仍然有效,失败情况会按表格列在响应字段 upload_errors[] 中。成功情况会以 {table, key, bytes} 的形式出现在 uploaded[] 中。
  • 远程保留策略不在支持范围内(已知限制):服务端永远不会删除远程对象。请配置存储桶生命周期规则,以过期方式清除旧版本。
  • 兼容性说明:上传使用单次 PutObject 请求(不使用分段上传),并且除非通过 MINI_APP_S3_CHECKSUM=sha256 选择启用,否则不会发送任何 x-amz-checksum-* 标头——会导致某些兼容 S3 的提供商出问题的两个 aws-sdk 端陷阱默认都不会触发。

不启用此功能的替代方案:保持服务端不变,并通过外部工具发送快照,例如添加 cron 条目 mini-app snapshot → rclone copy ~/.mini-app/_snapshots b2:my-bucket/mini-app-snapshots,或运行 restic backup ~/.mini-app 任务——效果相同,且凭据由外部工具持有。

定时备份

contrib/backup/mini-app-backup.sh 可将上传操作变成 cron 任务:它会在运行中的 --mcp-http 守护进程上调用 data_snapshot(upload=true)(通过可流式传输的 HTTP 执行 initialize → tools/call;依赖:curl、jq),并在调用失败或 upload_errors[] 非空时以非零状态码退出,以便在 cron 邮件 / journal / supercronic 日志中显示失败情况。针对本地守护进程:

# daily 03:00 — MINI_APP_URL / MINI_APP_HTTP_TOKEN via env if non-default
0 3 * * * /path/to/contrib/backup/mini-app-backup.sh

在容器镜像中,该脚本与 supercronic 一同打包为 mini-app-backup;设置 BACKUP_CRON(例如 0 3 * * *)后,入口点会安排脚本与服务端一同运行(参见 contrib/docker/entrypoint.sh)。

关系图用法

mini-app-mcp 与表结构无关,但一种常见模式是将单个表用作关系图层,为需要节点间带类型边的 Agent 提供支持(例如,persona-AI 记忆:sister_of、member_of_studio、mother_of)。

参考架构位于 examples/schemas/relations.schema.yaml:

字段类型必填说明
fromstring是源节点 ID
tostring是目标节点 ID
typestring是边标签(sister_of、member_of_studio……)
tsnumber是断言时的 Unix 时间戳(秒)
strengthnumber否边权重,范围为 [0.0, 1.0],默认为 1.0
metadataobject否自由格式属性
source_entry_idstring否指向日志 / 记录条目的外部引用
批量插入(原子操作插入 N 条边)

使用 schema_batch 搭配 query 操作,在一个 SAVEPOINT 中注册 N 条边:

{
  "ops": [
    {
      "op": "query",
      "sql": "INSERT INTO rows (id, data, created_at, updated_at) VALUES (?1, ?2, ?3, ?4)",
      "params": ["<uuid>", "{\"from\":\"persona-x\",\"to\":\"a\",\"type\":\"sister_of\",\"ts\":1779330000,\"strength\":1.0}", "1779330000", "1779330000"]
    }
  ]
}

任何操作失败都会回滚整个批次。

批量替换(完整替换边集合)

使用专用的 replace 操作,在一个原子 SAVEPOINT 中替换边集合。服务端负责生成 UUID、时间戳和序列化 JSON;调用方只需提供 match 范围和 items 列表。

{
  "ops": [
    {
      "op": "replace",
      "table": "relations",
      "match": {"from": "persona-x", "type": "sister_of"},
      "items": [
        {"from": "persona-x", "to": "a", "type": "sister_of", "ts": 1779330000, "strength": 1.0},
        {"from": "persona-x", "to": "b", "type": "sister_of", "ts": 1779330000, "strength": 1.0},
        {"from": "persona-x", "to": "c", "type": "sister_of", "ts": 1779330000, "strength": 0.8}
      ]
    }
  ]
}

replace 操作会在单个 SAVEPOINT 中执行 DELETE WHERE(match)和 N 次 INSERT。任一阶段发生失败,两个阶段都会一并回滚。响应中包含每个操作的影响行数:

{
  "committed": true,
  "ops_executed": 1,
  "affects": {"deleted": 5, "inserted": 3}
}

语义:

  • match 中的每个键都会转换为一个独立的 json_extract(data, '$.key') = value 谓词,并以 AND 连接。匹配值必须是标量(字符串 / 数字 / 布尔值 / null)。
  • 空 match({})会被拒绝并返回 VALIDATION_ERROR,以防意外清空整张表。
  • items 中的每一项都会在执行任何 SQL 之前根据表架构进行验证。
  • query 操作(原始 SQL 逃生口,见批量插入)仍可用于 replace 操作无法满足的场景。
列出边(范围少于 1000 条)

list 工具按 created_at DESC 顺序返回行,limit 上限为 1000。可选的 filter 参数支持服务端根据经过架构验证的字段筛选行。

筛选条件类型
type说明
eq相等匹配:{"type": "eq", "field": "...", "value": ...}
in集合成员匹配:{"type": "in", "field": "...", "values": [...]}
like对字符串字段进行部分匹配:{"type": "like", "field": "...", "pattern": "..."}。% 匹配任意子字符串;_ 匹配任意单个字符。仅限 string 类型字段。
orOR 组合:{"type": "or", "filters": [...]}
andAND 组合:{"type": "and", "filters": [...]}

or 和 and 接受递归的 Vec<ListFilter>,可构造任意深度的嵌套组合。

约束:字段名必须在表的 schema.yaml 中注册(未知字段会返回 VALIDATION_ERROR)。值会根据架构中字段声明的类型进行类型检查,与针对 BatchOp::Replace 匹配值执行的类型化标量验证相同。不传入 filter 时,会在 limit / offset 范围内返回所有行(完全向后兼容)。

邮箱式 OR 筛选示例

单次调用即可获取发送给 "alice" 或 "broadcast" 的消息:

{
  "filter": {
    "type": "or",
    "filters": [
      {"type": "eq", "field": "to", "value": "alice"},
      {"type": "eq", "field": "to", "value": "broadcast"}
    ]
  }
}

对于适合在客户端进行筛选的小型图:

edges = mini_app.list(table="relations", limit=1000)
sisters_of_x = [
    e for e in edges
    if e["data"]["from"] == "persona-x" and e["data"]["type"] == "sister_of"
]

更新语义:合并与替换

update 工具支持两种模式,由可选的 "mode" 字段控制。

合并模式(默认)
update(table="notes", id="<uuid>", data={"state": "done"})
// or equivalently:
update(table="notes", id="<uuid>", data={"state": "done"}, mode="merge")

合并模式遵循 RFC 7396 (JSON Merge Patch) 的浅层语义:

  • 缺失字段会保留 — data 中未出现的键会保留其已存储的值。
  • 非 null 值会覆盖 — data 中存在且值非 null 的键会替换对应键的已存储值。嵌套对象会整体替换(不进行深度合并)。
  • null 会删除可选字段 — 如果键的值为 null,且架构中该字段为 required: false,则会从已存储的行中移除该字段。
  • 必填字段为 null 时会产生验证错误 — 如果键的值为 null,且该字段为 required: true,调用会在进行任何写入前以 VALIDATION_ERROR 失败。
  • 合并后的结果会进行完整的架构验证 — 合并后,会根据架构验证完整的合并对象。任何约束违规(缺少必填字段、类型不匹配)都会返回验证错误,且不会更新该行。
替换模式
update(table="notes", id="<uuid>", data={...}, mode="replace")

替换模式会执行完整替换:根据架构验证 data,然后将其原样写入,完全覆盖已存储的行。这与 0.9 版本之前 update 的行为相同。依赖旧默认行为并发送不完整 data 对象的调用方,应切换到 mode="replace" 以恢复原有行为。

选择模式
模式适用场景
merge(默认)部分更新 — 只发送要更改的字段。省略的现有字段不会受到影响。
replace完整重写 — 提供该行预期完整状态。省略的字段会被删除。

字段投影

list、get 和 alias_run 均接受可选的 fields 参数,用于限制返回的 data 对象中包含哪些架构字段。无论选择器如何设置,响应封装中始终包含 id、created_at 和 updated_at。

选择器形式
{"mode": "all"}                              // default: return all schema fields
{"mode": "list", "fields": ["title", "state"]}   // return only the named fields

省略 fields 完全向后兼容 — 现有调用方仍会收到完整行,行为不会改变。

使用示例
list(table="issues", fields={"mode": "list", "fields": ["title", "state"]})
get(table="issues", id="<uuid>", fields={"mode": "list", "fields": ["title"]})
alias_run(table="issues", name="recent_open", fields={"mode": "list", "fields": ["title", "state"]})
验证

未知字段名会在执行任何查询前以 VALIDATION_ERROR 拒绝。字段名验证会查询 schema.yaml 中的规范字段定义,而非已存储行中实际存在的键,因此即使字段仅在某一行中缺失,也能可靠地捕获投影错误。

查询别名

查询别名可将 ListFilter 表达式(或 MiniJinja 筛选模板)保存为短名称并重放 — 还可按调用覆盖 limit / offset — 无需每次重复传入筛选 JSON。

创建静态别名
alias_create(
  table="notes",
  name="recent_open",
  filter={"type": "eq", "field": "state", "value": "open"},
  default_limit=20,
  description="Open notes, newest first"
)

别名存储在表自身的 SQLite 数据库 _aliases 中。别名名称在每张表内唯一;再次使用相同名称调用 alias_create 会返回 ALIAS_ALREADY_EXISTS。

创建参数化别名(筛选模板)

参数化别名使用 MiniJinja 模板字符串,而非固定筛选条件。运行时,调用方提供参数值,这些值会被渲染到模板中,然后执行。

alias_create(
  table="issues",
  name="by_state",
  filter_template='{"type": "eq", "field": "state", "value": "{{ state }}"}',
  params_schema=["state"],
  default_limit=50,
  description="Filter issues by state (parameterized)"
)

filter_template 和 filter 互斥 — 必须且只能提供其中一个。params_schema 是可选的参数名称数组;提供它可说明 alias_run 在其 params 对象中需要哪些键。

运行别名

静态别名(无参数):

alias_run(table="notes", name="recent_open")
# → uses stored default_limit=20

alias_run(table="notes", name="recent_open", limit=5)
# → runtime limit overrides the stored default_limit

alias_run(table="notes", name="recent_open", limit=10, offset=20)
# → pagination with runtime overrides

带参数的别名(带有 params):

alias_run(table="issues", name="by_state", params={"state": "open"})
# → renders template → {"type": "eq", "field": "state", "value": "open"}
# → validates as ListFilter → executes Store::list

alias_run(table="issues", name="by_state", params={"state": "open"}, limit=10)
# → runtime limit override with params injection

alias_run 会解析已存储的筛选条件,或渲染模板,并将结果传递给 Store::list。如果调用时提供了 limit,则会覆盖 default_limit;如果未提供,则使用别名中的 default_limit。offset 始终是仅在运行时使用的参数(不会存储)。

带参数别名的错误情况:

情况错误代码
设置了 params_schema,但未提供 paramsALIAS_PARAMS_REQUIRED
MiniJinja 渲染失败(语法错误或缺少变量)ALIAS_TEMPLATE_ERROR
渲染结果不是有效 JSON 或不是有效的 ListFilterVALIDATION_ERROR
列出和删除别名
alias_list(table="notes")
# → [{"name": "recent_open", "filter": {...}, "default_limit": 20, "description": "...", "params_schema": null}]

alias_list(table="issues")
# → [{"name": "by_state", "filter": null, "default_limit": 50, "description": "...", "params_schema": ["state"]}]

alias_delete(table="notes", name="recent_open")
别名隔离

每个表的 _aliases 存储在物理上相互独立:notes 的别名存储在 notes.db 中,issues 的别名存储在 issues.db 中。不存在全局别名命名空间,也无法通过别名操作读取或写入属于其他表的别名。

聚合

query_aggregate 可在一次工具调用中,对一个或多个表执行 COUNT / SUM / AVG / MIN / MAX / GROUP BY。多表数据源通过 SQLite ATTACH DATABASE 与字面量 UNION ALL 合并,从而消除调用方逐表获取行并在客户端侧汇总时产生的 N+1 次往返。该工具为只读且幂等。

结构
{
  "sources":   { "kind": "single", "value": "<table>" },          // or "multi" + ["t1","t2",...]
  "filter":    null,                                              // optional, same ListFilter shape as `list`
  "aggregator": { "kind": "count" }                               // or sum / avg / min / max / group_by
}

结果采用外部标记格式,方便调用方根据 kind 进行分派:

{ "kind": "count",  "value": 42 }
{ "kind": "value",  "value": 3.14 }
{ "kind": "groups", "value": [ { "key": "a", "count": 12, "value": 7.0 }, ... ] }
单数据源聚合
// COUNT all rows in `emo`.
{ "name": "query_aggregate", "arguments": {
    "sources":    { "kind": "single", "value": "emo" },
    "aggregator": { "kind": "count" }
}}

// SUM the `amount` field.
{ "name": "query_aggregate", "arguments": {
    "sources":    { "kind": "single", "value": "ledger" },
    "aggregator": { "kind": "sum", "field": "amount" }
}}
多表聚合

Multi 会通过 SQLite ATTACH DATABASE 挂载每个后端 .db 文件(上限为 10 个),并在应用外层聚合前,将各表的子查询通过 UNION ALL 组合起来。

// COUNT rows across two same-shape tables.
{ "name": "query_aggregate", "arguments": {
    "sources":    { "kind": "multi", "value": ["events_a", "events_b"] },
    "aggregator": { "kind": "count" }
}}

// GROUP BY tag with HAVING and an inner SUM(amount).
{ "name": "query_aggregate", "arguments": {
    "sources":    { "kind": "multi", "value": ["events_a", "events_b"] },
    "aggregator": {
        "kind":     "group_by",
        "by_field": "tag",
        "having":   { "type": "eq", "field": "tag", "value": "a" },
        "inner":    { "kind": "sum", "field": "amount" }
    }
}}

调用方负责确保所有 Multi 数据源的架构兼容;逐表字段验证属于 Phase 2 的后续工作。聚合器特有的错误(数据源为空、超出 ATTACH 上限、嵌套 GroupBy、非 UTF-8 数据库路径)会返回 AGGREGATOR_ERROR(data.code);未知表名会返回 TABLE_NOT_FOUND;字段或标识符被拒绝会返回 VALIDATION_ERROR。

全局别名(Phase 2)

Phase 2 会将各表的别名统一存储在一个全局存储中 (<project_dir>/_global.db + <user_dir>/_global.db,查找 优先级为 Project → User)。alias_create MCP 工具接受新增的可选参数 sources 和 aggregator,因此一个别名可以跨多个表,并保存聚合逻辑:

// Multi-table alias with COUNT aggregator (replays via execute_aggregate).
{ "name": "alias_create", "arguments": {
    "name":       "combined_events_count",
    "sources":    { "kind": "multi", "value": ["events_a", "events_b"] },
    "aggregator": { "kind": "count" },
    "filter":     { "type": "like", "field": "tag", "pattern": "%" }
}}

// Pattern source — resolved against the live registry's table list at
// alias_run time. Matches all currently-mounted `events_*` tables.
{ "name": "alias_create", "arguments": {
    "name":       "all_events_count",
    "sources":    { "kind": "pattern", "value": "events_*" },
    "aggregator": { "kind": "count" },
    "filter":     { "type": "like", "field": "tag", "pattern": "%" }
}}

// alias_run dispatches to execute_aggregate transparently.
{ "name": "alias_run", "arguments": { "name": "all_events_count" } }
// → { "kind": "count", "value": <sum across all events_* tables> }

仍然接受旧版 table 参数,并会静默将其规范化为 sources = { "kind": "single", "value": "<table>" }; 同时指定 table 和 sources 会报错。

迁移:每次调用 TableRegistry::mount_from_dirs 时,都会自动将现有的逐表 _aliases 行复制到项目作用域的 _global_aliases 中。迁移不会丢失数据 (保留全部五个旧字段,并填入 sources = Single(<table>)),且具有幂等性 (INSERT OR IGNORE 会跳过冲突项),因此每次服务器重启时运行都是安全的。逐表 _aliases 表不会被删除,以保留回滚路径,直到未来的次要版本发布。

Phase 2 的限制

  • Multi / Pattern 数据源别名需要提供 aggregator;不带聚合器运行 会返回结构化错误(逐表 list 路径无法提供跨表行)。
  • Pattern glob 仅支持 *(一个或多个);? / [] 为未来版本保留。
  • Pattern 匹配结果为零个表时,会在 alias_run 时返回 AGGREGATOR_ERROR(提前暴露错误——不会延迟到 ATTACH DATABASE 时)。
  • 筛选条件验证仅使用第一个数据源表的架构; 跨表架构差异由调用方负责处理(与 query_aggregate 的约束相同)。
  • 旧版单表模式(未设置 MINI_APP_USER_DIR / MINI_APP_PROJECT_DIR) 会回退到逐表 Store::alias_* 路径;此模式下不接受 Pattern 数据源。

行历史自动日志

每次 create、update 和 delete 调用都会自动将受影响行的完整 JSON 快照写入同一 SQLite 数据库中的 _row_history 表。快照会与数据变更在同一事务中原子写入——数据 DML 和历史记录要么一起提交,要么都不提交。

记录内容
事件op 值data_json
create"create"新行 JSON
update"update"合并后的结果 JSON
delete"delete"已删除行的 JSON

每条历史记录还包含一个单调递增的 version 计数器(按 table + row id 分别计数)以及一个 Unix 秒级时间戳 recorded_at。

任何条目的操作前状态都可以通过前一条记录的 data_json 获取,因此无需再次存储(由旧版本写入的数据库中,旧版 prev_data_json 列仍可读取,但新条目会将其留为 null——这会使历史记录写入量减半)。

两层存储:原始记录 + 压缩归档

历史记录永不丢弃,但也不能占满磁盘(一个 178KB 的行更新 949 次,过去会留下 222MB 的完整副本历史记录)。原始 _row_history 表只保留每行最新的记录;较早的记录会在同一写事务中自动压缩到 _row_history_archive 中的 zstd 压缩 JSONL 块。近似重复的 JSON 版本压缩后体积可缩小约三个数量级,因此长历史记录仍然占用很少空间,同时可以完整恢复:row_restore 和版本列表会透明地读取归档。

环境变量默认值含义
MINI_APP_HISTORY_KEEP_RECENT16每行保留的原始(未压缩)记录数——用于快速撤销的热数据集
MINI_APP_HISTORY_CHUNK_MIN48合并到一个归档块中的最少记录数(超过 KEEP_RECENT + CHUNK_MIN 后触发归档)
按表选择退出(history: off)

自动化任务高频写入的表(如心跳时间戳、日志缓存类行)可以在 schema.yaml 中完全禁用历史记录:

table: tick_stamps
history: off   # default: full
fields:
  - name: label
    type: string
    required: true

设置 history: off 后,对该表进行任何变更都不会写入 _row_history 记录(row_restore 也没有可供恢复的内容——仅在逐次写入历史没有恢复价值时使用)。

使用 row_restore 恢复行

row_restore 会获取指定 Unix 时间戳之前或当时的最新快照,并将其写回为当前行。如果该行已删除,则会使用相同的 id 重新插入;如果该行仍然存在,则会用快照数据替换。

{
  "name": "row_restore",
  "arguments": {
    "table": "issues",
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "at_unix_secs": 1750000000
  }
}

响应格式与 get 相同——包含 id、data、created_at 和 updated_at 的完整行对象。

清理 SOP

历史记录不会在每次更新时清理。清理是单独的操作,需要在应用代码中调用 purge_old_history,并指定所需的保留策略:

  • 保留期限:30 天(删除超过 30 天的记录)
  • 每行最大版本数:100(逐行删除超出 100 个的最旧版本)

清理不会接入任何自动钩子,以避免历史记录累积影响正常 create / update / delete 调用的延迟。

局部编辑工具

有三个工具可让 Agent 查看并精确编辑单个字符串字段,而无需获取或替换整行。非字符串字段(数组、对象、数字、布尔值)会被拒绝,并返回 TYPE_ERROR。

content_view

返回字符串字段的内容,并标注行号(cat -n 样式)。传入 view_range 可将输出限制为连续片段(从 1 开始计数,包含两端)。

// View lines 12–30 of a body field
content_view(table="issue", id="26a8a7f9", field="body", view_range=[12, 30])
// → {"content": "    12  first visible line\n    13  ...\n"}

// Omit view_range to get the full field
content_view(table="issue", id="26a8a7f9", field="body")
content_replace

在字符串字段中,将 old_str 的唯一匹配项替换为 new_str。

// Unique replacement — returns the number of substitutions
content_replace(table="issue", id="26a8a7f9", field="body",
                old_str="foo", new_str="bar")
// → {"matches": 1}

// Scope the search to a line range before replacing
content_replace(table="issue", id="26a8a7f9", field="body",
                old_str="foo", new_str="bar", view_range=[1, 10])

// Replace every occurrence in one call
content_replace(table="issue", id="26a8a7f9", field="body",
                old_str="foo", new_str="bar", replace_all=true)
// → {"matches": 4}

如果未找到 old_str,工具会返回 STRING_NOT_FOUND。如果它出现多次且未设置 replace_all,工具会返回 AMBIGUOUS,并附带 candidates 列表。

content_insert

在指定位置向字符串字段中插入一行或多行。

// Prepend (line=0 inserts before the first line)
content_insert(table="issue", id="26a8a7f9", field="body",
               line=0, content="Prepended line")
// → {"line": 0, "inserted_lines": 1}

// Insert after line N (1-indexed)
content_insert(table="issue", id="26a8a7f9", field="body",
               line=5, content="Inserted after line 5")
// → {"line": 5, "inserted_lines": 1}

line 值大于 total_lines + 1 时会返回 OUT_OF_RANGE。这三个工具都会通过常规的 row_history 钩子记录更改,因此每次编辑都可完整审计,并可通过 row_restore 还原。

存储说明

SQLite 数据库以 WAL 日志模式打开,以便在 reload 期间安全地并发访问。每个 .db 文件旁边都会创建边车文件 <db>.db-wal 和 <db>.db-shm——这些文件由 SQLite 管理,不应手动删除。

MCP Registry

mini-app-mcp 已列入 MCP Registry。每次发布带标签的版本时,都会将 OCI 镜像发布到 GitHub Container Registry。

Docker(无需 Rust 工具链)
{
  "mcpServers": {
    "mini-app": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "-v", "/path/to/data:/data",
        "ghcr.io/ynishi/mini-app-mcp:latest"
      ]
    }
  }
}

许可证

MIT OR Apache-2.0

评论

支持 Markdown

第一次接触 MCP?

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

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