diff --git a/AGENTS.md b/AGENTS.md index 05fecc4..80a702e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -183,6 +183,17 @@ MCP tools must not be the only implementation of TimePro behavior. Any business The MCP host resolves the tenant in this order: `--tenant NAME` → global active tenant → the sole tenant config if exactly one exists (single-tenant installs work without `tp tenant set`). `--tenant` does NOT change the global active tenant. +The server runs on `ModelContextProtocol` 2.2.0 (MCP 2026-07-28). No API our surface uses was +renamed by the 2.x upgrade: `[McpServerToolType]`, `[McpServerTool]`, `AddMcpServer()`, +`WithStdioServerTransport()`, `WithTools()` and `WithRequestFilters(... AddCallToolFilter ...)` +are all unchanged. 2.x dropped the experimental Tasks implementation from Core, so tools no longer +advertise `execution.taskSupport` in `tools/list`; the extension package is not referenced. +Legacy clients still negotiate `2024-11-05` through `2025-11-25` via `initialize`, 2026-07-28 +clients use `server/discover` plus a per-request `_meta` envelope, and the 2026-07-28-only result +fields (`resultType`, `ttlMs`, `cacheScope`, `_meta.serverInfo`) are gated on the negotiated +version, so down-level payloads are byte-identical to 1.4.0. Structured output, tool annotations +and elicitation are deliberate follow-ups, each in its own PR. + ## Tenants The `activeTenant` in `config.json` is the **filename** (without `.json`) of the tenant config file, not the `tenantId` property inside it. This allows multiple configs for the same tenant (e.g., `ssw` for prod, `ssw-staging` for staging) where both have `"tenantId": "ssw"` but different `apiUrl` and `apiKey`. diff --git a/Directory.Packages.props b/Directory.Packages.props index d6477ec..f4e2af9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,11 +6,11 @@ - - - + + + - + diff --git a/docs/mcp-sdk-2-testing.md b/docs/mcp-sdk-2-testing.md new file mode 100644 index 0000000..6524d7f --- /dev/null +++ b/docs/mcp-sdk-2-testing.md @@ -0,0 +1,140 @@ +# Manual MCP client testing for the SDK 2.x upgrade + +The automated harness (`tests/SSW.TimePro.Cli.Integration/Mcp`) proves the wire shapes and the +staging smoke proves one real read/write round trip. Neither can see what a real client does with +that wire: approval prompts, annotation rendering, how an error reads in the transcript, or whether +a client's own discovery path works. This doc is the manual pass that closes that gap. + +## Build the candidate server + +```bash +cd # e.g. .timepro-worktrees/mcp-v2, branch feat/mcp-sdk-2 +dotnet build -c Release +``` + +The server entry point is then: + +``` +dotnet /src/SSW.TimePro.Cli/bin/Release/net10.0/SSW.TimePro.Cli.dll mcp --tenant +``` + +Use `--tenant ssw-staging` for anything that writes. `dotnet run --project /src/SSW.TimePro.Cli -- mcp --tenant ssw-staging` +also works but adds build output on first start, which some clients treat as a startup failure — +prefer the built DLL. + +Do not install the branch as a global tool; that would replace the `tp` the rest of the machine uses. + +### Client registration + +Claude Code: + +```bash +claude mcp add timepro-sdk2 -- dotnet /src/SSW.TimePro.Cli/bin/Release/net10.0/SSW.TimePro.Cli.dll mcp --tenant ssw-staging +``` + +Codex CLI (`~/.codex/config.toml`): + +```toml +[mcp_servers.timepro-sdk2] +command = "dotnet" +args = ["/src/SSW.TimePro.Cli/bin/Release/net10.0/SSW.TimePro.Cli.dll", "mcp", "--tenant", "ssw-staging"] +``` + +Claude Desktop (`~/Library/Application Support/Claude/claude_desktop_config.json`) and +VS Code / Copilot (`.vscode/mcp.json` or the user `mcp.json`) take the same +`command` / `args` pair. + +Register it under a distinct name (`timepro-sdk2`) so the existing `timepro` entry stays untouched +and can be compared side by side. + +Identify the server by that registered name, not by `serverInfo.version`: every local build reports +`0.3.0.0` (`.0`), so the SDK 2 worktree and a locally built `main` look the same there. + +## Per-client checklist + +Run all of these against `ssw-staging`. Northwind (`NWIND`) is the only client to touch. Pick one +of its projects that uses iterations and one that does not; project ids are not committed to this +public repository, so get them from `tp project list --client NWIND --tenant ssw-staging --json`. + +1. **Discovery** — the client connects without error and lists the server as healthy. Note in the + table whether it used the 2026-07-28 `server/discover` path or the legacy `initialize` + handshake (the CLI logs neither; infer it from the client's own logs, e.g. `claude --debug`, + Codex `RUST_LOG=debug`, the Claude Desktop MCP log, or the VS Code MCP output channel). +2. **tools/list count** — 18 tools with the accounting feature off, 47 with + `tp feature accounting enable`. Anything else is a regression, not a client quirk. +3. **One read** — ask for this week's timesheets (`get_timesheets`) and a Northwind lookup + (`get_projects_for_client` with `NWIND`). Do **not** compare `get_timesheets` against + `tp ts get --json`: the two have always differed, and deliberately so — MCP returns a flat array + and skips weekends, the CLI returns an envelope grouped by day including weekends (recorded in + the parity table as "MCP reshapes the row; CLI returns the API shape"). Compare instead against + the shape in `tests/SSW.TimePro.Cli.Integration/Goldens/Mcp/Tools/GetTimesheets.populated.json`, + or against the same tool call on the installed 0.3.x release with identical arguments. +4. **One write** — create one timesheet on the iteration-using Northwind project with the note + `MCP SDK2 client check, safe to delete`, confirm it reads back, then delete it. Do not create + leave requests and do not import leave balances. +5. **Error rendering** — call a tool with a bad argument (e.g. `get_projects_for_client` with a + nonexistent client id) and check the failure reads as a message rather than a raw stack trace or + a silent empty result. Also try an unknown tool name if the client allows it: the server answers + JSON-RPC `-32602 Unknown tool: ''`. +6. **Cancellation** — start a read and interrupt it (Esc in Claude Code, Ctrl+C in Codex CLI). + The server must stay usable for the next call, not wedge or die. +7. **Shutdown** — quit the client and confirm no `dotnet ... mcp` process is left behind + (`pgrep -fl "SSW.TimePro.Cli.dll mcp"`). + +## What the harness cannot see — look for these explicitly + +- **Approval prompts.** Two tools are annotated today: `get_leave_balance_status` + (`readOnlyHint: true`, `destructiveHint: false`) and, on the accounting surface, + `import_leave_balances` (`destructiveHint: true`, `idempotentHint: true`, `readOnlyHint: false`). + Everything else is unannotated, so clients fall back to their default of treating each call as + potentially destructive. Confirm each client prompts before `create_timesheet` / + `update_timesheet` / `delete_timesheet` / `update_leave`, and note whether reads prompt too + (they will until the annotation follow-up widens the coverage). +- **Annotation rendering.** For those same two tools, check the badge the client shows matches the + advertised annotation: `get_leave_balance_status` read-only, `import_leave_balances` destructive. + For every other tool there is nothing to render, so a badge means the client is inferring from + the name — worth knowing before the annotation PR. +- **Tool description truncation.** Several descriptions are long; check how each client's tool + picker displays them. +- **Stdout hygiene in practice.** The harness fails loudly on non-protocol stdout. In a real client + it looks like a silent connection drop instead, so watch for a server that connects and then + disappears. +- **Latency and timeouts.** Staging is slower than the WireMock fixtures; a client with a short + tool timeout may cut off a slow week query. + +## Results + +Record the client version exactly as the client reports it (`claude --version`, +`codex --version`, Claude Desktop → About, VS Code → About plus the Copilot Chat extension +version). + +| Client | Version | Negotiated protocol | tools/list | Read | Write | Errors | Cancel | Shutdown | Notes | +|---|---|---|---|---|---|---|---|---|---| +| Claude Code | | | | | | | | | | +| Codex CLI | | | | | | | | | | +| Claude Desktop | | | | | | | | | | +| VS Code / Copilot | | | | | | | | | | + +## Automated evidence already on the branch + +Re-run these before starting the manual pass so a failure there is not mistaken for a client bug: + +```bash +dotnet test tests/SSW.TimePro.Cli.Tests/ +dotnet test tests/SSW.TimePro.Cli.Integration/ +TIMEPRO_MCP_SMOKE_PROJECT= \ + TIMEPRO_MCP_SMOKE_TP="dotnet $PWD/src/SSW.TimePro.Cli/bin/Release/net10.0/SSW.TimePro.Cli.dll" \ + scripts/e2e/test-mcp-smoke.sh +``` + +Protocol versions the server accepts under 2.2.0, verified over stdio: + +- `initialize` advertises `2024-11-05`, `2025-03-26`, `2025-06-18`, `2025-11-25`. Verified by + execution: `2025-11-25`, `2025-06-18` and `2024-11-05` each negotiate back the requested version + and then serve 18 tools. +- `initialize` with `2026-07-28` is refused with `-32022` and a `supported` list — correct, that + revision removed the handshake. +- `server/discover` answers `supportedVersions: ["2026-07-28"]`, after which `tools/list` and + `tools/call` work with the per-request `_meta` envelope (`io.modelcontextprotocol/protocolVersion`, + `clientInfo`, `clientCapabilities`; the server rejects the call when `clientCapabilities` is + missing). diff --git a/tests/SSW.TimePro.Cli.Integration/Goldens/Mcp/Discovery/tools-list.accounting.json b/tests/SSW.TimePro.Cli.Integration/Goldens/Mcp/Discovery/tools-list.accounting.json index 349556b..5e536a2 100644 --- a/tests/SSW.TimePro.Cli.Integration/Goldens/Mcp/Discovery/tools-list.accounting.json +++ b/tests/SSW.TimePro.Cli.Integration/Goldens/Mcp/Discovery/tools-list.accounting.json @@ -3,9 +3,6 @@ "tools": [ { "description": "Accept a suggested timesheet, converting it into a real timesheet. Returns the entry as saved.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "date": { @@ -54,9 +51,6 @@ }, { "description": "Validate a week of timesheets for gaps and issues (leave-aware). Returns per-day coverage with hours, leave, issues, plus allCovered and pendingSuggestions. week: 0=this week (default), -1=last week. The single most useful tool for confirming a week is complete before submitting.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "empId": { @@ -87,9 +81,6 @@ }, { "description": "Create a leave request for the current user. An explicit timezone override takes priority; otherwise uses the TimePro profile timezone first, then the MCP host machine timezone as the browser-equivalent fallback.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "approvedBy": { @@ -171,9 +162,6 @@ }, { "description": "Create a new timesheet entry. Some projects require an iteration ID \u2014 use ListIterations to check and find the correct ID.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "billableId": { @@ -247,9 +235,6 @@ }, { "description": "Delete a timesheet entry. Suggestions cannot be deleted; accept one first.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "date": { @@ -274,9 +259,6 @@ }, { "description": "Get the aged-debtor view for a client: outstanding invoices with days overdue.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "clientId": { @@ -293,9 +275,6 @@ }, { "description": "Get the current employee\u0027s billing rate for a client. Includes rate expiry info.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "clientId": { @@ -320,9 +299,6 @@ }, { "description": "List clients that have outstanding (unbilled) time, with OS balance and earliest unallocated timesheet date.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": {}, "type": "object" @@ -331,9 +307,6 @@ }, { "description": "Get CRM bookings/appointments for a date range.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "endDate": { @@ -355,9 +328,6 @@ }, { "description": "Get the current user (identity, tenant, display name) \u2014 handy for showing who an MCP session is authenticated as.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": {}, "type": "object" @@ -366,9 +336,6 @@ }, { "description": "Get a single invoice header. GST convention: SubTotal is ex-GST, SalesTaxAmt is GST, SellTotal is inc-GST; SalesTaxPct is the raw API tax rate and should be normalized before calculations.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "invoiceId": { @@ -385,9 +352,6 @@ }, { "description": "List line items (products) on an invoice. SellAmt and SellTotal are ex-GST; SalesTaxAmt is the GST component when returned.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "invoiceId": { @@ -404,9 +368,6 @@ }, { "description": "List receipts (payments) against a specific invoice.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "invoiceId": { @@ -423,9 +384,6 @@ }, { "description": "List timesheets billed on an invoice. SellTotal, BillableAmount, and Amount are treated as ex-GST; SalesTaxAmt/SalesTaxPct provide the tax component/rate when returned. Set type=\u0027writeoff\u0027 for written-off timesheets instead of allocated.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "invoiceId": { @@ -447,9 +405,6 @@ }, { "description": "List all invoices for a client (full history, unpaged). Invoice amount convention: SubTotal ex-GST, SalesTaxAmt GST, SellTotal inc-GST.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "clientId": { @@ -466,9 +421,6 @@ }, { "description": "Get leave stats for an employee: days since last leave and total leave hours taken in the last 12 months. Defaults to the current user\u0027s empId. (TimePro does not expose entitlement/remaining per leave type.)", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "empId": { @@ -498,9 +450,6 @@ "readOnlyHint": true }, "description": "Report when TimePro\u0027s leave balances were last imported from Xero, how many employees have a stored balance, and whether the data is stale. Read-only. Check this before importing so you can tell the user whether a re-import is actually needed.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": {}, "type": "object" @@ -509,9 +458,6 @@ }, { "description": "List EasyLeave entries. Use empId for one person; employeeId is accepted as an alias. Omit both to return all visible leave.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "empId": { @@ -564,9 +510,6 @@ }, { "description": "Get structured prepaid drawdown totals for an invoice: original, drawnDown, credited, and remaining amounts split into exGst/gst/incGst. remaining.exGst is sourced from the existing client invoice table endpoint\u0027s ledger-backed RemainingPrepaidCredit.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "invoiceId": { @@ -583,9 +526,6 @@ }, { "description": "Download the prepaid drawdown status report PDF for an invoice. Returns base64-encoded bytes inside a JSON envelope \u2014 decode on the client side to save as .pdf. Prefer GetPrepaidStatus for structured totals.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "invoiceId": { @@ -607,9 +547,6 @@ }, { "description": "Get a single product by ID.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "productId": { @@ -626,9 +563,6 @@ }, { "description": "Show product discounts configured for a client.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "clientId": { @@ -645,9 +579,6 @@ }, { "description": "Get projects for a client. Returns project IDs and names.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "clientId": { @@ -664,9 +595,6 @@ }, { "description": "Project-hours summary for one empId over a period (billable vs non-billable across projects). Defaults to the current user; employeeId is accepted as an alias.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "empId": { @@ -704,9 +632,6 @@ }, { "description": "Get a receipt with its invoice allocations (how the payment is split across invoices).", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "receiptId": { @@ -723,9 +648,6 @@ }, { "description": "Get a recurring invoice template (includes product lines).", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "recurringId": { @@ -742,9 +664,6 @@ }, { "description": "Get suggested timesheets for a date. Refreshes suggestions first.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "date": { @@ -761,9 +680,6 @@ }, { "description": "Get timesheets for a date or date range. Use empId to read another employee\u0027s timesheets; employeeId is accepted as an alias.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "date": { @@ -804,9 +720,6 @@ }, { "description": "List unbilled (unallocated) timesheets for a client - revenue still in the pipeline. SellTotal, BillableAmount, and Amount are treated as ex-GST.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "clientId": { @@ -855,9 +768,6 @@ }, { "description": "List unpaid invoices for a client. Invoice amount convention: SubTotal ex-GST, SalesTaxAmt GST, SellTotal inc-GST.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "clientId": { @@ -879,9 +789,6 @@ "readOnlyHint": false }, "description": "Import leave balances for EVERY employee from a Xero \u0027Leave Balances\u0027 CSV export, replacing what TimePro currently stores. No dry run and no undo, so confirm with the user first. Pass the path to the CSV file, not its contents. Always report the returned unmatchedEmployees (rows skipped) and warnings - the import succeeds despite them.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "csvPath": { @@ -898,9 +805,6 @@ }, { "description": "List all SKUs. Set isPrepaid=true to list only prepaid SKUs (useful for prepaid drawdown analysis).", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "isPrepaid": { @@ -915,9 +819,6 @@ }, { "description": "List timesheet billable-type codes (B = billable, BPP = prepaid, W = write-off, etc.).", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": {}, "type": "object" @@ -926,9 +827,6 @@ }, { "description": "List timesheet category codes (for labelling timesheets when creating or filtering queries).", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": {}, "type": "object" @@ -937,9 +835,6 @@ }, { "description": "List all configured client rates (all empIds by default). For just the current user\u0027s rate for a client, prefer GetClientRate. employeeId is accepted as an alias for empId.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "clientId": { @@ -1009,9 +904,6 @@ }, { "description": "List credit notes for a client.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "clientId": { @@ -1028,9 +920,6 @@ }, { "description": "List invoices (paged). Filter by query text, sort by DateCreated/DateInvoiced/SellTotal/ClientID. SellTotal is GST-inclusive. Set onlyRecurring=true for recurring-generated invoices.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "dir": { @@ -1073,9 +962,6 @@ }, { "description": "List iterations/sprints for a project. Returns empty list if the project doesn\u0027t use iterations. If the list is non-empty, an iteration ID is required when creating timesheets for this project.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "projectId": { @@ -1092,9 +978,6 @@ }, { "description": "List timesheet location codes (SSW, Home, Client, Travel, Other).", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": {}, "type": "object" @@ -1103,9 +986,6 @@ }, { "description": "List paid receipts (paged). Sort by PaymentDate descending by default. PaidTotal is NEGATIVE for incoming payments; take abs() for reported sales.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "dir": { @@ -1143,9 +1023,6 @@ }, { "description": "List products. With isExpand=true, each product includes its SKUs inline.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "isExpand": { @@ -1160,9 +1037,6 @@ }, { "description": "List recurring invoice templates.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "clientId": { @@ -1213,9 +1087,6 @@ }, { "description": "Query timesheets across empIds, clients, projects and a date range. Returns detailed rows including hours and sell price; sell prices/amounts are treated as ex-GST for invoice reconciliation. employeeIds is accepted as an alias for empIds.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "categoryIds": { @@ -1307,9 +1178,6 @@ }, { "description": "Search for clients by name. Returns client IDs and names.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "query": { @@ -1326,9 +1194,6 @@ }, { "description": "Update an existing leave request for the current user. Unspecified API-returned fields are preserved. List leave entries first to obtain the leave ID.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "approvedBy": { @@ -1440,9 +1305,6 @@ }, { "description": "Update an existing timesheet. Only specify fields you want to change; everything else is preserved. Returns the entry as saved.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "billableId": { diff --git a/tests/SSW.TimePro.Cli.Integration/Goldens/Mcp/Discovery/tools-list.default.json b/tests/SSW.TimePro.Cli.Integration/Goldens/Mcp/Discovery/tools-list.default.json index 4ea0e9d..d35415f 100644 --- a/tests/SSW.TimePro.Cli.Integration/Goldens/Mcp/Discovery/tools-list.default.json +++ b/tests/SSW.TimePro.Cli.Integration/Goldens/Mcp/Discovery/tools-list.default.json @@ -3,9 +3,6 @@ "tools": [ { "description": "Accept a suggested timesheet, converting it into a real timesheet. Returns the entry as saved.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "date": { @@ -54,9 +51,6 @@ }, { "description": "Validate a week of timesheets for gaps and issues (leave-aware). Returns per-day coverage with hours, leave, issues, plus allCovered and pendingSuggestions. week: 0=this week (default), -1=last week. The single most useful tool for confirming a week is complete before submitting.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "empId": { @@ -87,9 +81,6 @@ }, { "description": "Create a leave request for the current user. An explicit timezone override takes priority; otherwise uses the TimePro profile timezone first, then the MCP host machine timezone as the browser-equivalent fallback.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "approvedBy": { @@ -171,9 +162,6 @@ }, { "description": "Create a new timesheet entry. Some projects require an iteration ID \u2014 use ListIterations to check and find the correct ID.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "billableId": { @@ -247,9 +235,6 @@ }, { "description": "Delete a timesheet entry. Suggestions cannot be deleted; accept one first.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "date": { @@ -274,9 +259,6 @@ }, { "description": "Get the current employee\u0027s billing rate for a client. Includes rate expiry info.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "clientId": { @@ -301,9 +283,6 @@ }, { "description": "Get CRM bookings/appointments for a date range.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "endDate": { @@ -325,9 +304,6 @@ }, { "description": "Get leave stats for an employee: days since last leave and total leave hours taken in the last 12 months. Defaults to the current user\u0027s empId. (TimePro does not expose entitlement/remaining per leave type.)", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "empId": { @@ -357,9 +333,6 @@ "readOnlyHint": true }, "description": "Report when TimePro\u0027s leave balances were last imported from Xero, how many employees have a stored balance, and whether the data is stale. Read-only. Check this before importing so you can tell the user whether a re-import is actually needed.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": {}, "type": "object" @@ -368,9 +341,6 @@ }, { "description": "List EasyLeave entries. Use empId for one person; employeeId is accepted as an alias. Omit both to return all visible leave.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "empId": { @@ -423,9 +393,6 @@ }, { "description": "Get projects for a client. Returns project IDs and names.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "clientId": { @@ -442,9 +409,6 @@ }, { "description": "Get suggested timesheets for a date. Refreshes suggestions first.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "date": { @@ -461,9 +425,6 @@ }, { "description": "Get timesheets for a date or date range. Use empId to read another employee\u0027s timesheets; employeeId is accepted as an alias.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "date": { @@ -504,9 +465,6 @@ }, { "description": "List iterations/sprints for a project. Returns empty list if the project doesn\u0027t use iterations. If the list is non-empty, an iteration ID is required when creating timesheets for this project.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "projectId": { @@ -523,9 +481,6 @@ }, { "description": "Search for clients by name. Returns client IDs and names.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "query": { @@ -542,9 +497,6 @@ }, { "description": "Update an existing leave request for the current user. Unspecified API-returned fields are preserved. List leave entries first to obtain the leave ID.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "approvedBy": { @@ -656,9 +608,6 @@ }, { "description": "Update an existing timesheet. Only specify fields you want to change; everything else is preserved. Returns the entry as saved.", - "execution": { - "taskSupport": "optional" - }, "inputSchema": { "properties": { "billableId": {