Skip to content

feat(mcp): submit daily entries and record feed, water, purchases and expenses - #1325

Merged
mforce merged 14 commits into
mainfrom
feat/mcp-write-tools
Oct 18, 2026
Merged

mforce merged 14 commits into
mainfrom
feat/mcp-write-tools

Conversation

@mforce

@mforce mforce commented Oct 18, 2026

Copy link
Copy Markdown
Owner

A connected AI app can now submit a daily entry, and record feed use, water use, inventory purchases and expenses. Each action sits behind its own permission on the consent screen. Each one calls the REST route's own validator and handler, under that route's role policy.

Closes #1311
Closes #1313

Permissions

Consent permission Scope Tools Roles
Read farm data farm:read the read tools (unchanged) every role
Record daily entries daily-entries:write cluckwork_record_daily_entry (unchanged) Owner, Manager, Worker
Submit daily entries daily-entries:submit cluckwork_submit_daily_entry Owner, Manager, Worker
Record farm operations operations:write cluckwork_record_feed_usage, cluckwork_record_water_usage, cluckwork_record_inventory_purchase Owner, Manager, Worker
Record expenses expenses:write cluckwork_record_expense Owner, Manager

The scope names follow the existing <resource>:<verb> style. Submit shares the daily-entries resource with record because both act on the same record. A read tool requires exactly farm:read, and a write tool requires exactly one write scope. tools/list shows a tool only when the person's role and the token's scopes both allow it. The protected-resource metadata advertises all five scopes. The reasoning is in docs/decisions/1311-mcp-write-tools.md.

What changes for a farm

  • Records take effect at once. Feed, water, purchases and expenses have no draft, as in the app. Every reply carries correctInApp, which names the screen and control where an Admin or Manager corrects the record. One example: "Feed records are never edited. An Admin or Manager corrects the stock with Correct stock on Inventory." Consent says these records count at once.
  • Daily entries still start as a Draft. Only a person or the submit tool submits them. The submit tool takes the entry's id or a flock and date. It refuses an entry that is already submitted, with DailyEntry.NotDraft.
  • No free text comes back. Notes, lot numbers and expense descriptions are accepted where REST accepts them, and are never returned.
  • Audit. Feed use, water use and purchases now write an audit event (FeedUsage.Create, WaterUsage.Create, InventoryItem.Purchase), so each record names the person and the connected app. The REST routes write it too, because they share the handler. The Audit page's filters and labels gain the three actions in en, es and tl.
  • Existing connections gain nothing silently. An app reconnects, and consent marks each new permission New. Claude Code asks for every advertised scope on reconnect, so it needs no setup change. I measured this with a throwaway config.

Reviewer guide

Start with these files:

  • src/Cluckwork.Api/Mcp/McpWrite.cs: the MCP slice 6: write tool — record daily entry #809 claim, execute and publish protocol, moved out of the record tool. All six write tools use it. The record tool keeps its operation key, fingerprint and replay check.
  • src/Cluckwork.Api/Mcp/DailyEntryTools.cs (submit), InventoryTools.cs (feed, water, purchase) and ExpenseTools.cs.
  • McpNames.cs: items and expense categories match an active record exactly, ignoring case, or any record by id. A name shared by two farms is refused with both ids.
  • web/src/auth/oauthScopes.ts: one table that drives both the consent screen and Connected apps.

The currency race (fixed in review). The expense and purchase tools convert a decimal using a currency read taken before the handler locks the account. If a currency change commits in between, the handler would stamp the new currency on the old figure. Each tool now compares the stored currency and precision and refuses with Money.CurrencyChanged. That failure rolls the whole record back through IdempotentUnit. The expense reply reports the stored amount.

Mechanical or guard files:

  • tests/.../Architecture/Data/coupling-matrix.md is generated.
  • RealModuleLedger.Adapters.cs gains two adapter rows.
  • McpToolSurface.cs, McpToolSurfaceTests.cs and McpRouteParityTests.cs hold the scope rule and the reviewed reply-text list.
  • tools/oauth/mutation-check.sh gains 16 rows. It also repairs 2 anchors: one my change moved, and rpc-429-as-problem, which was already stale on main.

Tests

  • New McpWriteToolsTests (22 tests). They cover:
    • which write tools each scope and role lists;
    • submit by id and by flock and date;
    • a same-key retry that makes one set of lots;
    • each tool's audit row naming the app;
    • free text that is never echoed back;
    • a restricted Worker refused on another flock;
    • shared and inactive item names;
    • decimal precision;
    • two tools sharing one caller key (MCP slice 6: write tool — record daily entry #809 guard 20);
    • a controlled currency race for the expense and purchase tools.
  • Local runs:
    • the MCP slice and the inventory, purchase, currency, simulation and expense slices together: 385 of 385;
    • OAuth, audit, history and related slices: 542 of 542, before the review fixes;
    • Application tests: 782 of 782;
    • web: 3887 of 3887.
  • Mutations: I ran the 16 new rows and 4 existing rows whose code or anchor changed. All 20 were killed. The suite was green before and after each run, at 549 of 549 and then 551 of 551.
  • The precache is 1529.79 KiB, against 1526.42 KiB on main and a ceiling of 1600 KiB.

Live run

I used the TypeScript MCP SDK client against an isolated stack behind TLS, with real consent and step-up.

  1. Upgrade. An app connected with only Record daily entries, and its tools/list showed one tool. It then reconnected with the same client id. Consent marked Submit daily entries, Record farm operations and Record expenses New, and the five new tools appeared.
  2. All five permissions. I recorded an entry and submitted it, which made 3 egg lots (94 Large, 3 Dirty, 2 Cracked). Then I recorded feed, water, a purchase and an expense. Each one appears on its screen in the app, and in the Audit log with "via Cluckwork live verifier".
  3. Retry. Submitting again with the same key returned replayed: true with the same lot ids, and REST still shows one set of lots. A new key was refused with DailyEntry.NotDraft.
  4. Roles. The restricted Worker's consent marks Record expenses "Not allowed for your role". Its expense call is refused. Feed and submit on an unassigned flock are refused as not found. Water on its assigned flock is recorded.

Screenshots follow in a comment.

mforce added 12 commits October 18, 2026 00:54
Adds five MCP write tools behind three new OAuth scopes:
daily-entries:submit, operations:write and expenses:write. Each tool
calls its REST route's validator and handler under that route's role
policy, and runs through McpWrite, the #809 idempotency protocol lifted
out of the record tool so all six writes share it.

Feed use, water use and purchases now write an audit event in the
handler's transaction, so each record names the person and the app.

Refs #1311, #1313
The consent screen and Connected apps read one scope table. Help, the
glossaries, the decision record and the runbook describe the three new
permissions, the records that count at once, and where each is
corrected.

Refs #1311, #1313
MCP_CLIENT_FILE keeps one registration across connects, so a reconnect shows which permissions consent marks New.
Measured: Claude Code asks for every scope the resource metadata lists. Also moves the at-once flag into the consent scope table, and adds the write-tool steps to the live MCP check.
The tools convert a decimal with a currency read before the handler takes its account lock. A currency change committed in between made the handler stamp the new currency on the old figure. Each tool now compares the stored currency and fails, which rolls the record back; the expense reply reports the stored amount and currency.
The purchase tool read every lot of the item to find the one it had just inserted. RecordPurchaseHandler now returns the stored lot; the REST route still answers with its LotId.
…chases

A flock-scoped person's consent said the app records only for their flocks, but inventory purchases are farm-wide. Details now names what the limit covers and says purchases add to the whole farm's inventory, in en, es and tl.
@mforce

mforce commented Oct 18, 2026

Copy link
Copy Markdown
Owner Author

Screenshots, at 1:1 from a stack rebuilt at ae160b3. The "before" frames come from main at 39228ed.

Consent, English. Before, an app asking for every scope saw two permissions. After, it sees five.

Consent before, en
Consent after, en

Reconnect. An app first allowed only Record daily entries asks again. The three new permissions are marked New.

Reconnect consent with three New permissions, en

Restricted Worker, with Details open. Record expenses is marked Not allowed for your role. The flock limit names daily entries, feed and water, and purchases are described as farm-wide.

Worker consent with Details, en

Consent, Spanish.

Consent before, es
Consent after, es
Worker consent with Details, es

Help, connected-app items, English and Spanish.

Help before, en
Help after, en
Help before, es
Help after, es

The spec's Connecting an app entry gained the reconnect rule; the in-app definition now says it too, in en, es and tl, with each locale's consent labels.
@mforce
mforce merged commit f0df35f into main Oct 18, 2026
21 checks passed
@mforce
mforce deleted the feat/mcp-write-tools branch October 18, 2026 03:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant