@agenttax/mcp-server 1.1.1 → 1.3.0
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.
- package/CHANGELOG.md +28 -0
- package/README.md +49 -6
- package/classify.js +207 -0
- package/index.js +34 -52
- package/package.json +5 -2
- package/server.json +3 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.3.0
|
|
4
|
+
|
|
5
|
+
Adds `intent` and documents `resolution` on `calculate_tax`. No result changes when you omit `intent`.
|
|
6
|
+
|
|
7
|
+
Needs the AgentTax API release that accepts `intent` and returns `resolution` (live on agenttax.io since 2026-10-21). If you point `AGENTTAX_BASE_URL` at an older API, omit `intent`.
|
|
8
|
+
|
|
9
|
+
- **New optional `intent` argument on `calculate_tax`** (`estimate` | `record`). `estimate` quotes a price before a wallet signs: it is computed and counts toward your monthly quota, but nothing is saved to transaction history, it never counts toward nexus revenue, and it ignores the Idempotency-Key. `record` (the default for keyed calls) logs the calculation as before. Keyless demo calls are always estimates. The server sends `intent` only when you pass it, so existing calls are byte-for-byte unchanged.
|
|
10
|
+
- **`resolution` in the `calculate_tax` response** (from the same API deploy). `resolution.location`, `resolution.nexus` and `resolution.taxability` each report `status` (`resolved` | `partial` | `unknown`), `basis` and `detail`. `total_tax` is never withheld; to treat "unknown" as "do not charge", branch on `resolution.*.status`. The README has an example.
|
|
11
|
+
- `calculate_tax` keeps its writing annotation (`readOnlyHint: false`): annotations are static and a keyed call without `intent: "estimate"` still writes to history.
|
|
12
|
+
- **`work_type` accepts the five classification work types** on `calculate_tax` and `track_payment`: `payment_processing`, `web_hosting`, `software_maintenance`, `it_support`, `custom_software` (the API has accepted them since 2026-10-16; the 1.2.0 enum rejected them). `track_payment` maps each to its own `transaction_type` (`payment_processing_fee`, `web_hosting`, `software_maintenance`, `it_support`; `custom_software` is answered as a SaaS sale, since `track_payment` has no attestation). The Stripe webhook's `metadata.work_type` still accepts its five original values. Description guessing is unchanged and never produces these types.
|
|
13
|
+
- **`calculate_tax` carries the classification facts** and passes them through to the API unchanged, all optional: `payment_provider_type` (with `transaction_type: "payment_processing_fee"`), `it_support_scope` (`it_support`), `includes_prewritten_software_access` (`web_hosting`), `custom_software_attestation` (required with `work_type: "custom_software"` for the custom-software answer) and `maintenance_contract` (`software_maintenance`, including `itemized_amounts`). Names, nesting, enum values and strictness match the API: an unknown key inside `custom_software_attestation`, `maintenance_contract` or `itemized_amounts` is refused before the call is sent. Without them the API gives its conservative default, as before.
|
|
14
|
+
- **`transaction_type` accepts the four classification transaction types** on `calculate_tax` and `ingest_transactions`: `payment_processing_fee`, `web_hosting`, `software_maintenance`, `it_support` (the API has accepted them since 2026-10-16).
|
|
15
|
+
- `payment_provider_type`, `it_support_scope`, `use_context` and `digital_content_type` on `calculate_tax` are trimmed and lowercased before the enum check, as the API does, so `"Bank "` is accepted and sent as `"bank"`. `ingest_transactions` records already pass the same facts through to the API.
|
|
16
|
+
- The enums now live in `classify.js` and a test pins them to a copy of the API's values (`test/fixtures/api-calculate-enums.json`).
|
|
17
|
+
- No other tool changed.
|
|
18
|
+
|
|
19
|
+
## 1.2.0
|
|
20
|
+
|
|
21
|
+
Tax-safety release for `track_payment`. Some results change; read before upgrading.
|
|
22
|
+
|
|
23
|
+
- **Guessed consulting and trading are no longer exempt.** 1.1.1 guessed a work type from `description` and sent it as if you had declared it. A description that looked like consulting ("SOC 2 audit platform subscription", "tax advice") sold to an Arizona buyer came back $0 under the AZ professional-services exemption, and one that looked like trading ("game asset pack", "token swap fee") came back $0 in every state as a financial service. Both guesses are now taxed as SaaS. If the sale really is consulting or trading, pass `work_type` and the exemption applies as before.
|
|
24
|
+
- **New optional `work_type` argument on `track_payment`** (`compute` | `research` | `information_service` | `content` | `consulting` | `trading`). It overrides the description and is treated as a declaration. Pass it for an exact answer.
|
|
25
|
+
- **Guessed compute and research are unchanged** from 1.1.1. They are not exemptions, but those categories can tax less than SaaS in some states (e.g. CT, MD, SC, TN, WV). Pass `work_type` for an exact answer.
|
|
26
|
+
- **`is_b2b` is sent only when you pass it.** 1.1.1 always sent `is_b2b: false`, which silently declared every buyer a consumer. Ohio sellers to consumers: pass `is_b2b: false`, or the sale is now taxed (Ohio exempts SaaS and data processing only for a declared consumer; an unknown buyer is taxed). With `is_b2b` omitted, AgentTax may also auto-detect a business buyer when the counterparty has a KYA principal record marked as a business, and apply B2B rules (e.g. Maryland 3%, Iowa B2B exemption); 1.1.1's explicit `false` blocked that. Pass `is_b2b` yourself if you know the buyer.
|
|
27
|
+
- The `track_payment` response has a new `classification` field saying where the work type came from (`explicit`, `inferred_from_description` or `default`) and, when a consulting or trading guess was not used, a note telling you to resend with `work_type`.
|
|
28
|
+
- No other tool changed. `calculate_tax` and `ingest_transactions` already sent only what you gave them.
|
package/README.md
CHANGED
|
@@ -59,8 +59,7 @@ Every payment is automatically classified, taxed, and logged. No code changes re
|
|
|
59
59
|
### Claude Code
|
|
60
60
|
|
|
61
61
|
```bash
|
|
62
|
-
claude mcp add agenttax -- npx @agenttax/mcp-server
|
|
63
|
-
export AGENTTAX_API_KEY=atx_live_your_key
|
|
62
|
+
claude mcp add agenttax -e AGENTTAX_API_KEY=atx_live_your_key -- npx -y @agenttax/mcp-server
|
|
64
63
|
```
|
|
65
64
|
|
|
66
65
|
### Claude Desktop / Cursor / Windsurf
|
|
@@ -81,7 +80,7 @@ Add to your MCP config file:
|
|
|
81
80
|
}
|
|
82
81
|
```
|
|
83
82
|
|
|
84
|
-
Demo mode works without a key (
|
|
83
|
+
Demo mode works without a key (10 calls/day, no account required). Tools marked "Yes" below need `AGENTTAX_API_KEY`.
|
|
85
84
|
|
|
86
85
|
---
|
|
87
86
|
|
|
@@ -115,12 +114,15 @@ track_payment({
|
|
|
115
114
|
buyer_state: "TX",
|
|
116
115
|
buyer_zip: "78701",
|
|
117
116
|
description: "MCP tool subscription",
|
|
117
|
+
work_type: "content",
|
|
118
118
|
payment_id: "pi_stripe_abc123",
|
|
119
119
|
source: "stripe"
|
|
120
120
|
})
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
-
Returns `tax_owed`, `tax_rate`, `taxable`, `transaction_id
|
|
123
|
+
Returns `tax_owed`, `tax_rate`, `taxable`, `transaction_id`, a `classification` (where the work type came from) and a `compliance_note`. Sellers get $0 in states where no nexus is configured; the response says so in `nexus_warning`.
|
|
124
|
+
|
|
125
|
+
**Pass `work_type` explicitly** (`compute` | `research` | `information_service` | `content` | `consulting` | `trading` | `payment_processing` | `web_hosting` | `software_maintenance` | `it_support` | `custom_software`) when you know what you sold. The last five are the classification work types the API added in October 2026: a payment or facilitator fee, hosting the buyer's own site, a software maintenance contract, IT support, and software written for one buyer. Each sends its own `transaction_type` and the API answers with that state's rule for it. It is a declaration and gives the exact answer, and it is the only way the consulting or trading exemption applies: for example, AI consulting sold to an Arizona buyer is an exempt professional service (A.R.S. §42-5061(A)(1), TPR 93-48), so `work_type: "consulting"` returns $0 in AZ. Without `work_type` the server guesses from `description`, but a guess never takes the consulting or trading exemption: a description that looks like consulting (e.g. "SOC 2 audit platform subscription") or trading is taxed as SaaS, and `classification.note` tells you to resend with `work_type` if the guess was right. A description that looks like compute or research keeps the 1.1.1 classification, which can tax less than SaaS in some states (e.g. CT, MD, SC, TN, WV); pass `work_type` for an exact answer. Leave `is_b2b` out when you don't know the buyer; `false` declares a consumer. Upgrading from 1.1.1? Some `track_payment` results change; see [CHANGELOG.md](CHANGELOG.md).
|
|
124
126
|
|
|
125
127
|
### calculate_tax
|
|
126
128
|
|
|
@@ -137,7 +139,48 @@ calculate_tax({
|
|
|
137
139
|
})
|
|
138
140
|
```
|
|
139
141
|
|
|
140
|
-
Optional fields: `seller_state` / `seller_zip` (origin-sourced intrastate sales in TX, UT, AZ, TN and some OH sales), `use_context` (Maryland B2B), `digital_content_type` (with `transaction_type: "digital_good"`), `seller_remitting`.
|
|
142
|
+
Optional fields: `seller_state` / `seller_zip` (origin-sourced intrastate sales in TX, UT, AZ, TN and some OH sales), `use_context` (Maryland B2B), `digital_content_type` (with `transaction_type: "digital_good"`), `seller_remitting`. `transaction_type` and `work_type` are sent exactly as given and treated as declared facts; don't guess them (`consulting` is exempt in Arizona).
|
|
143
|
+
|
|
144
|
+
**Classification facts** (optional; sent to the API as given, except that `payment_provider_type` and `it_support_scope` are trimmed and lowercased first, as the API does). Each one applies to one of the classification types; leave it out and the API gives its conservative default for that type.
|
|
145
|
+
|
|
146
|
+
- `payment_provider_type` (`bank` | `licensed_money_transmitter` | `card_network` | `processor_routing_to_bank_or_network` | `other`), with `transaction_type: "payment_processing_fee"`: who provides the payment processing.
|
|
147
|
+
- `it_support_scope` (`help_desk_advice` | `remote_repair_or_config` | `software_installation` | `monitoring` | `security_monitoring` | `managed_bundle`), with `it_support`: what the engagement is. Absent means `managed_bundle`.
|
|
148
|
+
- `includes_prewritten_software_access` (boolean), with `transaction_type: "web_hosting"`: `true` when the plan gives the buyer vendor software beyond an incidental control panel or file manager (site builder, CMS or app platform, email or office suite, managed application).
|
|
149
|
+
- `custom_software_attestation`, required with `work_type: "custom_software"`; without a complete one the sale is answered as prewritten software / SaaS. Required keys: `written_for_buyer`, `not_offered_to_others`, `deliverable` (`source_code` | `compiled_artifact` | `hosted`), `is_modification_of_prewritten`. Optional state facts: `separately_stated_charge`, `all_rights_transferred`, `delivery` (`electronic` | `tangible`), `il_requirements_analysis_and_adaptation`, `il_assembled_modules_50pct`.
|
|
150
|
+
- `maintenance_contract`, with `transaction_type: "software_maintenance"`. All keys optional: `mandatory`, `includes_upgrades`, `includes_support`, `separately_itemized`, `itemized_amounts` (`{ updates, support }`, summing to `amount`), `update_delivery` (`electronic` | `tangible`), `support_kind` (`help_desk_only` | `includes_fixes`), `seller_sold_software`, `business_use`.
|
|
151
|
+
|
|
152
|
+
Both objects are strict, as in the API: an unknown key is refused before the call is sent. The facts are your representation and are recorded with the transaction. `track_payment` does not take them; use `calculate_tax` when you have them. `ingest_transactions` records take the same facts per record and pass them through to the API, which validates them; leave them out and an ingested classification sale gets the conservative answer.
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
calculate_tax({
|
|
156
|
+
role: "seller", amount: 12000, buyer_state: "TX", counterparty_id: "client-42",
|
|
157
|
+
transaction_type: "saas", work_type: "custom_software",
|
|
158
|
+
custom_software_attestation: {
|
|
159
|
+
written_for_buyer: true, not_offered_to_others: true,
|
|
160
|
+
deliverable: "source_code", is_modification_of_prewritten: false
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**`intent`** (optional, `"estimate"` | `"record"`). Pass `"estimate"` to quote a price before a wallet signs. An estimate is computed and counts toward your monthly quota, but it is never saved to your transaction history, never counts toward nexus revenue, and ignores any idempotency key. With an API key and no `intent`, the call is a `"record"` and is logged. Keyless demo calls are always estimates. The server sends `intent` only when you pass it. It needs the AgentTax API release that accepts `intent` (live on agenttax.io since 2026-10-21).
|
|
166
|
+
|
|
167
|
+
**`resolution`** in the response says how sure each part of the answer is. `resolution.location`, `resolution.nexus` and `resolution.taxability` each carry a `status` (`resolved`, `partial` or `unknown`), a `basis` (for example `zip_local_rate`, `state_only`, `no_nexus_profile`, `fact_not_declared`) and a `detail` sentence. `total_tax` is always returned, never withheld. If your rule is "unknown means do not charge", branch on the status yourself:
|
|
168
|
+
|
|
169
|
+
```js
|
|
170
|
+
// Pseudo-code. quote = the parsed JSON result of the calculate_tax tool call
|
|
171
|
+
const price = 20;
|
|
172
|
+
const quote = await calculate_tax({
|
|
173
|
+
role: "seller", amount: price, buyer_state: "NY", buyer_zip: "10001",
|
|
174
|
+
transaction_type: "saas", counterparty_id: "buyer-agent-7", intent: "estimate"
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (quote.resolution.nexus.status === "unknown") {
|
|
178
|
+
// No nexus profile configured: total_tax may be $0 only because nexus was never set up.
|
|
179
|
+
// Run configure_nexus, or hold the sale for review, instead of charging $0.
|
|
180
|
+
} else {
|
|
181
|
+
charge(price + quote.total_tax);
|
|
182
|
+
}
|
|
183
|
+
```
|
|
141
184
|
|
|
142
185
|
### Nexus
|
|
143
186
|
|
|
@@ -165,7 +208,7 @@ curl -X POST https://agenttax.io/api/v1/auth/signup \
|
|
|
165
208
|
-d '{"email": "you@example.com", "password": "securepass", "agent_name": "my-mcp-server", "agent_work_type": "compute"}'
|
|
166
209
|
```
|
|
167
210
|
|
|
168
|
-
All four fields are required; `agent_work_type` is one of `compute`, `research`, `information_service`, `content`, `consulting`, `trading`. Save the `api_key.key` from the response — it's only shown once.
|
|
211
|
+
All four fields are required; `agent_work_type` is one of `compute`, `research`, `information_service`, `content`, `consulting`, `trading`, `payment_processing`, `web_hosting`, `software_maintenance`, `it_support`, `custom_software`. Save the `api_key.key` from the response — it's only shown once.
|
|
169
212
|
|
|
170
213
|
## Pricing
|
|
171
214
|
|
package/classify.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// How track_payment turns its arguments into a POST /api/v1/calculate body,
|
|
2
|
+
// plus the enums and classification-fact schema calculate_tax shares with the
|
|
3
|
+
// API. Kept apart from index.js (which starts the stdio server on import) so
|
|
4
|
+
// it can be unit-tested.
|
|
5
|
+
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
// The work types the AgentTax API accepts (api/_lib/schemas/common.js
|
|
9
|
+
// WORK_TYPES). 1.3.0 adds the five classification work types the API has
|
|
10
|
+
// taken since 2026-10-16; before this the zod enum rejected them, so an MCP
|
|
11
|
+
// caller could not declare a payment fee, hosting, maintenance, IT support or
|
|
12
|
+
// custom software at all. The API decides taxability; nothing here does.
|
|
13
|
+
export const WORK_TYPES = [
|
|
14
|
+
"compute", "research", "information_service", "content", "consulting", "trading",
|
|
15
|
+
"payment_processing", "web_hosting", "software_maintenance", "it_support", "custom_software",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
// The transaction types the AgentTax API accepts (api/_lib/schemas/common.js
|
|
19
|
+
// TRANSACTION_TYPES), including the four classification types the API has
|
|
20
|
+
// taken since 2026-10-16.
|
|
21
|
+
export const TRANSACTION_TYPES = [
|
|
22
|
+
"compute", "api_access", "data_purchase", "saas", "ai_labor", "storage",
|
|
23
|
+
"digital_good", "consulting", "data_processing", "cloud_infrastructure",
|
|
24
|
+
"ai_model_access", "marketplace_fee", "subscription", "license", "service",
|
|
25
|
+
"payment_processing_fee", "web_hosting", "software_maintenance", "it_support",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
// digital_content_type values (api/_lib/schemas/common.js DIGITAL_CONTENT_TYPES).
|
|
29
|
+
export const DIGITAL_CONTENT_TYPES = [
|
|
30
|
+
"ebook", "audio", "video", "art_image", "photograph", "printable_document", "software", "data", "other",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// Classification-fact enums, copied from api/_lib/schemas/common.js. Pinned
|
|
34
|
+
// against the API by test/enum-parity.test.js.
|
|
35
|
+
export const PAYMENT_PROVIDER_TYPES = [
|
|
36
|
+
"bank", "licensed_money_transmitter", "card_network", "processor_routing_to_bank_or_network", "other",
|
|
37
|
+
];
|
|
38
|
+
export const IT_SUPPORT_SCOPES = [
|
|
39
|
+
"help_desk_advice", "remote_repair_or_config", "software_installation", "monitoring", "security_monitoring", "managed_bundle",
|
|
40
|
+
];
|
|
41
|
+
export const MAINTENANCE_UPDATE_DELIVERY = ["electronic", "tangible"];
|
|
42
|
+
export const MAINTENANCE_SUPPORT_KINDS = ["help_desk_only", "includes_fixes"];
|
|
43
|
+
export const CUSTOM_SOFTWARE_DELIVERABLES = ["source_code", "compiled_artifact", "hosted"];
|
|
44
|
+
export const CUSTOM_SOFTWARE_DELIVERY = ["electronic", "tangible"];
|
|
45
|
+
|
|
46
|
+
// The API trims and lowercases these string facts before its enum check
|
|
47
|
+
// (api/_lib/schemas/calculate.js: .trim().transform(toLowerCase).pipe(z.enum)),
|
|
48
|
+
// so "Bank " is valid there. Mirror that here, the way index.js stateCode
|
|
49
|
+
// uppercases, so the MCP layer does not refuse what the API accepts. The
|
|
50
|
+
// normalised value is what gets forwarded. The published JSON Schema still
|
|
51
|
+
// shows the enum.
|
|
52
|
+
export const lowerEnum = (values) =>
|
|
53
|
+
z.preprocess((v) => (typeof v === "string" ? v.trim().toLowerCase() : v), z.enum(values));
|
|
54
|
+
|
|
55
|
+
// The optional classification facts calculate_tax forwards to the API.
|
|
56
|
+
// Names, nesting, enum values and strictness match calculateRequest in
|
|
57
|
+
// api/_lib/schemas/calculate.js; the API decides what each fact does to the
|
|
58
|
+
// answer. Every field is optional: absent means the API's conservative
|
|
59
|
+
// default.
|
|
60
|
+
export const CLASSIFICATION_FACTS_SHAPE = {
|
|
61
|
+
payment_provider_type: lowerEnum(PAYMENT_PROVIDER_TYPES).optional()
|
|
62
|
+
.describe("With transaction_type payment_processing_fee: who provides the payment processing. " +
|
|
63
|
+
"Changes the answer in some states; absent = the conservative answer. Your representation, recorded with the transaction"),
|
|
64
|
+
it_support_scope: lowerEnum(IT_SUPPORT_SCOPES).optional()
|
|
65
|
+
.describe("With transaction_type or work_type it_support: what the remote IT support engagement is. " +
|
|
66
|
+
"Absent = managed_bundle, the per-state default; some states answer specific scopes differently"),
|
|
67
|
+
includes_prewritten_software_access: z.boolean().optional()
|
|
68
|
+
.describe("With transaction_type web_hosting: true when the plan gives the buyer vendor prewritten software beyond an incidental " +
|
|
69
|
+
"control panel or file manager (site builder, CMS or app platform, email or office suite, managed application). " +
|
|
70
|
+
"Absent or false = plain hosting of the buyer's own site or application"),
|
|
71
|
+
custom_software_attestation: z.object({
|
|
72
|
+
written_for_buyer: z.boolean(),
|
|
73
|
+
not_offered_to_others: z.boolean(),
|
|
74
|
+
deliverable: z.enum(CUSTOM_SOFTWARE_DELIVERABLES),
|
|
75
|
+
is_modification_of_prewritten: z.boolean(),
|
|
76
|
+
separately_stated_charge: z.boolean().optional(),
|
|
77
|
+
all_rights_transferred: z.boolean().optional(),
|
|
78
|
+
delivery: z.enum(CUSTOM_SOFTWARE_DELIVERY).optional(),
|
|
79
|
+
il_requirements_analysis_and_adaptation: z.boolean().optional(),
|
|
80
|
+
il_assembled_modules_50pct: z.boolean().optional(),
|
|
81
|
+
}).strict().optional()
|
|
82
|
+
.describe("Required with work_type custom_software; without a complete attestation the sale is answered as prewritten software / SaaS. " +
|
|
83
|
+
"Complete = written_for_buyer true, not_offered_to_others true, deliverable source_code or compiled_artifact (hosted is SaaS), " +
|
|
84
|
+
"is_modification_of_prewritten set, and separately_stated_charge true when it is a modification. The other fields are state-specific facts. " +
|
|
85
|
+
"Your representation, recorded with the transaction; not a state safe harbor"),
|
|
86
|
+
maintenance_contract: z.object({
|
|
87
|
+
mandatory: z.boolean().optional(),
|
|
88
|
+
includes_upgrades: z.boolean().optional(),
|
|
89
|
+
includes_support: z.boolean().optional(),
|
|
90
|
+
separately_itemized: z.boolean().optional(),
|
|
91
|
+
update_delivery: z.enum(MAINTENANCE_UPDATE_DELIVERY).optional(),
|
|
92
|
+
support_kind: z.enum(MAINTENANCE_SUPPORT_KINDS).optional(),
|
|
93
|
+
seller_sold_software: z.boolean().optional(),
|
|
94
|
+
business_use: z.boolean().optional(),
|
|
95
|
+
itemized_amounts: z.object({
|
|
96
|
+
updates: z.number().nonnegative().finite(),
|
|
97
|
+
support: z.number().nonnegative().finite(),
|
|
98
|
+
}).strict().optional(),
|
|
99
|
+
}).strict().optional()
|
|
100
|
+
.describe("With transaction_type software_maintenance: facts about a maintenance contract on prewritten software. Every field is optional; " +
|
|
101
|
+
"a missing fact is answered conservatively (updates assumed included). separately_itemized + itemized_amounts {updates, support} " +
|
|
102
|
+
"(summing to amount) split the charge where a state taxes the parts differently"),
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// transaction_type sent for each work_type. The classification work types
|
|
106
|
+
// have their own transaction types (AGENTS.md "Classification facts");
|
|
107
|
+
// custom_software is a work_type on a SaaS/service sale, and without a
|
|
108
|
+
// complete custom_software_attestation the API answers it as prewritten
|
|
109
|
+
// software or SaaS, which is the conservative default a payment log wants.
|
|
110
|
+
export const WORK_TYPE_TO_TX_TYPE = {
|
|
111
|
+
compute: "compute",
|
|
112
|
+
research: "api_access",
|
|
113
|
+
information_service: "api_access",
|
|
114
|
+
content: "saas",
|
|
115
|
+
consulting: "consulting",
|
|
116
|
+
trading: "digital_good",
|
|
117
|
+
payment_processing: "payment_processing_fee",
|
|
118
|
+
web_hosting: "web_hosting",
|
|
119
|
+
software_maintenance: "software_maintenance",
|
|
120
|
+
it_support: "it_support",
|
|
121
|
+
custom_software: "saas",
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Work types that, in the AgentTax engine, can route a sale to an exemption:
|
|
125
|
+
// consulting -> exempt professional service in Arizona (A.R.S. 42-5061(A)(1),
|
|
126
|
+
// TPR 93-48), and the engine also exempts AZ when only
|
|
127
|
+
// transaction_type "consulting" is sent;
|
|
128
|
+
// trading -> financial_service, exempt in every state.
|
|
129
|
+
// The engine grants these only on a DECLARED fact. The public calculate API
|
|
130
|
+
// has no way to mark a work type as guessed, so a keyword match on the
|
|
131
|
+
// description must never send either one. A guess that lands here is
|
|
132
|
+
// treated as unclassified (the same default as a description with no
|
|
133
|
+
// keyword), which is taxed the way the engine taxes SaaS.
|
|
134
|
+
export const EXEMPTION_BEARING_WORK_TYPES = new Set(["consulting", "trading"]);
|
|
135
|
+
|
|
136
|
+
// Guess a work type from a plain-English description. Returns null when no
|
|
137
|
+
// keyword matches. A guess is a hint only: see buildTrackPaymentRequest.
|
|
138
|
+
export function guessWorkType(description) {
|
|
139
|
+
if (!description) return null;
|
|
140
|
+
const d = description.toLowerCase();
|
|
141
|
+
if (/\b(compute|inference|gpu|process|api[\s-]?call|token)\b/.test(d)) return "compute";
|
|
142
|
+
if (/\b(research|analysis|analytics|data[\s-]?(process|feed))\b/.test(d)) return "research";
|
|
143
|
+
if (/\b(consult|consulting|advisory|advice|audit)\b/.test(d)) return "consulting";
|
|
144
|
+
if (/\b(trade|trading|swap|asset)\b/.test(d)) return "trading";
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Returns { body, classification } where body is the calculate request and
|
|
149
|
+
// classification says where the work type came from.
|
|
150
|
+
//
|
|
151
|
+
// explicit work_type -> sent as work_type, with its transaction_type
|
|
152
|
+
// guessed compute / research -> sent as in 1.1.1. Not an exemption, but their
|
|
153
|
+
// categories can tax less than SaaS in some states
|
|
154
|
+
// (e.g. CT, MD, SC, TN, WV); changing that is a
|
|
155
|
+
// tax-value decision, so it is left as is.
|
|
156
|
+
// guessed consulting/trading -> NOT sent; transaction_type "saas", no work_type
|
|
157
|
+
// no guess -> transaction_type "saas", no work_type
|
|
158
|
+
// is_b2b -> sent only when the caller passed a boolean
|
|
159
|
+
export function buildTrackPaymentRequest(params, { now = Date.now } = {}) {
|
|
160
|
+
const counterpartyId = params.payment_id || `payment_${now()}`;
|
|
161
|
+
let workType;
|
|
162
|
+
let transactionType;
|
|
163
|
+
let classification;
|
|
164
|
+
|
|
165
|
+
if (params.work_type) {
|
|
166
|
+
workType = params.work_type;
|
|
167
|
+
transactionType = WORK_TYPE_TO_TX_TYPE[workType] || "saas";
|
|
168
|
+
classification = { source: "explicit", work_type: workType };
|
|
169
|
+
} else {
|
|
170
|
+
const guess = guessWorkType(params.description);
|
|
171
|
+
if (guess && !EXEMPTION_BEARING_WORK_TYPES.has(guess)) {
|
|
172
|
+
workType = guess;
|
|
173
|
+
transactionType = WORK_TYPE_TO_TX_TYPE[guess];
|
|
174
|
+
classification = { source: "inferred_from_description", work_type: guess };
|
|
175
|
+
} else {
|
|
176
|
+
workType = undefined; // let the API default from transaction_type
|
|
177
|
+
transactionType = "saas";
|
|
178
|
+
classification = guess
|
|
179
|
+
? {
|
|
180
|
+
source: "default",
|
|
181
|
+
work_type: null,
|
|
182
|
+
guess_not_used: guess,
|
|
183
|
+
note:
|
|
184
|
+
`The description looks like "${guess}", but a guess never takes the ${guess} exemption, so this sale was ` +
|
|
185
|
+
`taxed as a SaaS / digital service. If the sale really is ${guess}, call track_payment again with ` +
|
|
186
|
+
`work_type: "${guess}".`,
|
|
187
|
+
}
|
|
188
|
+
: { source: "default", work_type: null };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const body = {
|
|
193
|
+
role: "seller",
|
|
194
|
+
amount: params.amount,
|
|
195
|
+
buyer_state: params.buyer_state,
|
|
196
|
+
buyer_zip: params.buyer_zip,
|
|
197
|
+
transaction_type: transactionType,
|
|
198
|
+
counterparty_id: counterpartyId,
|
|
199
|
+
};
|
|
200
|
+
if (workType) body.work_type = workType;
|
|
201
|
+
// Send is_b2b only when the caller said. The API treats a present
|
|
202
|
+
// is_b2b:false as a DECLARED consumer, which takes explicit-only consumer
|
|
203
|
+
// exemptions (e.g. Ohio SaaS, R.C. 5739.01(B)(3)(e)); an unknown buyer must
|
|
204
|
+
// stay unknown so it is taxed conservatively.
|
|
205
|
+
if (typeof params.is_b2b === "boolean") body.is_b2b = params.is_b2b;
|
|
206
|
+
return { body, classification };
|
|
207
|
+
}
|
package/index.js
CHANGED
|
@@ -4,32 +4,15 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { z } from "zod";
|
|
7
|
+
import {
|
|
8
|
+
WORK_TYPES, TRANSACTION_TYPES, DIGITAL_CONTENT_TYPES, CLASSIFICATION_FACTS_SHAPE, lowerEnum, buildTrackPaymentRequest,
|
|
9
|
+
} from "./classify.js";
|
|
7
10
|
|
|
8
11
|
const { version: PKG_VERSION } = createRequire(import.meta.url)("./package.json");
|
|
9
12
|
|
|
10
13
|
const BASE_URL = process.env.AGENTTAX_BASE_URL || "https://agenttax.io";
|
|
11
14
|
const API_KEY = process.env.AGENTTAX_API_KEY || "";
|
|
12
15
|
|
|
13
|
-
// Classify work type from a plain-English description.
|
|
14
|
-
// Developers can describe what they sold and we auto-classify for tax purposes.
|
|
15
|
-
function classifyWorkType(description) {
|
|
16
|
-
if (!description) return "content";
|
|
17
|
-
const d = description.toLowerCase();
|
|
18
|
-
if (/\b(compute|inference|gpu|process|api[\s-]?call|token)\b/.test(d)) return "compute";
|
|
19
|
-
if (/\b(research|analysis|analytics|data[\s-]?(process|feed))\b/.test(d)) return "research";
|
|
20
|
-
if (/\b(consult|advisory|advice|audit)\b/.test(d)) return "consulting";
|
|
21
|
-
if (/\b(trade|trading|swap|asset)\b/.test(d)) return "trading";
|
|
22
|
-
return "content"; // Default: SaaS / digital service
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const WORK_TYPE_TO_TX_TYPE = {
|
|
26
|
-
compute: "compute",
|
|
27
|
-
research: "api_access",
|
|
28
|
-
content: "saas",
|
|
29
|
-
consulting: "consulting",
|
|
30
|
-
trading: "digital_good",
|
|
31
|
-
};
|
|
32
|
-
|
|
33
16
|
async function apiCall(method, path, body = null) {
|
|
34
17
|
const headers = { "Content-Type": "application/json" };
|
|
35
18
|
if (API_KEY) headers["X-API-Key"] = API_KEY;
|
|
@@ -71,16 +54,6 @@ const STATES = [
|
|
|
71
54
|
const stateCode = z.preprocess((v) => (typeof v === "string" ? v.toUpperCase() : v), z.enum(STATES));
|
|
72
55
|
const zip5 = z.string().regex(/^\d{5}$/);
|
|
73
56
|
|
|
74
|
-
const WORK_TYPES = ["compute", "research", "information_service", "content", "consulting", "trading"];
|
|
75
|
-
const TRANSACTION_TYPES = [
|
|
76
|
-
"compute", "api_access", "data_purchase", "saas", "ai_labor", "storage",
|
|
77
|
-
"digital_good", "consulting", "data_processing", "cloud_infrastructure",
|
|
78
|
-
"ai_model_access", "marketplace_fee", "subscription", "license", "service",
|
|
79
|
-
];
|
|
80
|
-
const DIGITAL_CONTENT_TYPES = [
|
|
81
|
-
"ebook", "audio", "video", "art_image", "photograph", "printable_document", "software", "data", "other",
|
|
82
|
-
];
|
|
83
|
-
|
|
84
57
|
const jsonResult = (result) => ({ content: [{ type: "text", text: JSON.stringify(result, null, 2) }] });
|
|
85
58
|
|
|
86
59
|
function withQuery(path, params) {
|
|
@@ -108,7 +81,12 @@ server.registerTool(
|
|
|
108
81
|
description:
|
|
109
82
|
"Calculate US sales tax (seller) or use tax (buyer) for an AI agent transaction. Returns tax amount, rate, jurisdiction, " +
|
|
110
83
|
"audit trail, confidence score, and advisories. Sellers get $0 sales tax in states without configured nexus " +
|
|
111
|
-
"(see configure_nexus); a $0 total means no collection obligation, not necessarily no tax, so read nexus_warning and advisories."
|
|
84
|
+
"(see configure_nexus); a $0 total means no collection obligation, not necessarily no tax, so read nexus_warning and advisories. " +
|
|
85
|
+
"work_type and transaction_type are sent as you give them and treated as declared facts (e.g. consulting is exempt in Arizona), so do not guess them. " +
|
|
86
|
+
"The response carries resolution.location / resolution.nexus / resolution.taxability, each with status resolved, partial or unknown plus a basis; " +
|
|
87
|
+
"total_tax is always returned, so if you want \"unknown means do not charge\", branch on resolution.*.status. " +
|
|
88
|
+
"Pass intent \"estimate\" to quote before a wallet signs: nothing is saved to history or nexus revenue, but it counts toward your monthly quota. " +
|
|
89
|
+
"Without intent (or with \"record\"), a keyed call is logged to your history, which is why this tool is annotated as writing even for estimates.",
|
|
112
90
|
inputSchema: {
|
|
113
91
|
role: z.enum(["buyer", "seller"]).describe("Your role in the transaction"),
|
|
114
92
|
amount: z.number().positive().max(100_000_000).describe("Transaction amount in USD"),
|
|
@@ -121,15 +99,25 @@ server.registerTool(
|
|
|
121
99
|
seller_zip: zip5.optional()
|
|
122
100
|
.describe("Seller's 5-digit zip. Required for TX/UT/AZ/TN origin-sourced intrastate sales and intrastate OH license/digital_good sales"),
|
|
123
101
|
work_type: z.enum(WORK_TYPES).optional()
|
|
124
|
-
.describe("What the agent's work is economically; drives per-state taxability (compute, research, information_service, content, consulting, trading
|
|
102
|
+
.describe("What the agent's work is economically; drives per-state taxability (compute, research, information_service, content, consulting, trading, " +
|
|
103
|
+
"payment_processing, web_hosting, software_maintenance, it_support, custom_software). " +
|
|
104
|
+
"Pass it only when you know it: it is a declaration. consulting is an exempt professional service in Arizona. " +
|
|
105
|
+
"custom_software needs a complete custom_software_attestation, or it is answered as prewritten software / SaaS"),
|
|
125
106
|
is_b2b: z.boolean().optional().describe("Business-to-business transaction (triggers state B2B rules, e.g. IA, MD)"),
|
|
126
|
-
use_context:
|
|
107
|
+
use_context: lowerEnum(["enterprise_system", "individual", "mixed"]).optional()
|
|
127
108
|
.describe("Maryland only, with is_b2b=true: whether the purchase is solely for use in an enterprise computer system"),
|
|
128
|
-
digital_content_type:
|
|
109
|
+
digital_content_type: lowerEnum(DIGITAL_CONTENT_TYPES).optional()
|
|
129
110
|
.describe("With transaction_type digital_good: what the download is. Several states tax e-books, music and video but not digital art or printables"),
|
|
130
111
|
seller_remitting: z.boolean().optional().describe("Buyer side: whether the seller is already collecting the tax"),
|
|
112
|
+
intent: z.enum(["estimate", "record"]).optional()
|
|
113
|
+
.describe("\"estimate\" = quote before a wallet signs; nothing is saved to history or nexus revenue; counts toward your quota. " +
|
|
114
|
+
"\"record\" (the default with an API key) logs the calculation. Keyless demo calls are always estimates. Omit it if your AgentTax API does not accept intent yet"),
|
|
115
|
+
// Classification facts for the five classification work types. Optional;
|
|
116
|
+
// forwarded verbatim like every other argument.
|
|
117
|
+
...CLASSIFICATION_FACTS_SHAPE,
|
|
131
118
|
},
|
|
132
|
-
|
|
119
|
+
// Static annotation: with an API key and no intent "estimate", the calculation is logged to your history.
|
|
120
|
+
annotations: WRITES,
|
|
133
121
|
},
|
|
134
122
|
async (params) => {
|
|
135
123
|
try {
|
|
@@ -150,35 +138,28 @@ server.registerTool(
|
|
|
150
138
|
title: "Track a payment you received",
|
|
151
139
|
description:
|
|
152
140
|
"Track a payment you received and calculate your sales tax liability. Call this after receiving any payment — Stripe, x402, or direct. " +
|
|
153
|
-
"
|
|
141
|
+
"Calculates tax owed and logs it to your AgentTax account (requires API key for history). " +
|
|
142
|
+
"Pass work_type when you know what you sold, for an exact answer; it is the only way to get the consulting exemption (AI consulting is exempt in Arizona) or the trading exemption. " +
|
|
143
|
+
"Without it the sale is classified from description: a description that looks like consulting or trading is taxed as SaaS, never exempt; " +
|
|
144
|
+
"one that looks like compute or research keeps that classification, which can tax less than SaaS in some states (e.g. CT, MD, SC, TN, WV).",
|
|
154
145
|
inputSchema: {
|
|
155
146
|
amount: z.number().positive().max(100_000_000).describe("Payment amount in USD"),
|
|
156
147
|
buyer_state: stateCode.describe("2-letter US state where the buyer is located (e.g. TX, NY, CA)"),
|
|
157
148
|
buyer_zip: zip5.optional().describe("Buyer's 5-digit zip code for local tax rates (more precise)"),
|
|
158
|
-
description: z.string().optional().describe("What you sold
|
|
149
|
+
description: z.string().optional().describe("What you sold (e.g. 'API access', 'MCP tool subscription', 'compute credits'). Used only when work_type is not given; a guess never takes the consulting or trading exemption"),
|
|
150
|
+
work_type: z.enum(WORK_TYPES).optional()
|
|
151
|
+
.describe("What you sold, declared: compute, research, information_service, content, consulting, trading. Overrides description. consulting is an exempt professional service in Arizona"),
|
|
159
152
|
payment_id: z.string().regex(/^[A-Za-z0-9_\-.:@]{1,200}$/).optional()
|
|
160
153
|
.describe("Your payment reference ID (Stripe payment_intent ID, x402 receipt, invoice number); letters, digits and _-.:@ only"),
|
|
161
154
|
source: z.enum(["stripe", "x402", "direct", "other"]).optional().describe("Payment processor used"),
|
|
162
|
-
is_b2b: z.boolean().optional().describe("Buyer is a business (affects rates in MD, IA, NJ)"),
|
|
155
|
+
is_b2b: z.boolean().optional().describe("Buyer is a business (affects rates in MD, IA, NJ, OH). Omit if unknown: false declares a consumer"),
|
|
163
156
|
},
|
|
164
157
|
annotations: WRITES,
|
|
165
158
|
},
|
|
166
159
|
async (params) => {
|
|
167
160
|
try {
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
const counterpartyId = params.payment_id || `payment_${Date.now()}`;
|
|
171
|
-
|
|
172
|
-
const result = await apiCall("POST", "/api/v1/calculate", {
|
|
173
|
-
role: "seller",
|
|
174
|
-
amount: params.amount,
|
|
175
|
-
buyer_state: params.buyer_state,
|
|
176
|
-
buyer_zip: params.buyer_zip,
|
|
177
|
-
transaction_type: transactionType,
|
|
178
|
-
work_type: workType,
|
|
179
|
-
counterparty_id: counterpartyId,
|
|
180
|
-
is_b2b: params.is_b2b || false,
|
|
181
|
-
});
|
|
161
|
+
const { body, classification } = buildTrackPaymentRequest(params);
|
|
162
|
+
const result = await apiCall("POST", "/api/v1/calculate", body);
|
|
182
163
|
|
|
183
164
|
if (!result.success) {
|
|
184
165
|
return {
|
|
@@ -197,6 +178,7 @@ server.registerTool(
|
|
|
197
178
|
tax_rate: result.sales_tax?.rate || result.combined_rate || 0,
|
|
198
179
|
taxable: taxOwed > 0,
|
|
199
180
|
work_type: result.work_type,
|
|
181
|
+
classification,
|
|
200
182
|
transaction_id: result.transaction_id,
|
|
201
183
|
nexus_warning: result.nexus_warning,
|
|
202
184
|
compliance_note: taxOwed > 0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agenttax/mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"mcpName": "io.github.AgentTax/agenttax-mcp",
|
|
5
5
|
"description": "MCP server for AgentTax — US sales tax, nexus thresholds, capital gains and 1099-DA tools for AI agents",
|
|
6
6
|
"main": "index.js",
|
|
@@ -10,12 +10,15 @@
|
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"index.js",
|
|
13
|
+
"classify.js",
|
|
13
14
|
"README.md",
|
|
15
|
+
"CHANGELOG.md",
|
|
14
16
|
"LICENSE",
|
|
15
17
|
"server.json"
|
|
16
18
|
],
|
|
17
19
|
"scripts": {
|
|
18
|
-
"start": "node index.js"
|
|
20
|
+
"start": "node index.js",
|
|
21
|
+
"test": "node --test test/track-payment.test.js test/tools.test.js test/enum-parity.test.js test/calculate-facts.test.js"
|
|
19
22
|
},
|
|
20
23
|
"keywords": [
|
|
21
24
|
"1099-da",
|
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.AgentTax/agenttax-mcp",
|
|
4
4
|
"title": "AgentTax",
|
|
5
5
|
"description": "US sales/use tax, nexus thresholds, capital gains and 1099-DA tools for AI agent transactions.",
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.3.0",
|
|
7
7
|
"websiteUrl": "https://agenttax.io",
|
|
8
8
|
"repository": {
|
|
9
9
|
"url": "https://github.com/AgentTax/agenttax-mcp",
|
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
{
|
|
14
14
|
"registryType": "npm",
|
|
15
15
|
"identifier": "@agenttax/mcp-server",
|
|
16
|
-
"version": "1.
|
|
16
|
+
"version": "1.3.0",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
19
19
|
},
|
|
20
20
|
"environmentVariables": [
|
|
21
21
|
{
|
|
22
22
|
"name": "AGENTTAX_API_KEY",
|
|
23
|
-
"description": "AgentTax API key (free tier at agenttax.io). Optional: without it the server runs in demo mode,
|
|
23
|
+
"description": "AgentTax API key (free tier at agenttax.io). Optional: without it the server runs in demo mode, 10 calls/day.",
|
|
24
24
|
"isRequired": false,
|
|
25
25
|
"isSecret": true
|
|
26
26
|
}
|