There's a temptation that "one tool per case" is the right thing: it stays explicit, granular, "clean". But the model doesn't live in your architecture world. it lives in deciding what to invoke when the user says something. And that decision gets worse when there are too many similar options.
Imagine you're the model. The user says: "put Em as medic on the night shift on the 14th".
With 30 roster tools that include assign_crewmate, update_role_in_shift, add_crewmate_to_existing_shift, set_shift_crewmate_role, you have to read 30 descriptions and compare. Near-50/50 decisions are the worst: the model picks one with low confidence, and if it picks the "wrong" one (all do almost the same, but with different DB writes), it breaks.
With 3 tools (create_shift, update_shift_assignment, set_shift_status), you pick fast: you need update_shift_assignment with action: "assign" and role: "medic". Clear decision.
Patterns that work:
assign_x, remove_x, change_x, use update_x with action: "assign"|"remove"|"change".create_day_shift, create_night_shift, use create_shift with shift_type enum.list_active_shifts, list_cancelled_shifts, use list_shifts(status?) with default "active".Patterns that DON'T work:
command string: do_action(command: string, payload: any). the model doesn't know what to put in payload and gives up. You need typed parameters.create_shift and cancel_shift have 0 common parameters, they're different tools. Don't force them into one.Every new tool costs tokens in the system prompt and precision in the choice. Before adding one, ask: does this deserve its own name and schema, or is it an enum value of another tool?