@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 +71 -27
- package/dist/index.js +0 -0
- package/dist/server.js +12 -0
- package/dist/tools/createCampaign.js +3 -2
- package/dist/tools/createTeamMember.js +204 -0
- package/package.json +2 -1
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
|
-
| `
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
22
|
-
| `
|
|
23
|
-
| `
|
|
24
|
-
| `
|
|
25
|
-
| `
|
|
26
|
-
| `
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
29
|
-
| `
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
34
|
-
| `
|
|
35
|
-
| `
|
|
36
|
-
| `
|
|
37
|
-
| `
|
|
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 settings — rotates 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
|
-
-
|
|
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.
|
|
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",
|