@affset/mcp 0.1.0 → 0.1.1

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/README.md CHANGED
@@ -9,32 +9,33 @@ It's a thin wrapper over the existing affset tenant API (`Bearer` token +
9
9
 
10
10
  ## Tools
11
11
 
12
- | Tool | What it does |
13
- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
14
- | `whoami` | Show the tenant this server is bound to: namespace, API base, derived dashboard URL, and (when readable) company / timezone / custom API domain. Read-only. |
15
- | `get_stats` | Traffic stats grouped by a dimension (date, campaign, zone, country, sub1–5, …). Returns clicks, conversions, CR, payout, media cost and ROI as a table. Sub columns use the tenant's sub labels when configured. |
16
- | `list_campaigns` | List campaigns (status / name filter, pagination). |
17
- | `list_zones` | List traffic-source zones (status / name filter, pagination). |
18
- | `list_team` | List team members (email, role, manager). **Never returns API tokens.** |
19
- | `create_campaign` | Create a campaign from an advertiser email, offer URL, geo, payout and name. Defaults: CPA / rate 0, **paused**, global payout rule, ready tracking link with `source_click_id={clickid}` + sub placeholders. **Dry-run by default**; `confirm: true` to apply. |
20
- | `set_campaign_status` | **Run** or **pause** a campaign (`action: "run" \| "pause"`). **Dry-run by default**; `confirm: true` to apply. Running can hit the plan's active-campaign limit. |
21
- | `update_campaign` | Partial update (name, offer URL, status, rate, budgets, dates, …). **Dry-run by default**; `confirm: true` to apply. Prefer `set_campaign_status` for run/pause. |
22
- | `create_zone` | Create a traffic-source zone (name + optional postback/site/traffic-back URLs). Always created `active`. **Dry-run by default**; `confirm: true` to apply. |
23
- | `update_zone` | Partial update (name, status, URLs). **Dry-run by default**; `confirm: true` to apply. Pass `null` to clear a URL. |
24
- | `get_zone_url` | The `/serve` URL to paste into a network's campaign settings rotates across the zone's **active** campaigns. Prefilled sub convention, optional `cost` macro. Warns when no active campaigns are visible. |
25
- | `get_tracking_link` | The `/track/click` link for an existing campaign + zone straight to one active campaign, with no rotation or targeting checks. Re-derives what `create_campaign` echoed on create. |
26
- | `cut_zones` | Blacklist underperforming zones on a campaign by threshold (CR / spend / ROI). **Dry-run by default**; `confirm: true` to apply. |
27
- | `list_payout_rules` | List a campaign's global + per-zone payout rules and its `payout_goal_type`. |
28
- | `set_payout_rule` | Upsert a global or zone-specific payout. **Dry-run by default**; `confirm: true` to apply. |
29
- | `delete_payout_rule` | Delete a global or zone-specific payout rule. **Dry-run by default**; `confirm: true` to apply. |
30
- | `set_payout_goal` | Set or clear `payout_goal_type` (goal-based conversions). **Dry-run by default**; `confirm: true` to apply. |
31
- | `list_targeting_types` | Catalog of targeting rule types, flagging the seeded ones `/serve` never evaluates. |
32
- | `list_targeting_rules` | List a campaign's targeting rules, flagging any that have no effect. |
33
- | `set_targeting_rule` | Upsert one targeting rule (safe merge), normalised to what `/serve` matches. **Dry-run by default**; `confirm: true` to apply. |
34
- | `remove_targeting_rule` | Remove one targeting rule by id or type+method. **Dry-run by default**; `confirm: true` to apply. |
35
- | `list_sub_labels` | List tenant display names for sub1–sub5. |
36
- | `set_sub_labels` | Set or clear sub labels (partial; `null` clears). **Dry-run by default**; `confirm: true` to apply. |
37
- | `list_conversions` | List conversion audit records (payout, spend, pixel type, payload, postback). Optional client-side filters on the current page. |
12
+ | Tool | What it does |
13
+ | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
14
+ | `whoami` | Show the tenant this server is bound to: namespace, API base, derived dashboard URL, and (when readable) company / timezone / custom API domain. Read-only. |
15
+ | `get_stats` | Traffic stats grouped by a dimension (date, campaign, zone, country, sub1–5, …). Returns clicks, conversions, CR, payout, media cost and ROI as a table. Sub columns use the tenant's sub labels when configured. |
16
+ | `list_campaigns` | List campaigns (status / name filter, pagination). |
17
+ | `list_zones` | List traffic-source zones (status / name filter, pagination). |
18
+ | `list_team` | List team members (email, role, manager). **Never returns API tokens.** |
19
+ | `create_team_member` | Invite a team member (owner, manager, publisher, advertiser, publisher_manager, advertiser_manager). A scoped manager key can only create its own managed role, self-assigned. Returns the new API key **once** — `list_team` never shows it again. **Dry-run by default**; `confirm: true` to apply. |
20
+ | `create_campaign` | Create a campaign from an advertiser email, offer URL, geo, payout and name. Defaults: CPA / rate 0, **paused**, global payout rule, ready tracking link with `source_click_id={clickid}` + sub placeholders. **Dry-run by default**; `confirm: true` to apply. |
21
+ | `set_campaign_status` | **Run** or **pause** a campaign (`action: "run" \| "pause"`). **Dry-run by default**; `confirm: true` to apply. Running can hit the plan's active-campaign limit. |
22
+ | `update_campaign` | Partial update (name, offer URL, status, rate, budgets, dates, …). **Dry-run by default**; `confirm: true` to apply. Prefer `set_campaign_status` for run/pause. |
23
+ | `create_zone` | Create a traffic-source zone (name + optional postback/site/traffic-back URLs). Always created `active`. **Dry-run by default**; `confirm: true` to apply. |
24
+ | `update_zone` | Partial update (name, status, URLs). **Dry-run by default**; `confirm: true` to apply. Pass `null` to clear a URL. |
25
+ | `get_zone_url` | The `/serve` URL to paste into a network's campaign settingsrotates across the zone's **active** campaigns. Prefilled sub convention, optional `cost` macro. Warns when no active campaigns are visible. |
26
+ | `get_tracking_link` | The `/track/click` link for an existing campaign + zone straight to one active campaign, with no rotation or targeting checks. Re-derives what `create_campaign` echoed on create. |
27
+ | `cut_zones` | Blacklist underperforming zones on a campaign by threshold (CR / spend / ROI). **Dry-run by default**; `confirm: true` to apply. |
28
+ | `list_payout_rules` | List a campaign's global + per-zone payout rules and its `payout_goal_type`. |
29
+ | `set_payout_rule` | Upsert a global or zone-specific payout. **Dry-run by default**; `confirm: true` to apply. |
30
+ | `delete_payout_rule` | Delete a global or zone-specific payout rule. **Dry-run by default**; `confirm: true` to apply. |
31
+ | `set_payout_goal` | Set or clear `payout_goal_type` (goal-based conversions). **Dry-run by default**; `confirm: true` to apply. |
32
+ | `list_targeting_types` | Catalog of targeting rule types, flagging the seeded ones `/serve` never evaluates. |
33
+ | `list_targeting_rules` | List a campaign's targeting rules, flagging any that have no effect. |
34
+ | `set_targeting_rule` | Upsert one targeting rule (safe merge), normalised to what `/serve` matches. **Dry-run by default**; `confirm: true` to apply. |
35
+ | `remove_targeting_rule` | Remove one targeting rule by id or type+method. **Dry-run by default**; `confirm: true` to apply. |
36
+ | `list_sub_labels` | List tenant display names for sub1–sub5. |
37
+ | `set_sub_labels` | Set or clear sub labels (partial; `null` clears). **Dry-run by default**; `confirm: true` to apply. |
38
+ | `list_conversions` | List conversion audit records (payout, spend, pixel type, payload, postback). Optional client-side filters on the current page. |
38
39
 
39
40
  ### Which URL do I give the network?
40
41
 
@@ -110,9 +111,44 @@ claude mcp add affset \
110
111
  -- npx -y @affset/mcp
111
112
  ```
112
113
 
114
+ Same env flags with `-- npx -y github:affset/mcp` if you install from GitHub
115
+ instead of the npm registry (see below).
116
+
113
117
  Add `-e AFFSET_READ_ONLY=true` for a stats/reporting-only instance (see
114
118
  [Security](#security)).
115
119
 
120
+ ### From GitHub directly (no npm publish required)
121
+
122
+ `npx` can install straight from the git repo instead of the npm registry —
123
+ useful if you'd rather not publish, or just want to track `main` without a
124
+ release step:
125
+
126
+ ```json
127
+ {
128
+ "mcpServers": {
129
+ "affset": {
130
+ "command": "npx",
131
+ "args": ["-y", "github:affset/mcp"],
132
+ "env": {
133
+ "AFFSET_BASE_URL": "https://api.affset.com",
134
+ "AFFSET_API_KEY": "sk_live_...",
135
+ "AFFSET_NAMESPACE": "your-namespace"
136
+ }
137
+ }
138
+ }
139
+ }
140
+ ```
141
+
142
+ A push to `main` makes that commit available to this unpinned install path — no
143
+ npm publish is required. On resolution, npm fetches the repository and runs the
144
+ `prepare` script to build `dist/` before starting the binary. npm may reuse its
145
+ cache on later starts; an already running MCP process is not updated until it is
146
+ restarted and `npx` resolves the dependency again.
147
+
148
+ For reproducible deployments, pin a reviewed ref instead of floating on `main`:
149
+ `github:affset/mcp#<commit-sha>` or `github:affset/mcp#<tag>`. Restart the MCP
150
+ process deliberately when you want it to resolve and run a newer revision.
151
+
116
152
  ### From source
117
153
 
118
154
  ```bash
@@ -132,6 +168,8 @@ for `"command": "node"`, `"args": ["/absolute/path/to/affset-mcp/dist/index.js"]
132
168
  >
133
169
  > **who's on the team?** → `list_team()`
134
170
  >
171
+ > **add sarah@offer.com as a publisher** → `create_team_member(email: "sarah@offer.com", role: "publisher")` (dry-run) → confirm
172
+ >
135
173
  > **stats for today by sub1** → `get_stats(group_by: "sub1")`
136
174
  >
137
175
  > **create a RichAds zone with postback** → `create_zone(name: "RichAds", postback_url: "https://…/{source_click_id}")` (dry-run) → confirm
@@ -206,7 +244,11 @@ for `"command": "node"`, `"args": ["/absolute/path/to/affset-mcp/dist/index.js"]
206
244
  has no campaign/zone/date filters; optional filters apply to the current page only.
207
245
  Rows do not include campaign_id/zone_id. Publisher-side roles do not see `spend` and
208
246
  advertiser-side roles do not see `payout`, so `zero_payout` needs a role that can.
209
- - Out of scope: deleting campaigns/zones/conversions, billing, creative management, invite flows.
247
+ - **`create_team_member`** creates the API key directly (like the dashboard's "Add Team
248
+ Member") — it does not send an invite email. Hand the returned key to the person
249
+ yourself. Revoking/removing a team member is not yet a tool; use the dashboard's
250
+ Team page.
251
+ - Out of scope: deleting campaigns/zones/conversions, billing, creative management.
210
252
  - **Tenant signup is deliberately not a tool.** `POST /api/public/create-instance`
211
253
  is Origin-gated and fails closed, which is what keeps signup browser-only; a
212
254
  server-side caller would have to spoof an allowlisted Origin to get past it. The
@@ -237,6 +279,8 @@ npm run dev # watch mode
237
279
  - Create a **dedicated, least-privilege API key** for the MCP rather than reusing an
238
280
  owner key, and give it an expiry — affset's RBAC roles (owner/manager/publisher/
239
281
  advertiser) apply to MCP tool calls exactly as they do to the dashboard.
282
+ - Pin GitHub installs to a reviewed commit or tag in long-lived environments. A
283
+ floating `main` spec can run newer repository code the next time `npx` resolves it.
240
284
 
241
285
  ### Prompt injection via conversion/click data
242
286
 
package/dist/index.js CHANGED
File without changes
package/dist/server.js CHANGED
@@ -6,6 +6,7 @@ import { createCampaign, createCampaignInputSchema, CREATE_CAMPAIGN_DESCRIPTION,
6
6
  import { listCampaigns, listCampaignsInputSchema, LIST_CAMPAIGNS_DESCRIPTION, } from "./tools/listCampaigns.js";
7
7
  import { listZones, listZonesInputSchema, LIST_ZONES_DESCRIPTION } from "./tools/listZones.js";
8
8
  import { listTeam, listTeamInputSchema, LIST_TEAM_DESCRIPTION } from "./tools/listTeam.js";
9
+ import { createTeamMember, createTeamMemberInputSchema, CREATE_TEAM_MEMBER_DESCRIPTION, } from "./tools/createTeamMember.js";
9
10
  import { createZone, createZoneInputSchema, CREATE_ZONE_DESCRIPTION } from "./tools/createZone.js";
10
11
  import { getZoneUrl, getZoneUrlInputSchema, GET_ZONE_URL_DESCRIPTION } from "./tools/getZoneUrl.js";
11
12
  import { getTrackingLink, getTrackingLinkInputSchema, GET_TRACKING_LINK_DESCRIPTION, } from "./tools/getTrackingLink.js";
@@ -102,6 +103,17 @@ export function createServer(config) {
102
103
  openWorldHint: true,
103
104
  },
104
105
  }, (args) => listTeam(client, args));
106
+ registerTool("create_team_member", {
107
+ title: "Invite a team member",
108
+ description: CREATE_TEAM_MEMBER_DESCRIPTION,
109
+ inputSchema: createTeamMemberInputSchema,
110
+ annotations: {
111
+ readOnlyHint: false,
112
+ destructiveHint: false,
113
+ idempotentHint: false,
114
+ openWorldHint: true,
115
+ },
116
+ }, (args) => createTeamMember(client, args));
105
117
  registerTool("get_zone_url", {
106
118
  title: "Get the zone URL for a traffic source",
107
119
  description: GET_ZONE_URL_DESCRIPTION,
@@ -13,7 +13,8 @@ const PAYOUT_MAX = 9999.99999;
13
13
  const NAME_MAX = 120;
14
14
  export const CREATE_CAMPAIGN_DESCRIPTION = "Create a campaign (offer) in the current namespace from a compact spec: advertiser " +
15
15
  "email, offer URL, geo whitelist, payout, name. The advertiser (user_email) is required " +
16
- "— it must already exist as a team member (same as the Advertiser dropdown in the dashboard). " +
16
+ "— it must already exist as a team member (same as the Advertiser dropdown in the dashboard); " +
17
+ "create one first with create_team_member if needed. " +
17
18
  "Everything else gets media-buying defaults: CPA model, " +
18
19
  "rate 0 (no internal advertiser billing), created paused, a global payout rule when " +
19
20
  "payout is given, and a ready-to-use tracking link prefilled with the sub convention " +
@@ -29,7 +30,7 @@ export const createCampaignInputSchema = {
29
30
  .email()
30
31
  .describe("Advertiser email that owns this campaign (required — the API rejects the call without it). " +
31
32
  "Must already exist as a team member with the advertiser role; same as the dashboard's " +
32
- "Advertiser dropdown. List candidates with list_team."),
33
+ "Advertiser dropdown. List candidates with list_team, or create one with create_team_member."),
33
34
  offer_url: z
34
35
  .string()
35
36
  .min(1)
@@ -0,0 +1,204 @@
1
+ import { z } from "zod";
2
+ import { mdCell } from "../lib/format.js";
3
+ import { errorResult, textError, textResult } from "../lib/toolResult.js";
4
+ /** Matches lite-adserver's TENANT_ROLES. */
5
+ const TENANT_ROLES = [
6
+ "owner",
7
+ "manager",
8
+ "advertiser_manager",
9
+ "publisher_manager",
10
+ "advertiser",
11
+ "publisher",
12
+ ];
13
+ /** manager_email is only meaningful for these two roles. */
14
+ const MANAGED_ROLES = ["advertiser", "publisher"];
15
+ const PERMISSION_VALUES = ["read", "write"];
16
+ const MAX_EPOCH_MS = 8_640_000_000_000_000;
17
+ /** affset tokens are `sk_live_` + 64 hex (~72 chars). Bound what we echo into model context. */
18
+ const MAX_TOKEN_LENGTH = 200;
19
+ export const CREATE_TEAM_MEMBER_DESCRIPTION = "Invite a team member: create a user API key with an email in the current namespace — " +
20
+ 'the same operation as the dashboard\'s "Add Team Member". Requires owner/manager, or a ' +
21
+ "scoped manager role (publisher_manager / advertiser_manager), which can only create its " +
22
+ "own managed role (publisher / advertiser respectively) assigned to itself — the API " +
23
+ "enforces this, not this tool, so a scoped manager's role/manager_email args may be " +
24
+ "overridden. Returns the new member's plaintext API key ONCE, in the confirmed response " +
25
+ "— it is not shown again by list_team, which deliberately never echoes tokens, so copy " +
26
+ "it now and send it to them over a private channel. Does not send an invite email; the " +
27
+ "API key is the credential, handed over out of band. " +
28
+ "DRY-RUN by default; pass confirm=true to apply.";
29
+ export const createTeamMemberInputSchema = {
30
+ email: z
31
+ .string()
32
+ .trim()
33
+ .email()
34
+ .describe("New team member's email — their login / API identity."),
35
+ role: z
36
+ .enum(TENANT_ROLES)
37
+ .describe("owner | manager | advertiser_manager | publisher_manager | advertiser | publisher. " +
38
+ "What your own API key's role is allowed to create is enforced by the API."),
39
+ manager_email: z
40
+ .string()
41
+ .trim()
42
+ .email()
43
+ .optional()
44
+ .describe("Assign to a publisher_manager / advertiser_manager. Only valid with role=publisher or " +
45
+ "role=advertiser. A scoped manager key ignores this and assigns itself instead."),
46
+ permissions: z
47
+ .array(z.enum(PERMISSION_VALUES))
48
+ .nonempty()
49
+ .optional()
50
+ .describe('Defaults to ["read","write"] (matches the dashboard\'s default). "read" is always included.'),
51
+ expires_at: z
52
+ .number()
53
+ .int()
54
+ .positive()
55
+ .max(MAX_EPOCH_MS)
56
+ .optional()
57
+ .describe("Optional future expiry as epoch milliseconds. Omit for a key that never expires."),
58
+ confirm: z
59
+ .boolean()
60
+ .default(false)
61
+ .describe("false = dry-run preview (default). true = create the member and issue the key."),
62
+ };
63
+ export async function createTeamMember(client, args) {
64
+ try {
65
+ const email = args.email.trim();
66
+ if (!email)
67
+ return textError("email is required.");
68
+ const managerEmail = args.manager_email?.trim();
69
+ if (args.manager_email !== undefined && !managerEmail) {
70
+ return textError("manager_email must not be blank when provided.");
71
+ }
72
+ if (managerEmail !== undefined && !MANAGED_ROLES.includes(args.role)) {
73
+ return textError("manager_email is only allowed with role=publisher or role=advertiser.");
74
+ }
75
+ const expiryError = validateExpiry(args.expires_at);
76
+ if (expiryError)
77
+ return textError(expiryError);
78
+ const permissions = normalizePermissions(args.permissions);
79
+ const expiresNote = args.expires_at !== undefined ? fmtDay(args.expires_at) : "never";
80
+ const summaryTable = [
81
+ "| Field | Value |",
82
+ "|---|---|",
83
+ `| Email | ${mdCell(email)} |`,
84
+ `| Role | ${mdCell(args.role)} |`,
85
+ `| Manager | ${mdCell(managerEmail ?? "—")} |`,
86
+ `| Permissions | ${permissions.join(", ")} |`,
87
+ `| Expires | ${mdCell(expiresNote)} |`,
88
+ ].join("\n");
89
+ if (!args.confirm) {
90
+ return textResult([
91
+ "**Dry run** — would create a team member with:",
92
+ "",
93
+ summaryTable,
94
+ "",
95
+ "Call again with `confirm: true` to create it and issue the API key. A scoped " +
96
+ "manager key (publisher_manager / advertiser_manager) can only create its own " +
97
+ "managed role, assigned to itself — the fields above may be overridden by the API.",
98
+ ].join("\n"));
99
+ }
100
+ const body = { email, role: args.role, permissions };
101
+ if (managerEmail !== undefined)
102
+ body.manager_email = managerEmail;
103
+ if (args.expires_at !== undefined)
104
+ body.expires_at = args.expires_at;
105
+ // Treat the response as untrusted at runtime. Most importantly, do not let a
106
+ // malformed optional field throw after the non-idempotent POST succeeded: that
107
+ // would report an error and make a duplicate retry look appropriate.
108
+ const rawCreated = await client.post("/api/api-keys?type=user", body);
109
+ const created = isRecord(rawCreated) ? rawCreated : {};
110
+ const createdEmail = stringField(created.email, email);
111
+ const createdRole = stringField(created.role, args.role);
112
+ const createdManager = nullableStringField(created.manager_email, managerEmail ?? "—");
113
+ const createdPermissions = permissionFields(created.permissions, permissions);
114
+ const createdExpiry = expiryField(created.expires_at, expiresNote);
115
+ const token = extractToken(created.token);
116
+ const result = [
117
+ `✅ Team member **${mdCell(createdEmail)}** created.`,
118
+ "",
119
+ "| Field | Value |",
120
+ "|---|---|",
121
+ `| Email | ${mdCell(createdEmail)} |`,
122
+ `| Role | ${mdCell(createdRole)} |`,
123
+ `| Manager | ${mdCell(createdManager)} |`,
124
+ `| Permissions | ${createdPermissions.map(mdCell).join(", ")} |`,
125
+ `| Expires | ${mdCell(createdExpiry)} |`,
126
+ "",
127
+ ];
128
+ if (token) {
129
+ result.push("**API key** (shown once here — treat it like a password, send it over a private channel):", secretBlock(token), "`list_team` will show this person from now on, but never their token. If it leaks, " +
130
+ "revoke access from the dashboard's Team page.");
131
+ }
132
+ else {
133
+ result.push("⚠️ The member was created, but the API response did not include a usable API key. " +
134
+ "Do not retry this create call: that could create a duplicate. Retrieve or rotate " +
135
+ "the key from the dashboard's Team page.");
136
+ }
137
+ return textResult(result.join("\n"));
138
+ }
139
+ catch (err) {
140
+ return errorResult(err);
141
+ }
142
+ }
143
+ function normalizePermissions(raw) {
144
+ const set = new Set(raw && raw.length > 0 ? raw : PERMISSION_VALUES);
145
+ set.add("read");
146
+ return PERMISSION_VALUES.filter((permission) => set.has(permission));
147
+ }
148
+ function fmtDay(ms) {
149
+ return new Date(ms).toISOString().split("T", 1)[0];
150
+ }
151
+ function validateExpiry(ms) {
152
+ if (ms === undefined)
153
+ return null;
154
+ if (!Number.isInteger(ms) || ms <= 0 || ms > MAX_EPOCH_MS) {
155
+ return "expires_at must be a valid positive epoch-millisecond timestamp.";
156
+ }
157
+ if (ms <= Date.now())
158
+ return "expires_at must be in the future.";
159
+ return null;
160
+ }
161
+ function isRecord(value) {
162
+ return value !== null && typeof value === "object" && !Array.isArray(value);
163
+ }
164
+ function extractToken(value) {
165
+ if (typeof value !== "string")
166
+ return null;
167
+ const token = value.trim();
168
+ if (!token || token.length > MAX_TOKEN_LENGTH)
169
+ return null;
170
+ return token;
171
+ }
172
+ function stringField(value, fallback) {
173
+ return typeof value === "string" && value.trim() ? value : fallback;
174
+ }
175
+ function nullableStringField(value, fallback) {
176
+ if (value === null)
177
+ return "—";
178
+ return stringField(value, fallback);
179
+ }
180
+ function permissionFields(value, fallback) {
181
+ if (!Array.isArray(value))
182
+ return fallback;
183
+ const permissions = value.filter((permission) => typeof permission === "string" && permission.trim() !== "");
184
+ return permissions.length > 0 ? permissions : fallback;
185
+ }
186
+ function expiryField(value, fallback) {
187
+ if (value === null)
188
+ return "never";
189
+ if (typeof value !== "number" || !Number.isFinite(value))
190
+ return fallback;
191
+ try {
192
+ return fmtDay(value);
193
+ }
194
+ catch {
195
+ return fallback;
196
+ }
197
+ }
198
+ /** Use a fence longer than any backtick run in the secret, so it cannot close the block. */
199
+ function secretBlock(secret) {
200
+ const longestRun = Math.max(0, ...(secret.match(/`+/g) ?? []).map((run) => run.length));
201
+ const fence = "`".repeat(Math.max(3, longestRun + 1));
202
+ return `${fence}\n${secret}\n${fence}`;
203
+ }
204
+ //# sourceMappingURL=createTeamMember.js.map
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@affset/mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
+ "mcpName": "io.github.affset/mcp",
4
5
  "description": "MCP server for the affset ad platform — stats, campaigns, zones, payouts, targeting, sub labels, and team from your chat client.",
5
6
  "type": "module",
6
7
  "license": "MIT",