Skip to content

refactor(mcp): tighten tool param types + fix latent type errors pyright caught#67

Merged
dougborg merged 2 commits intomainfrom
refactor/mcp-tool-param-aliases
May 6, 2026
Merged

refactor(mcp): tighten tool param types + fix latent type errors pyright caught#67
dougborg merged 2 commits intomainfrom
refactor/mcp-tool-param-aliases

Conversation

@dougborg
Copy link
Copy Markdown
Owner

@dougborg dougborg commented May 6, 2026

Summary

Two-commit PR cleaning up MCP tool parameter typing in statuspro_mcp_server/. Surfaced during a /techdebt-scan followed by code-modernizer invocation, where pyright (LSP) caught real type errors that ty (validation) silently misses today (see #56).

Commit 1 — fix(mcp): correct latent type errors pyright surfaced

Three small, behavior-preserving type bugs:

  • tools/schemas.pyBatchOrderResult.order_id and .error switched from positional Field(None, …) (pyright reads as required) to Field(default=None, …). Fixes "Argument missing for parameter order_id" at three constructor sites in orders.py.
  • tools/orders.pyUpdateOrderStatusRequest(comment=…) and BulkStatusUpdateRequest(comment=…) were passing str | None from the tool param into attrs fields typed str | Unset. Wrapped with to_unset(comment) per CLAUDE.md's None → UNSET convention.
  • tools/orders.py_bounded_gather[T] and inner bounded[T] declared a TypeVar that appeared only in the return type. Typed inputs as list[Awaitable[T]] / Awaitable[T] so T binds at call sites.

Commit 2 — refactor(mcp): extract OrderIdParam and ConfirmFlag tool param aliases

Tool param annotations had drifted: bare int / bool types on some mutation tools, Annotated[int, Field(description=…)] wrappers on others, and one outlier confirm description. New tools/param_types.py mirrors the tools/list_coercion.py convention of collapsing repeated Annotated[…, Field(…)] boilerplate into a single alias.

  • OrderIdParam — used by 6 tools (get_order, get_order_history, update_order_status, add_order_comment, update_order_due_date, get_viable_statuses)
  • ConfirmFlag — used by 4 mutation tools

Why

Both classes of bug were latent — they'd been incorrect for as long as the code's been there, and ty check --exclude statuspro_mcp_server/ never reported them. Concrete data point added to #56 (comment).

Test plan

  • uv run poe check — ruff format/check, prettier, ty, yamllint, pytest 295 passed
  • No behavior change — pure type-correctness + alias extraction
  • to_unset(None) returns UNSET, to_unset("text") returns "text" (per domain/converters.py)
  • FastMCP processes Annotated[…, Field(…)] at registration time only (cached); no per-call overhead from the alias indirection

🤖 Generated with Claude Code

dougborg and others added 2 commits May 6, 2026 17:05
Three small but real type bugs in statuspro_mcp_server, surfaced when
the LSP (pyright) re-analyzed orders.py after the param-annotation pass
and confirmed pre-existing in main. ty (the project's validation
checker) misses them today because statuspro_mcp_server/ is in the
exclude list — see #56.

- schemas.py: `BatchOrderResult.order_id` and `.error` used the
  positional `Field(None, …)` form, which pyright reads as
  default=missing — flagging every constructor that omitted the field.
  Switched to `Field(default=None, …)` so the default is unambiguous.

- orders.py: `UpdateOrderStatusRequest(comment=comment, …)` and
  `BulkStatusUpdateRequest(comment=comment, …)` were passing the tool's
  `str | None` parameter into attrs fields typed `str | Unset`. Wrapped
  with `to_unset(comment)` per CLAUDE.md's documented None → UNSET
  convention.

- orders.py: `_bounded_gather[T]` and the inner `bounded[T]` declared a
  TypeVar that appeared only in the return type, so pyright couldn't
  infer T at any call site (warning: "TypeVar T appears only once").
  Typed the input as `list[Awaitable[T]]` / `Awaitable[T]` so T binds
  from the caller's coroutines and the return shape carries the
  element type through.

Behaviorally identical; type-correctness only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tool param annotations had drifted: bare `int` / `bool` types on some
mutation tools, `Annotated[int, Field(description=…)]` wrappers on
others, and one outlier description ("Must be true to apply the
change" vs. "Set to true to apply the change") on `confirm`. The MCP
schema FastMCP advertises to clients was inconsistent as a result.

- New `tools/param_types.py` module — mirrors the
  `tools/list_coercion.py` convention of collapsing repeated
  `Annotated[..., Field(...)]` boilerplate into a single readable
  alias at the call site:

  - `OrderIdParam = Annotated[int, Field(description="StatusPro order id")]`
  - `ConfirmFlag = Annotated[bool, Field(description="Set to true to apply the change")]`

- `orders.py` — `order_id` on every mutation tool (5 sites) now uses
  `OrderIdParam`; `confirm` on every mutation tool (4 sites) now uses
  `ConfirmFlag`. Normalizes the previously-divergent `update_order_status`
  description.

- `statuses.py` — `get_viable_statuses` `order_id` adopts `OrderIdParam`;
  drops the now-unused `Annotated` / `Field` imports.

Behavior unchanged. Tool JSON schema descriptions are now uniform
across all mutation tools, and adding a new mutation tool is one
import + one annotation rather than 14 characters of `Annotated[…]`
boilerplate per parameter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 6, 2026 23:06
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR tightens and standardizes typing for StatusPro MCP tool parameters and fixes a few latent typing mismatches that pyright surfaced in statuspro_mcp_server/, without intended behavior changes.

Changes:

  • Fixes Pydantic Field(...) defaults in shared tool schemas so optional fields are treated as optional by type checkers.
  • Aligns optional comment handling with attrs request models by converting None to UNSET via to_unset(...).
  • Extracts reusable Annotated[..., Field(...)] parameter aliases (OrderIdParam, ConfirmFlag) and applies them across tools for consistent JSON schema metadata.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
statuspro_mcp_server/src/statuspro_mcp/tools/statuses.py Uses OrderIdParam for consistent tool param typing/schema metadata.
statuspro_mcp_server/src/statuspro_mcp/tools/schemas.py Makes optional schema fields explicitly default=None to satisfy static typing expectations.
statuspro_mcp_server/src/statuspro_mcp/tools/param_types.py Introduces shared Annotated aliases to avoid drift across tool definitions.
statuspro_mcp_server/src/statuspro_mcp/tools/orders.py Binds generic type params properly for async helpers and normalizes order_id/confirm param annotations; converts comment via to_unset.

@dougborg dougborg merged commit f280520 into main May 6, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants