@affset/mcp 0.2.0 → 0.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/README.md +112 -54
- package/dist/lib/integrationUrls.d.ts +34 -4
- package/dist/lib/integrationUrls.js +84 -4
- package/dist/lib/linkArgs.d.ts +5 -0
- package/dist/lib/linkArgs.js +14 -3
- package/dist/lib/zones.js +9 -1
- package/dist/registerTools.js +36 -0
- package/dist/tools/createCampaign.js +20 -8
- package/dist/tools/createTrafficSource.d.ts +25 -0
- package/dist/tools/createTrafficSource.js +191 -0
- package/dist/tools/createZone.d.ts +2 -0
- package/dist/tools/createZone.js +13 -1
- package/dist/tools/getStats.d.ts +2 -0
- package/dist/tools/getStats.js +19 -3
- package/dist/tools/getTrackingLink.js +26 -9
- package/dist/tools/getZoneUrl.js +30 -13
- package/dist/tools/listConversions.d.ts +2 -0
- package/dist/tools/listConversions.js +29 -10
- package/dist/tools/listTrafficSources.d.ts +17 -0
- package/dist/tools/listTrafficSources.js +63 -0
- package/dist/tools/listZones.js +10 -5
- package/dist/tools/updateTrafficSource.d.ts +25 -0
- package/dist/tools/updateTrafficSource.js +171 -0
- package/dist/tools/updateZone.d.ts +2 -0
- package/dist/tools/updateZone.js +13 -3
- package/dist/types.d.ts +47 -0
- package/dist/types.js +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -6,14 +6,26 @@ import { SUB_KEYS, } from "../types.js";
|
|
|
6
6
|
const SORT_FIELDS = ["created_at", "ad_event_id", "click_id"];
|
|
7
7
|
export const LIST_CONVERSIONS_DESCRIPTION = "List recent conversion records (audit trail) for debugging payouts and pixel params. " +
|
|
8
8
|
"Shows payout, spend, pixel `type`, source_click_id, click_id, subs, postback outcome, " +
|
|
9
|
-
"and the raw payload.
|
|
10
|
-
"
|
|
11
|
-
"
|
|
9
|
+
"and the raw payload. `paid_only: true` drops informative conversions server-side — " +
|
|
10
|
+
"rows recorded with postback_skipped=non_goal_type because the pixel type missed the " +
|
|
11
|
+
"campaign's payout_goal_type. Silent conversions and other skip reasons still come back " +
|
|
12
|
+
"(not a payout>0 filter). Default false, all rows. Beyond pagination/sort/paid_only, " +
|
|
13
|
+
"the optional click_id / source_click_id / type / payload_contains / zero_payout filters " +
|
|
14
|
+
"run client-side on the current page. Does not include campaign_id/zone_id (not returned " +
|
|
15
|
+
"by the API).";
|
|
12
16
|
export const listConversionsInputSchema = {
|
|
13
17
|
limit: z.number().int().min(1).max(100).default(20).describe("Page size (1–100). Default 20."),
|
|
14
18
|
offset: z.number().int().min(0).default(0).describe("Pagination offset. Default 0."),
|
|
15
19
|
sort: z.enum(SORT_FIELDS).default("created_at").describe("Sort field. Default created_at."),
|
|
16
20
|
order: z.enum(["asc", "desc"]).default("desc").describe("Sort order. Default desc."),
|
|
21
|
+
paid_only: z
|
|
22
|
+
.boolean()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe("true drops informative conversions — rows recorded with " +
|
|
25
|
+
"postback_skipped=non_goal_type because the pixel type missed the campaign's " +
|
|
26
|
+
"payout_goal_type. Silent conversions and other skip reasons still come back " +
|
|
27
|
+
"(not a payout>0 filter). Server-side (filters the whole dataset, not just this page). " +
|
|
28
|
+
"Works without payout visibility. Default false (all rows)."),
|
|
17
29
|
click_id: z
|
|
18
30
|
.string()
|
|
19
31
|
.min(1)
|
|
@@ -42,13 +54,17 @@ export const listConversionsInputSchema = {
|
|
|
42
54
|
};
|
|
43
55
|
export async function listConversions(client, args) {
|
|
44
56
|
try {
|
|
57
|
+
const query = {
|
|
58
|
+
limit: args.limit,
|
|
59
|
+
offset: args.offset,
|
|
60
|
+
sort: args.sort,
|
|
61
|
+
order: args.order,
|
|
62
|
+
};
|
|
63
|
+
// The API accepts only the literal strings "true"/"false"; omitted = false.
|
|
64
|
+
if (args.paid_only !== undefined)
|
|
65
|
+
query.paid_only = String(args.paid_only);
|
|
45
66
|
const [data, settings] = await Promise.all([
|
|
46
|
-
client.get("/api/conversions",
|
|
47
|
-
limit: args.limit,
|
|
48
|
-
offset: args.offset,
|
|
49
|
-
sort: args.sort,
|
|
50
|
-
order: args.order,
|
|
51
|
-
}),
|
|
67
|
+
client.get("/api/conversions", query),
|
|
52
68
|
// One read covers both the sub labels and the zone timestamps are shown in.
|
|
53
69
|
client.get("/api/tenant").catch(() => ({})),
|
|
54
70
|
]);
|
|
@@ -67,6 +83,8 @@ export async function listConversions(client, args) {
|
|
|
67
83
|
const total = pagination?.total ?? all.length;
|
|
68
84
|
const clientFiltered = filtered.length !== all.length;
|
|
69
85
|
const filterBits = [];
|
|
86
|
+
if (args.paid_only)
|
|
87
|
+
filterBits.push("paid_only");
|
|
70
88
|
if (args.click_id)
|
|
71
89
|
filterBits.push(`click_id=${args.click_id}`);
|
|
72
90
|
if (args.source_click_id)
|
|
@@ -98,7 +116,8 @@ export async function listConversions(client, args) {
|
|
|
98
116
|
]
|
|
99
117
|
: []),
|
|
100
118
|
"",
|
|
101
|
-
"_API has no campaign/zone/date filters
|
|
119
|
+
"_API has no campaign/zone/date filters. `paid_only` is server-side; other optional " +
|
|
120
|
+
"filters apply to this page. Page with limit/offset._",
|
|
102
121
|
payoutHidden
|
|
103
122
|
? "_`payout` is hidden for this role — the column shows `—` for every row._"
|
|
104
123
|
: "_`$0` payout with a non-empty type often means payout_goal_type mismatch or no payout rule._",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import type { AffsetClient } from "../client.js";
|
|
4
|
+
import { TRAFFIC_SOURCE_STATUSES } from "../types.js";
|
|
5
|
+
export declare const LIST_TRAFFIC_SOURCES_DESCRIPTION: string;
|
|
6
|
+
export declare const listTrafficSourcesInputSchema: {
|
|
7
|
+
status: z.ZodOptional<z.ZodEnum<["active", "archived"]>>;
|
|
8
|
+
limit: z.ZodDefault<z.ZodNumber>;
|
|
9
|
+
offset: z.ZodDefault<z.ZodNumber>;
|
|
10
|
+
};
|
|
11
|
+
type ListTrafficSourcesArgs = {
|
|
12
|
+
status?: (typeof TRAFFIC_SOURCE_STATUSES)[number];
|
|
13
|
+
limit: number;
|
|
14
|
+
offset: number;
|
|
15
|
+
};
|
|
16
|
+
export declare function listTrafficSources(client: AffsetClient, args: ListTrafficSourcesArgs): Promise<CallToolResult>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { capUntrusted, mdCell } from "../lib/format.js";
|
|
3
|
+
import { errorResult, textResult } from "../lib/toolResult.js";
|
|
4
|
+
import { TRAFFIC_SOURCE_STATUSES } from "../types.js";
|
|
5
|
+
/** Templates run to 2000 chars server-side; the table shows enough to identify one. */
|
|
6
|
+
const TEMPLATE_PREVIEW_MAX = 150;
|
|
7
|
+
export const LIST_TRAFFIC_SOURCES_DESCRIPTION = "List the tenant's traffic sources — the ad networks bought from, each with the " +
|
|
8
|
+
"tracking template its linked zones render in get_zone_url/get_tracking_link and an " +
|
|
9
|
+
"optional postback template. The stored network API token is write-only and shown " +
|
|
10
|
+
"only as set/none. Read-only.";
|
|
11
|
+
export const listTrafficSourcesInputSchema = {
|
|
12
|
+
status: z
|
|
13
|
+
.enum(TRAFFIC_SOURCE_STATUSES)
|
|
14
|
+
.optional()
|
|
15
|
+
.describe("Filter by status. Omit for all sources."),
|
|
16
|
+
limit: z.number().int().min(1).max(100).default(50).describe("Sources per page (1–100)."),
|
|
17
|
+
offset: z.number().int().min(0).default(0).describe("Pagination offset."),
|
|
18
|
+
};
|
|
19
|
+
export async function listTrafficSources(client, args) {
|
|
20
|
+
try {
|
|
21
|
+
const query = {
|
|
22
|
+
limit: args.limit,
|
|
23
|
+
offset: args.offset,
|
|
24
|
+
sort: "created_at",
|
|
25
|
+
order: "desc",
|
|
26
|
+
};
|
|
27
|
+
if (args.status)
|
|
28
|
+
query.status = args.status;
|
|
29
|
+
const res = await client.get("/api/traffic-sources", query);
|
|
30
|
+
const sources = res.traffic_sources ?? [];
|
|
31
|
+
const total = res.pagination?.total ?? sources.length;
|
|
32
|
+
if (sources.length === 0) {
|
|
33
|
+
return textResult(args.status
|
|
34
|
+
? `No ${args.status} traffic sources.`
|
|
35
|
+
: "No traffic sources yet. Create one with `create_traffic_source` — start from " +
|
|
36
|
+
"a preset to get the network's tracking template ready-made, then link zones " +
|
|
37
|
+
"to it via `create_zone`/`update_zone` `traffic_source_id`.");
|
|
38
|
+
}
|
|
39
|
+
const rows = sources.map((s) => [
|
|
40
|
+
`| ${mdCell(s.name)} `,
|
|
41
|
+
`| \`${mdCell(s.id)}\` `,
|
|
42
|
+
`| ${s.preset ? mdCell(s.preset) : "—"} `,
|
|
43
|
+
`| ${mdCell(s.status)} `,
|
|
44
|
+
`| ${s.has_api_token ? "set" : "—"} `,
|
|
45
|
+
`| ${s.tracking_template ? mdCell(capUntrusted(s.tracking_template, TEMPLATE_PREVIEW_MAX)) : "—"} `,
|
|
46
|
+
`| ${s.postback_template ? mdCell(capUntrusted(s.postback_template, TEMPLATE_PREVIEW_MAX)) : "—"} |`,
|
|
47
|
+
].join(""));
|
|
48
|
+
return textResult([
|
|
49
|
+
`**Traffic sources** (${sources.length} of ${total}):`,
|
|
50
|
+
"",
|
|
51
|
+
"| Name | Id | Preset | Status | API token | Tracking template | Postback template |",
|
|
52
|
+
"|---|---|---|---|---|---|---|",
|
|
53
|
+
...rows,
|
|
54
|
+
"",
|
|
55
|
+
"_Zones linked to a source render its tracking template in `get_zone_url` / " +
|
|
56
|
+
"`get_tracking_link`. Edit a source with `update_traffic_source`._",
|
|
57
|
+
].join("\n"));
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
return errorResult(err);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=listTrafficSources.js.map
|
package/dist/tools/listZones.js
CHANGED
|
@@ -3,8 +3,8 @@ import { mdCell } from "../lib/format.js";
|
|
|
3
3
|
import { errorResult } from "../lib/toolResult.js";
|
|
4
4
|
import { ZONE_STATUSES } from "../types.js";
|
|
5
5
|
export const LIST_ZONES_DESCRIPTION = "List traffic-source zones in the current namespace. Filter by status and optionally " +
|
|
6
|
-
"by name (client-side contains match). Returns id, name,
|
|
7
|
-
"site_url, publisher. Paginated (default 20, max 100).";
|
|
6
|
+
"by name (client-side contains match). Returns id, name, linked traffic source, " +
|
|
7
|
+
"status, postback_url, site_url, publisher. Paginated (default 20, max 100).";
|
|
8
8
|
export const listZonesInputSchema = {
|
|
9
9
|
status: z.enum(ZONE_STATUSES).optional().describe("Filter by zone status."),
|
|
10
10
|
name_contains: z
|
|
@@ -54,11 +54,16 @@ function renderTable(zones) {
|
|
|
54
54
|
if (zones.length === 0)
|
|
55
55
|
return "_No zones matched._";
|
|
56
56
|
const lines = [
|
|
57
|
-
"| ID | Name | Status | Postback | Site | Publisher |",
|
|
58
|
-
"
|
|
57
|
+
"| ID | Name | Source | Status | Postback | Site | Publisher |",
|
|
58
|
+
"|---|---|---|---|---|---|---|",
|
|
59
59
|
];
|
|
60
60
|
for (const z of zones) {
|
|
61
|
-
|
|
61
|
+
const source = z.traffic_source_name
|
|
62
|
+
? mdCell(z.traffic_source_name)
|
|
63
|
+
: z.traffic_source_id
|
|
64
|
+
? `\`${mdCell(z.traffic_source_id)}\``
|
|
65
|
+
: "—";
|
|
66
|
+
lines.push(`| \`${z.id}\` | ${mdCell(z.name)} | ${source} | ${z.status} | ${z.postback_url ? mdCell(shortUrl(z.postback_url)) : "⚠️ none"} | ${mdCell(shortUrl(z.site_url))} | ${mdCell(z.user_email ?? "—")} |`);
|
|
62
67
|
}
|
|
63
68
|
return lines.join("\n");
|
|
64
69
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import { type AffsetClient } from "../client.js";
|
|
4
|
+
import { TRAFFIC_SOURCE_STATUSES } from "../types.js";
|
|
5
|
+
export declare const UPDATE_TRAFFIC_SOURCE_DESCRIPTION: string;
|
|
6
|
+
export declare const updateTrafficSourceInputSchema: {
|
|
7
|
+
traffic_source_id: z.ZodString;
|
|
8
|
+
name: z.ZodOptional<z.ZodString>;
|
|
9
|
+
tracking_template: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNull]>>;
|
|
10
|
+
postback_template: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNull]>>;
|
|
11
|
+
api_token: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNull]>>;
|
|
12
|
+
status: z.ZodOptional<z.ZodEnum<["active", "archived"]>>;
|
|
13
|
+
confirm: z.ZodDefault<z.ZodBoolean>;
|
|
14
|
+
};
|
|
15
|
+
type UpdateTrafficSourceArgs = {
|
|
16
|
+
traffic_source_id: string;
|
|
17
|
+
name?: string;
|
|
18
|
+
tracking_template?: string | null;
|
|
19
|
+
postback_template?: string | null;
|
|
20
|
+
api_token?: string | null;
|
|
21
|
+
status?: (typeof TRAFFIC_SOURCE_STATUSES)[number];
|
|
22
|
+
confirm: boolean;
|
|
23
|
+
};
|
|
24
|
+
export declare function updateTrafficSource(client: AffsetClient, args: UpdateTrafficSourceArgs): Promise<CallToolResult>;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { AffsetApiError } from "../client.js";
|
|
3
|
+
import { SOURCE_CLICK_ID_PARAM, templateHasParam } from "../lib/integrationUrls.js";
|
|
4
|
+
import { displayValue, renderDiff } from "../lib/patch.js";
|
|
5
|
+
import { errorResult, textError, textResult } from "../lib/toolResult.js";
|
|
6
|
+
import { httpUrlError } from "../lib/urls.js";
|
|
7
|
+
import { TRAFFIC_SOURCE_STATUSES, } from "../types.js";
|
|
8
|
+
const NAME_MAX = 200;
|
|
9
|
+
const TEMPLATE_MAX = 2000;
|
|
10
|
+
const TOKEN_MAX = 500;
|
|
11
|
+
/** Zod: replacement string, or null to clear. */
|
|
12
|
+
const clearableTemplate = z.union([z.string().max(TEMPLATE_MAX), z.null()]);
|
|
13
|
+
export const UPDATE_TRAFFIC_SOURCE_DESCRIPTION = "Update a traffic source (name, tracking_template, postback_template, api_token, " +
|
|
14
|
+
'status). Partial update — only provided fields change; pass null (or "") to clear a ' +
|
|
15
|
+
"template or the stored api_token. Zones linked to the source pick the new tracking " +
|
|
16
|
+
"template up immediately in get_zone_url/get_tracking_link. " +
|
|
17
|
+
"DRY-RUN by default; pass confirm=true to apply.";
|
|
18
|
+
export const updateTrafficSourceInputSchema = {
|
|
19
|
+
traffic_source_id: z.string().trim().min(1).describe("Traffic source id to update."),
|
|
20
|
+
name: z
|
|
21
|
+
.string()
|
|
22
|
+
.trim()
|
|
23
|
+
.min(1)
|
|
24
|
+
.max(NAME_MAX)
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("New display name (unique per tenant)."),
|
|
27
|
+
tracking_template: clearableTemplate
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("New tracking template (query string appended to the zone URL; must not start " +
|
|
30
|
+
"with ? or &), or null to clear it."),
|
|
31
|
+
postback_template: clearableTemplate
|
|
32
|
+
.optional()
|
|
33
|
+
.describe("New postback template (http(s) URL with our macros), or null to clear it."),
|
|
34
|
+
api_token: z
|
|
35
|
+
.union([z.string().max(TOKEN_MAX), z.null()])
|
|
36
|
+
.optional()
|
|
37
|
+
.describe('Replacement network API credential (stored write-only), or null/"" to clear the ' +
|
|
38
|
+
"stored one. Omit to leave it unchanged."),
|
|
39
|
+
status: z
|
|
40
|
+
.enum(TRAFFIC_SOURCE_STATUSES)
|
|
41
|
+
.optional()
|
|
42
|
+
.describe("active or archived. Archiving keeps the source linked to its zones — deletion is " +
|
|
43
|
+
"only possible once no zone references it."),
|
|
44
|
+
confirm: z
|
|
45
|
+
.boolean()
|
|
46
|
+
.default(false)
|
|
47
|
+
.describe("false = dry-run preview (default). true = apply the update."),
|
|
48
|
+
};
|
|
49
|
+
export async function updateTrafficSource(client, args) {
|
|
50
|
+
try {
|
|
51
|
+
const patch = buildPatch(args);
|
|
52
|
+
if ("error" in patch)
|
|
53
|
+
return textError(patch.error);
|
|
54
|
+
if (Object.keys(patch.body).length === 0) {
|
|
55
|
+
return textError("Provide at least one field to update: name, tracking_template, " +
|
|
56
|
+
"postback_template, api_token, status.");
|
|
57
|
+
}
|
|
58
|
+
let existing;
|
|
59
|
+
try {
|
|
60
|
+
existing = await client.get(`/api/traffic-sources/${encodeURIComponent(args.traffic_source_id)}`);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
if (err instanceof AffsetApiError && err.status === 404) {
|
|
64
|
+
return textError(`Traffic source \`${args.traffic_source_id}\` not found in this namespace.`);
|
|
65
|
+
}
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
const changes = diffSource(existing, patch.body);
|
|
69
|
+
if (changes.length === 0) {
|
|
70
|
+
return textResult(`Nothing to change on traffic source \`${existing.id}\` (${existing.name}) — ` +
|
|
71
|
+
"provided fields already match current values.");
|
|
72
|
+
}
|
|
73
|
+
const warnings = [];
|
|
74
|
+
const linkedZones = existing.linked_zones ?? 0;
|
|
75
|
+
if ("tracking_template" in patch.body && linkedZones > 0) {
|
|
76
|
+
warnings.push(`The new tracking template applies to the ${linkedZones} linked ` +
|
|
77
|
+
`zone${linkedZones === 1 ? "" : "s"} the next time a URL is rendered — links ` +
|
|
78
|
+
"already pasted into network campaigns keep their old parameters until replaced.");
|
|
79
|
+
}
|
|
80
|
+
if (typeof patch.body.tracking_template === "string" &&
|
|
81
|
+
patch.body.tracking_template !== "" &&
|
|
82
|
+
!templateHasParam(patch.body.tracking_template, SOURCE_CLICK_ID_PARAM)) {
|
|
83
|
+
warnings.push("⚠️ The new tracking template has no `source_click_id=` parameter — the " +
|
|
84
|
+
"network's click token will not be captured on linked zones' URLs.");
|
|
85
|
+
}
|
|
86
|
+
if (patch.body.status === "archived" && existing.status !== "archived") {
|
|
87
|
+
warnings.push("Archived sources stay linked to their zones and keep rendering their template; " +
|
|
88
|
+
"archiving only hides the source from active pickers.");
|
|
89
|
+
}
|
|
90
|
+
if (!args.confirm) {
|
|
91
|
+
return textResult([
|
|
92
|
+
`**Dry run** — would update traffic source \`${existing.id}\` (${existing.name}).`,
|
|
93
|
+
"",
|
|
94
|
+
renderDiff(changes),
|
|
95
|
+
...(warnings.length ? ["", ...warnings] : []),
|
|
96
|
+
"",
|
|
97
|
+
"Call again with `confirm: true` to apply.",
|
|
98
|
+
].join("\n"));
|
|
99
|
+
}
|
|
100
|
+
const updated = await client.put(`/api/traffic-sources/${encodeURIComponent(args.traffic_source_id)}`, patch.body);
|
|
101
|
+
return textResult([
|
|
102
|
+
`✅ Traffic source \`${updated.id}\` updated.`,
|
|
103
|
+
"",
|
|
104
|
+
renderDiff(changes),
|
|
105
|
+
...(warnings.length ? ["", ...warnings] : []),
|
|
106
|
+
].join("\n"));
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
return errorResult(err);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function buildPatch(args) {
|
|
113
|
+
const body = {};
|
|
114
|
+
if (args.name !== undefined)
|
|
115
|
+
body.name = args.name;
|
|
116
|
+
if (args.status !== undefined)
|
|
117
|
+
body.status = args.status;
|
|
118
|
+
// Templates clear with "" — the server rejects null for them (api_token is the
|
|
119
|
+
// one field where null also clears).
|
|
120
|
+
if (args.tracking_template !== undefined) {
|
|
121
|
+
const value = args.tracking_template === null ? "" : args.tracking_template.trim();
|
|
122
|
+
if (value && /^[?&]/.test(value)) {
|
|
123
|
+
return {
|
|
124
|
+
error: "tracking_template must not start with ? or & — it is appended after ? automatically.",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
body.tracking_template = value;
|
|
128
|
+
}
|
|
129
|
+
if (args.postback_template !== undefined) {
|
|
130
|
+
const value = args.postback_template === null ? "" : args.postback_template.trim();
|
|
131
|
+
if (value) {
|
|
132
|
+
const err = httpUrlError(value, "postback_template");
|
|
133
|
+
if (err)
|
|
134
|
+
return { error: err };
|
|
135
|
+
}
|
|
136
|
+
body.postback_template = value;
|
|
137
|
+
}
|
|
138
|
+
if (args.api_token !== undefined) {
|
|
139
|
+
body.api_token = args.api_token === null ? null : args.api_token.trim();
|
|
140
|
+
}
|
|
141
|
+
return { body };
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* api_token never round-trips: the existing row only carries has_api_token, and
|
|
145
|
+
* the diff must not echo the new value either.
|
|
146
|
+
*/
|
|
147
|
+
function diffSource(existing, body) {
|
|
148
|
+
const changes = [];
|
|
149
|
+
for (const [field, to] of Object.entries(body)) {
|
|
150
|
+
if (field === "api_token") {
|
|
151
|
+
const from = existing.has_api_token ? "(set)" : "(none)";
|
|
152
|
+
const toDisplay = to === null || to === "" ? "(cleared)" : "(set — write-only)";
|
|
153
|
+
if (existing.has_api_token && (to === null || to === "")) {
|
|
154
|
+
changes.push({ field, from, to: toDisplay });
|
|
155
|
+
}
|
|
156
|
+
else if (to !== null && to !== "") {
|
|
157
|
+
changes.push({ field, from, to: toDisplay });
|
|
158
|
+
}
|
|
159
|
+
else if (!existing.has_api_token && (to === null || to === "")) {
|
|
160
|
+
// Clearing an absent token is a no-op; skip so "nothing to change" stays honest.
|
|
161
|
+
}
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const from = existing[field];
|
|
165
|
+
if (String(from ?? "") === String(to ?? ""))
|
|
166
|
+
continue;
|
|
167
|
+
changes.push({ field, from: displayValue(from ?? null), to: displayValue(to) });
|
|
168
|
+
}
|
|
169
|
+
return changes;
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=updateTrafficSource.js.map
|
|
@@ -10,6 +10,7 @@ export declare const updateZoneInputSchema: {
|
|
|
10
10
|
postback_url: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNull]>>;
|
|
11
11
|
site_url: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNull]>>;
|
|
12
12
|
traffic_back_url: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNull]>>;
|
|
13
|
+
traffic_source_id: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNull]>>;
|
|
13
14
|
confirm: z.ZodDefault<z.ZodBoolean>;
|
|
14
15
|
};
|
|
15
16
|
type UpdateZoneArgs = {
|
|
@@ -19,6 +20,7 @@ type UpdateZoneArgs = {
|
|
|
19
20
|
postback_url?: string | null;
|
|
20
21
|
site_url?: string | null;
|
|
21
22
|
traffic_back_url?: string | null;
|
|
23
|
+
traffic_source_id?: string | null;
|
|
22
24
|
confirm: boolean;
|
|
23
25
|
};
|
|
24
26
|
export declare function updateZone(client: AffsetClient, args: UpdateZoneArgs): Promise<CallToolResult>;
|
package/dist/tools/updateZone.js
CHANGED
|
@@ -6,8 +6,10 @@ import { httpUrlError } from "../lib/urls.js";
|
|
|
6
6
|
import { ZONE_STATUSES } from "../types.js";
|
|
7
7
|
/** Zod: optional http(s) URL, or null to clear. */
|
|
8
8
|
const clearableUrl = z.union([z.string().min(1), z.null()]);
|
|
9
|
-
export const UPDATE_ZONE_DESCRIPTION = "Update a traffic-source zone (name, status, postback_url, site_url, traffic_back_url
|
|
10
|
-
"Partial update — only provided fields change. Pass null for a URL
|
|
9
|
+
export const UPDATE_ZONE_DESCRIPTION = "Update a traffic-source zone (name, status, postback_url, site_url, traffic_back_url, " +
|
|
10
|
+
"traffic_source_id). Partial update — only provided fields change. Pass null for a URL " +
|
|
11
|
+
"field or traffic_source_id to clear it. Linking a traffic source makes " +
|
|
12
|
+
"get_zone_url/get_tracking_link render its tracking template. " +
|
|
11
13
|
"DRY-RUN by default; pass confirm=true to apply.";
|
|
12
14
|
export const updateZoneInputSchema = {
|
|
13
15
|
zone_id: z.string().min(1).describe("Zone id to update."),
|
|
@@ -18,6 +20,11 @@ export const updateZoneInputSchema = {
|
|
|
18
20
|
.describe("S2S postback URL, or null to clear. Prefer including {source_click_id}."),
|
|
19
21
|
site_url: clearableUrl.optional().describe("Site URL, or null to clear."),
|
|
20
22
|
traffic_back_url: clearableUrl.optional().describe("Traffic-back URL, or null to clear."),
|
|
23
|
+
traffic_source_id: z
|
|
24
|
+
.union([z.string().trim().min(1), z.null()])
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Traffic source to link (see list_traffic_sources; must be in this namespace), " +
|
|
27
|
+
"or null to unlink."),
|
|
21
28
|
confirm: z
|
|
22
29
|
.boolean()
|
|
23
30
|
.default(false)
|
|
@@ -29,7 +36,8 @@ export async function updateZone(client, args) {
|
|
|
29
36
|
if ("error" in patch)
|
|
30
37
|
return textError(patch.error);
|
|
31
38
|
if (Object.keys(patch.body).length === 0) {
|
|
32
|
-
return textError("Provide at least one field to update: name, status, postback_url, site_url,
|
|
39
|
+
return textError("Provide at least one field to update: name, status, postback_url, site_url, " +
|
|
40
|
+
"traffic_back_url, traffic_source_id.");
|
|
33
41
|
}
|
|
34
42
|
let existing;
|
|
35
43
|
try {
|
|
@@ -103,6 +111,8 @@ function buildPatch(args) {
|
|
|
103
111
|
return { error: err };
|
|
104
112
|
body[key] = args[key];
|
|
105
113
|
}
|
|
114
|
+
if (args.traffic_source_id !== undefined)
|
|
115
|
+
body.traffic_source_id = args.traffic_source_id;
|
|
106
116
|
return { body };
|
|
107
117
|
}
|
|
108
118
|
function diffZone(existing, body) {
|
package/dist/types.d.ts
CHANGED
|
@@ -111,6 +111,9 @@ export interface Zone {
|
|
|
111
111
|
postback_url?: string | null;
|
|
112
112
|
user_email?: string | null;
|
|
113
113
|
manager_email?: string | null;
|
|
114
|
+
/** Linked traffic source; its tracking template drives the URL tools. */
|
|
115
|
+
traffic_source_id?: string | null;
|
|
116
|
+
traffic_source_name?: string | null;
|
|
114
117
|
created_at?: number;
|
|
115
118
|
updated_at?: number;
|
|
116
119
|
}
|
|
@@ -131,6 +134,49 @@ export interface UpdateCampaignResponse {
|
|
|
131
134
|
id: number;
|
|
132
135
|
updated_at: number;
|
|
133
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* One row from GET /api/traffic-sources. The stored network API credential is
|
|
139
|
+
* write-only — reads carry `has_api_token`, never the token itself.
|
|
140
|
+
*/
|
|
141
|
+
export interface TrafficSource {
|
|
142
|
+
id: string;
|
|
143
|
+
name: string;
|
|
144
|
+
preset: string | null;
|
|
145
|
+
tracking_template: string;
|
|
146
|
+
postback_template: string;
|
|
147
|
+
has_api_token: boolean;
|
|
148
|
+
status: string;
|
|
149
|
+
created_at?: number;
|
|
150
|
+
updated_at?: number;
|
|
151
|
+
/** Present on GET /api/traffic-sources/{id} only. */
|
|
152
|
+
linked_zones?: number;
|
|
153
|
+
}
|
|
154
|
+
export interface TrafficSourcesResponse {
|
|
155
|
+
traffic_sources: TrafficSource[];
|
|
156
|
+
pagination: Pagination;
|
|
157
|
+
}
|
|
158
|
+
export interface CreateTrafficSourceResponse {
|
|
159
|
+
id: string;
|
|
160
|
+
status: string;
|
|
161
|
+
created_at: number;
|
|
162
|
+
}
|
|
163
|
+
export interface UpdateTrafficSourceResponse {
|
|
164
|
+
id: string;
|
|
165
|
+
updated_at: number;
|
|
166
|
+
}
|
|
167
|
+
/** One entry from GET /api/traffic-source-presets. */
|
|
168
|
+
export interface TrafficSourcePreset {
|
|
169
|
+
id: string;
|
|
170
|
+
name: string;
|
|
171
|
+
doc_url: string;
|
|
172
|
+
tracking_template: string;
|
|
173
|
+
postback_template: string;
|
|
174
|
+
sub_meanings: Partial<Record<SubKey, string>>;
|
|
175
|
+
notes: string;
|
|
176
|
+
}
|
|
177
|
+
export interface TrafficSourcePresetsResponse {
|
|
178
|
+
presets: TrafficSourcePreset[];
|
|
179
|
+
}
|
|
134
180
|
/** Subset of GET /api/tenant this server reads. */
|
|
135
181
|
export interface TenantSettingsResponse {
|
|
136
182
|
company?: string;
|
|
@@ -206,5 +252,6 @@ export declare const GROUP_BY_VALUES: readonly ["date", "campaign_id", "zone_id"
|
|
|
206
252
|
export type GroupBy = (typeof GROUP_BY_VALUES)[number];
|
|
207
253
|
export declare const CAMPAIGN_STATUSES: readonly ["active", "paused", "archived"];
|
|
208
254
|
export declare const ZONE_STATUSES: readonly ["active", "inactive"];
|
|
255
|
+
export declare const TRAFFIC_SOURCE_STATUSES: readonly ["active", "archived"];
|
|
209
256
|
export declare const PAYMENT_MODELS: readonly ["cpa", "cpm"];
|
|
210
257
|
export declare const PACING_VALUES: readonly ["asap", "even"];
|
package/dist/types.js
CHANGED
|
@@ -14,6 +14,7 @@ export const GROUP_BY_VALUES = [
|
|
|
14
14
|
];
|
|
15
15
|
export const CAMPAIGN_STATUSES = ["active", "paused", "archived"];
|
|
16
16
|
export const ZONE_STATUSES = ["active", "inactive"];
|
|
17
|
+
export const TRAFFIC_SOURCE_STATUSES = ["active", "archived"];
|
|
17
18
|
export const PAYMENT_MODELS = ["cpa", "cpm"];
|
|
18
19
|
export const PACING_VALUES = ["asap", "even"];
|
|
19
20
|
//# sourceMappingURL=types.js.map
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@affset/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"mcpName": "io.github.affset/mcp",
|
|
5
5
|
"description": "MCP server for the affset ad platform — stats, campaigns, zones, payouts, targeting, sub labels, and team from your chat client.",
|
|
6
6
|
"type": "module",
|