@attrkit/mcp 0.4.5 → 1.0.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.
Files changed (4) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/README.md +24 -5
  3. package/dist/cli.js +1529 -456
  4. package/package.json +6 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,125 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0 (2026-09-30)
4
+
5
+ Major version release with input contract formalization, bug fixes, and security hardening.
6
+
7
+ ### Breaking Changes and Migration Notes
8
+
9
+ #### 1. Paired Ad Identifiers in `import_spend`
10
+ - **Change:** In `import_spend`, `adset_id` and `ad_id` (and their camelCase aliases `adsetId` and `adId`) must now be provided together or both omitted.
11
+ - **Previous behavior:** At 0.4.6 the MCP schema accepted an unpaired `adset_id`/`ad_id`, but the API already refused it (`dashboard.ts` refine: `.refine((row) => Boolean(row.adsetId) === Boolean(row.adId), "adsetId and adId must be provided together")`). The change moves the refusal client-side.
12
+ - **Migration:** Callers submitting ad-grain spend records must provide both `adset_id` and `ad_id` for ad-level rows. If reporting at campaign grain only, omit both identifiers.
13
+
14
+ #### 2. Strict UUID Format for `campaign_id` in `record_campaign_decision`
15
+ - **Change:** In `record_campaign_decision`, `campaign_id` is now validated against RFC 4122 UUID syntax via `z.string().uuid()`.
16
+ - **Previous behavior:** In version 0.4.6, `campaign_id` accepted arbitrary non-empty strings.
17
+ - **Migration:** Provide the AttrKit internal campaign UUID returned by `list_campaigns` or `get_campaign_decision`. Non-UUID external network identifiers must not be passed.
18
+
19
+ #### 3. Strict API Base URL Validation and Startup Refusals in `ATTRKIT_API_URL`
20
+ - **Overview:** At version 0.4.6, `readConfig` performed no URL validation on `ATTRKIT_API_URL` (any string was passed through, merely trimming trailing slashes). In version 1.0.0, `readConfig` and `validatedApiBaseUrl` enforce four startup-fatal refusals plus validation of unparsable URLs. Violations print the fatal error to stderr and exit with process exit code 1.
21
+
22
+ ##### 3.1 Non-Loopback HTTP Protocol Refusal
23
+ - **Change:** Refuses `http:` URLs unless the host is a loopback interface (`localhost`, `127.0.0.1`, `[::1]`). Throws `protocol must be HTTPS, except HTTP is allowed for localhost development`.
24
+ - **Consumer-visible effect:** Process startup fails with exit code 1 when configured with a non-loopback HTTP endpoint (e.g. `ATTRKIT_API_URL=http://api.internal.example.com`).
25
+ - **Migration:** Switch endpoints to HTTPS (`https://`). Unencrypted HTTP is permitted only for local development on loopback interfaces (`localhost`, `127.0.0.1`, `[::1]`).
26
+
27
+ ##### 3.2 URL Credentials Refusal
28
+ - **Change:** Refuses URLs containing basic authentication credentials (username or password). Throws `credentials are not allowed in ATTRKIT_API_URL`.
29
+ - **Consumer-visible effect:** Process startup fails with exit code 1 when configured with URL credentials (e.g. `ATTRKIT_API_URL=https://svc:token@api.internal.example.com`).
30
+ - **Migration:** Remove credentials from the URL. Authentication must be provided via the `ATTRKIT_API_KEY` environment variable. Internal endpoints or proxies requiring basic authentication must terminate credentials at the proxy or network layer, not inside the base URL.
31
+
32
+ ##### 3.3 Query String Refusal
33
+ - **Change:** Refuses URLs containing a query string delimiter (`?`). Throws `query strings are not allowed in ATTRKIT_API_URL`.
34
+ - **Consumer-visible effect:** Process startup fails with exit code 1 when configured with query parameters (e.g. `ATTRKIT_API_URL=https://api.example.com?env=staging`).
35
+ - **Migration:** Remove query strings from `ATTRKIT_API_URL`. The base URL must be a clean origin/path root; query parameters for specific API routes are attached by the individual tool callers.
36
+
37
+ ##### 3.4 URL Fragment Refusal
38
+ - **Change:** Refuses URLs containing a fragment delimiter (`#`). Throws `fragments are not allowed in ATTRKIT_API_URL`.
39
+ - **Consumer-visible effect:** Process startup fails with exit code 1 when configured with a fragment (e.g. `ATTRKIT_API_URL=https://api.example.com#section`).
40
+ - **Migration:** Remove fragments (`#...`) from `ATTRKIT_API_URL`.
41
+
42
+ ##### 3.5 Unparsable URL Syntax Refusal
43
+ - **Change:** Refuses malformed URL strings that cannot be parsed by standard URL parsing (`new URL()`), throwing `Invalid URL` (`TypeError`).
44
+ - **Consumer-visible effect:** Process startup fails with exit code 1 on non-URL or malformed strings (e.g. `ATTRKIT_API_URL=not a valid url`).
45
+ - **Migration:** Provide a valid, well-formed absolute URL string (e.g. `https://api.example.com`).
46
+
47
+ ### Additional Measured Dispositions
48
+
49
+ #### 4. CSV Header Response Validation in `export_events`
50
+ - **Disposition:** Intentional verification restriction.
51
+ - **Detail:** Validates that `/api/v1/export` responses begin with expected AttrKit column identifiers (`type` or `kind` followed by `id`). This prevents non-CSV responses (such as plain text errors or gateway pages) from being accepted as valid exports.
52
+
53
+ #### 5. App Identifier URL Encoding Consistency and Route Protection (`get_app`, `update_app`, `archive_app`, `get_publishable_key`, `rotate_publishable_key`, `get_revenuecat`, `configure_revenuecat`)
54
+ - **Disposition:** Security hardening and consistency fix.
55
+ - **Detail:** Dot-segment ids escaped the app route under 0.4.6-style encoding; 1.0.0 keeps them inside the route across all seven app tools. Under 0.4.6-style encoding (`encodeURIComponent`), dot segments were left unescaped, causing standard URL resolution to normalize `..` to `/api/v1/` (the API index) or sibling paths outside `/api/v1/apps/` and `.` to `/api/v1/apps/` (the collection root), escaping the intended app item route. Version 1.0.0 applies opaque-path encoding (`encodeOpaquePathSegment`, encoding `.` as `%252E`) across all seven app-scoped tools (`get_app`, `update_app`, `archive_app`, `get_publishable_key`, `rotate_publishable_key`, `get_revenuecat`, and `configure_revenuecat`), keeping requests strictly within `/api/v1/apps/`. In AttrKit, servable app ids are UUIDs or `"demo-app"` (enforced server-side via `appIdInput`); because no servable id contains a dot, valid UUID and demo-app calls remain byte-identical under both encodings, with no 404 behaviour changes for existing callers.
56
+
57
+ #### 6. Diagnostic Format Filtering for `Retry-After`
58
+ - **Disposition:** Intentional diagnostic hygiene restriction.
59
+ - **Detail:** Validates upstream `Retry-After` headers against RFC 9110 specifications (integer seconds or IMF-fixdate format) before inclusion in tool error messages, preventing header injection into model context.
60
+
61
+ #### 7. `tracking_url` Documented in `create_campaign_link` and `list_links`
62
+ - **Disposition:** Documentation fix, no contract change.
63
+ - **Detail:** Every version from 0.4.6 passes the API response through unchanged, so `tracking_url` has reached clients since the API began returning it. The tool descriptions now say that `tracking_url` is the campaign link to put in the ad and that `url` is only the App Store destination; 0.4.6 said "Returns the link id and url", which led agents to hand users the store URL as the campaign link.
64
+
65
+ ### Other Changes in 1.0.0
66
+
67
+ #### 8. `record_campaign_decision` Takes the Reviewed Period, Not Client-Supplied Numbers
68
+ - **Change:** The inputs `actor`, `p05`, `p50`, `p95`, `spend_minor_units`, `currency`, `band_width_pct`, `threshold`, `model_version`, `cost_model_version`, `revenue_per_install_minor_units` and `revenue_per_install_quality` are removed; `period` is added. AttriKit freezes the campaign range, spend, costs, revenue basis and model version server-side.
69
+ - **Consumer-visible effect:** A 0.4.6-style call that still sends the removed fields succeeds; the fields are stripped and the API never used them.
70
+ - **Migration:** Pass `period` equal to the window you reviewed. Omitted, this tool and `get_campaign_decision` use `all`, while `list_campaigns` defaults to `30d`, so a receipt recorded after `list_campaigns` without a period freezes a different window.
71
+
72
+ #### 9. `get_campaign_decision` Accepts `period`, `cursor` and `limit`
73
+ - **Change:** New optional inputs, forwarded to `/api/v1/decisions`.
74
+
75
+ #### 10. Every Write Carries an Idempotency Key
76
+ - **Change:** A POST, PATCH or PUT sent without `idempotency_key` now carries a generated one, so a retried write cannot apply twice. Supply your own key to make retries across restarts idempotent too.
77
+
78
+ #### 11. `import_spend` Refuses Disagreeing Alias Pairs
79
+ - **Change:** A row that sends both spellings of a field (`campaign_id` and `campaignId`, `metric_date` and `metricDate`, and the other pairs) with different values is refused with `<field> and <alias> must match when both are supplied`. 0.4.6 silently used the first.
80
+
81
+ #### 12. Empty `app_id` Is Refused Where an App Is Required
82
+ - **Change:** Tools whose `app_id` is required reject an empty string before any request is sent.
83
+
84
+ #### 13. `set_usage_cap` and `configure_revenuecat` Are Marked Destructive
85
+ - **Change:** Both now carry `destructiveHint: true`, so clients that confirm destructive tools will ask before running them.
86
+
87
+ #### 14. `create_campaign_link` Separates the Network's Campaign Id
88
+ - **Change:** New optional `network_campaign_id` (the ad network's own campaign id, stored as the campaign's external id that `pull_network_spend` matches on). `campaign_id` is documented as an existing AttriKit campaign uuid. The API answers 400 naming `network_campaign_id` when `campaign_id` is not a uuid (it answered 500 before), and 400 when both are sent. `pull_network_spend`, `create_integration` and `import_spend` descriptions are rewritten to match.
89
+
90
+ #### 15. Integration Ids Are Opaque-Path Encoded
91
+ - **Change:** `update_integration` and `delete_integration` encode `connection_id` the way the app tools encode `app_id` (item 5), so `..` or `.` stays inside `/api/v1/integrations/` instead of resolving to `/api/v1/` or the collection.
92
+
93
+ #### 16. `set_cost_model.assumptions` Keeps a Flat Tool Schema
94
+ - **Change:** `assumptions` is advertised as an object of unconstrained values, as in 0.4.6, not as a recursive JSON schema with `$ref`, which some clients' function-calling formats refuse. Values are forwarded unchanged and the API validates them.
95
+
96
+ #### 17. `update_link_status` Attaches a Network Campaign Id
97
+ - **Change:** `status` is now optional and a new optional `network_campaign_id` is accepted; send either or both. It attaches the ad network's own campaign id to a link created without one (`PATCH /api/v1/links/{id}`), so `pull_network_spend` can match that link's spend without recreating it. A campaign that already carries a different network id is refused with 409 `network_campaign_conflict`.
98
+ - **Consumer-visible effect:** A 0.4.6-style call sending only `link_id` and `status` behaves as before.
99
+ - **Related:** `pull_network_spend` now says AttriKit pulls every enabled connection every 6 hours (today and the 3 days before); call it for an immediate pull or a longer window.
100
+
101
+ #### 18. `get_attribution_rollup` Returns the Period It Is Given
102
+ - **Change:** No change in the package. The tool has always forwarded `period` to `/api/v1/reports/overview`, and until 2026-09-30 the API ignored it and always answered with the last 30 days. The API now honors it.
103
+ - **Consumer-visible effect:** Every version of this package, 0.4.6 included, now gets the window it asks for. A call with `period: "90d"` or `"all"` returns different numbers than it did before. A call without `period` still returns the last 30 days.
104
+
105
+ #### 19. `export_events` Reads the Version 2 Export, With Paging
106
+ - **Change:** New optional `version` (1 or 2), and, for version 2, `format` (`csv` or `ndjson`), `limit` (1 to 10,000) and `cursor`, forwarded to `GET /api/v1/export`. Version 2 is the wide record export (campaign, ad, evidence and revenue columns), oldest first, paged. When another page follows, the tool result carries the `AttriKit-Next-Cursor` response header as a second text block (`next_cursor: ...`) and as `structuredContent.next_cursor`; send it back as `cursor`. A result with no `next_cursor` is the last page.
107
+ - **Consumer-visible effect:** A 0.4.6-style call without `version` sends the same request and returns the same result as before, byte for byte: the version 1 CSV. `format`, `limit` or `cursor` without `version: 2` is refused with `invalid_request` before any request, because the API ignores them on version 1 and a paging agent would be handed the same capped file again.
108
+ - **Related:** The API now applies version 1's 100,000-event cap inside the requested range, so a range older than the app's newest 100,000 events returns its events.
109
+
110
+ #### 20. New Tool: `send_server_event`
111
+ - **Change:** `POST /api/v1/apps/{appId}/events`. Reports an event for one of your users from your own backend by the `customer_user_id` your app passed to `AttriKit.setUserID`, with `event_id`, `event_name`, `event_version`, `occurred_at`, `consent`, optional `properties` and `idempotency_key`. Write scope. Annotated not read-only and not destructive; replaying an `event_id` answers `duplicate`.
112
+ - **Consumer-visible effect:** Additive: the tool count is 38 (see 21).
113
+
114
+ #### 21. New Tool: `lookup_customer_attribution`
115
+ - **Change:** `POST /api/v1/apps/{appId}/attribution/lookup`. Returns the campaign, ad set, ad, network, confidence and `attribution_status` of one user's newest install, looked up by `customer_user_id`. Read scope; annotated read-only (POST on the wire only so the id stays out of URLs).
116
+ - **Consumer-visible effect:** Additive. With 20, the tool count goes from 36 to 38.
117
+
118
+ #### 22. Explicit Windows and Paging on the Read Tools
119
+ - **Change:** Optional `from` and `to` (UTC `YYYY-MM-DD`) on `list_campaigns`, `list_ads` and `get_attribution_rollup`, replacing `period` for an explicit window (sending both is a 400 at the API). `list_recent_events` gains `from`, `to` and `cursor`; `get_spend` gains `from`, `to`, `cursor` and `limit`; `list_ads` gains `cursor`. Any of `from`, `to` or `cursor` puts events and spend into paged mode, which adds `next_cursor` to the response body.
120
+ - **Consumer-visible effect:** Additive. A call that sends none of the new inputs sends the same request as before and gets the same unpaged answer.
121
+
122
+ #### 23. `configure_revenuecat` Accepts `signing_secret`; a Read Over POST Is Not Retried as a Write
123
+ - **Change:** New optional `signing_secret` (16 to 1,024 characters), RevenueCat's HMAC secret for the `X-RevenueCat-Webhook-Signature` header, forwarded to `PUT /api/v1/apps/{appId}/revenuecat`. It is redacted in error output like `authorization_secret`.
124
+ - **Consumer-visible effect:** Additive. A call without it sends the same body as before and the API keeps the stored signing secret.
125
+ - **Also:** `lookup_customer_attribution` (item 21) is a read carried by POST. It sends no generated idempotency key, and a timeout or network error says the request was read-only instead of telling the agent to retry with an `idempotency_key`.
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # @attrkit/mcp
2
2
 
3
3
  A stdio [MCP](https://modelcontextprotocol.io) server that lets you drive
4
- [AttrKit](https://attrikit.io) from Codex, Cursor, Gemini, or another MCP client.
5
- It is a thin client over AttrKit's public API. It holds no database access; your
4
+ [AttriKit](https://attrikit.io) from Codex, Cursor, Gemini, or another MCP client.
5
+ It is a thin client over AttriKit's public API. It holds no database access; your
6
6
  tenant scope is resolved from your secret management key.
7
7
 
8
8
  ## What it exposes
9
9
 
10
- The server provides 36 tools.
10
+ The server provides 38 tools.
11
11
 
12
12
  | Area | Tools |
13
13
  |---|---|
@@ -17,12 +17,13 @@ The server provides 36 tools.
17
17
  | SDK keys | `get_publishable_key`, `rotate_publishable_key` |
18
18
  | Management keys | `list_api_keys`, `create_api_key`, `revoke_api_key` |
19
19
  | Campaign links | `list_links`, `create_campaign_link`, `update_link_status` |
20
- | Decisions and reporting | `list_campaigns`, `list_ads`, `list_recent_events`, `get_campaign_decision`, `record_campaign_decision`, `get_attribution_rollup` |
20
+ | Decisions and reporting | `list_campaigns`, `list_ads`, `list_recent_events`, `get_campaign_decision`, `record_campaign_decision`, `get_attribution_rollup`, `lookup_customer_attribution` |
21
21
  | Spend and costs | `get_spend`, `import_spend`, `set_cost_model`, `pull_network_spend` |
22
22
  | Ad integrations | `list_integrations`, `create_integration`, `update_integration`, `delete_integration` |
23
23
  | RevenueCat | `get_revenuecat`, `configure_revenuecat` |
24
24
  | Outbound webhooks | `list_webhooks`, `create_webhook`, `delete_webhook` |
25
25
  | Data export | `export_events` |
26
+ | Server events | `send_server_event` |
26
27
 
27
28
  `get_campaign_decision` returns the engine's call verbatim. The model reports it;
28
29
  it does not recompute it.
@@ -47,7 +48,7 @@ manage workspace-wide integrations.
47
48
  Load the key into the shell that starts your MCP client:
48
49
 
49
50
  ```bash
50
- printf "AttrKit management key: "
51
+ printf "AttriKit management key: "
51
52
  IFS= read -r -s ATTRKIT_API_KEY
52
53
  export ATTRKIT_API_KEY
53
54
  printf "\n"
@@ -189,6 +190,24 @@ surface.
189
190
  configuration containing a key. Restrict access to the file and revoke any
190
191
  key that is exposed.
191
192
 
193
+ ## Migration to 1.0.0
194
+
195
+ Version 1.0.0 formalizes tool input schemas, refines runtime configuration validation, and introduces three breaking restrictions from version 0.4.6:
196
+
197
+ 1. **Paired ad identifiers in `import_spend`**: `adset_id` and `ad_id` (and their camelCase aliases `adsetId` and `adId`) must now be provided together or both omitted. At version 0.4.6 the schema accepted either field independently, but the underlying API rejected unpaired calls; version 1.0.0 moves this validation client-side.
198
+ 2. **Strict UUID format for `campaign_id` in `record_campaign_decision`**: `campaign_id` must now be a valid RFC 4122 UUID. Version 0.4.6 accepted arbitrary non-empty strings.
199
+ 3. **Strict API base URL validation in `readConfig` (`ATTRKIT_API_URL`)**: `readConfig` enforces four startup-fatal refusals (violations print to stderr and exit with code 1):
200
+ - **Protocol refusal**: Non-loopback HTTP endpoints are rejected (`protocol must be HTTPS, except HTTP is allowed for localhost development`). HTTP is permitted only for loopback interfaces (`localhost`, `127.0.0.1`, `[::1]`); remote hosts must use `https:`.
201
+ - **Credentials refusal**: Embedded URL basic-auth credentials are rejected (`credentials are not allowed in ATTRKIT_API_URL`). Pass API authentication via `ATTRKIT_API_KEY`.
202
+ - **Query string refusal**: Query parameters in the base URL are rejected (`query strings are not allowed in ATTRKIT_API_URL`).
203
+ - **Fragment refusal**: Fragment identifiers in the base URL are rejected (`fragments are not allowed in ATTRKIT_API_URL`).
204
+ (In addition, malformed strings that fail URL parsing throw `Invalid URL` and exit 1).
205
+
206
+ Additionally, dot-segment ids escaped the app route under 0.4.6-style encoding; 1.0.0 keeps them inside the route across all seven app tools (`get_app`, `update_app`, `archive_app`, `get_publishable_key`, `rotate_publishable_key`, `get_revenuecat`, and `configure_revenuecat`).
207
+
208
+ See [CHANGELOG.md](./CHANGELOG.md) for full release details.
209
+
192
210
  ## License
193
211
 
194
212
  MIT. See [LICENSE](./LICENSE).
213
+