Skip to content

[Decision] How should an MCP agent create a record whose required file field must be filled (e.g. an expense line with a required e-invoice)? #32686

Closed

Description

@os-bob

Filing category: ② a decision only the maintainer can make.

维护者速读

  • 现象:报销明细的「电子发票」是必填文件字段。MCP 的 create_record 没法带文件,被拒(「电子发票 is required」);create_upload_link 只能给已存在的记录生成上传链接。先有鸡还是先有蛋:agent 无法通过 MCP 新增任何带必填文件字段的记录。附件通道(attachments: true)对普通成员也被拒(无 sys_attachment 创建权)。
  • 要你裁:给 MCP 一条「先传文件、再建记录」的路,还是接受这类对象只能在界面里建。
  • 推荐:A:create_upload_link 支持「为将要创建的记录」上传:返回一个文件令牌,create_record 用这个令牌填文件字段(与界面上传后再提交表单是同一个顺序)。

One-sentence problem

"Help me file this taxi receipt" works in the console (pick the PDF, save) but cannot be done by an agent, because the agent can only ask for an upload after the record exists, and the record cannot exist without the upload.

Evidence (reach)

Hosted tenant (pnpm dev:stack --with-ui, Team plan; cloud ff373e02, framework d7c3d44e), member's MCP OAuth token (data:write):

  • create_record k7en_claim_line { …, invoice_no, amount } (no file) → 电子发票 is required.
  • create_upload_link { objectName: 'k7en_claim', recordId, attachments: true } → 403 … your permissions do not allow creating attachments (sys_attachment).
  • create_upload_link { objectName: 'k7en_claim_line', recordId: <existing line>, field: 'invoice_file' } → link; opened in the member's browser, upload replaced the file ("Done. The file was attached"). So the link path works for an existing record only.
  • read_file on the line returned the PDF's text (invoice number, amount) — the read half is fine.

Governing text

  • objectstack#30892 (closed): "MCP cannot read or attach files" — read_file / create_upload_link were its answer; the create-with-required-file case was not covered.
  • docs/NORTH-STAR.md — "files" is on the road; "Every step of the road must hold for both kinds of user".

Options

Option What it does What the customer sees Effort
A create_upload_link without recordId (object + field) → the user uploads; the answer gives a short-lived file token the same user's create_record / update_record may put in that field Agent: "open this link and pick the invoice", then it files the line storage + mcp seats · 1–2 PRs · precedent: the console's own upload-then-save
B Let agents create the record with the file field empty when the caller will attach next (a "pending file" state) Records exist briefly without their required file data + mcp seats · 2 PRs · loosens required
C Keep as is; document that objects with a required file are filed in the console Agents cannot file expenses with invoices none

Business reading: A = the agent works like the form (file first, then save); B = the rule bends for agents; C = invoices are a human-only step.

Recommendation

A. It keeps required meaning required, mirrors how the console already orders upload and save, and keeps the token bound to the uploading user. Confidence gap: token lifetime and reuse across records — single-use, same user, same object/field, recommended; untested.
Self-check: only ① picks A; ②③④ do not flip it.

After the ruling

A: storage issues an unbound upload for (object, field, user); create_record accepts the token in that field; e2e: an agent files an expense line with a required invoice in two calls plus one browser upload.

os-decision-facets
① 项目长远合理性: A 复用「先上传、再保存」这一个顺序,不放宽 required,特例最小。
② 实际业务拉动: 报销、合同、证照等「必须附文件」的对象都卡在这一步(本次实测:报销明细)。
③ 防 AI 犯错: A 下文件与字段强绑定、单次、同一用户;B 让 agent 建出缺文件的记录,静默违反必填。
④ 创业阶段不扩散: A 给现有工具加一个模式;C 零成本但留下 North Star 缺口。
Prior rulings read: "create_upload_link required file field" → 2 hits (#30892, #31486); thread: none

Reader who acts: the storage seat and the packages/mcp seat, after the ruling.
Dedupe: repo:objectstack-ai/objectstack is:issue create_upload_link required file field → 2 hits (#30892 closed — the origin of the tool; #31486 QA run).

Activity

  1. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Nov 12, 2026
  2. objectstack-fleet commented on Nov 12, 2026

    @objectstack-fleet
    Contributor

    triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

    Path: none

    分诊首触 + 决策分析(fable 档独立评估)· 2026-11-12T16:54Z

    维护者速读

    • 报销明细的「电子发票」是必填字段。agent 建记录时带不了文件,而上传链接只能发给已存在的记录,两头卡死。
    • 界面的顺序本来就是先上传、再保存。给 agent 同样的顺序即可,不放松必填。
    • 第一方应用里目前没有必填文件字段,拉动只来自本次实测。这会放宽一道已评审过的上传门。
    • 请裁:A 链接可不带记录(按对象 + 字段),上传后 agent 建记录时引用文件 id / B 允许先建缺文件的记录 / C 不做。推荐 A。

    是否已有裁决: 未找到。#30892 裁决 B(链接仅限已登录的铸造者本人)仍然约束本卡,A 与它不冲突。

    前提核实:

    • create_upload_link 只针对已有记录(packages/mcp/src/mcp-http-tools.ts:3283)。
    • 引擎已有「写入时点名文件 id,由钩子认领」的机制(file-reference-lifecycle.ts,即界面的先传后存)。所以不需要新的令牌类型。
    选项 客户看到 工作量
    A 无记录链接 + 文件 id 「打开链接选发票」,随后 agent 建单 storage + mcp · 2 个 PR + 契约评审;先例 #31009
    B 先建缺文件的记录 短暂存在违反必填的记录 2 个 PR;放松必填
    C 不做 这类对象只能在界面建 0

    四轴:

    • ① 长远:和 Stripe Files、Salesforce ContentVersion 的「先传后引用」一致,必填保持原意。
    • ② 拉动:第一方为 0,但国内报销场景里发票必填是常态。
    • ③ 防 AI:B 静默违反必填,出局。
    • ④ 不扩散:复用现有文件 id,不加工具、不加令牌。

    推荐 A,与 #32747 的 A 合成一次设计,顺序为 #32686 → #32747。置信缺口:MCP create_record 带本人未认领文件 id 时现有钩子是否接受,未实测;无记录时铸造链接要判什么权限,需契约评审。自检: 只看①选 A;不翻转。人工底线项:放宽上传门(仍须登录)。

    os-decision-facets

    • ① 长远:先上传后保存是界面已有的唯一顺序,必填不放松
    • ② 拉动:第一方必填文件字段为 0;报销场景实测卡死一次
    • ③ 防 AI:B 静默违反必填,出局;A 下文件归属由现有认领钩子判定
    • ④ 不扩散:复用现有文件 id,不新增令牌类型和工具

    Prior rulings read: #30892 ruling B (6691366983); ADR-0104 D3; thread: none


    Generated by Claude Code

  3. added
    area:filesFiles — upload, download, signed URLs, access derived from the parent record
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Nov 12, 2026
  4. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

    Brief-claimed-by: session_01Q4EjjMnyLqJdzuktEmRPXH · batch #1 · 2026-11-13T04:19Z

    Decision batch #1 (the MCP agent-capability family, under epic #32897), taken on the maintainer's 「5张一批按总监决裁格式给我」.

  5. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

    Ruling recorded · #32686 · A · 2026-11-13T04:40Z

    Maintainer, verbatim (triage#1 session): 「以上原则同意」, answering decision batch #1 (32641 B / 32591 A / 32592 C / 32686 A / 32747 A). The batch was analysed independently at the fable tier, with prior rulings checked first; the full analysis was presented in the session.


    Generated by Claude Code

  6. added
    pm:queueReady for the PM dispatch loop
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Nov 13, 2026
  7. added
    priority:p0Queue-jump: outranks batch and breaks the round — never exempts claiming or same-file serial
    and removed on Nov 13, 2026
  8. os-bob commented on Nov 13, 2026

    @os-bob
    ContributorAuthor

    Re-grade on the maintainer's instruction: priority:p2 → priority:p0. State unchanged (pm:queue, unassigned). Written by a test session, not a seat.


    Generated by Claude Code

  9. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

    Path: none

    Triage: p0 → p1 · not an 18.4.0 blocker · 2026-11-13T07:55Z

    ⛔ Not a claim; the claim and state are unchanged, and the work continues.


    Generated by Claude Code

  10. added
    priority:p1High: required for production / M2
    and removed
    priority:p0Queue-jump: outranks batch and breaks the round — never exempts claiming or same-file serial
    on Nov 13, 2026
  11. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

    Path: none

    Triage: H14 · pm:blocking derived · 2026-11-13T12:20Z

    ⛔ Not a claim; grade and state unchanged. Open card(s) #32747 name this card on a line-leading Blocked-by:, so the derived cache is added. It comes off when they close or drop the line.

  12. added
    pm:blockingDerived cache from the Blocked-by reverse index: open card with open dependents — never hand-set
    on Nov 13, 2026
  13. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx

    Claim: PM loop round 1, card thread · 2026-11-13T14:28Z
    Session: session_01L9qibwGvonkT2ydXH6AKHm
    Account: os-tesla
    Branch: claude/project-thread-vlkibi
    Worktree: objectstack-issue-32686
    Domain: domain:server
    Seat: domain:server#7
    File surface: packages/services/service-storage/src/** (pre-record upload binding, the upload route, record file access), packages/mcp/src/mcp-http-tools.ts (create_upload_link, create_record, update_record), packages/mcp/src/skill-md.ts and its console mirror ui/apps/console/src/pages/developer/objectstack-skill.ts, packages/runtime/src/domains/mcp.ts, the create/update file-field write path only where the file id is accepted, a dogfood test under packages/qa/dogfood/test/, at most one error-code ledger row in packages/spec/src/api/error-code-ledger.zod.ts (spec carry-along, declared), root changeset. Stop on breach.
    Container & model: L, mode:thread, model: default (dispatch-gates --tier: no path-derived mandate; PM default), effort: xhigh
    Review: contract review owed before landing (the ruling names it: which permission a pre-record link checks, orphan-file reclamation), by the review lane thread or the seat
    Clause-②: yes — a link without a record and a file id accepted in a required file field on create are new accepted call shapes
    Metadata-protocol: none (expected; no metadata author key changes; the dev derives it)
    Responsibility: n/a — not a defect card (ruled capability, A 6844207600)
    Thread-read: 6852728166
    Premise: holds (ruling A 6844207600 recorded after the card's last body edit; the dev re-measures the card's three readings at origin/main 2c98485d first)
    Serial constraints cleared: #32442 (domain:server#5, branch claude/project-thread-bb7kzf, mcp-http-tools.ts incl. lines near create_record, runtime/src/domains/mcp.ts): written in parallel, landed serially, merge main after it lands; #32872 (this seat) shares mcp-http-tools.ts / skill-md.ts in other regions, same rule; #32747 is Blocked-by: this card and is designed with it (shape left additive)

  14. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 13, 2026
  15. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx

    Claim amendment to this seat's claim 6855856900 on #32686 · 2026-11-13T15:00Z: the file surface adds two docs pages for this lane's packages, which go with the server seat (lanes/server.md):

    • content/docs/ai/connect-mcp.mdx, section "Files: read_file and create_upload_link": one paragraph on the two-call flow (a link for a new record, then the create naming the file id);
    • content/docs/permissions/system-context.mdx, row 47e: the new caller-scoped verb, which refuses the system context like the other verbs listed there.
      Everything else in the claim stands.
  16. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    domain:server#7 · os-tesla · session_01L9qibwGvonkT2ydXH6AKHm
    os-dev-report

    {
      "issue": 32686,
      "status": "done",
      "branch": "claude/project-thread-vlkibi",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/33123",
      "session": "session_01L9qibwGvonkT2ydXH6AKHm",
      "premise_still_valid": true,
      "summary": "create_upload_link takes objectName + field with no recordId where the storage service serves it: minting asks the object's create verdict, the field's writability and readonly; the answer carries fileIds (an array, so #32747 adds a count and more entries without a new key). The upload is judged against the field's accept/maxSize before the link is spent and becomes a committed file bound to (user, object, field, org, upload + 1h) in sys_file.metadata; applyCopyOnClaim refuses another user or a system write (unreadable-file words), another slot, a second record within the hour, and an expired claim, all as ADR-0114 field refusals; downloads of the new class are uploader-only while no record holds it. Docs: connect-mcp.mdx paragraph + system-context.mdx rows 47e and 47f (47f added beyond the amendment's 47e because its 'a system write claims any file' sentence would otherwise be false).",
      "tests": "At head a9a6589d (merged origin/main 7c36f524), under os-verify-lock (VERDICT command-exit 0 each): service-storage whole package Test Files 82/82, Tests 1540 passed; mcp whole package 63/63 files, 1104 tests; runtime src/mcp-record-file-bridge.test.ts 7/7; dogfood test/showcase-mcp-record-files.dogfood.test.ts 18/18 (3 new: two agent calls + one upload create dogfood_expense_line with its required invoice; another user's create refused + no download; no create permission → 403 PERMISSION_DENIED, nothing reserved). New pins: record-file-access.integration.test.ts 13 cases (incl. control: console-style presign/PUT/complete + save unchanged, no binding), pre-record-upload.test.ts 13, mcp-file-tools.test.ts 7, mcp-record-file-bridge.test.ts 2. Ablations via scripts/ablation-replace.mjs WRAP, committed first, each landed and restored to the HEAD blob: 19 legs; 17 red as expected (other user both arms, system write, other slot, spent, expired, uploader-only download, create verdict, writability, readonly, accept/maxSize before spend, page copy, reserved row uploader-only, unit not_yours arm, MCP optional schema, MCP no-record attachments, runtime fileIds, runtime member → dogfood acceptance red with 'expected string, received undefined at recordId'); 2 green by design (other-user integration pin with only one of its two arms removed: the read check refuses first). Logs: /mnt/project-files/server-seat-7/r2026-11-13/out/32686-evidence/.",
      "gates": "dispatch-gates --commands at the final diff: 106 families, each run with </dev/null, exit codes on disk before any pipe; plus the 4 roster gates under these paths (check:changeset-fixed, check:error-code-casing, check:filter-alias-parity, check:route-ledger-census) exit 0. Two exited 3 then 0: check-plugin-teardown-shape.mjs --self-test (fixture 621a4876 outside the shallow clone; fetched --depth=1) and check:dual-build-cjs-loads (PREREQUISITE no dist for 11 packages; 0 after check:type-check-debt's closure build, and 0 again at the merged head). --ran: '106 derived famil(ies) accounted for — 106 run, 0 NOT-MEASURED (a DERIVED zero)'. pnpm lint (whole tree) exit 0 at a9a6589d; eslint on 17 changed .ts exit 0; typecheck service-storage/mcp/runtime/dogfood exit 0 (runtime and dogfood need NODE_OPTIONS=--max-old-space-size=8192 here, default heap OOMs). CI: not yet run on this head; the seat reads the check-runs.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm; the touched dogfood file was run: showcase-mcp-record-files.dogfood.test.ts 18/18, lock VERDICT command-exit 0",
      "mcp_calls": "1 — mcp__github__pull_request_read (get #33074, read-only, to mirror a landed PR body); 0 writes",
      "api_writes": "2 — fleet relay: pr_create (objectstack-ai/objectstack#33123, run 43454921502, body read back identical, assignee os-tesla) and this os-dev-report comment on #32686 via post-stamped; git push is not a REST write",
      "open_questions": [],
      "security_findings": "withheld",
      "out_of_scope_findings": [
        "class: a · reach: PUT /api/v1/storage/upload-links/:token on a link that names no record, then no create_record: the committed sys_file row (ref_id null) is still there after its hour (integration case 'expires' reads it back unclaimed) and no lifecycle rule nominates it, because sys_file's lifecycle reaps only deleted_at (30d) and status pending (7d); the same holds for a Console upload (presign/PUT/complete) never saved. Bytes are never reclaimed; after the hour the binding makes the file usable by no write. Evidence: objects/system-file.object.ts:306-307, stranded-orphan-inventory.ts header ('Committed rows carry neither trigger … so they are immortal'), verify-file-references.ts unreferenced_file advisory reports but does not reclaim · dedupe words: committed unclaimed sys_file immortal, unreferenced_file advisory reclaim, abandoned field upload, pre_record_upload orphan, stranded orphan inventory"
      ]
    }
  17. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx

    Claim amendment to this seat's claim 6855856900 on #32686 (after 6856718179) · 2026-11-13T15:45Z: the file surface adds one sentence in content/docs/api/plugin-endpoints.mdx (the PUT /storage/upload-links/:token row), so it also describes a link that names no record. Docs page of this lane's package; everything else stands.

  18. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx

    Claim amendment to this seat's claim 6855856900 on #32686 (after 6857811650) · 2026-11-13T16:32Z: the content/docs/permissions/system-context.mdx entry covers row 47f as well as 47e. Row 47f's "a system write claims any file" became false for pre-record bound files under this card; the dev disclosed the edit, and the seat's contract review checked it. Everything else stands.

  19. removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    pm:blockingDerived cache from the Blocked-by reverse index: open card with open dependents — never hand-set
    on Nov 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:filesFiles — upload, download, signed URLs, access derived from the parent recorddomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions