Repository navigation
fix(mcp): match flock names ignoring case, plain optional schema types, quiet idempotent replays - #1237
Conversation
…s, quiet idempotent replays - ListByNameAsync compares lower(Name), so flock:"barn a" resolves "Barn A" and names differing only in case are ambiguous (#1228). - cluckwork_list_sales_orders no longer echoes an unknown status, like the flock list and its REST route. - Tool schemas advertise optional parameters as their plain type, without a null union or a null default; GradeLine.grade is a plain string (#1230). - The idempotency claim is INSERT ... ON CONFLICT DO NOTHING, so a replay no longer raises a unique violation that EF logged twice at Error; the unit logs the existing claim once at Debug (#1230).
Live MCP client runI ran this at head Connect. I registered a client dynamically, signed in, and allowed consent with the password step-up. Allow with no password stayed on the consent screen. The token came back with #1228: names ignore case. #1230: plain schema types. #1230: replays log no errors. I sent the same The new Debug line is below this stack's Production log level. That SDK logs every Afterwards I took the stack down with |
SaveChanges ran under EnableRetryOnFailure, so a lost acknowledgment replayed the claim and inspection found our own row. Raw SQL gets no retry of its own, so RetryBoundaryTests saw the transient fault as a 500.
Lower-casing the input in .NET and the stored name in PostgreSQL disagreed on names such as "İpek": under the invariant culture the exact name stopped resolving. The lookup is now an exact ILIKE through LiteralSearch, so the database folds both sides, and '%', '_' and '\' in a name match only themselves. Review R1 on #1237.
…test The hand-written registration and its coverage walk both skipped static methods, which WithToolsFromAssembly registers. A static tool now gets the SDK's static McpServerTool.Create overload. Review R2 on #1237.
…es-polish # Conflicts: # src/Cluckwork.Api/Hosting/CluckworkMcp.cs
|
Fixes for the Codex review of R1 (P2), flock names folded by .NET and PostgreSQL differently. Fixed in
R2 (P3), static tool methods skipped. Fixed in
Merge. Live check. I rebuilt the isolated stack at Locally on the merged tree, the MCP, flock-name, idempotency and retry-boundary integration tests pass (265), and so does the full Application project (771). |

MCP clients can now name a flock in any case. Tool schemas no longer offer
nullfor optional parameters. A replayed write no longer logs two database errors.Closes #1228
Closes #1230
What changed
Flock names ignore case (#1228).
FlockRepository.ListByNameAsyncis an exactILIKEthroughLiteralSearch.ExactPattern, the escaping the flock and customer searches already use, with no wildcards added. PostgreSQL folds the case of both sides, so a name such asİpekstill resolves when typed exactly, and%,_and\in a name match only themselves.flock:"barn a"now resolves "Barn A". Two flocks that differ only in case get the existing ambiguity refusal with both ids. Theflockparameter description no longer says "exact".Unknown
statusis not echoed (#1228).cluckwork_list_sales_ordersanswers an unknownstatuswith the accepted list only, likecluckwork_list_flocksand the REST flock list. The REST sales-order list still echoes it (SaleEndpoints.cs:255). That route is outside this PR.Plain optional types in tool schemas (#1230 item 1).
WithToolsFromAssemblytakes no schema options, soAddCluckworkMcpnow registers the tools itself with the same reflection walk as the SDK: instance methods get anActivatorUtilitiestarget, and static methods use the SDK's staticMcpServerTool.Createoverload. It passesAIJsonSchemaCreateOptions.TransformSchemaNode, which removes"null"from everytypeunion and drops a"default": null.gradesis nowarray, its itemsobject, andgradestring. Binding still uses each parameter's C# default, so an omitted parameter behaves as before.A replay logs no errors (#1230 item 2). The claim insert was an EF
SaveChangesthat raised23505on every replay. EF logged it twice at Error, once asDatabase.Command.CommandErrorand once asUpdate.SaveChangesFailed, beforeIdempotentUnitcaught it. EF'sCommandErrorevent carries no exception, so a log filter could match it only by SQL text. Instead, the claim is nowINSERT ... ON CONFLICT (AccountId, EndpointHash, IdempotencyKeyHash) DO NOTHING. A replay or a lost race returns 0 rows, the unit logs one Debug line, and the existing steal-or-inspect path runs unchanged. Nothing suppresses23505. REST and MCP replays both use this path.The insert runs under
CreateExecutionStrategy().SaveChangeswas retried byEnableRetryOnFailure, but raw SQL gets no retry of its own. Without the wrapper, a lost acknowledgment on the claim insert became a 500.RetryBoundaryTests.LostAcknowledgmentOnTheClaimInsert_IsRecognisedAsOurOwnClaim_AndTheHandlerStillRunscaught this in CI on the first push. With the wrapper, the replay finds our own row as before.The insert writes
"AccountId" = {scope.AccountId}. Both callers build the scope from the resolved tenant, like the steal, publish and release statements already in this class.BypassAllowListgets the new hash and a justification that names the insert. TheFilterFreeSetSitesrow for the removeddb.IdempotencyRecords.Addis now stale, so this PR deletes it.Where to start
src/Cluckwork.Api/Middleware/IdempotentUnit.cs: the claim insert. This is the only change to shared write behaviour.src/Cluckwork.Api/Hosting/CluckworkMcp.cs: tool registration and the schema transform.FlockRepository.cs,LiteralSearch.csandCommerceTools.cs: a few lines each.Tests
Each test below fails when its fix is reverted:
FlockNameResolutionTests.Name_MatchesIgnoringCase,NamesDifferingOnlyInCase_AreAmbiguousf.Name == nameFlockNameResolutionTests.UnicodeName_TypedExactly_ResolvesUnderAnInvariantCultureCallerFlockNameResolutionTests.WildcardCharacters_MatchOnlyThemselvesILIKEunescapedMcpCommerceToolsTests.UnknownStatus_IsRefusedWithoutEchoingIt{status}back in the messageMcpEndpointTests.ToolSchemas_AreServedForEveryTool_WithoutNullTransformSchemaNode; separately, keepdefault: null; separately, leave a valid static tool out of registrationIdempotencyReplayTests.Replay_LogsTheClaimConflictOnce_AtDebugThe schema test lists tools through a real client and also checks that every
[McpServerTool]method is still served, so the hand-written registration cannot drop a tool. The existingRecordDailyEntryToolTests.OmittedGrades_KeepTheDraftsLines_AndAnEmptyListRemovesThempasses unchanged. Locally, the integration classes matchingIdempotency,Mcp,FlockNameResolutionandDailyEntrpass (287 tests). The full Application and Domain suites also pass.Live MCP client run
The full live run was at
25289f9e, through real OAuth consent with the password step-up; the details are in this comment. A case-only duplicate got the ambiguity refusal. Inspector--strictreported no schema findings. Two replays, one over MCP and one over REST, logged no23505and no Error line of their own.After the review fixes, I rebuilt the stack at
d9ff13e9and checked the changed lookup again:flock:"sim house a"still resolves "Sim House A" (reply).