@attrkit/mcp 1.2.1 → 1.4.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 CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.4.0 (2026-10-21)
4
+
5
+ ### Added
6
+
7
+ - `create_integration` accepts two optional options for a Meta or TikTok integration:
8
+ `delivery_scope` (`all` or `attributed`) and `renewal_event` (`purchase` or `custom`). Each is
9
+ sent only when you give it, so a create without them sends exactly what 1.3.0 sent, and the API
10
+ applies its defaults, `all` and `purchase`. Existing integrations are unchanged.
11
+ - `update_integration` has a new action, `set_delivery_options`, which sets `delivery_scope`,
12
+ `renewal_event` or both in any status and leaves the credential and status as they were. The
13
+ API refuses the action when it names neither option, and refuses `attributed` or `custom` on a
14
+ Google integration.
15
+ - `list_event_deliveries` names `attributed_only_suppressed` among the reasons it can return: the
16
+ connection sends only the events of installs attributed to its network, and this install is
17
+ organic or attributed to another network.
18
+
19
+ ## 1.3.0 (2026-10-20)
20
+
21
+ ### Added
22
+
23
+ - `list_event_deliveries` (`GET /api/v1/apps/{appId}/deliveries`, read scope): what happened to
24
+ each event the app sent to its ad networks, one row per delivery, newest first. Each row gives
25
+ the state (`held`, `queued`, `sending`, `retrying`, `sent`, `suppressed` or `failed`), the reason
26
+ with a one-line explanation, the attempt count, the last attempt's time and HTTP status, and for
27
+ a held row the time it is expected to be released. Filters: `install_id`, `event_id`,
28
+ `connection_id`, `network`, `state`, `from`, `to`; paged with `cursor` and `limit` (50 by
29
+ default, 200 at most). No other tool changes.
30
+
3
31
  ## 1.2.1 (2026-10-03)
4
32
 
5
33
  ### Fixed
package/README.md CHANGED
@@ -7,7 +7,7 @@ tenant scope is resolved from your secret management key.
7
7
 
8
8
  ## What it exposes
9
9
 
10
- The server provides 38 tools.
10
+ The server provides 39 tools.
11
11
 
12
12
  | Area | Tools |
13
13
  |---|---|
@@ -17,7 +17,7 @@ The server provides 38 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`, `lookup_customer_attribution` |
20
+ | Decisions and reporting | `list_campaigns`, `list_ads`, `list_recent_events`, `list_event_deliveries`, `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` |
@@ -28,6 +28,16 @@ The server provides 38 tools.
28
28
  `get_campaign_decision` returns the engine's call verbatim. The model reports it;
29
29
  it does not recompute it.
30
30
 
31
+ A Meta or TikTok integration has two delivery options. `delivery_scope` is `all`
32
+ (the default: every install with measurement consent, organic ones included) or
33
+ `attributed` (only installs AttriKit attributes to that network: a click ID, a link
34
+ token, or a device match at or above the delivery floor). `renewal_event`
35
+ is `purchase` (the default: a renewal is sent as Purchase) or `custom` (it is
36
+ sent as the custom event `subscription_renewed`, with its value). Set them on
37
+ `create_integration`, or later with `update_integration` and the action
38
+ `set_delivery_options`, which leaves the credential and status as they are. A
39
+ Google integration refuses `attributed` and `custom`.
40
+
31
41
  ## Before you connect
32
42
 
33
43
  Install Node.js 18 or newer. The `npx -y @attrkit/mcp` command downloads and
package/dist/cli.js CHANGED
@@ -13710,6 +13710,33 @@ var CLASS_LISTS = [
13710
13710
  { ipClass: "relay", parsed: parseRanges(ip_class_ranges_default.apple_private_relay_egress, "relay") }
13711
13711
  ];
13712
13712
  var IP_CLASS_RANGES_VERSION = ip_class_ranges_default.ranges_version;
13713
+ // ../shared/src/meta-errors.ts
13714
+ var META_NOT_SERVING_CODES = new Set([
13715
+ 2,
13716
+ 4,
13717
+ 17,
13718
+ 32,
13719
+ 341,
13720
+ 613,
13721
+ 80000,
13722
+ 80001,
13723
+ 80002,
13724
+ 80003,
13725
+ 80004,
13726
+ 80005,
13727
+ 80006,
13728
+ 80008,
13729
+ 80009,
13730
+ 80014
13731
+ ]);
13732
+ var META_TOKEN_CODES = new Set([102, 190]);
13733
+ var META_ACCESS_CODES = new Set([
13734
+ 10,
13735
+ ...Array.from({ length: 100 }, (_, offset) => 200 + offset)
13736
+ ]);
13737
+ var META_ACCESS_SUBCODES = new Map([
13738
+ [100, new Set([33])]
13739
+ ]);
13713
13740
  // ../shared/src/canonical-events.ts
13714
13741
  var LEDGER_ONLY_CANONICAL_EVENT_NAMES = Object.freeze(["refund_reversed"]);
13715
13742
  // ../shared/src/derived-events.ts
@@ -14850,11 +14877,11 @@ function invalidResult(code, message) {
14850
14877
  json: { error: { code, message } }
14851
14878
  };
14852
14879
  }
14853
- function buildRequestUrl(baseUrl2, path, query) {
14880
+ function buildRequestUrl(baseUrl, path, query) {
14854
14881
  if (!path.startsWith("/") || hasUnsafePathCharacter(path)) {
14855
14882
  throw new Error("path must be an absolute URL path without whitespace or control characters");
14856
14883
  }
14857
- const url = new URL(`${baseUrl2.href.replace(/\/+$/, "")}${path}`);
14884
+ const url = new URL(`${baseUrl.href.replace(/\/+$/, "")}${path}`);
14858
14885
  for (const [key, value] of Object.entries(query ?? {})) {
14859
14886
  if (value !== undefined && value !== "")
14860
14887
  url.searchParams.set(key, value);
@@ -14919,15 +14946,15 @@ function apiResultFromResponse(response, text, expectedResponse) {
14919
14946
  }
14920
14947
  async function callApi(config, path, options = {}) {
14921
14948
  const doFetch = options.fetchImpl ?? fetch;
14922
- let baseUrl2;
14949
+ let baseUrl;
14923
14950
  try {
14924
- baseUrl2 = validatedApiBaseUrl(config.apiUrl);
14951
+ baseUrl = validatedApiBaseUrl(config.apiUrl);
14925
14952
  } catch (error) {
14926
14953
  return invalidResult("invalid_api_url", `ATTRKIT_API_URL is invalid: ${error instanceof Error ? error.message : String(error)}`);
14927
14954
  }
14928
14955
  let url;
14929
14956
  try {
14930
- url = buildRequestUrl(baseUrl2, path, options.query);
14957
+ url = buildRequestUrl(baseUrl, path, options.query);
14931
14958
  } catch (error) {
14932
14959
  return invalidResult("invalid_path", `AttriKit API path is invalid: ${error instanceof Error ? error.message : String(error)}`);
14933
14960
  }
@@ -15055,13 +15082,15 @@ ${body}`
15055
15082
  structuredContent: structured
15056
15083
  }, result.nextCursor);
15057
15084
  }
15058
- var SERVER_VERSION = "1.2.1";
15085
+ var SERVER_VERSION = "1.4.0";
15059
15086
  var periodSchema = z5.enum(["7d", "30d", "90d", "all"]).describe("Reporting window. Omit to use the endpoint's documented default.").optional();
15060
15087
  var appIdSchema = z5.string().describe("Optional app id to scope the result to one app.").optional();
15061
15088
  var requiredAppIdSchema = z5.string().min(1).describe("The AttriKit app id.");
15062
15089
  var encodeOpaquePathSegment = (value) => encodeURIComponent(value).replaceAll(".", "%252E");
15063
15090
  var idempotencyKeySchema = z5.string().max(255).describe("Stable retry key for this exact mutation.").optional();
15064
15091
  var ownersSchema = z5.record(z5.string(), z5.enum(["attrkit", "revenuecat", "customer"]));
15092
+ var deliveryScopeSchema = z5.enum(["all", "attributed"]).optional().describe("Which events a Meta or TikTok integration sends. all (the API's default): the events of every install with measurement consent, organic ones included. attributed: send only events of installs AttriKit attributes to this connection's network (click ID, link token, or device match at or above the delivery floor); others are suppressed with attributed_only_suppressed. Meta and TikTok only: a Google integration refuses attributed.");
15093
+ var renewalEventSchema = z5.enum(["purchase", "custom"]).optional().describe("How a renewal reaches Meta website and TikTok web events. purchase (the API's default): as Purchase with its price and currency. custom: as the custom event subscription_renewed with its price and currency, so Purchase counts one-time purchases only. App events are unchanged. A Google integration refuses custom.");
15065
15094
  var cursorSchema = z5.string().optional().describe("Pass the next_cursor from the previous response to fetch the following page. Omit for the first page.");
15066
15095
  var listLimitSchema = z5.number().int().min(1).max(200).optional().describe("Rows per page, 1-200. The API defaults to 50, so a workspace with more than that needs paging.");
15067
15096
  var calendarDaySchema = z5.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Use a UTC calendar day, YYYY-MM-DD.");
@@ -15413,6 +15442,35 @@ function createServer(config, fetchImpl) {
15413
15442
  limit: limit === undefined ? undefined : String(limit)
15414
15443
  }
15415
15444
  })));
15445
+ server.registerTool("list_event_deliveries", {
15446
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
15447
+ title: "List what happened to each event sent to ad networks",
15448
+ description: "Return one row per delivery of an event to a network connection, newest first: its state (held, queued, sending, retrying, sent, suppressed or failed), the reason with a one-line explanation (for example consent_suppressed, attributed_only_suppressed or destination_event_too_old), attempts, the last HTTP status, and for a held row when it is expected to be released. Use it to answer why an event did not reach Meta, TikTok or Google. Read scope is enough.",
15449
+ inputSchema: {
15450
+ app_id: requiredAppIdSchema,
15451
+ install_id: z5.string().optional().describe("Only this install: the install epoch id the events feed and the export call the install id."),
15452
+ event_id: z5.string().optional().describe("Only this event's deliveries, one per connection that receives it."),
15453
+ connection_id: z5.string().optional().describe("Only this network connection, as returned by list_integrations."),
15454
+ network: z5.string().optional().describe("Only connections to this network, for example meta."),
15455
+ state: z5.enum(["held", "queued", "sending", "retrying", "sent", "suppressed", "failed"]).optional().describe("Only deliveries in this state."),
15456
+ from: calendarDaySchema.optional().describe("First UTC day the event was received, YYYY-MM-DD. May be sent without to."),
15457
+ to: calendarDaySchema.optional().describe("Last UTC day the event was received, YYYY-MM-DD, inclusive. May be sent without from."),
15458
+ cursor: cursorSchema,
15459
+ limit: listLimitSchema
15460
+ }
15461
+ }, async ({ app_id, install_id, event_id, connection_id, network, state, from, to, cursor, limit }) => toToolResult(await call(`/api/v1/apps/${encodeOpaquePathSegment(app_id)}/deliveries`, {
15462
+ query: {
15463
+ installId: install_id,
15464
+ eventId: event_id,
15465
+ connectionId: connection_id,
15466
+ network,
15467
+ state,
15468
+ from,
15469
+ to,
15470
+ cursor,
15471
+ limit: limit === undefined ? undefined : String(limit)
15472
+ }
15473
+ })));
15416
15474
  server.registerTool("get_campaign_decision", {
15417
15475
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
15418
15476
  title: "Get campaign decisions (scale / cut / hold)",
@@ -15661,33 +15719,48 @@ function createServer(config, fetchImpl) {
15661
15719
  server.registerTool("create_integration", {
15662
15720
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
15663
15721
  title: "Create an ad-network integration",
15664
- description: "Store encrypted network credentials and choose, for each of the eight event families (install, session, trial, subscription, purchase, refund, paywall, business), which source's copy AttriKit forwards to the network. AttriKit always does the sending; the owner only picks the copy. attrkit = the events your app reports through the AttriKit SDK (the RevenueCat copy of that family is dropped). revenuecat = the events AttriKit received from your RevenueCat webhook (the SDK copy is dropped); RevenueCat itself sends nothing to the network. customer = events your own servers post to AttriKit. If your app reports subscriptions only through RevenueCat, set trial, subscription, purchase and refund to revenuecat, or none of them reach the network.",
15722
+ description: "Store encrypted network credentials and choose, for each of the eight event families (install, session, trial, subscription, purchase, refund, paywall, business), which source's copy AttriKit forwards to the network. AttriKit always does the sending; the owner only picks the copy. attrkit = the events your app reports through the AttriKit SDK (the RevenueCat copy of that family is dropped). revenuecat = the events AttriKit received from your RevenueCat webhook (the SDK copy is dropped); RevenueCat itself sends nothing to the network. customer = events your own servers post to AttriKit. If your app reports subscriptions only through RevenueCat, set trial, subscription, purchase and refund to revenuecat, or none of them reach the network. For Meta and TikTok, delivery_scope and renewal_event are optional; left out, the integration sends every install's events and renewals as Purchase.",
15665
15723
  inputSchema: {
15666
15724
  network: z5.enum(["meta", "google", "tiktok"]),
15667
15725
  account_ref: z5.string().min(1),
15668
15726
  credential: z5.string().min(1),
15669
15727
  owners: ownersSchema.describe("For each event family, whose copy is forwarded: attrkit, revenuecat or customer (see the tool description). Enabling a destination requires ALL eight families to be present, or the API rejects the call."),
15670
15728
  enable: z5.boolean().optional(),
15729
+ delivery_scope: deliveryScopeSchema,
15730
+ renewal_event: renewalEventSchema,
15671
15731
  idempotency_key: idempotencyKeySchema
15672
15732
  }
15673
- }, async ({ network, account_ref, credential, owners, enable, idempotency_key }) => toToolResult(await call("/api/v1/integrations", {
15674
- method: "POST",
15675
- idempotencyKey: idempotency_key,
15676
- body: { network, account_ref, credential, owners, enable: enable ?? false }
15677
- })));
15733
+ }, async ({ network, account_ref, credential, owners, enable, delivery_scope, renewal_event, idempotency_key }) => {
15734
+ const body = { network, account_ref, credential, owners, enable: enable ?? false };
15735
+ if (delivery_scope !== undefined)
15736
+ body.delivery_scope = delivery_scope;
15737
+ if (renewal_event !== undefined)
15738
+ body.renewal_event = renewal_event;
15739
+ return toToolResult(await call("/api/v1/integrations", {
15740
+ method: "POST",
15741
+ idempotencyKey: idempotency_key,
15742
+ body
15743
+ }));
15744
+ });
15678
15745
  server.registerTool("update_integration", {
15679
15746
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
15680
15747
  title: "Update an ad-network integration",
15681
- description: "Rotate credentials or enable/disable an integration.",
15748
+ description: "Rotate credentials or enable/disable an integration. set_delivery_options changes delivery_scope, renewal_event or both on a Meta or TikTok integration, in any status, without touching its credential or status; give at least one of the two.",
15682
15749
  inputSchema: {
15683
15750
  connection_id: z5.string(),
15684
- action: z5.enum(["rotate", "disable", "enable"]),
15751
+ action: z5.enum(["rotate", "disable", "enable", "set_delivery_options"]),
15685
15752
  credential: z5.string().optional(),
15686
15753
  owners: ownersSchema.optional(),
15754
+ delivery_scope: deliveryScopeSchema,
15755
+ renewal_event: renewalEventSchema,
15687
15756
  idempotency_key: idempotencyKeySchema
15688
15757
  }
15689
- }, async ({ connection_id, action, credential, owners, idempotency_key }) => {
15758
+ }, async ({ connection_id, action, credential, owners, delivery_scope, renewal_event, idempotency_key }) => {
15690
15759
  const body = { action };
15760
+ if (delivery_scope !== undefined)
15761
+ body.delivery_scope = delivery_scope;
15762
+ if (renewal_event !== undefined)
15763
+ body.renewal_event = renewal_event;
15691
15764
  if (credential !== undefined)
15692
15765
  body.credential = credential;
15693
15766
  if (owners)
@@ -15957,8 +16030,15 @@ function readHidden(input, prompt, write, idleMs = 50) {
15957
16030
  };
15958
16031
  const onData = (chunk) => {
15959
16032
  for (const character of chunk) {
16033
+ if (character === "\x03" || character === "\x04")
16034
+ return finish(null);
15960
16035
  if (mode === "drain")
15961
16036
  continue;
16037
+ if (character === "\r" || character === `
16038
+ `) {
16039
+ mode = "drain";
16040
+ continue;
16041
+ }
15962
16042
  if (mode === "escape") {
15963
16043
  mode = character === "[" ? "csi" : character === "O" ? "ss3" : "text";
15964
16044
  } else if (mode === "csi") {
@@ -15966,13 +16046,8 @@ function readHidden(input, prompt, write, idleMs = 50) {
15966
16046
  mode = "text";
15967
16047
  } else if (mode === "ss3") {
15968
16048
  mode = "text";
15969
- } else if (character === "\x03") {
15970
- return finish(null);
15971
16049
  } else if (character === "\x1B") {
15972
16050
  mode = "escape";
15973
- } else if (character === "\r" || character === `
15974
- `) {
15975
- mode = "drain";
15976
16051
  } else if (character === "" || character === "\b") {
15977
16052
  value = value.slice(0, -1);
15978
16053
  } else if (character >= " ") {
@@ -16071,10 +16146,19 @@ async function runLogout(options = {}) {
16071
16146
  return 1;
16072
16147
  }
16073
16148
  const result = await (options.runSecurity ?? defaultSecurityRunner)(["delete-generic-password", "-s", KEYCHAIN_SERVICE]);
16074
- write(result.code === 0 ? `Removed the attrkit-mcp item from the macOS Keychain.
16075
- ` : `Nothing was stored in the Keychain for attrkit-mcp.
16149
+ if (result.code === 0) {
16150
+ write(`Removed the attrkit-mcp item from the macOS Keychain.
16076
16151
  `);
16077
- return 0;
16152
+ return 0;
16153
+ }
16154
+ if (result.code === 44) {
16155
+ write(`No attrkit-mcp item was found in the macOS Keychain; nothing to remove.
16156
+ `);
16157
+ return 0;
16158
+ }
16159
+ write(`Could not remove the attrkit-mcp item from the macOS Keychain (security exit ${result.code}). It may still be stored.
16160
+ `);
16161
+ return 1;
16078
16162
  }
16079
16163
 
16080
16164
  // src/main.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@attrkit/mcp",
3
- "version": "1.2.1",
3
+ "version": "1.4.0",
4
4
  "description": "AttriKit's stdio MCP server for its public Management API",
5
5
  "keywords": [
6
6
  "attrkit",
@@ -32,19 +32,19 @@
32
32
  "scripts": {
33
33
  "build": "bun build ./src/cli.ts --outdir ./dist --target=node --format=esm --external @modelcontextprotocol/sdk --external zod",
34
34
  "prepack": "bun run build",
35
- "typecheck": "../../node_modules/typescript-7/bin/tsc --noEmit",
36
- "lint": "eslint .",
35
+ "typecheck": "tsrs --noEmit",
36
+ "lint": "node ../../scripts/checks/code-quality-eslint.mjs --workspace packages/mcp",
37
37
  "test": "cd ../.. && vitest run packages/mcp/src"
38
38
  },
39
39
  "dependencies": {
40
- "@modelcontextprotocol/sdk": "1.30.0",
41
- "zod": "4.4.3"
40
+ "@modelcontextprotocol/sdk": "1.32.1",
41
+ "zod": "4.6.7"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@attrkit/shared": "workspace:*",
45
- "@types/node": "26.1.1",
45
+ "@types/node": "26.6.5",
46
46
  "typescript": "6.0.3",
47
- "vitest": "4.1.10"
47
+ "vitest": "5.0.4"
48
48
  },
49
49
  "publishConfig": {
50
50
  "access": "public"