Repository navigation
feat(mcp): record a draft daily entry through an MCP write tool - #1210
Merged
Merged
Conversation
…entry The tool takes a flock and grades by name or id, derives the farm and house from the flock, validates and records through the Egg Operations contract, and runs under IdempotentUnit with a required idempotencyKey keyed on the tool and the user. It needs the ProductionWrite role and the daily-entries:write scope. Closes #809
Guard 23 is the absence of a second DailyEntryUpdate row. Asserting the replayed flag first let a bypassed unit fail on the flag, so the audit assertion was never shown to catch it. The tool description also says that omitting grades keeps the draft's grade lines. Refs #809
A replay never ran the filtered flock lookup, so a Worker unassigned from a flock since the first call still got that entry's id, flock and date back. The tool now reads the stored flock through IFlockLookup.GetAsync first and answers with the same Flock.NotFound a fresh call gets. A token without the write scope is refused by [Authorize] before the unit runs. Refs #809
A default lets calls that omit the key share it. Refs #809
…ey retry The middleware now writes the response after the unit returns, so a write failure reaches the error handling instead of re-emitting the stored result. The decision record states it, and the test proves the retry replays without a second write. Refs #809
…ntry DailyEntryTools now holds both tools: #807's cluckwork_list_daily_entries and #809's cluckwork_record_daily_entry. The write tool resolves flocks through FlockTools.ResolveAsync, so a shared name, including one past the ten-candidate cap (MoreExist), is refused with the pointer to cluckwork_list_flocks and never picked. It mirrors RecordDailyEntry.
Owner
Author
Live MCP run against a running stackI ran the full procedure from #1214 against
Two behaviours match the design. Recording without These findings do not block this PR:
|
mforce
marked this pull request as ready for review
October 11, 2026 03:58
This was referenced Oct 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.





Closes #809
An MCP client can now record a flock's daily production as a draft. It sends
cluckwork_record_daily_entrywith a flock name and the day's numbers, and gets back the draft's id. A person still reviews and submits the draft in Cluckwork.What changed
The #307 protocol moved into
IdempotentUnit.IdempotencyMiddlewareused to hold the claim, execute and publish code itself. That code now lives insrc/Cluckwork.Api/Middleware/IdempotentUnit.cs, with its comments, and the middleware is its HTTP adapter. The unit takes an explicit outcome from the operation and publishes only a success. The middleware treats a 2xx as a success, as before. The #307 suites pass with no test edited.The tool. It lives in
src/Cluckwork.Api/Mcp/DailyEntryTools.cs, beside #807'scluckwork_list_daily_entries, and mirrors theRecordDailyEntryroute. It resolves the flock by name or id throughFlockTools.ResolveAsync, the read tools' resolver. A shared name is refused with the candidate ids, and past the ten-candidate cap the refusal says more flocks share the name and points tocluckwork_list_flocks. It never picks one. The tool takesFarmIdandHouseIdfromFlockDetailsand resolves grades by name or id against the farm's active grades. It then runsIValidator<RecordDailyEntryCommand>andIEggOperationsModule.RecordDailyEntryAsync. It needsProductionWriteanddaily-entries:write, and it works on a connection withoutfarm:read.Idempotency.
idempotencyKeyis required. The claim is keyed onmcp:tool:cluckwork_record_daily_entry, on the calling user and the key, and it fingerprints the arguments. A replay returns the first reply withreplayed: trueand writes nothing. A reused key with different values is refused. A domain refusal, such as a depleted flock, releases the claim, so the same call records once the farm fixes the flock.docs/decisions/809-mcp-write-idempotency.mdrecords why loopback was dropped and why the key is scoped to the user.Where to look
ae72d251is the extraction alone. CompareIdempotentUnit.RunAsyncwith the oldIdempotencyMiddleware.InvokeAsyncside by side. The steps and comments are the same, apart from three changes. Success is nowIdempotentResponse.Succeeded. The ambiguous-commit probe returnsExecuted, notReplayed. The middleware now writes the response after the unit returns, not inside the transaction callback. So if that write fails after a committed success, the client sees a failed request instead of the middleware trying a second write, and the same-key retry replays the stored result. The decision record states this, andIdempotencyResponseWriteFailureTestsproves the retry.32350983is the tool, its tests and the docs.06ae1b82,0a901369andf0bc165fanswer review round 1: a replay re-checks flock access, guard 21 refuses a key with a default, and the response-write test.1eff1309merges feat(mcp): read tools for flocks, stock, egg lots and daily entries #1209. The add/add conflict onDailyEntryTools.csbecame one class holding both tools, and the write tool now usesFlockTools.ResolveAsyncinstead of its own resolver.Tests
AtomicIdempotencyProtocolTests,IdempotencyBoundedWaitTests,IdempotencyReplayTests,IdempotencyUserScopeTests,MultiInstanceIdempotencyTests,StealLossConnectionReleaseTests,RetryBoundaryTests,KestrelResponseWriteTests,ExceptionHandlerReExecutionTests,ConfirmSaleAtomicityTests,MortalityLedgerAtomicityTests,IdempotencyRecordPurgeSweepTests,KestrelRequestBodyLimitTests,ChangeUserEmailRaceTests. 63 passed.Cluckwork.Api.IntegrationTests.Mcp(including MCP slice 4: read tools — flocks, stock, egg lots, daily entries #807's route-parity tests),IdempotencyResponseWriteFailureTests,FlockNameResolutionTestsand the Make API write idempotency atomic across replicas #307 family together: 214 passed. Architecture, tenant-bypass and documentation guards: 470 passed. New tests are inRecordDailyEntryToolTestsandRecordDailyEntryToolFaultTests. They cover role and scope refusals, a write-only connection, a Worker refused for an unassigned flock (with a positive control on their own flock), another farm's flock id, the farm switch, the audit row's connected app, replay without a secondDailyEntryUpdaterow (with a control that a new key does write one), a replay refused once the Worker is unassigned from the flock (the same refusal a fresh key gets), a replay refused for a token without the write scope, a reused key, a released refusal, a shared flock name, eleven flocks sharing a name (refused, no count stated, pointer tocluckwork_list_flocks), validator and grade refusals, REST and MCP keys not aliasing, a stolen lease rolled back, and a lost commit acknowledgment.IdempotencyMiddleware.csleft the file-size legacy list.DailyEntryToolshas an adapter row for Egg Operations and Flock Management. The coupling matrix was regenerated.Mutation checks
Each mutation was applied to the source, the named test was run, and the file was restored. All sixteen went red.
DomainFailure_IsReleased_SoTheSameKeyRecordsOnceFixedSameKey_DifferentArguments_IsRefused_AndChangesNothingSameKeyAndArguments_Replays_WithoutASecondAuditRow, on the secondDailyEntry.UpdaterowLostCommitAcknowledgment_ReturnsThePublishedEntry_WrittenOnceStolenLease_RollsTheWriteBack_AndTheKeyRecoversSameKey_OnTheRestRouteAndTheTool_DoNotAliasidempotencyKeyMcpToolSurfaceTests.RealTree_EveryWriteToolTakesAnIdempotencyKeySharedFlockName_IsAmbiguous_AndAnIdRecordsMoreExistpointerElevenFlocksSharingAName_AreRefused_WithThePointerToTheFlockListElevenFlocksSharingAName_AreRefused_WithThePointerToTheFlockListRoleOrScopeWithoutTheWrite_HidesAndRefusesTheTool(Manager withfarm:readonly)AuditWriterdrops the connected appRecordsADraft_ByFlockAndGradeNames_AndTheAuditRowNamesTheConnectedAppReplay_AfterTheFlockIsUnassigned_IsRefusedLikeAFreshCallWriteToolWithoutARequiredKey_IsAFinding_AndAReadOrAKeyedWriteIsNotIdempotency-KeyIdempotencyResponseWriteFailureTests, on a secondDailyEntry.UpdaterowReplay_WithATokenLackingTheWriteScope_IsRefusedBeforeTheUnitNot in this PR
FlockScopeGuardis behind it, and no MCP test reaches it alone.FlockDetails.CanRecordProductionOnitself. The handler already runs that check, and a second copy in the tool is what guard 25 forbids.DomainFailure_IsReleased_SoTheSameKeyRecordsOnceFixedproves the handler's check runs on the MCP path.