@countfinancial/cli 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/LICENSE +15 -0
  2. package/dist/__tests__/credentialStore.test.js +9 -0
  3. package/dist/__tests__/credentialStore.test.js.map +1 -1
  4. package/dist/cli.js +14 -0
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/testTools.command.d.ts +7 -0
  7. package/dist/commands/testTools.command.js +42 -0
  8. package/dist/commands/testTools.command.js.map +1 -0
  9. package/dist/constants.d.ts +2 -2
  10. package/dist/constants.js +2 -2
  11. package/dist/constants.js.map +1 -1
  12. package/dist/helpers/toolSmokeTestFixtures.helper.d.ts +16 -0
  13. package/dist/helpers/toolSmokeTestFixtures.helper.js +491 -0
  14. package/dist/helpers/toolSmokeTestFixtures.helper.js.map +1 -0
  15. package/dist/partner-mcp/helpers/mcpKnowledge.helper.js +114 -4
  16. package/dist/partner-mcp/helpers/mcpKnowledge.helper.js.map +1 -1
  17. package/dist/partner-mcp/helpers/mcpPayloadValidation.helper.d.ts +1 -1
  18. package/dist/partner-mcp/helpers/mcpPayloadValidation.helper.js +61 -84
  19. package/dist/partner-mcp/helpers/mcpPayloadValidation.helper.js.map +1 -1
  20. package/dist/partner-mcp/helpers/mcpPlaybooks.helper.js +181 -2
  21. package/dist/partner-mcp/helpers/mcpPlaybooks.helper.js.map +1 -1
  22. package/dist/partner-mcp/helpers/mcpRecoveryHint.helper.js +79 -0
  23. package/dist/partner-mcp/helpers/mcpRecoveryHint.helper.js.map +1 -1
  24. package/dist/partner-mcp/schemas/bodies.d.ts +584 -0
  25. package/dist/partner-mcp/schemas/bodies.js +440 -0
  26. package/dist/partner-mcp/schemas/bodies.js.map +1 -0
  27. package/dist/partner-mcp/schemas/builders.d.ts +36 -0
  28. package/dist/partner-mcp/schemas/builders.js +106 -0
  29. package/dist/partner-mcp/schemas/builders.js.map +1 -0
  30. package/dist/partner-mcp/schemas/index.d.ts +5 -0
  31. package/dist/partner-mcp/schemas/index.js +6 -0
  32. package/dist/partner-mcp/schemas/index.js.map +1 -0
  33. package/dist/partner-mcp/schemas/primitives.d.ts +39 -0
  34. package/dist/partner-mcp/schemas/primitives.js +107 -0
  35. package/dist/partner-mcp/schemas/primitives.js.map +1 -0
  36. package/dist/partner-mcp/schemas/queries.d.ts +311 -0
  37. package/dist/partner-mcp/schemas/queries.js +175 -0
  38. package/dist/partner-mcp/schemas/queries.js.map +1 -0
  39. package/dist/partner-mcp/schemas/toolInputSchemas.d.ts +23 -0
  40. package/dist/partner-mcp/schemas/toolInputSchemas.js +263 -0
  41. package/dist/partner-mcp/schemas/toolInputSchemas.js.map +1 -0
  42. package/dist/partner-mcp/tools/definitions.js +106 -135
  43. package/dist/partner-mcp/tools/definitions.js.map +1 -1
  44. package/dist/partner-mcp/tools/registerTools.d.ts +8 -0
  45. package/dist/partner-mcp/tools/registerTools.js +8 -3
  46. package/dist/partner-mcp/tools/registerTools.js.map +1 -1
  47. package/dist/services/toolSmokeTest.service.d.ts +38 -0
  48. package/dist/services/toolSmokeTest.service.js +495 -0
  49. package/dist/services/toolSmokeTest.service.js.map +1 -0
  50. package/package.json +4 -3
@@ -1,12 +1,11 @@
1
- import { bodyInputSchema, describeEndpointInputSchema, emptyInputSchema, idBodyInputSchema, idInputSchema, idOptionalBodyInputSchema, idVersionNumberBodyInputSchema, knowledgeInputSchema, playbooksInputSchema, queryInputSchema, resolveReferencesInputSchema, validatePayloadInputSchema, } from './schemas.js';
2
- const tools = [
1
+ import * as toolInputSchemas from '../schemas/toolInputSchemas.js';
2
+ const rawTools = [
3
3
  {
4
4
  name: 'COUNT_auth_status',
5
5
  title: 'COUNT Auth Status',
6
6
  description: 'Return the MCP server authentication configuration state without exposing secrets.',
7
7
  method: 'GET',
8
8
  pathTemplate: '/__local/auth-status',
9
- inputSchema: emptyInputSchema,
10
9
  requiresUserAuth: false,
11
10
  readOnly: true,
12
11
  destructive: false,
@@ -17,7 +16,6 @@ const tools = [
17
16
  description: 'Return human-readable guidance for a specific COUNT tool, including the partner API path it wraps, expected query/body fields from the public COUNT developer documentation, and tips for which IDs to pass. Useful for checking request shape before unfamiliar create/update operations.',
18
17
  method: 'GET',
19
18
  pathTemplate: '/__local/describe-endpoint',
20
- inputSchema: describeEndpointInputSchema,
21
19
  requiresUserAuth: false,
22
20
  readOnly: true,
23
21
  destructive: false,
@@ -28,7 +26,6 @@ const tools = [
28
26
  description: 'COUNT connector and workflow FAQ — call this for “how do I…?” questions before guessing. Covers: authorizing additional workspaces (disconnect + reconnect), Claude/ChatGPT reconnect steps, multiple workspace usage, why auth_status may show fewer workspaces than the COUNT web app, external UUID conventions, P&L category filters (e.g. shipping/Freight & Courier), and bulk import batch sizes. Optional input: `topic` (FAQ id) or `search` (free text). Omit both to list all topics with summaries.',
29
27
  method: 'GET',
30
28
  pathTemplate: '/__local/knowledge',
31
- inputSchema: knowledgeInputSchema,
32
29
  requiresUserAuth: false,
33
30
  readOnly: true,
34
31
  destructive: false,
@@ -36,10 +33,9 @@ const tools = [
36
33
  {
37
34
  name: 'COUNT_playbooks',
38
35
  title: 'COUNT Playbooks',
39
- description: 'End-to-end accounting workflows with ordered steps and tool names. Call before multi-step tasks such as paying a vendor bill, bulk migration imports, or month-end review. Optional input: `playbook` (id) or `search` (free text). Omit both to list all playbooks with summaries. Playbook ids: pay_vendor_bill, migration_import, month_end_review.',
36
+ description: 'End-to-end accounting workflows with ordered steps and tool names. Call before multi-step tasks such as paying a vendor bill, creating and sending an invoice, bulk migration imports, chart-of-accounts setup with transaction import, budget planning with actuals review, or month-end review. Optional input: `playbook` (id) or `search` (free text). Omit both to list all playbooks with summaries. Playbook ids: pay_vendor_bill, create_invoice_and_send, migration_import, budget_import, month_end_review, setup_accounts_and_import_transactions, plan_budget_and_review_actuals.',
40
37
  method: 'GET',
41
38
  pathTemplate: '/__local/playbooks',
42
- inputSchema: playbooksInputSchema,
43
39
  requiresUserAuth: false,
44
40
  readOnly: true,
45
41
  destructive: false,
@@ -50,7 +46,6 @@ const tools = [
50
46
  description: 'Resolve human-readable vendor, customer, account, project, or tag names to external COUNT UUIDs for this workspace. Pass one or more name fields; each returns uuid, matchConfidence (exact/fuzzy/ambiguous/not_found), and candidates when ambiguous. Use before bulk imports instead of guessing UUIDs from large list results.',
51
47
  method: 'GET',
52
48
  pathTemplate: '/__local/resolve-references',
53
- inputSchema: resolveReferencesInputSchema,
54
49
  requiresUserAuth: false,
55
50
  readOnly: true,
56
51
  destructive: false,
@@ -58,10 +53,9 @@ const tools = [
58
53
  {
59
54
  name: 'COUNT_validate_payload',
60
55
  title: 'Validate Payload',
61
- description: 'Preflight validation for mutation tool payloads — no data is written. Checks bulk batch size (cap 100), legacy numeric field rejection, and required fields for bulk create and create_bill. Set verifyReferences: true to confirm UUIDs exist in the workspace. Call before COUNT_bulk_create_* imports.',
56
+ description: 'Preflight validation for partner API tool payloads — no data is written. Runs typed schema validation on query and body, bulk batch size (cap 100), legacy numeric field rejection, and required fields for bulk create and create_bill. Set verifyReferences: true to confirm UUIDs exist in the workspace. Works for list and mutation tools.',
62
57
  method: 'GET',
63
58
  pathTemplate: '/__local/validate-payload',
64
- inputSchema: validatePayloadInputSchema,
65
59
  requiresUserAuth: false,
66
60
  readOnly: true,
67
61
  destructive: false,
@@ -72,7 +66,6 @@ const tools = [
72
66
  description: 'Refresh the COUNT partner access token using the configured refresh token. The refreshed token is kept in this MCP process memory.',
73
67
  method: 'POST',
74
68
  pathTemplate: '/partners/refresh-user-access-token',
75
- inputSchema: emptyInputSchema,
76
69
  requiresUserAuth: false,
77
70
  readOnly: false,
78
71
  destructive: false,
@@ -81,10 +74,9 @@ const tools = [
81
74
  {
82
75
  name: 'COUNT_list_transactions',
83
76
  title: 'List Transactions',
84
- description: 'List register/bank transactions for the authenticated workspace. Common `query` filters: `accUuid` (single bank/cash account UUID), `categoryAccountUuid`, `vendorUuid`, `customerUuid`, `projectUuid`, `tagUuids` (comma-separated), `startDate` / `endDate` (YYYY-MM-DD), `transactionTypes` ("Expense" | "Income" | "Transfer" | "Journal Entry"; `type` is also accepted as an alias), `reviewed: "true" | "false"`, `reconciled: "true" | "false"`, `search` (substring against description), `page`, and `limit` (default 50, server cap applies). Returns transaction rows with `id` exposed as the external UUID — pass that same UUID back to `update_transaction` / `change_transaction_category` / `delete_transaction`.',
77
+ description: 'List register/bank transactions for the authenticated workspace. Returns transaction rows with `id` as the external UUID — pass that UUID to update/change-category/delete tools. Filter fields are documented on the `query` input schema.',
85
78
  method: 'GET',
86
79
  pathTemplate: '/partners/transactions',
87
- inputSchema: queryInputSchema,
88
80
  requiresUserAuth: true,
89
81
  readOnly: true,
90
82
  destructive: false,
@@ -95,7 +87,6 @@ const tools = [
95
87
  description: 'Get a single transaction by external UUID. Returns transactions hidden from the workspace UI as well. Optional `query` fields are forwarded as Sequelize `include` directives to expand related entities (e.g. category, vendor, customer, tags, taxes). Use the returned `id` (the external UUID) as the next call argument for `update_transaction`, `change_transaction_category`, or `delete_transaction`.',
96
88
  method: 'GET',
97
89
  pathTemplate: '/partners/transactions/{id}',
98
- inputSchema: idInputSchema,
99
90
  requiresUserAuth: true,
100
91
  readOnly: true,
101
92
  destructive: false,
@@ -106,7 +97,6 @@ const tools = [
106
97
  description: 'Create a register/bank transaction in the authenticated COUNT workspace. **Set the category, vendor/customer, project, tags, and taxes in this single call — do NOT follow up with `change_transaction_category` when you already know the category at creation time.** The create endpoint resolves the category UUID, posts the journal entries, and writes tax associations atomically. Required body: `accUuid` (account UUID from `list_accounts` — the bank/cash account the money moves through), `amount` (positive decimal — see sign convention below), and either `postedDate` or `date` (YYYY-MM-DD). Optional body: `description`, `currency`, `type` (`Expense` | `Income` | `Transfer` | `Journal Entry` — case-sensitive, but lowercase `expense`/`income`/`transfer`/`journal`/`journal_entry` are also accepted and normalized server-side), `categoryAccountUuid` (income/expense category — same UUID source as `accUuid`; **set this here at creation, no separate `change_transaction_category` call needed**), `vendorUuid`, `customerUuid`, `projectUuid`, `tagUuids: string[]`, `taxes: number[]` (numeric internal tax ids — taxes are not yet UUID-resolved on the partner API), `notes`, `authorizedDate`. **Sign convention**: pass `amount` as a positive number; the server flips the sign for income internally. **Type omission**: when `type` is not sent, the server infers it from the category-account type. **FK resolution**: `vendorUuid`/`customerUuid`/`projectUuid`/`tagUuids` are resolved into the internal numeric ids the service expects; do not send `vendorId`/`customerId`/`projectId`/`tags` numeric arrays unless you really do have internal ids. **Use `change_transaction_category` only to modify the category on an EXISTING transaction.**',
107
98
  method: 'POST',
108
99
  pathTemplate: '/partners/transactions',
109
- inputSchema: bodyInputSchema,
110
100
  requiresUserAuth: true,
111
101
  readOnly: false,
112
102
  destructive: false,
@@ -117,7 +107,6 @@ const tools = [
117
107
  description: 'Create up to 100 register/bank transactions in a single call. Body: `{ "transactions": [<row>, ...] }` where each `<row>` uses the EXACT same shape as `COUNT_create_transaction` (required `accUuid`, `amount`, `postedDate`/`date`; optional `description`, `currency`, `type`, `categoryAccountUuid`, `vendorUuid`, `customerUuid`, `projectUuid`, `tagUuids: string[]`, `taxes: number[]`, `notes`, `authorizedDate`). **Set the category, vendor/customer, project, tags, and taxes ON EACH ROW in this single call — no follow-up `change_transaction_category` needed.** **Partial-success contract**: each row runs in its own DB transaction; one row failing never rolls back another row\'s writes. Response envelope: `{ "successCount": N, "errorCount": M, "results": [{ "index": 0, "success": true, "transaction": {...} } | { "index": 1, "success": false, "error": "..." }, ...] }` — read `errorCount` first; if non-zero, iterate `results` and only retry the rows where `success: false`. The HTTP status is always `201` when the batch was accepted, even if all rows failed. **Cap**: 100 rows per call (split larger imports into multiple calls). **Recommended batch size**: ~25 rows for large historical imports (e.g. billing-system backfills); larger batches increase timeout risk. **Use cases**: ingest a CSV of bank transactions, replay a missed sync window, backfill historical activity. For a single transaction, prefer `COUNT_create_transaction` — the response shape is simpler.',
118
108
  method: 'POST',
119
109
  pathTemplate: '/partners/transactions/bulk',
120
- inputSchema: bodyInputSchema,
121
110
  requiresUserAuth: true,
122
111
  readOnly: false,
123
112
  destructive: false,
@@ -128,7 +117,6 @@ const tools = [
128
117
  description: 'Update non-category fields on an EXISTING transaction by external UUID. Body fields: `vendorUuid`, `customerUuid`, `projectUuid`, `tagUuids: string[]` (replaces all tags — `[]` clears them), plus regular safe Transaction columns (`description`, `notes`, `amount`, `postedDate`, `authorizedDate`, etc.). Do not send `categoryAccountUuid` or `categoryAccountId` here; use `COUNT_change_transaction_category` to change/clear the category on an existing transaction so taxes, transfers, fixed assets, and journal side effects run through the accounting workflow. (When CREATING a new transaction, pass `categoryAccountUuid` directly to `COUNT_create_transaction` — no follow-up call is needed.) The response includes `_partnerWarnings: [{ field, reason, message }]` when fields you sent were ignored (`reason: "internal_only"` for fields managed internally — e.g. `linkedTransferTransactionId`, `providerTransactionId`, `billId`; `reason: "unknown_field"` for typos / fields not on the transaction model).',
129
118
  method: 'PATCH',
130
119
  pathTemplate: '/partners/transactions/{id}',
131
- inputSchema: idBodyInputSchema,
132
120
  requiresUserAuth: true,
133
121
  readOnly: false,
134
122
  destructive: false,
@@ -139,7 +127,6 @@ const tools = [
139
127
  description: 'Change, clear, or transfer-link the category (GL account) on an EXISTING transaction by external UUID. **Use this tool only when modifying the category of a transaction that already exists.** When creating a NEW transaction, pass `categoryAccountUuid` directly to `COUNT_create_transaction` instead — the create endpoint resolves the category, posts the journal entries, and writes tax associations in a single call (no follow-up `change_transaction_category` is needed). Body: `categoryAccountUuid` (the `id`, `accUuid`, or `accountUuid` of the target account from `list_accounts`); pass `null` to uncategorize. Optional body fields: `notes`, `taxes`, `autoCreateMatching`, `autoReview`, `linkedTransferTransactionUuid` (or UUID-shaped `linkedTransferTransactionId` from `list_transactions`), `fixedAssetAssignment`. The category-change workflow ensures taxes, transfers, fixed assets, and journal side effects all stay in sync on existing rows.',
140
128
  method: 'PATCH',
141
129
  pathTemplate: '/partners/transactions/{id}/change-category',
142
- inputSchema: idBodyInputSchema,
143
130
  requiresUserAuth: true,
144
131
  readOnly: false,
145
132
  destructive: false,
@@ -150,7 +137,16 @@ const tools = [
150
137
  description: 'Apply an existing bank/expense/income transaction as payment against one or more invoices or bills. URL `{id}` is the transaction UUID from `list_transactions`. Body: `{ matchingType: "invoice" | "bill", records: [{ id: <invoice or bill UUID>, paymentAmount: <decimal>, notes?: string }], withCaution?: boolean, parentCategoryAccountUuid?: string }`. For invoices use `matchingType: "invoice"` with an **Income** transaction (bank deposit); invoice refunds use **Expense**. For bills use `matchingType: "bill"` with an **Expense** transaction (vendor payment); vendor memos use **Income**. `paymentAmount` must not exceed the document open balance; partial payments may split the transaction. Optional `parentCategoryAccountUuid` sets the category for any remainder when the payment is less than the transaction amount. Do not set `invoiceId` / `billId` on transaction update — use this endpoint instead.',
151
138
  method: 'POST',
152
139
  pathTemplate: '/partners/transactions/{id}/assign-to-bills-invoices',
153
- inputSchema: idBodyInputSchema,
140
+ requiresUserAuth: true,
141
+ readOnly: false,
142
+ destructive: false,
143
+ },
144
+ {
145
+ name: 'COUNT_split_transaction',
146
+ title: 'Split Transaction',
147
+ description: 'Split an existing transaction into a parent row plus child rows before assigning part of the amount to a bill or invoice. URL `{id}` is the transaction UUID from `list_transactions`. Body mirrors the COUNT UI split flow: `{ forAttachment?: boolean, splitASplit?: boolean, withCaution?: boolean, parent?: { categoryAccountUuid, amount?, assignThis?, originalAmount?, type? }, splits?: [{ categoryAccountUuid, amount }] }`. Use `categoryAccountUuid` values from `list_accounts` (cannot target a system control account). Transfer and deposit transactions cannot be split. Reconciled transactions require `withCaution: true`. **400s if the transaction is already linked to a bill or invoice** — unassign it first. **Omitting `splits` (or sending `[]`) does not no-op — it UN-SPLITS an already-split transaction**, permanently destroying every existing split-child row and restoring the parent to its original un-split amount; never send an empty `splits` array unless that is the intent. After splitting, assign the intended child row with `COUNT_assign_transaction_to_bills_invoices`.',
148
+ method: 'PUT',
149
+ pathTemplate: '/partners/transactions/{id}/split',
154
150
  requiresUserAuth: true,
155
151
  readOnly: false,
156
152
  destructive: false,
@@ -161,18 +157,36 @@ const tools = [
161
157
  description: 'Delete a transaction by external UUID.',
162
158
  method: 'DELETE',
163
159
  pathTemplate: '/partners/transactions/{id}',
164
- inputSchema: idInputSchema,
165
160
  requiresUserAuth: true,
166
161
  readOnly: false,
167
162
  destructive: true,
168
163
  },
164
+ {
165
+ name: 'COUNT_bulk_exclude_transactions',
166
+ title: 'Bulk Exclude Transactions',
167
+ description: 'Exclude or un-exclude up to 100 transactions in one call by external UUID (`body.transactionUuids`, `body.excluded: boolean`). Excluding hides a transaction from the ledger without deleting it — preserves the audit trail and survives bank-feed re-sync, unlike `COUNT_delete_transaction`. This is a per-row partial-success operation, same as `COUNT_bulk_change_transaction_category`: an unresolvable UUID, or a transaction that is reviewed/reconciled/pending/attached to a bill/invoice/transfer/deposit, becomes one entry in the response `failures` array instead of failing the whole batch — exclude only unreviewed transactions to avoid failures. Response: `{ updatedTransactions, successCount, failedCount, failures: [{ transactionUuid, reasons: string[] }] }`. To exclude by date range, first call `COUNT_list_transactions` with `startDate`/`endDate` (and `reviewed: false`) to collect UUIDs, then pass them here.',
168
+ method: 'PATCH',
169
+ pathTemplate: '/partners/transactions/exclude-bulk',
170
+ requiresUserAuth: true,
171
+ readOnly: false,
172
+ destructive: false,
173
+ },
174
+ {
175
+ name: 'COUNT_bulk_change_transaction_category',
176
+ title: 'Bulk Change Transaction Category',
177
+ description: 'Categorize (or uncategorize) up to 100 EXISTING transactions in one call. Body: `categoryAccountUuid` (a category account UUID from `list_accounts`, or `null` to uncategorize) and `transactionUuids` (array). This is a per-row partial-success operation, same as `COUNT_bulk_exclude_transactions`: an unresolvable UUID, or a transaction that is reviewed/reconciled/pending/attached to a bill/invoice/transfer/deposit, becomes one entry in the response `failures` array rather than failing the whole batch. Response: `{ updatedTransactions, successCount, failedCount, failures: [{ transactionUuid, reasons: string[] }] }`. For NEW transactions, pass `categoryAccountUuid` directly to `COUNT_create_transaction`/`COUNT_bulk_create_transactions` instead — no follow-up call needed. For a single existing transaction, `COUNT_change_transaction_category` is equivalent and returns the full updated row.',
178
+ method: 'PATCH',
179
+ pathTemplate: '/partners/transactions/change-category-bulk',
180
+ requiresUserAuth: true,
181
+ readOnly: false,
182
+ destructive: false,
183
+ },
169
184
  {
170
185
  name: 'COUNT_list_account_sub_types',
171
186
  title: 'List Account Sub-Types',
172
187
  description: 'List every chart-of-accounts sub-type available in COUNT (global catalog — the same integer ids apply in every workspace). **Call this before `COUNT_create_account`** to discover the correct `subTypeId`; e.g. set `query.type` to `"Liabilities"` and pick the row whose `name` is `Credit Card`. Optional `query.type`: one of `Assets`, `Liabilities`, `Equity`, `Income`, `Expenses` (case-sensitive, plural). Each row returns `id` (pass as `subTypeId` on create), `type`, `name`, `description`, and `anchorTier`. **Never guess or sequentially probe subType ids** — they are sparse and not predictable from other ids.',
173
188
  method: 'GET',
174
189
  pathTemplate: '/partners/account-sub-types',
175
- inputSchema: queryInputSchema,
176
190
  requiresUserAuth: true,
177
191
  readOnly: true,
178
192
  destructive: false,
@@ -180,10 +194,9 @@ const tools = [
180
194
  {
181
195
  name: 'COUNT_list_accounts',
182
196
  title: 'List Chart Of Accounts',
183
- description: 'List chart of accounts for the authenticated COUNT workspace. When you need a `subTypeId` for a **new** account and no similar account exists yet, prefer `COUNT_list_account_sub_types` instead of guessing from sparse ids. Optional filters (pass under `query`): `type` must be one of `Assets`, `Liabilities`, `Equity`, `Income`, `Expenses` (case-sensitive, plural — singular forms like "Asset" are rejected); `subTypeId` is a positive integer; `search` is a substring matched against account name / number; boolean-string filters `inactive`, `includeBalances`, `includeHidden`, `includeHiddenAccounts`, `is1099Box`, `notAssignedToReporter`, `onlyCategoryAccounts`, `includeDeleteMeta` accept `"true"` / `"false"`. Existing rows include `subType.id` — you may reuse that integer when creating another account in the same bucket.',
197
+ description: 'List chart of accounts for the authenticated COUNT workspace. When you need a `subTypeId` for a **new** account and no similar account exists yet, prefer `COUNT_list_account_sub_types` instead of guessing from sparse ids. Optional filters (pass under `query`): `type` must be one of `Assets`, `Liabilities`, `Equity`, `Income`, `Expenses` (case-sensitive, plural — singular forms like "Asset" are rejected); `subTypeId` is a positive integer; `search` is a substring matched against account name / number; boolean-string filters `inactive`, `includeBalances`, `includeHidden`, `includeHiddenAccounts`, `is1099Box`, `notAssignedToReporter`, `onlyCategoryAccounts`, `includeDeleteMeta` accept `"true"` / `"false"`. Each row includes `editable` (`false` for system/control/connected accounts). When `includeDeleteMeta=true`, each row also includes `canDelete` and `deleteBlockedReason` (`NOT_EDITABLE`, `HAS_JOURNAL_ENTRIES`, `HAS_SUB_ACCOUNTS`, `HAS_INVOICE_PRODUCTS`, `HAS_PAYROLL_MAPPINGS`) so you can tell whether update/delete will succeed before calling those tools. Existing rows include `subType.id` — you may reuse that integer when creating another account in the same bucket.',
184
198
  method: 'GET',
185
199
  pathTemplate: '/partners/chart-of-accounts',
186
- inputSchema: queryInputSchema,
187
200
  requiresUserAuth: true,
188
201
  readOnly: true,
189
202
  destructive: false,
@@ -194,7 +207,16 @@ const tools = [
194
207
  description: 'Create a chart-of-accounts entry. **Always resolve `subTypeId` first — never guess or probe ids.** Preferred: `COUNT_list_account_sub_types` (filter `query.type`, pick the matching `name`, use its `id`). Fallback: `COUNT_list_accounts` with `query.type` and reuse `subType.id` from an existing row in that bucket. Required body: `name` (string) and `subTypeId` (integer from list_account_sub_types or list_accounts). Optional body: `accountNumber`, `currency` (defaults to the workspace currency), `description`, `color` (hex), `parentAccountId` (numeric id of an existing parent account — sub-accounts inherit the parent\'s `type` / `currency` / `status` and a sub-account cannot itself have sub-accounts), `institutionId`, `taxes: number[]` (internal numeric tax ids — required for NZ workspaces), `status` ("active" | "inactive"). The high-level `type` (Assets/Liabilities/Equity/Income/Expenses) is derived from `subTypeId` server-side; do not send it. If creation fails, tell the user COUNT could not create the account and include the `requestId` when present — **never** quote stack traces, source file paths, line numbers, or internal database field names in user-visible text.',
195
208
  method: 'POST',
196
209
  pathTemplate: '/partners/chart-of-accounts',
197
- inputSchema: bodyInputSchema,
210
+ requiresUserAuth: true,
211
+ readOnly: false,
212
+ destructive: false,
213
+ },
214
+ {
215
+ name: 'COUNT_bulk_create_accounts',
216
+ title: 'Bulk Create Accounts',
217
+ description: 'Create up to 100 chart-of-accounts entries in a single call. Body: `{ "accounts": [<row>, ...] }` where each `<row>` uses the EXACT same shape as `COUNT_create_account` (required `name` and `subTypeId`; optional `accountNumber`, `currency`, `description`, `color`, `parentAccountId`, `institutionId`, `taxes`, `status`). **Partial-success contract**: each row runs in its own DB transaction; one row failing never rolls back another row\'s writes. Response envelope: `{ "successCount": N, "errorCount": M, "results": [{ "index": 0, "success": true, "account": {...} } | { "index": 1, "success": false, "error": "..." }, ...] }` — read `errorCount` first; if non-zero, iterate `results` and only retry the rows where `success: false`. The HTTP status is always `201` when the batch was accepted, even if all rows failed. **Cap**: 100 rows per call. **Recommended batch size**: ~25 rows for migrations. **Complete the full chart of accounts before importing bills or transactions** — accounts with journal entries cannot be deleted (`deleteBlockedReason: HAS_JOURNAL_ENTRIES`); use `COUNT_update_account` with `status: inactive` instead.',
218
+ method: 'POST',
219
+ pathTemplate: '/partners/chart-of-accounts/bulk',
198
220
  requiresUserAuth: true,
199
221
  readOnly: false,
200
222
  destructive: false,
@@ -205,7 +227,6 @@ const tools = [
205
227
  description: 'Update a chart-of-accounts entry by external UUID. Pass only the fields you want to change. Editable: `name`, `accountNumber`, `description`, `color`, `status` ("active" | "inactive"), `subTypeId` (must keep the same high-level `type` — switching a Bank account into an Income sub-type is rejected), `parentAccountId` (set to a valid parent id to nest, or pass `isChildAccount: false` to detach), `connectionSyncEnabled` (only meaningful on connected feed accounts). Do not send `type` (derived from `subTypeId`), `teamId`, `connectionId`, or `systemCreated` — they are stripped. System / non-editable accounts (e.g. payroll defaults) accept only a small allowlist of cosmetic fields plus name on physical accounts; expect a 403 if you try to edit a protected field. An empty body returns 400.',
206
228
  method: 'PATCH',
207
229
  pathTemplate: '/partners/chart-of-accounts/{id}',
208
- inputSchema: idBodyInputSchema,
209
230
  requiresUserAuth: true,
210
231
  readOnly: false,
211
232
  destructive: false,
@@ -213,10 +234,9 @@ const tools = [
213
234
  {
214
235
  name: 'COUNT_delete_account',
215
236
  title: 'Delete Account',
216
- description: 'Delete a chart of accounts entry by external UUID.',
237
+ description: 'Delete a chart of accounts entry by external UUID. Partner-created accounts are editable and deletable until they have journal entries, sub-accounts, invoice products, or payroll mappings. If delete fails because the account has journal entries, use `COUNT_update_account` with `status: inactive` instead. Call `COUNT_list_accounts` with `query.includeDeleteMeta: "true"` to read `canDelete` and `deleteBlockedReason` before attempting delete.',
217
238
  method: 'DELETE',
218
239
  pathTemplate: '/partners/chart-of-accounts/{id}',
219
- inputSchema: idInputSchema,
220
240
  requiresUserAuth: true,
221
241
  readOnly: false,
222
242
  destructive: true,
@@ -227,7 +247,6 @@ const tools = [
227
247
  description: 'List vendors for the authenticated COUNT workspace.',
228
248
  method: 'GET',
229
249
  pathTemplate: '/partners/vendors',
230
- inputSchema: queryInputSchema,
231
250
  requiresUserAuth: true,
232
251
  readOnly: true,
233
252
  destructive: false,
@@ -238,7 +257,6 @@ const tools = [
238
257
  description: 'Create a vendor.',
239
258
  method: 'POST',
240
259
  pathTemplate: '/partners/vendors',
241
- inputSchema: bodyInputSchema,
242
260
  requiresUserAuth: true,
243
261
  readOnly: false,
244
262
  destructive: false,
@@ -249,7 +267,6 @@ const tools = [
249
267
  description: 'Update a vendor by external UUID.',
250
268
  method: 'PATCH',
251
269
  pathTemplate: '/partners/vendors/{id}',
252
- inputSchema: idBodyInputSchema,
253
270
  requiresUserAuth: true,
254
271
  readOnly: false,
255
272
  destructive: false,
@@ -260,7 +277,6 @@ const tools = [
260
277
  description: 'Delete a vendor by external UUID.',
261
278
  method: 'DELETE',
262
279
  pathTemplate: '/partners/vendors/{id}',
263
- inputSchema: idInputSchema,
264
280
  requiresUserAuth: true,
265
281
  readOnly: false,
266
282
  destructive: true,
@@ -271,7 +287,6 @@ const tools = [
271
287
  description: 'List customers for the authenticated COUNT workspace.',
272
288
  method: 'GET',
273
289
  pathTemplate: '/partners/customers',
274
- inputSchema: queryInputSchema,
275
290
  requiresUserAuth: true,
276
291
  readOnly: true,
277
292
  destructive: false,
@@ -282,7 +297,6 @@ const tools = [
282
297
  description: 'Get a customer by external UUID.',
283
298
  method: 'GET',
284
299
  pathTemplate: '/partners/customers/{id}',
285
- inputSchema: idInputSchema,
286
300
  requiresUserAuth: true,
287
301
  readOnly: true,
288
302
  destructive: false,
@@ -293,7 +307,6 @@ const tools = [
293
307
  description: 'Create a customer. Required body: `customer` (business or person name). Optional body: `email`, `mainPhone`, `website`, `notes`, `status` (`active` | `inactive`), `paymentTerm`, `taxNumber`, `taxAutoCalculate`, `taxExcluded`, `taxes` (numeric tax ids), `billingAddress`, `shippingAddress`, `contacts: [{ firstName?, lastName?, email?, phone?, isPrimary? }]`. Returns 201 with the created customer; `id` in the response is the external UUID.',
294
308
  method: 'POST',
295
309
  pathTemplate: '/partners/customers',
296
- inputSchema: bodyInputSchema,
297
310
  requiresUserAuth: true,
298
311
  readOnly: false,
299
312
  destructive: false,
@@ -304,7 +317,6 @@ const tools = [
304
317
  description: 'Update a customer by external UUID.',
305
318
  method: 'PUT',
306
319
  pathTemplate: '/partners/customers/{id}',
307
- inputSchema: idBodyInputSchema,
308
320
  requiresUserAuth: true,
309
321
  readOnly: false,
310
322
  destructive: false,
@@ -315,7 +327,6 @@ const tools = [
315
327
  description: 'Create up to 100 customers in a single call. Body: `{ "customers": [<row>, ...] }` where each `<row>` uses the EXACT same shape as `COUNT_create_customer` (required `customer`; optional `email`, `mainPhone`, `website`, `notes`, `status`, `paymentTerm`, `taxNumber`, `taxAutoCalculate`, `taxExcluded`, `taxes`, `billingAddress`, `shippingAddress`, `contacts`). **Partial-success contract**: each row runs in its own DB transaction; one row failing never rolls back another row\'s writes. Response envelope: `{ "successCount": N, "errorCount": M, "results": [{ "index": 0, "success": true, "customer": {...} } | { "index": 1, "success": false, "error": "..." }, ...] }` — read `errorCount` first; if non-zero, iterate `results` and only retry the rows where `success: false`. The HTTP status is always `201` when the batch was accepted, even if all rows failed. **Cap**: 100 rows per call. **Recommended batch size**: ~25 rows for large migrations (e.g. importing customers from an external billing system).',
316
328
  method: 'POST',
317
329
  pathTemplate: '/partners/customers/bulk',
318
- inputSchema: bodyInputSchema,
319
330
  requiresUserAuth: true,
320
331
  readOnly: false,
321
332
  destructive: false,
@@ -326,7 +337,6 @@ const tools = [
326
337
  description: 'Update up to 100 customers in a single call. Body: `{ "customers": [{ "uuid": "<external COUNT customer id>", ...fields }] }` where each row must include `uuid` plus the fields to patch (same writable fields as `COUNT_update_customer`, e.g. `email`, `mainPhone`, `website`, `notes`, `status`, `paymentTerm`, `billingAddress`, `shippingAddress`, `contacts`). **Partial-success contract**: each row is updated independently; one row failing never rolls back another row\'s writes. Response envelope: `{ "successCount": N, "errorCount": M, "results": [{ "index": 0, "success": true, "customer": {...} } | { "index": 1, "success": false, "error": "..." }, ...] }`. The HTTP status is always `201` when the batch was accepted. **Recommended batch size**: ~25 rows for backfilling email/phone on migrated customers.',
327
338
  method: 'PATCH',
328
339
  pathTemplate: '/partners/customers/bulk',
329
- inputSchema: bodyInputSchema,
330
340
  requiresUserAuth: true,
331
341
  readOnly: false,
332
342
  destructive: false,
@@ -337,7 +347,6 @@ const tools = [
337
347
  description: 'Delete a customer by external UUID.',
338
348
  method: 'DELETE',
339
349
  pathTemplate: '/partners/customers/{id}',
340
- inputSchema: idInputSchema,
341
350
  requiresUserAuth: true,
342
351
  readOnly: false,
343
352
  destructive: true,
@@ -348,7 +357,6 @@ const tools = [
348
357
  description: 'List people records for the authenticated COUNT workspace.',
349
358
  method: 'GET',
350
359
  pathTemplate: '/partners/people',
351
- inputSchema: queryInputSchema,
352
360
  requiresUserAuth: true,
353
361
  readOnly: true,
354
362
  destructive: false,
@@ -359,7 +367,6 @@ const tools = [
359
367
  description: 'Get a people record by external UUID.',
360
368
  method: 'GET',
361
369
  pathTemplate: '/partners/people/{id}',
362
- inputSchema: idInputSchema,
363
370
  requiresUserAuth: true,
364
371
  readOnly: true,
365
372
  destructive: false,
@@ -370,7 +377,6 @@ const tools = [
370
377
  description: 'List products and services for the authenticated COUNT workspace.',
371
378
  method: 'GET',
372
379
  pathTemplate: '/partners/products',
373
- inputSchema: queryInputSchema,
374
380
  requiresUserAuth: true,
375
381
  readOnly: true,
376
382
  destructive: false,
@@ -381,7 +387,6 @@ const tools = [
381
387
  description: 'Get a product or service by external UUID.',
382
388
  method: 'GET',
383
389
  pathTemplate: '/partners/products/{id}',
384
- inputSchema: idInputSchema,
385
390
  requiresUserAuth: true,
386
391
  readOnly: true,
387
392
  destructive: false,
@@ -389,10 +394,9 @@ const tools = [
389
394
  {
390
395
  name: 'COUNT_create_product',
391
396
  title: 'Create Product',
392
- description: 'Create a product or service.',
397
+ description: 'Create a product or service. Use `categoryAccountUuid` and `purchaseCategoryAccountUuid` (from `list_accounts`) for income/purchase category accounts — do not pass UUID-shaped values in `categoryAccountId`/`purchaseCategoryAccountId`. Use `taxUuids` (from `list_taxes`) for taxes.',
393
398
  method: 'POST',
394
399
  pathTemplate: '/partners/products',
395
- inputSchema: bodyInputSchema,
396
400
  requiresUserAuth: true,
397
401
  readOnly: false,
398
402
  destructive: false,
@@ -400,10 +404,9 @@ const tools = [
400
404
  {
401
405
  name: 'COUNT_update_product',
402
406
  title: 'Update Product',
403
- description: 'Update a product or service by external UUID.',
407
+ description: 'Update a product or service by external UUID. Use `categoryAccountUuid`, `purchaseCategoryAccountUuid`, and `taxUuids` for account/tax references (see `COUNT_create_product`).',
404
408
  method: 'PATCH',
405
409
  pathTemplate: '/partners/products/{id}',
406
- inputSchema: idBodyInputSchema,
407
410
  requiresUserAuth: true,
408
411
  readOnly: false,
409
412
  destructive: false,
@@ -414,7 +417,6 @@ const tools = [
414
417
  description: 'Delete a product or service by external UUID.',
415
418
  method: 'DELETE',
416
419
  pathTemplate: '/partners/products/{id}',
417
- inputSchema: idInputSchema,
418
420
  requiresUserAuth: true,
419
421
  readOnly: false,
420
422
  destructive: true,
@@ -425,7 +427,6 @@ const tools = [
425
427
  description: 'List tags for the authenticated COUNT workspace.',
426
428
  method: 'GET',
427
429
  pathTemplate: '/partners/tags',
428
- inputSchema: queryInputSchema,
429
430
  requiresUserAuth: true,
430
431
  readOnly: true,
431
432
  destructive: false,
@@ -436,7 +437,6 @@ const tools = [
436
437
  description: 'Get a tag by external UUID.',
437
438
  method: 'GET',
438
439
  pathTemplate: '/partners/tags/{id}',
439
- inputSchema: idInputSchema,
440
440
  requiresUserAuth: true,
441
441
  readOnly: true,
442
442
  destructive: false,
@@ -447,7 +447,6 @@ const tools = [
447
447
  description: 'Create a tag.',
448
448
  method: 'POST',
449
449
  pathTemplate: '/partners/tags',
450
- inputSchema: bodyInputSchema,
451
450
  requiresUserAuth: true,
452
451
  readOnly: false,
453
452
  destructive: false,
@@ -458,7 +457,6 @@ const tools = [
458
457
  description: 'Update a tag by external UUID.',
459
458
  method: 'PATCH',
460
459
  pathTemplate: '/partners/tags/{id}',
461
- inputSchema: idBodyInputSchema,
462
460
  requiresUserAuth: true,
463
461
  readOnly: false,
464
462
  destructive: false,
@@ -469,7 +467,6 @@ const tools = [
469
467
  description: 'Delete a tag by external UUID.',
470
468
  method: 'DELETE',
471
469
  pathTemplate: '/partners/tags/{id}',
472
- inputSchema: idInputSchema,
473
470
  requiresUserAuth: true,
474
471
  readOnly: false,
475
472
  destructive: true,
@@ -480,7 +477,6 @@ const tools = [
480
477
  description: 'List tag groups for the authenticated COUNT workspace.',
481
478
  method: 'GET',
482
479
  pathTemplate: '/partners/tags/groups',
483
- inputSchema: queryInputSchema,
484
480
  requiresUserAuth: true,
485
481
  readOnly: true,
486
482
  destructive: false,
@@ -491,7 +487,6 @@ const tools = [
491
487
  description: 'Get a tag group by external UUID.',
492
488
  method: 'GET',
493
489
  pathTemplate: '/partners/tags/groups/{id}',
494
- inputSchema: idInputSchema,
495
490
  requiresUserAuth: true,
496
491
  readOnly: true,
497
492
  destructive: false,
@@ -502,7 +497,6 @@ const tools = [
502
497
  description: 'Create a tag group. Body: `name` (required, string, unique per workspace), `color` (optional string), and `tagUuids` (optional array of tag UUIDs from `list_tags`) — each tag may belong to at most one group, so passing a tag already in another group rejects the request with a 400. Send tag UUIDs (not numeric ids); the server resolves them to internal ids automatically.',
503
498
  method: 'POST',
504
499
  pathTemplate: '/partners/tags/groups',
505
- inputSchema: bodyInputSchema,
506
500
  requiresUserAuth: true,
507
501
  readOnly: false,
508
502
  destructive: false,
@@ -513,7 +507,6 @@ const tools = [
513
507
  description: 'Update a tag group by external UUID. Body: optional `name`, `color`, and `tagUuids` (array of tag UUIDs from `list_tags`). When `tagUuids` is sent the group\'s membership is REPLACED with that exact set (existing tags not in the array are removed, new ones added); pass `[]` to clear all tags. A tag can only belong to one group at a time. Send tag UUIDs (not numeric ids); the server resolves them to internal ids automatically.',
514
508
  method: 'PATCH',
515
509
  pathTemplate: '/partners/tags/groups/{id}',
516
- inputSchema: idBodyInputSchema,
517
510
  requiresUserAuth: true,
518
511
  readOnly: false,
519
512
  destructive: false,
@@ -524,7 +517,6 @@ const tools = [
524
517
  description: 'Delete a tag group by external UUID.',
525
518
  method: 'DELETE',
526
519
  pathTemplate: '/partners/tags/groups/{id}',
527
- inputSchema: idInputSchema,
528
520
  requiresUserAuth: true,
529
521
  readOnly: false,
530
522
  destructive: true,
@@ -535,7 +527,6 @@ const tools = [
535
527
  description: 'List invoices for the authenticated COUNT workspace.',
536
528
  method: 'GET',
537
529
  pathTemplate: '/partners/invoices',
538
- inputSchema: queryInputSchema,
539
530
  requiresUserAuth: true,
540
531
  readOnly: true,
541
532
  destructive: false,
@@ -546,7 +537,6 @@ const tools = [
546
537
  description: 'Get an invoice by external UUID.',
547
538
  method: 'GET',
548
539
  pathTemplate: '/partners/invoices/{id}',
549
- inputSchema: idInputSchema,
550
540
  requiresUserAuth: true,
551
541
  readOnly: true,
552
542
  destructive: false,
@@ -557,7 +547,6 @@ const tools = [
557
547
  description: 'Create an invoice, credit memo, or estimate. Required body: `customerUuid` (from `list_customers`), `date` (YYYY-MM-DD), and `products: [{ productUuid | uuid, quantity, unitPrice, ... }]` (each line resolves a product UUID into the internal product id; `unitPrice` is per-unit in workspace currency). Optional body: `invoiceType` ("invoice" — default | "estimate" | "memo"), `invoiceNumber`, `dueDate` (YYYY-MM-DD), `currency` (defaults to workspace currency), `discount` + `discountDescription`, `notes`, `tagUuids: string[]` (replaces all tags), `appliedToInvoiceUuid` (only for `invoiceType: "memo"` — links the credit memo to the original invoice), `isDraft` (boolean — drafts skip approval / journals), `applyCredit` + `creditMemo: { id, amount }` (apply an existing memo), and recurring-template fields (`recurrencePattern`, `inAdvanceCreationDays`) when creating a recurring schedule via this endpoint. **Do not send numeric ids for `customer`/`products[].id`/`tags`/`appliedToInvoiceId`** — the partner middleware resolves UUIDs into them. Memos cannot be recurring; recurring memos return 400.',
558
548
  method: 'POST',
559
549
  pathTemplate: '/partners/invoices',
560
- inputSchema: bodyInputSchema,
561
550
  requiresUserAuth: true,
562
551
  readOnly: false,
563
552
  destructive: false,
@@ -570,7 +559,6 @@ const tools = [
570
559
  'When the body is empty or contains internal-only or unknown fields, the response is still 200 with the unchanged invoice but includes a `_partnerWarnings: [{ field, reason, message }]` array describing each ignored field — read it before assuming the update succeeded.',
571
560
  method: 'PATCH',
572
561
  pathTemplate: '/partners/invoices/{id}',
573
- inputSchema: idBodyInputSchema,
574
562
  requiresUserAuth: true,
575
563
  readOnly: false,
576
564
  destructive: false,
@@ -581,7 +569,6 @@ const tools = [
581
569
  description: 'Delete an invoice by external UUID.',
582
570
  method: 'DELETE',
583
571
  pathTemplate: '/partners/invoices/{id}',
584
- inputSchema: idInputSchema,
585
572
  requiresUserAuth: true,
586
573
  readOnly: false,
587
574
  destructive: true,
@@ -592,7 +579,6 @@ const tools = [
592
579
  description: 'Approve a draft invoice by external UUID. Moves the invoice from `draft` → `approved`, posts the underlying journal entries, and makes the invoice eligible for `send`. Body is optional — pass nothing for the standard approval. Errors with 400 if the invoice is already approved, sent, or paid; not valid for `invoiceType: "memo"` drafts (use the regular update + send flow on memos).',
593
580
  method: 'PATCH',
594
581
  pathTemplate: '/partners/invoices/{id}/approve',
595
- inputSchema: idOptionalBodyInputSchema,
596
582
  requiresUserAuth: true,
597
583
  readOnly: false,
598
584
  destructive: false,
@@ -603,7 +589,6 @@ const tools = [
603
589
  description: 'Send (or re-send) an approved invoice to its customer by external UUID. Optional body: `recipients: string[]` (override the customer\'s default email), `subject`, `message`, `attachPdf: boolean` (defaults to true), `cc: string[]`, `bcc: string[]`. The invoice must be approved first via `approve_invoice`; calling `send` on a draft returns 400. After a successful send the invoice transitions to `sent`/`unpaid` and becomes the canonical version the customer sees in the public link.',
604
590
  method: 'POST',
605
591
  pathTemplate: '/partners/invoices/{id}/send',
606
- inputSchema: idOptionalBodyInputSchema,
607
592
  requiresUserAuth: true,
608
593
  readOnly: false,
609
594
  destructive: false,
@@ -614,7 +599,6 @@ const tools = [
614
599
  description: 'Get a public payment/view link for an invoice by external UUID.',
615
600
  method: 'GET',
616
601
  pathTemplate: '/partners/invoices/{id}/public-link',
617
- inputSchema: idInputSchema,
618
602
  requiresUserAuth: true,
619
603
  readOnly: true,
620
604
  destructive: false,
@@ -625,7 +609,6 @@ const tools = [
625
609
  description: 'Get the audit log for an invoice by external UUID.',
626
610
  method: 'GET',
627
611
  pathTemplate: '/partners/invoices/{id}/audit-log',
628
- inputSchema: idInputSchema,
629
612
  requiresUserAuth: true,
630
613
  readOnly: true,
631
614
  destructive: false,
@@ -636,7 +619,6 @@ const tools = [
636
619
  description: 'Attach documents to an invoice (or estimate / memo) by external UUID using already-hosted URLs — the COUNT server fetches each URL once and stores it as a regular invoice attachment. Body: `{ attachments: [{ url: string, title?: string }] }`. The URL must be publicly fetchable from the COUNT server (no signed-cookie / VPN-only links). For uploading raw bytes use the dedicated `/attachments/upload` multipart endpoint instead — that path is not exposed as an MCP tool because MCP tools cannot send multipart bodies.',
637
620
  method: 'POST',
638
621
  pathTemplate: '/partners/invoices/{id}/attachments',
639
- inputSchema: idBodyInputSchema,
640
622
  requiresUserAuth: true,
641
623
  readOnly: false,
642
624
  destructive: false,
@@ -647,7 +629,6 @@ const tools = [
647
629
  description: 'Apply one or more credit memos to a single invoice by external UUID. Body: `{ creditMemos: [{ id: <invoice or memo UUID>, amount: <decimal> }, ...] }`. Each `id` accepts a UUID, a numeric id, or a numeric string — the partner middleware resolves them and asserts each memo belongs to the same workspace. The sum of `amount` values cannot exceed the open balance of the target invoice or the available balance on the memos; the request rolls back atomically if any memo is over-applied.',
648
630
  method: 'PATCH',
649
631
  pathTemplate: '/partners/invoices/{id}/apply-multiple-credit-to-invoice',
650
- inputSchema: idBodyInputSchema,
651
632
  requiresUserAuth: true,
652
633
  readOnly: false,
653
634
  destructive: false,
@@ -658,7 +639,6 @@ const tools = [
658
639
  description: 'Apply a single credit memo (the URL `id`) across multiple target invoices in one call. Body: `{ invoices: [{ id: <invoice UUID or numeric id>, amount: <decimal> }, ...] }`. The URL `id` is the credit memo to draw from; each entry in `invoices` is a target invoice plus the dollar amount of the memo to apply to it. Total applied amount cannot exceed the memo\'s remaining balance. The partner middleware resolves UUIDs to numeric ids and rejects cross-workspace references with 404.',
659
640
  method: 'PATCH',
660
641
  pathTemplate: '/partners/invoices/{id}/apply-credit-to-multiple-invoices',
661
- inputSchema: idBodyInputSchema,
662
642
  requiresUserAuth: true,
663
643
  readOnly: false,
664
644
  destructive: false,
@@ -669,7 +649,6 @@ const tools = [
669
649
  description: 'Remove a previously-applied credit memo from an invoice by external UUID. Body: `{ memoId: <credit memo UUID or numeric id> }`. The memo\'s balance is restored and the target invoice\'s open balance increases by the unapplied amount. 404 if the memo isn\'t currently applied to this invoice.',
670
650
  method: 'PATCH',
671
651
  pathTemplate: '/partners/invoices/{id}/remove-credit',
672
- inputSchema: idBodyInputSchema,
673
652
  requiresUserAuth: true,
674
653
  readOnly: false,
675
654
  destructive: false,
@@ -680,7 +659,6 @@ const tools = [
680
659
  description: 'Remove a previously-applied transaction payment from an invoice by external UUID. Body: `{ transactionId: <transaction UUID from list_transactions>, withCaution?: boolean }`. Restores the invoice open balance and unlinks the transaction from Accounts Receivable. Manual payment links only — automated Stripe links may not be removable via this endpoint.',
681
660
  method: 'PATCH',
682
661
  pathTemplate: '/partners/invoices/{id}/remove-transaction',
683
- inputSchema: idBodyInputSchema,
684
662
  requiresUserAuth: true,
685
663
  readOnly: false,
686
664
  destructive: false,
@@ -691,7 +669,6 @@ const tools = [
691
669
  description: 'List recurring invoice templates for the authenticated COUNT workspace.',
692
670
  method: 'GET',
693
671
  pathTemplate: '/partners/recurring-invoice-templates',
694
- inputSchema: queryInputSchema,
695
672
  requiresUserAuth: true,
696
673
  readOnly: true,
697
674
  destructive: false,
@@ -702,7 +679,6 @@ const tools = [
702
679
  description: 'Get a recurring invoice template by external UUID.',
703
680
  method: 'GET',
704
681
  pathTemplate: '/partners/recurring-invoice-templates/{id}',
705
- inputSchema: idInputSchema,
706
682
  requiresUserAuth: true,
707
683
  readOnly: true,
708
684
  destructive: false,
@@ -710,10 +686,9 @@ const tools = [
710
686
  {
711
687
  name: 'COUNT_create_recurring_invoice_template',
712
688
  title: 'Create Recurring Invoice Template',
713
- description: 'Create a recurring invoice template. Same body shape as `create_invoice` (`customerUuid`, `products`, etc.) PLUS a required `recurrencePattern` object describing the cadence (e.g. monthly, weekly). Optional `inAdvanceCreationDays` controls how many days before each scheduled date the next invoice instance is generated. `invoiceType` defaults to "invoice"; "memo" is rejected (credit memos cannot recur). `isDraft` defaults to true so the template is paused until you explicitly resume it via `resume_recurring_invoice_template`.',
689
+ description: 'Create a recurring invoice template. Same body shape as `create_invoice` (`customerUuid`, `products`, etc.) PLUS a required `recurrencePattern` string cadence (daily, weekly, biweekly, monthly, quarterly, or yearly). Optional `inAdvanceCreationDays` controls how many days before each scheduled date the next invoice instance is generated. `invoiceType` defaults to "invoice"; "memo" is rejected (credit memos cannot recur). `isDraft` defaults to true so the template is paused until you explicitly resume it via `resume_recurring_invoice_template`.',
714
690
  method: 'POST',
715
691
  pathTemplate: '/partners/recurring-invoice-templates',
716
- inputSchema: bodyInputSchema,
717
692
  requiresUserAuth: true,
718
693
  readOnly: false,
719
694
  destructive: false,
@@ -724,7 +699,6 @@ const tools = [
724
699
  description: 'Update a recurring invoice template by external UUID.',
725
700
  method: 'PATCH',
726
701
  pathTemplate: '/partners/recurring-invoice-templates/{id}',
727
- inputSchema: idBodyInputSchema,
728
702
  requiresUserAuth: true,
729
703
  readOnly: false,
730
704
  destructive: false,
@@ -735,7 +709,6 @@ const tools = [
735
709
  description: 'Delete a recurring invoice template by external UUID.',
736
710
  method: 'DELETE',
737
711
  pathTemplate: '/partners/recurring-invoice-templates/{id}',
738
- inputSchema: idInputSchema,
739
712
  requiresUserAuth: true,
740
713
  readOnly: false,
741
714
  destructive: true,
@@ -746,7 +719,6 @@ const tools = [
746
719
  description: 'Pause a recurring invoice template by external UUID.',
747
720
  method: 'POST',
748
721
  pathTemplate: '/partners/recurring-invoice-templates/{id}/pause',
749
- inputSchema: idOptionalBodyInputSchema,
750
722
  requiresUserAuth: true,
751
723
  readOnly: false,
752
724
  destructive: false,
@@ -757,7 +729,6 @@ const tools = [
757
729
  description: 'Resume a recurring invoice template by external UUID.',
758
730
  method: 'POST',
759
731
  pathTemplate: '/partners/recurring-invoice-templates/{id}/resume',
760
- inputSchema: idOptionalBodyInputSchema,
761
732
  requiresUserAuth: true,
762
733
  readOnly: false,
763
734
  destructive: false,
@@ -765,10 +736,9 @@ const tools = [
765
736
  {
766
737
  name: 'COUNT_list_bills',
767
738
  title: 'List Bills',
768
- description: 'List vendor bills for the authenticated workspace. Common `query` filters: `page`, `limit` (default 50), `orderBy`, `orderDirection`, `projectUuids` (comma-separated project UUIDs), `vendorUuids` (comma-separated vendor UUIDs from `list_vendors` — do NOT pass numeric `vendors`), `approvalStatus`, `status` (comma-separated combination of approval + payment status tokens, e.g. `"draft,approved,paid"`), `billType` (`"bill"` | `"vendor_memo"`), `currency`, `search` (substring against bill number / vendor name), `startDate` / `endDate` (bill-date window, YYYY-MM-DD), `startDueDate` / `endDueDate` (due-date window), `amount` + `amountOperator` (e.g. `amount=100&amountOperator=gte`). Returns bill rows with `id` exposed as the external UUID pass that UUID back to `get_bill` / `update_bill` / `approve_bill` / `delete_bill`.',
739
+ description: 'List vendor bills for the authenticated workspace. Returns bill rows with `id` as the external UUID. Filter fields are documented on the `query` input schema.',
769
740
  method: 'GET',
770
741
  pathTemplate: '/partners/bills',
771
- inputSchema: queryInputSchema,
772
742
  requiresUserAuth: true,
773
743
  readOnly: true,
774
744
  destructive: false,
@@ -779,7 +749,6 @@ const tools = [
779
749
  description: 'Get a single bill by external UUID. Returns the bill record with line items, attachments, payments, current approval/payment status, and the linked vendor. Note: this endpoint also returns soft-deleted bills, so check the `isDeleted` field on the response if you need to filter them out client-side.',
780
750
  method: 'GET',
781
751
  pathTemplate: '/partners/bills/{id}',
782
- inputSchema: idInputSchema,
783
752
  requiresUserAuth: true,
784
753
  readOnly: true,
785
754
  destructive: false,
@@ -790,7 +759,6 @@ const tools = [
790
759
  description: 'Create a vendor bill (accounts payable). Required body: `vendorUuid` (vendor UUID from `list_vendors`), `date` (YYYY-MM-DD), `dueDate` (YYYY-MM-DD), and `lineItems: [{ categoryAccountUuid, description?, quantity, price, total?, taxes?, projectUuid?, customerUuid? }]` where `categoryAccountUuid` is the expense/category account UUID from `list_accounts`. Optional body: `billNumber`, `purchaseOrderNumber`, `notes`, `currency` (defaults to workspace currency), `attachments: [{ url, title? }]`, `tagUuids: string[]`, `projectUuid`. Do NOT send numeric `vendorId`, `categoryAccountId`, or `tags` — use UUID fields only. The created bill enters the `draft` state; call `approve_bill` to post journals and make it eligible for payment. Returns 201 with the bill, or a `{ statusCode, message }` error envelope on validation failure.',
791
760
  method: 'POST',
792
761
  pathTemplate: '/partners/bills',
793
- inputSchema: bodyInputSchema,
794
762
  requiresUserAuth: true,
795
763
  readOnly: false,
796
764
  destructive: false,
@@ -801,7 +769,6 @@ const tools = [
801
769
  description: 'Update a bill by external UUID. Pass only the fields you want to change. Editable: `vendorUuid`, `date`, `dueDate`, `billNumber`, `purchaseOrderNumber`, `notes`, `currency`, `lineItems` (replaces all lines when provided; each line uses `categoryAccountUuid`, `quantity`, `price`, optional `projectUuid`, `customerUuid`), `tagUuids`, `projectUuid`, `attachments`. Do NOT send numeric `vendorId`, `categoryAccountId`, or `tags`. Approved/paid bills have a more restrictive editable set — expect 400 on date/line changes after approval; use `delete_bill` + recreate if you need a structural change post-approval.',
802
770
  method: 'PATCH',
803
771
  pathTemplate: '/partners/bills/{id}',
804
- inputSchema: idBodyInputSchema,
805
772
  requiresUserAuth: true,
806
773
  readOnly: false,
807
774
  destructive: false,
@@ -812,7 +779,6 @@ const tools = [
812
779
  description: 'Delete a bill by external UUID. Soft-delete on draft bills; bills that have any payments applied (`paidAmount > 0`) cannot be deleted and return 400 — unapply payments first via the standard payment endpoints.',
813
780
  method: 'DELETE',
814
781
  pathTemplate: '/partners/bills/{id}',
815
- inputSchema: idInputSchema,
816
782
  requiresUserAuth: true,
817
783
  readOnly: false,
818
784
  destructive: true,
@@ -823,7 +789,6 @@ const tools = [
823
789
  description: 'Approve a draft bill by external UUID. Posts the underlying journal entries (debit each line\'s expense account, credit Accounts Payable) and makes the bill eligible for payment. Body is optional. Errors with 400 if the bill is already approved or has no line items.',
824
790
  method: 'POST',
825
791
  pathTemplate: '/partners/bills/{id}/approve',
826
- inputSchema: idOptionalBodyInputSchema,
827
792
  requiresUserAuth: true,
828
793
  readOnly: false,
829
794
  destructive: false,
@@ -834,7 +799,6 @@ const tools = [
834
799
  description: 'Apply one or more vendor credit memos toward an approved bill by external UUID. Body: `{ memosBills: [{ id: <vendor-memo bill UUID from list_bills with billType vendor_memo>, amount: <decimal> }, ...] }`. Each memo must belong to the same vendor as the target bill; total applied cannot exceed the bill open balance or memo available balance. Vendor memos are bill records with `billType: "vendor_memo"` — use their UUID as `id`.',
835
800
  method: 'POST',
836
801
  pathTemplate: '/partners/bills/{id}/apply-vendor-memos',
837
- inputSchema: idBodyInputSchema,
838
802
  requiresUserAuth: true,
839
803
  readOnly: false,
840
804
  destructive: false,
@@ -845,7 +809,6 @@ const tools = [
845
809
  description: 'Remove a previously-applied transaction payment from a bill by external UUID. Body: `{ transactionId: <transaction UUID from list_transactions>, withCaution?: boolean }`. Restores the bill open balance and clears the transaction\'s bill link.',
846
810
  method: 'POST',
847
811
  pathTemplate: '/partners/bills/{id}/unassign-transaction',
848
- inputSchema: idBodyInputSchema,
849
812
  requiresUserAuth: true,
850
813
  readOnly: false,
851
814
  destructive: false,
@@ -856,7 +819,6 @@ const tools = [
856
819
  description: 'List journal entries for the authenticated COUNT workspace.',
857
820
  method: 'GET',
858
821
  pathTemplate: '/partners/journal-entries',
859
- inputSchema: queryInputSchema,
860
822
  requiresUserAuth: true,
861
823
  readOnly: true,
862
824
  destructive: false,
@@ -867,7 +829,6 @@ const tools = [
867
829
  description: 'Create a manual journal entry. Body: required `descriptionEntry` (memo), `date` (YYYY-MM-DD), and `lines` (array). Each line needs `accountUuid` (the `id`, `accUuid`, or `accountUuid` from `list_accounts`) plus exactly one of `amountDebit` or `amountCredit` (decimal numbers as strings or numbers — never both on the same line, never neither). Optional per-line: `descriptionLine`. Optional top-level: `refNumber`, `withCaution: true` to skip book-closeness checks (use sparingly). The server validates each account exists in this workspace before posting; manual entries can fail if any line\'s posting date falls within a reconciled period for that account.',
868
830
  method: 'POST',
869
831
  pathTemplate: '/partners/journal-entries',
870
- inputSchema: bodyInputSchema,
871
832
  requiresUserAuth: true,
872
833
  readOnly: false,
873
834
  destructive: false,
@@ -878,7 +839,6 @@ const tools = [
878
839
  description: 'Create up to 100 manual journal entry postings in a single call. Body: `{ "journalEntries": [<posting>, ...] }` where each `<posting>` uses the EXACT same shape as `COUNT_create_journal_entry` (required `descriptionEntry`, `date`, `lines`; each line needs `accountUuid` + exactly one of `amountDebit` / `amountCredit`; optional per-line `descriptionLine`; optional top-level `refNumber`, `withCaution`). **Partial-success contract**: each posting runs in its own DB transaction; one bad posting never rolls back another posting\'s writes. Response envelope: `{ "successCount": N, "errorCount": M, "results": [{ "index": 0, "success": true, "journalEntry": [...lines] } | { "index": 1, "success": false, "error": "..." }, ...] }` — read `errorCount` first; if non-zero, iterate `results` and only retry the postings where `success: false`. The HTTP status is always `201` when the batch was accepted, even if all postings failed. **Cap**: 100 postings per call (split larger imports into multiple calls). **Recommended batch size**: ~25 postings for large historical imports; larger batches increase timeout risk. **Performance note**: account UUIDs are resolved in a SINGLE batched lookup across the whole batch, so a 100-posting × 10-line request issues one account query — bulk is strictly cheaper than calling `COUNT_create_journal_entry` 100 times. **Use cases**: replay manual postings from another GL, backfill accruals, ingest a CSV of journal entries. For a single posting, prefer `COUNT_create_journal_entry` — the response shape is simpler.',
879
840
  method: 'POST',
880
841
  pathTemplate: '/partners/journal-entries/bulk',
881
- inputSchema: bodyInputSchema,
882
842
  requiresUserAuth: true,
883
843
  readOnly: false,
884
844
  destructive: false,
@@ -886,10 +846,9 @@ const tools = [
886
846
  {
887
847
  name: 'COUNT_update_journal_entry',
888
848
  title: 'Update Journal Entry',
889
- description: 'Update a manually-created journal entry by external UUID. Same body shape as `create_journal_entry` (top-level `descriptionEntry`, `date`, `refNumber`, plus the full replacement `lines` array each line takes `accountUuid` and exactly one of `amountDebit`/`amountCredit`). Only manually-created entries (and Square integration entries) can be updated; system-generated journal entries from invoices, bills, payroll, etc. are read-only and return 400.',
849
+ description: 'Update a manually-created journal entry by external UUID. PATCH accepts partial bodies send only the fields you want to change (`descriptionEntry`, `date`, `refNumber`, and/or the full replacement `lines` array). When `lines` is included, each line takes `accountUuid` and exactly one of `amountDebit`/`amountCredit`. Only manually-created entries (and Square integration entries) can be updated; system-generated journal entries from invoices, bills, payroll, etc. are read-only and return 400.',
890
850
  method: 'PATCH',
891
851
  pathTemplate: '/partners/journal-entries/{id}',
892
- inputSchema: idBodyInputSchema,
893
852
  requiresUserAuth: true,
894
853
  readOnly: false,
895
854
  destructive: false,
@@ -900,7 +859,6 @@ const tools = [
900
859
  description: 'Delete a journal entry by external UUID.',
901
860
  method: 'DELETE',
902
861
  pathTemplate: '/partners/journal-entries/{id}',
903
- inputSchema: idInputSchema,
904
862
  requiresUserAuth: true,
905
863
  readOnly: false,
906
864
  destructive: true,
@@ -911,7 +869,6 @@ const tools = [
911
869
  description: 'List tasks for the authenticated workspace. `query` filters that accept comma-separated tokens where each token is EITHER an external UUID OR a workspace-scoped numeric id (partner middleware resolves both): `statusId` (task statuses), `assigneeId` / `assignerId` / `createdById` (workspace user references), `projects` (project filter), `commentId` (single token only — passing multiple returns 400). Pass-through filters: `page`, `limit`, `orderBy`, `orderDirection`, `search` (ILIKE on task name), `customId`, `visibility` (`"firm-team"` | `"team-only"` | `"firm-only"` — defaults to `["firm-team","team-only"]` when omitted), `priority`, `overduesOnly`, `deadlineDate` / `createdAt` / `updatedAt` (date windows), `status` (string `task.status` column). The legacy filters `clients`, `openTaskOnly`, and `isForClosing` are always stripped server-side. Numeric internal foreign keys are removed from response rows.',
912
870
  method: 'GET',
913
871
  pathTemplate: '/partners/tasks',
914
- inputSchema: queryInputSchema,
915
872
  requiresUserAuth: true,
916
873
  readOnly: true,
917
874
  destructive: false,
@@ -922,7 +879,6 @@ const tools = [
922
879
  description: 'Get a single task by external UUID. Default visibility is `["firm-team","team-only"]`; tasks created with `firm-only` visibility (notably system-generated INTERNAL_TASK rows) return 404 unless you explicitly pass `visibility=firm-only` under `query`.',
923
880
  method: 'GET',
924
881
  pathTemplate: '/partners/tasks/{id}',
925
- inputSchema: idInputSchema,
926
882
  requiresUserAuth: true,
927
883
  readOnly: true,
928
884
  destructive: false,
@@ -933,7 +889,6 @@ const tools = [
933
889
  description: 'Create a task. Required body: `type` (string — the task type identifier). The following optional FK fields each accept EITHER an external UUID OR a workspace-scoped numeric id (partner middleware resolves both and verifies workspace scope): `assigneeId`, `statusId`, `onCompleteStatusId`, `onCompleteAssigneeId`, `projectId`, `billId`, `transactionId`, `taskListId`, `taskListTemplateId`. Other safe body fields: `name`, `description`, `priority`, `deadline` (YYYY-MM-DD), `customId`, `visibility`. `tags` accepts an array OR a JSON-encoded string — every element must be the same kind (all UUIDs or all positive integer ids); mixed inputs return 400. If `assigneeId` is omitted it defaults to the partner-acting user. INTERNAL_TASK rows are always created with `firm-only` visibility regardless of the `visibility` value you pass. The partner endpoint is JSON-only — multipart attachments are not supported.',
934
890
  method: 'POST',
935
891
  pathTemplate: '/partners/tasks',
936
- inputSchema: bodyInputSchema,
937
892
  requiresUserAuth: true,
938
893
  readOnly: false,
939
894
  destructive: false,
@@ -944,7 +899,6 @@ const tools = [
944
899
  description: 'Update a task by external UUID. Pass only the fields you want to change; the same body shape as `create_task` applies. `tags` accepts either an array or a JSON-encoded string and replaces all tags atomically. UUID-resolvable optional FKs: `assigneeId`, `statusId`, `onCompleteStatusId`, `onCompleteAssigneeId`, `projectId`, `billId`, `transactionId`, `taskListId`, `taskListTemplateId` — each accepts a UUID or a workspace-scoped numeric id.',
945
900
  method: 'PATCH',
946
901
  pathTemplate: '/partners/tasks/{id}',
947
- inputSchema: idBodyInputSchema,
948
902
  requiresUserAuth: true,
949
903
  readOnly: false,
950
904
  destructive: false,
@@ -955,7 +909,6 @@ const tools = [
955
909
  description: 'Delete a task by external UUID.',
956
910
  method: 'DELETE',
957
911
  pathTemplate: '/partners/tasks/{id}',
958
- inputSchema: idInputSchema,
959
912
  requiresUserAuth: true,
960
913
  readOnly: false,
961
914
  destructive: true,
@@ -966,7 +919,6 @@ const tools = [
966
919
  description: 'List projects for the authenticated workspace. Soft-deleted projects are always excluded. `query` filters: `page` (default 1), `limit` (default 20), `search` (ILIKE on project name), `customerUuids` (comma-separated customer UUIDs — partner middleware resolves these into the internal `customers` filter), and `statusUuids` (comma-separated project-status UUIDs — discover them via `list_project_statuses`). The internal `customers`, `statusIds`, and `status` filters are stripped server-side; only the `*Uuids` variants take effect.',
967
920
  method: 'GET',
968
921
  pathTemplate: '/partners/projects',
969
- inputSchema: queryInputSchema,
970
922
  requiresUserAuth: true,
971
923
  readOnly: true,
972
924
  destructive: false,
@@ -977,7 +929,6 @@ const tools = [
977
929
  description: 'List the project statuses available in the authenticated workspace. Returns each status with its name, color, ordering, and external UUID — use the returned UUID as `statusUuid` on `create_project` / `update_project`, or in `statusUuids` (CSV) on `list_projects`.',
978
930
  method: 'GET',
979
931
  pathTemplate: '/partners/projects/statuses',
980
- inputSchema: queryInputSchema,
981
932
  requiresUserAuth: true,
982
933
  readOnly: true,
983
934
  destructive: false,
@@ -988,7 +939,6 @@ const tools = [
988
939
  description: 'Get a single project by external UUID. Soft-deleted projects return 404.',
989
940
  method: 'GET',
990
941
  pathTemplate: '/partners/projects/{id}',
991
- inputSchema: idInputSchema,
992
942
  requiresUserAuth: true,
993
943
  readOnly: true,
994
944
  destructive: false,
@@ -999,7 +949,6 @@ const tools = [
999
949
  description: 'List the tasks attached to a project by external UUID. `query` filters: `page` (default 1), `limit` (default 20), `search` (ILIKE on task name). Returns the same partner-sanitized task envelope as `list_tasks`.',
1000
950
  method: 'GET',
1001
951
  pathTemplate: '/partners/projects/{id}/tasks',
1002
- inputSchema: idInputSchema,
1003
952
  requiresUserAuth: true,
1004
953
  readOnly: true,
1005
954
  destructive: false,
@@ -1010,7 +959,6 @@ const tools = [
1010
959
  description: 'Create a project. Required body: non-empty `name`. Optional body: `customerUuid` (from `list_customers`; pass `null` or omit to leave the project uncustomered), `statusUuid` (from `list_project_statuses` — defaults to the workspace\'s default status if omitted), plus any safe project columns (e.g. `description`, `customId`, `startDate`, `endDate`). The numeric internal FKs `customerId`, `statusId`, and `teamId` are always stripped before the request is processed — use the `*Uuid` variants instead.',
1011
960
  method: 'POST',
1012
961
  pathTemplate: '/partners/projects',
1013
- inputSchema: bodyInputSchema,
1014
962
  requiresUserAuth: true,
1015
963
  readOnly: false,
1016
964
  destructive: false,
@@ -1021,7 +969,6 @@ const tools = [
1021
969
  description: 'Update a project by external UUID. Pass only the fields you want to change. `customerUuid` accepts a UUID (set the customer), `null`/`""` (clear the customer), or omission (leave unchanged). `statusUuid` accepts a UUID (set the status) or omission (leave unchanged). The numeric `customerId`, `statusId`, and `teamId` fields are stripped server-side. If you pass `customId` matching the existing value it may be discarded silently.',
1022
970
  method: 'PATCH',
1023
971
  pathTemplate: '/partners/projects/{id}',
1024
- inputSchema: idBodyInputSchema,
1025
972
  requiresUserAuth: true,
1026
973
  readOnly: false,
1027
974
  destructive: false,
@@ -1032,7 +979,6 @@ const tools = [
1032
979
  description: 'Delete a project by external UUID (soft delete).',
1033
980
  method: 'DELETE',
1034
981
  pathTemplate: '/partners/projects/{id}',
1035
- inputSchema: idInputSchema,
1036
982
  requiresUserAuth: true,
1037
983
  readOnly: false,
1038
984
  destructive: true,
@@ -1043,7 +989,6 @@ const tools = [
1043
989
  description: 'List time entries for the authenticated workspace. Soft-deleted entries are always excluded. `query` filters: `page` (default 1), `limit` (default 50), and the comma-separated UUID filters `peopleUuids`, `projectUuids`, `customerUuids`, `productServiceUuids` (each resolved into its internal numeric filter by the partner middleware). Direct numeric filters (`people`, `projects`, `customers`, `productServices`) are stripped server-side; only the `*Uuids` variants take effect. Date window filter: both `startDate` AND `endDate` (YYYY-MM-DD) must be provided together for the `createdAt`-range filter to apply — passing only one is ignored.',
1044
990
  method: 'GET',
1045
991
  pathTemplate: '/partners/time-entries',
1046
- inputSchema: queryInputSchema,
1047
992
  requiresUserAuth: true,
1048
993
  readOnly: true,
1049
994
  destructive: false,
@@ -1054,7 +999,6 @@ const tools = [
1054
999
  description: 'Get a single time entry by external UUID. Soft-deleted entries return 404.',
1055
1000
  method: 'GET',
1056
1001
  pathTemplate: '/partners/time-entries/{id}',
1057
- inputSchema: idInputSchema,
1058
1002
  requiresUserAuth: true,
1059
1003
  readOnly: true,
1060
1004
  destructive: false,
@@ -1065,7 +1009,6 @@ const tools = [
1065
1009
  description: 'Create one or more time entry rows. Required body: `peopleUuid` (from `list_people` — must be present and non-empty on create; partner middleware resolves it into the internal `peopleId`) and `timeEntries: [{ date: "YYYY-MM-DD", minutes: <integer>, ... }]` (array of row objects). Per-row optional fields: `description`, `projectUuid` (resolves to the project; setting it auto-fills the matching customer), `customerUuid`, `productServiceUuid`, `billable`, `wage`. Daily cap: the sum of `minutes` across entries on the same calendar day for the same person cannot exceed 24h — the server returns 400 with the remaining-minutes if exceeded. The numeric `peopleId`, `projectId`, `customerId`, `productServiceId`, and `teamId` fields are stripped server-side; use the `*Uuid` variants.',
1066
1010
  method: 'POST',
1067
1011
  pathTemplate: '/partners/time-entries',
1068
- inputSchema: bodyInputSchema,
1069
1012
  requiresUserAuth: true,
1070
1013
  readOnly: false,
1071
1014
  destructive: false,
@@ -1076,7 +1019,6 @@ const tools = [
1076
1019
  description: 'Update a time entry by external UUID. Pass only the fields you want to change. `peopleUuid` cannot be cleared on update (passing `null`/`""` returns 400). `projectUuid` / `customerUuid` / `productServiceUuid` each accept a UUID (set) or `null` (clear). Time entries that belong to a processed pay period are read-only and return 400.',
1077
1020
  method: 'PATCH',
1078
1021
  pathTemplate: '/partners/time-entries/{id}',
1079
- inputSchema: idBodyInputSchema,
1080
1022
  requiresUserAuth: true,
1081
1023
  readOnly: false,
1082
1024
  destructive: false,
@@ -1087,7 +1029,6 @@ const tools = [
1087
1029
  description: 'Delete a time entry by external UUID (soft delete). Time entries that belong to a processed pay period cannot be deleted and return 400 — adjust the pay period first.',
1088
1030
  method: 'DELETE',
1089
1031
  pathTemplate: '/partners/time-entries/{id}',
1090
- inputSchema: idInputSchema,
1091
1032
  requiresUserAuth: true,
1092
1033
  readOnly: false,
1093
1034
  destructive: true,
@@ -1101,7 +1042,6 @@ const tools = [
1101
1042
  description: 'List expense receipts (pending receipts) for the authenticated COUNT workspace.',
1102
1043
  method: 'GET',
1103
1044
  pathTemplate: '/partners/expense-receipts',
1104
- inputSchema: queryInputSchema,
1105
1045
  requiresUserAuth: true,
1106
1046
  readOnly: true,
1107
1047
  destructive: false,
@@ -1112,7 +1052,6 @@ const tools = [
1112
1052
  description: 'List unmatched expense receipts (pending receipts) for the authenticated COUNT workspace.',
1113
1053
  method: 'GET',
1114
1054
  pathTemplate: '/partners/expense-receipts/unmatched',
1115
- inputSchema: queryInputSchema,
1116
1055
  requiresUserAuth: true,
1117
1056
  readOnly: true,
1118
1057
  destructive: false,
@@ -1123,7 +1062,6 @@ const tools = [
1123
1062
  description: 'Create an expense receipt (pending receipt). MCP forwards JSON only; to attach a receipt image call the partner HTTP API directly as multipart/form-data with field `receipt`. Partner bodies use UUID fields such as accountUuid, categoryAccountUuid, optional expenseReportTypeUuid, vendorUuid, projectUuid, tagUuids, taxUuids — see COUNT Partner API docs. API responses omit receiptUrl by design.',
1124
1063
  method: 'POST',
1125
1064
  pathTemplate: '/partners/expense-receipts',
1126
- inputSchema: bodyInputSchema,
1127
1065
  requiresUserAuth: true,
1128
1066
  readOnly: false,
1129
1067
  destructive: false,
@@ -1134,7 +1072,6 @@ const tools = [
1134
1072
  description: 'Update an expense receipt by external UUID. MCP forwards JSON only; to replace the receipt image use multipart field `receipt` on the partner HTTP API.',
1135
1073
  method: 'PATCH',
1136
1074
  pathTemplate: '/partners/expense-receipts/{id}',
1137
- inputSchema: idBodyInputSchema,
1138
1075
  requiresUserAuth: true,
1139
1076
  readOnly: false,
1140
1077
  destructive: false,
@@ -1145,7 +1082,6 @@ const tools = [
1145
1082
  description: 'Delete an unmatched expense receipt by external UUID.',
1146
1083
  method: 'DELETE',
1147
1084
  pathTemplate: '/partners/expense-receipts/{id}',
1148
- inputSchema: idInputSchema,
1149
1085
  requiresUserAuth: true,
1150
1086
  readOnly: false,
1151
1087
  destructive: true,
@@ -1156,7 +1092,6 @@ const tools = [
1156
1092
  description: 'Match an expense receipt to an expense transaction. Body uses transactionUuid (COUNT transaction external UUID).',
1157
1093
  method: 'POST',
1158
1094
  pathTemplate: '/partners/expense-receipts/{id}/match-manually',
1159
- inputSchema: idBodyInputSchema,
1160
1095
  requiresUserAuth: true,
1161
1096
  readOnly: false,
1162
1097
  destructive: false,
@@ -1167,7 +1102,6 @@ const tools = [
1167
1102
  description: 'Unmatch an expense receipt from its linked transaction.',
1168
1103
  method: 'DELETE',
1169
1104
  pathTemplate: '/partners/expense-receipts/{id}/unmatch',
1170
- inputSchema: idInputSchema,
1171
1105
  requiresUserAuth: true,
1172
1106
  readOnly: false,
1173
1107
  destructive: true,
@@ -1178,7 +1112,6 @@ const tools = [
1178
1112
  description: 'Generate a trial balance for the authenticated workspace. Put filters under `query`: `endDate` (required, YYYY-MM-DD — the as-of date), `startDate` (optional, defaults to fiscal-year start), `currency` (defaults to workspace currency), `transactionStatus` ("all" | "reviewed" | "unreviewed" | "reconciled" | "unreconciled"), `accounts` or `accountUuids` (comma-separated account UUIDs from `list_accounts` — do NOT pass internal numeric ids), `tags` or `tagUuids` (comma-separated tag UUIDs from `list_tags`), `noTags: "true"` (only entries with no tags), `accountType` (one of "Assets", "Liabilities", "Equity", "Income", "Expenses"). This MCP tool is read-only; no `body` is accepted.',
1179
1113
  method: 'POST',
1180
1114
  pathTemplate: '/partners/reports/trial-balance',
1181
- inputSchema: queryInputSchema,
1182
1115
  requiresUserAuth: true,
1183
1116
  readOnly: true,
1184
1117
  destructive: false,
@@ -1189,7 +1122,6 @@ const tools = [
1189
1122
  description: 'Generate a P&L (income statement) for the authenticated workspace. Put filters under `query`: required `startDate` and `endDate` (YYYY-MM-DD); optional `currency`, `transactionStatus` ("all" | "reviewed" | "unreviewed" | "reconciled" | "unreconciled"), `categoryAccount`, `categoryAccountUuid`, or `categoryAccountUuids` (comma-separated account UUIDs from `list_accounts` — use this to filter to one or more income/expense categories such as "Freight & Courier"; do NOT pass internal numeric ids), `tags` or `tagUuids`, `customers` or `customerUuids`, `vendors` or `vendorUuids`, `projects` or `projectUuids`, `products` or `productUuids`, `isPnLByTag: true` (column-per-tag breakdown), `includeNoTag: true`, `reportYear` ("financial" | "calendar"). This MCP tool is read-only; no `body` is accepted. **Sign convention**: revenue rows are returned as positive amounts and expense rows as negative; net profit is the algebraic sum.',
1190
1123
  method: 'POST',
1191
1124
  pathTemplate: '/partners/reports/pnl',
1192
- inputSchema: queryInputSchema,
1193
1125
  requiresUserAuth: true,
1194
1126
  readOnly: true,
1195
1127
  destructive: false,
@@ -1200,7 +1132,6 @@ const tools = [
1200
1132
  description: 'Generate a balance sheet for the authenticated workspace. Put filters under `query`: required `endDate` (YYYY-MM-DD — the as-of date); optional `startDate` (used as the prior-period comparison anchor), `currency`, `transactionStatus`, `accounts` or `accountUuids` (comma-separated account UUIDs from `list_accounts` — do NOT pass internal numeric ids), `tags` or `tagUuids`. This MCP tool is read-only; no `body` is accepted. Returns Assets / Liabilities / Equity sections with subtotals; the report fails to balance only when there are unposted manual entries — those are surfaced in the response payload.',
1201
1133
  method: 'POST',
1202
1134
  pathTemplate: '/partners/reports/balance-sheet',
1203
- inputSchema: queryInputSchema,
1204
1135
  requiresUserAuth: true,
1205
1136
  readOnly: true,
1206
1137
  destructive: false,
@@ -1212,7 +1143,6 @@ const tools = [
1212
1143
  description: 'List P&L budgets for the authenticated workspace. Optional `query.status`: `draft`, `published`, or `archived`. Each budget returns its external UUID as `id`, metadata (name, startPeriod, cadence, actualPeriods, budgetPeriods, currencyCode, status, isOverall), and version summaries (versionNumber, label, isPublished).',
1213
1144
  method: 'GET',
1214
1145
  pathTemplate: '/partners/budgets',
1215
- inputSchema: queryInputSchema,
1216
1146
  requiresUserAuth: true,
1217
1147
  readOnly: true,
1218
1148
  destructive: false,
@@ -1223,7 +1153,6 @@ const tools = [
1223
1153
  description: 'Get the workspace Overall Budget (if configured). Returns the same budget object shape as `list_budgets` rows, or null when no overall budget exists.',
1224
1154
  method: 'GET',
1225
1155
  pathTemplate: '/partners/budgets/overall',
1226
- inputSchema: queryInputSchema,
1227
1156
  requiresUserAuth: true,
1228
1157
  readOnly: true,
1229
1158
  destructive: false,
@@ -1234,7 +1163,6 @@ const tools = [
1234
1163
  description: 'Create a draft budget and seed P&L budget lines for every active Income/Expenses account. Required body: `name`, `startPeriod` (YYYY-MM-DD), `cadence` (`monthly` | `yearly`), `actualPeriods` (>= 0), `budgetPeriods` (>= 1). Optional: `currencyCode` (defaults to workspace currency), `isOverall` (boolean — only one overall budget per workspace). Returns `{ budget, version }` with the initial versionNumber (usually 1). After create, import amounts with `bulk_update_budget_cells`.',
1235
1164
  method: 'POST',
1236
1165
  pathTemplate: '/partners/budgets',
1237
- inputSchema: bodyInputSchema,
1238
1166
  requiresUserAuth: true,
1239
1167
  readOnly: false,
1240
1168
  destructive: false,
@@ -1245,7 +1173,6 @@ const tools = [
1245
1173
  description: 'Get a single budget by external UUID, including version summaries.',
1246
1174
  method: 'GET',
1247
1175
  pathTemplate: '/partners/budgets/{id}',
1248
- inputSchema: idInputSchema,
1249
1176
  requiresUserAuth: true,
1250
1177
  readOnly: true,
1251
1178
  destructive: false,
@@ -1256,7 +1183,6 @@ const tools = [
1256
1183
  description: 'Update draft budget metadata by external UUID. Pass only fields to change: `name`, `startPeriod`, `cadence`, `actualPeriods`, `budgetPeriods`, `isOverall`. Rejected when the budget is published or locked.',
1257
1184
  method: 'PATCH',
1258
1185
  pathTemplate: '/partners/budgets/{id}',
1259
- inputSchema: idBodyInputSchema,
1260
1186
  requiresUserAuth: true,
1261
1187
  readOnly: false,
1262
1188
  destructive: false,
@@ -1267,7 +1193,6 @@ const tools = [
1267
1193
  description: 'Export a budget grid for Excel round-trips. Returns `columns` (actual + budget period labels) and `rows` with `accountUuid`, `accountName`, `accountPath`, and per-period `values`. Optional `query.versionNumber` (defaults to latest version), `query.reportType` (`accrual` | `cash`), and `query.includeActuals` (`false` skips journal-entry actuals for faster budget-only export). Use `list_accounts` / `resolve_references` to map account names before re-importing edited rows.',
1268
1194
  method: 'GET',
1269
1195
  pathTemplate: '/partners/budgets/{id}/grid',
1270
- inputSchema: idInputSchema,
1271
1196
  requiresUserAuth: true,
1272
1197
  readOnly: true,
1273
1198
  destructive: false,
@@ -1278,7 +1203,6 @@ const tools = [
1278
1203
  description: 'List version summaries for a budget: `versionNumber`, `label`, `isPublished`, `createdAt`. Use `versionNumber` on cell update tools.',
1279
1204
  method: 'GET',
1280
1205
  pathTemplate: '/partners/budgets/{id}/versions',
1281
- inputSchema: idInputSchema,
1282
1206
  requiresUserAuth: true,
1283
1207
  readOnly: true,
1284
1208
  destructive: false,
@@ -1289,7 +1213,6 @@ const tools = [
1289
1213
  description: 'Atomically update up to 100 budget cell amounts on a **draft** version. Requires `id` (budget UUID) and `versionNumber`. Body: `{ updates: [{ accountUuid, periodStart (YYYY-MM-DD), amount }] }`. Each row must reference a P&L account UUID from `list_accounts` and a budget column periodStart from `get_budget_grid`. All rows succeed or fail together.',
1290
1214
  method: 'PATCH',
1291
1215
  pathTemplate: '/partners/budgets/{id}/versions/{versionNumber}/cells',
1292
- inputSchema: idVersionNumberBodyInputSchema,
1293
1216
  requiresUserAuth: true,
1294
1217
  readOnly: false,
1295
1218
  destructive: false,
@@ -1300,7 +1223,6 @@ const tools = [
1300
1223
  description: 'Import budget amounts with partial-success semantics (recommended for spreadsheet imports). Same row shape as `update_budget_cells`: `{ updates: [{ accountUuid, periodStart, amount }, ...] }`. **Cap**: 100 rows per call; **recommended batch size**: ~25. Response: `{ successCount, errorCount, results: [{ index, success, budgetCellUpdate? | error }] }`. HTTP status is `201` when the batch is accepted even if some rows fail — retry only failed indices.',
1301
1224
  method: 'POST',
1302
1225
  pathTemplate: '/partners/budgets/{id}/versions/{versionNumber}/cells/bulk',
1303
- inputSchema: idVersionNumberBodyInputSchema,
1304
1226
  requiresUserAuth: true,
1305
1227
  readOnly: false,
1306
1228
  destructive: false,
@@ -1311,7 +1233,6 @@ const tools = [
1311
1233
  description: 'Create a new draft budget version (copies lines and cell values from the previous version). Optional body: `label`. Returns the new version with an incremented `versionNumber`.',
1312
1234
  method: 'POST',
1313
1235
  pathTemplate: '/partners/budgets/{id}/versions',
1314
- inputSchema: idOptionalBodyInputSchema,
1315
1236
  requiresUserAuth: true,
1316
1237
  readOnly: false,
1317
1238
  destructive: false,
@@ -1322,7 +1243,6 @@ const tools = [
1322
1243
  description: 'Publish a budget version and set budget status to `published`. Optional body: `versionNumber` (defaults to latest draft). Rejects cell edits on published versions.',
1323
1244
  method: 'POST',
1324
1245
  pathTemplate: '/partners/budgets/{id}/publish',
1325
- inputSchema: idOptionalBodyInputSchema,
1326
1246
  requiresUserAuth: true,
1327
1247
  readOnly: false,
1328
1248
  destructive: false,
@@ -1333,7 +1253,6 @@ const tools = [
1333
1253
  description: 'Archive a budget by external UUID. Cannot archive the Overall Budget.',
1334
1254
  method: 'POST',
1335
1255
  pathTemplate: '/partners/budgets/{id}/archive',
1336
- inputSchema: idOptionalBodyInputSchema,
1337
1256
  requiresUserAuth: true,
1338
1257
  readOnly: false,
1339
1258
  destructive: true,
@@ -1344,7 +1263,6 @@ const tools = [
1344
1263
  description: 'Duplicate a budget into a new draft. Required body: `newName`. Optional: `carryValues` (copy cell amounts), `isOverall`, `versionNumber` (source version; defaults to latest).',
1345
1264
  method: 'POST',
1346
1265
  pathTemplate: '/partners/budgets/{id}/duplicate',
1347
- inputSchema: idBodyInputSchema,
1348
1266
  requiresUserAuth: true,
1349
1267
  readOnly: false,
1350
1268
  destructive: false,
@@ -1365,12 +1283,65 @@ const tools = [
1365
1283
  'Optional `query.include` is a comma-separated list of blocks to compute (omit any block you do not need): `workspace,cash,profitability,receivables,payables,taxObligations,connections`. Omitting `include` returns every block. This is the cheapest single call to populate an end-of-period dashboard or a "how is the business doing right now" summary.',
1366
1284
  method: 'GET',
1367
1285
  pathTemplate: '/partners/workspace-stats',
1368
- inputSchema: queryInputSchema,
1369
1286
  requiresUserAuth: true,
1370
1287
  readOnly: true,
1371
1288
  destructive: false,
1372
1289
  },
1290
+ {
1291
+ name: 'COUNT_update_workspace',
1292
+ title: 'Update Workspace',
1293
+ description: 'Update workspace-level settings for the authenticated workspace. Currently supports `cutoverDate` (required, YYYY-MM-DD) — the date COUNT-managed bookkeeping begins; everything on/before it is summarized by opening balances rather than tracked line-by-line. Moving it backward (earlier) is always allowed. Moving it forward (later) is only allowed when the workspace has zero posted journal entries yet — this is a re-baseline guard, not a UI limitation, so it rejects with a 400 explaining the entry count if the workspace already has posted activity. Setting the cutover date for the first time is rejected with a 400 if the workspace already has an imported trial balance, since that would leave opening balances anchored to no cutover date — contact support to resolve first. Use `get_workspace_stats` (`workspace.cutoverDate`) to read the current value first.',
1294
+ method: 'PATCH',
1295
+ pathTemplate: '/partners/workspace',
1296
+ requiresUserAuth: true,
1297
+ readOnly: false,
1298
+ destructive: false,
1299
+ },
1300
+ {
1301
+ name: 'COUNT_get_opening_balance',
1302
+ title: 'Get Opening Balance',
1303
+ description: 'Read the workspace\'s opening balance (COUNT calls this the "conversion balance" internally): status (`none`/`draft`/`published`), the cutover date it is anchored to, and the current rows (`accountUuid`, `accountName`, `accountType`, `debit`, `credit`). Call this before `COUNT_set_opening_balance` to check whether one already exists and needs `replaceExisting: true`.',
1304
+ method: 'GET',
1305
+ pathTemplate: '/partners/opening-balance',
1306
+ requiresUserAuth: true,
1307
+ readOnly: true,
1308
+ destructive: false,
1309
+ },
1310
+ {
1311
+ name: 'COUNT_set_opening_balance',
1312
+ title: 'Set Opening Balance',
1313
+ description: 'Set the workspace\'s opening balance as of its cutover date (use `COUNT_update_workspace` to set the cutover date first). Body: `rows` (required, non-empty array of `{ accountUuid, debit, credit }` — total debits must equal total credits) and `replaceExisting` (required `true` to overwrite an existing draft or published opening balance; omitted/false rejects with 409 if one already exists — check first with `COUNT_get_opening_balance`). This drafts and immediately publishes in one call: it posts the balancing journal entry at the cutover date, so it only works while the workspace is still onboarding (`team.isOnboarding === true`) and rejects with 400 once onboarding is complete. If publish-time validation fails (e.g. unbalanced control accounts) after the draft was saved, the draft persists — retry with `replaceExisting: true` after fixing the rows.',
1314
+ method: 'POST',
1315
+ pathTemplate: '/partners/opening-balance',
1316
+ requiresUserAuth: true,
1317
+ readOnly: false,
1318
+ destructive: false,
1319
+ },
1320
+ {
1321
+ name: 'COUNT_create_reconciliation',
1322
+ title: 'Create Reconciliation',
1323
+ description: 'Start a bank reconciliation for an account: creates a DRAFT reconciliation record for a statement period. Body: `accountUuid` (required, from `list_accounts`), `endingBalanceDate` (required, YYYY-MM-DD — the statement ending date), `endingBalanceAmount` (required — the statement ending balance). Rejects with 400 if a draft or completed reconciliation already exists for that date, or if a later completed reconciliation already exists for this account (reconciliations must complete in date order). The opening balance is taken automatically from the account\'s last completed reconciliation. The response `id` is the reconciliation UUID — pass it to `COUNT_complete_reconciliation` to lock it once the ending balance is verified. Only reconciles journal entries already marked reviewed unless `COUNT_complete_reconciliation` is called with `autoReconcile: true`.',
1324
+ method: 'POST',
1325
+ pathTemplate: '/partners/reconciliations',
1326
+ requiresUserAuth: true,
1327
+ readOnly: false,
1328
+ destructive: false,
1329
+ },
1330
+ {
1331
+ name: 'COUNT_complete_reconciliation',
1332
+ title: 'Complete Reconciliation',
1333
+ description: 'Lock a draft reconciliation by its UUID (from `COUNT_create_reconciliation`), marking every reviewed journal entry in the account for that period as `reconciled: true` and stamping them with this reconciliation. Optional `body.autoReconcile: true` also marks unreviewed entries in the period as reviewed and reconciles them (defaults to false — reconciling only what a human already reviewed). Rejects with 404 if the reconciliation is not a pending draft (already completed, or does not exist), and with 400 if the ending balance date falls in a book-closed sealed period, or if completing it would violate reconciliation date ordering (an earlier draft is still open, or a later reconciliation is already complete). This is the only way to set `reconciled: true` on transactions/journal entries via the partner API — there is no direct writable field for it.',
1334
+ method: 'PATCH',
1335
+ pathTemplate: '/partners/reconciliations/{id}/complete',
1336
+ requiresUserAuth: true,
1337
+ readOnly: false,
1338
+ destructive: false,
1339
+ },
1373
1340
  ];
1341
+ const tools = rawTools.map((_tool) => ({
1342
+ ..._tool,
1343
+ inputSchema: toolInputSchemas.getToolInputSchema({ toolName: _tool.name }),
1344
+ }));
1374
1345
  export const toolDefinitions = tools;
1375
1346
  export function getToolDefinition(params) {
1376
1347
  const { toolName } = params;