@affset/mcp 0.1.1 → 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/.env.example +5 -0
- package/README.md +191 -56
- package/dist/client.d.ts +43 -0
- package/dist/client.js +15 -2
- package/dist/config.d.ts +7 -0
- package/dist/config.js +15 -54
- package/dist/core.d.ts +18 -0
- package/dist/core.js +18 -0
- package/dist/docs.d.ts +39 -0
- package/dist/docs.js +105 -0
- package/dist/index.d.ts +2 -0
- package/dist/lib/format.d.ts +46 -0
- package/dist/lib/format.js +3 -0
- package/dist/lib/integrationUrls.d.ts +87 -0
- package/dist/lib/integrationUrls.js +84 -4
- package/dist/lib/linkArgs.d.ts +32 -0
- package/dist/lib/linkArgs.js +14 -3
- package/dist/lib/patch.d.ts +10 -0
- package/dist/lib/payoutRules.d.ts +38 -0
- package/dist/lib/readBody.d.ts +17 -0
- package/dist/lib/readBody.js +69 -0
- package/dist/lib/targeting.d.ts +51 -0
- package/dist/lib/time.d.ts +38 -0
- package/dist/lib/toolResult.d.ts +7 -0
- package/dist/lib/urls.d.ts +2 -0
- package/dist/lib/zones.d.ts +24 -0
- package/dist/lib/zones.js +9 -1
- package/dist/registerTools.d.ts +40 -0
- package/dist/registerTools.js +437 -0
- package/dist/runtimeConfig.d.ts +37 -0
- package/dist/runtimeConfig.js +93 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +3 -318
- package/dist/tools/createCampaign.d.ts +25 -0
- package/dist/tools/createCampaign.js +20 -8
- package/dist/tools/createTeamMember.d.ts +26 -0
- package/dist/tools/createTrafficSource.d.ts +25 -0
- package/dist/tools/createTrafficSource.js +191 -0
- package/dist/tools/createZone.d.ts +24 -0
- package/dist/tools/createZone.js +13 -1
- package/dist/tools/cutZones.d.ts +29 -0
- package/dist/tools/deletePayoutRule.d.ts +16 -0
- package/dist/tools/getCampaign.d.ts +12 -0
- package/dist/tools/getCampaign.js +150 -0
- package/dist/tools/getStats.d.ts +42 -0
- package/dist/tools/getStats.js +54 -3
- package/dist/tools/getTrackingLink.d.ts +23 -0
- package/dist/tools/getTrackingLink.js +26 -9
- package/dist/tools/getZoneUrl.d.ts +21 -0
- package/dist/tools/getZoneUrl.js +30 -13
- package/dist/tools/listCampaigns.d.ts +23 -0
- package/dist/tools/listConversions.d.ts +31 -0
- package/dist/tools/listConversions.js +29 -10
- package/dist/tools/listPayoutRules.d.ts +12 -0
- package/dist/tools/listSubLabels.d.ts +5 -0
- package/dist/tools/listTargetingRules.d.ts +12 -0
- package/dist/tools/listTargetingTypes.d.ts +5 -0
- package/dist/tools/listTeam.d.ts +14 -0
- package/dist/tools/listTrafficSources.d.ts +17 -0
- package/dist/tools/listTrafficSources.js +63 -0
- package/dist/tools/listZones.d.ts +23 -0
- package/dist/tools/listZones.js +10 -5
- package/dist/tools/removeTargetingRule.d.ts +21 -0
- package/dist/tools/setCampaignStatus.d.ts +18 -0
- package/dist/tools/setPayoutGoal.d.ts +16 -0
- package/dist/tools/setPayoutRule.d.ts +18 -0
- package/dist/tools/setSubLabels.d.ts +22 -0
- package/dist/tools/setTargetingRule.d.ts +21 -0
- package/dist/tools/updateCampaign.d.ts +35 -0
- package/dist/tools/updateTrafficSource.d.ts +25 -0
- package/dist/tools/updateTrafficSource.js +171 -0
- package/dist/tools/updateZone.d.ts +27 -0
- package/dist/tools/updateZone.js +13 -3
- package/dist/tools/whoami.d.ts +6 -0
- package/dist/types.d.ts +257 -0
- package/dist/types.js +2 -0
- package/dist/version.d.ts +7 -0
- package/dist/version.js +8 -0
- package/package.json +15 -2
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Targeting helpers shared by the targeting tools.
|
|
3
|
+
*
|
|
4
|
+
* The serve path matches `targeting_rules.rule` with an exact, case-sensitive
|
|
5
|
+
* string compare against what Cloudflare and Bowser produce for the request
|
|
6
|
+
* (lite-adserver `campaignSelectionService`): `CF-IPCountry` is upper-case
|
|
7
|
+
* ISO-3166 alpha-2, OS/browser names are Bowser's own spellings, device type is
|
|
8
|
+
* one of three lower-case words. A rule an operator reads as obviously correct —
|
|
9
|
+
* `geo: br,mx`, `os: android` — therefore matches nothing, and a whitelist that
|
|
10
|
+
* matches nothing takes the campaign out of rotation without an error anywhere.
|
|
11
|
+
* Everything written through these tools is normalised to the serve path's
|
|
12
|
+
* spelling first, and anything unrecognised comes back as a warning rather than
|
|
13
|
+
* being silently stored.
|
|
14
|
+
*/
|
|
15
|
+
import type { AffsetClient } from "../client.js";
|
|
16
|
+
import type { TargetingRule, TargetingRuleType } from "../types.js";
|
|
17
|
+
export type TargetingMethod = "whitelist" | "blacklist";
|
|
18
|
+
/**
|
|
19
|
+
* Seeded types the `/serve` path never evaluates. They accept writes through the
|
|
20
|
+
* API and then do nothing, so an hours rule reads as a working dayparting setup
|
|
21
|
+
* while the campaign keeps buying around the clock.
|
|
22
|
+
*/
|
|
23
|
+
export declare const UNENFORCED_TYPES: Record<string, string>;
|
|
24
|
+
export declare function fetchTargetingTypes(client: AffsetClient): Promise<TargetingRuleType[]>;
|
|
25
|
+
export declare function fetchTargetingRules(client: AffsetClient, campaignId: string): Promise<TargetingRule[]>;
|
|
26
|
+
/**
|
|
27
|
+
* Write the campaign's full rule set. The endpoint is a sync, not an append:
|
|
28
|
+
* every rule the caller wants to keep must be echoed back with its id, or it is
|
|
29
|
+
* deleted. Callers build `rules` from a fresh read for exactly that reason.
|
|
30
|
+
*/
|
|
31
|
+
export declare function syncTargetingRules(client: AffsetClient, campaignId: string, rules: TargetingRule[]): Promise<TargetingRule[]>;
|
|
32
|
+
/** Strip everything the sync endpoint does not accept back. */
|
|
33
|
+
export declare function toSyncPayload(rules: TargetingRule[]): TargetingRule[];
|
|
34
|
+
/** Resolve a caller-supplied type id or name against an already-fetched catalog. */
|
|
35
|
+
export declare function resolveTargetingType(types: TargetingRuleType[], type: string | number): {
|
|
36
|
+
type: TargetingRuleType;
|
|
37
|
+
} | {
|
|
38
|
+
error: string;
|
|
39
|
+
};
|
|
40
|
+
export type NormalizedRule = {
|
|
41
|
+
value: string;
|
|
42
|
+
notes: string[];
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Normalise a rule value to the spelling the serve path compares against.
|
|
46
|
+
* Returns an error for values that provably cannot match, and notes for values
|
|
47
|
+
* that were rewritten or that could not be checked.
|
|
48
|
+
*/
|
|
49
|
+
export declare function normalizeRuleValue(typeName: string, rule: string): NormalizedRule | {
|
|
50
|
+
error: string;
|
|
51
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date-range resolution for stats queries.
|
|
3
|
+
*
|
|
4
|
+
* Every boundary here is computed in the TENANT's timezone, not the timezone of
|
|
5
|
+
* the machine running this server. The API buckets `group_by=date` by the tenant
|
|
6
|
+
* timezone, so resolving "today" against the operator's laptop clock would ask
|
|
7
|
+
* for a window that straddles two of the buckets it gets back — a single "today"
|
|
8
|
+
* arriving as two partial rows. Callers fetch the tenant timezone (see
|
|
9
|
+
* AffsetClient#getTenantTimezone) and pass it in.
|
|
10
|
+
*/
|
|
11
|
+
export declare const RANGE_PRESETS: readonly ["today", "yesterday", "last_7_days", "last_30_days", "this_month"];
|
|
12
|
+
export type RangePreset = (typeof RANGE_PRESETS)[number];
|
|
13
|
+
export interface ResolvedRange {
|
|
14
|
+
/** Inclusive lower bound, epoch ms. */
|
|
15
|
+
from: number;
|
|
16
|
+
/** Inclusive upper bound, epoch ms. */
|
|
17
|
+
to: number;
|
|
18
|
+
/** Human label for display. */
|
|
19
|
+
label: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Parse one campaign schedule boundary. Date-only values use the tenant's
|
|
23
|
+
* timezone; explicit ISO timestamps and epoch milliseconds remain exact.
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseCampaignDateBound(value: string | number | null, role: "start" | "end", timeZone: string): number | null;
|
|
26
|
+
/**
|
|
27
|
+
* Resolve a range from an optional preset and/or explicit from/to bounds.
|
|
28
|
+
* Explicit bounds win over the preset. All day boundaries are in `timeZone`
|
|
29
|
+
* (the tenant's), so the window lines up with the API's date buckets.
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveRange(preset: RangePreset | undefined, from: string | undefined, to: string | undefined, timeZone: string): ResolvedRange;
|
|
32
|
+
/**
|
|
33
|
+
* Format an instant as `YYYY-MM-DD HH:mm` in the tenant timezone. Row timestamps
|
|
34
|
+
* have to agree with the date buckets stats are read in: a conversion at 23:30
|
|
35
|
+
* tenant-local rendered in UTC lands on the next day's row and reads as a
|
|
36
|
+
* missing conversion.
|
|
37
|
+
*/
|
|
38
|
+
export declare function formatInstant(ms: number, timeZone: string): string;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
/** Build a successful text tool result. */
|
|
3
|
+
export declare function textResult(body: string): CallToolResult;
|
|
4
|
+
/** Build an error tool result (isError: true). */
|
|
5
|
+
export declare function errorResult(err: unknown): CallToolResult;
|
|
6
|
+
/** Build an error tool result from a plain string. */
|
|
7
|
+
export declare function textError(body: string): CallToolResult;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zone lookup shared by every tool that needs a traffic source: campaign creation
|
|
3
|
+
* and both integration-URL tools all take an optional `zone_id` and auto-pick when
|
|
4
|
+
* the namespace has exactly one active zone.
|
|
5
|
+
*/
|
|
6
|
+
import { type AffsetClient } from "../client.js";
|
|
7
|
+
import type { Zone } from "../types.js";
|
|
8
|
+
export type ResolvedZone = {
|
|
9
|
+
zone: Zone;
|
|
10
|
+
inactiveWarning?: string;
|
|
11
|
+
};
|
|
12
|
+
export type ZoneResolution = ResolvedZone | {
|
|
13
|
+
error: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Resolve the zone to build URLs against. An explicit id is fetched directly — the
|
|
17
|
+
* paginated list must not be the arbiter of whether a zone exists.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveZone(client: AffsetClient, zoneId: string | undefined): Promise<ZoneResolution>;
|
|
20
|
+
/**
|
|
21
|
+
* The line every integration URL needs under it: without a postback URL on the zone,
|
|
22
|
+
* conversions never make it back to the traffic source and its optimizer stays blind.
|
|
23
|
+
*/
|
|
24
|
+
export declare function zonePostbackNote(zone: Zone): string;
|
package/dist/lib/zones.js
CHANGED
|
@@ -60,7 +60,15 @@ async function listActiveZones(client) {
|
|
|
60
60
|
return { zones, total: res.pagination?.total ?? zones.length };
|
|
61
61
|
}
|
|
62
62
|
function zoneList(zones, total) {
|
|
63
|
-
const lines = zones.map((z) =>
|
|
63
|
+
const lines = zones.map((z) => {
|
|
64
|
+
const source = z.traffic_source_name
|
|
65
|
+
? ` · ${mdCell(z.traffic_source_name)}`
|
|
66
|
+
: z.traffic_source_id
|
|
67
|
+
? ` · \`${mdCell(z.traffic_source_id)}\``
|
|
68
|
+
: "";
|
|
69
|
+
const postback = z.postback_url ? "" : " (no postback URL)";
|
|
70
|
+
return `- \`${z.id}\` — ${mdCell(z.name)}${source}${postback}`;
|
|
71
|
+
});
|
|
64
72
|
if (total > zones.length) {
|
|
65
73
|
lines.push(`- …and ${total - zones.length} more`);
|
|
66
74
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type Config } from "./runtimeConfig.js";
|
|
2
|
+
/**
|
|
3
|
+
* Structural stand-in for the SDK's `McpServer`, so a consumer's own SDK
|
|
4
|
+
* install is accepted. `McpServer` carries private fields, which makes two
|
|
5
|
+
* copies of the class (this package's and a consumer's — e.g. the Workers
|
|
6
|
+
* gateway bundling its own `@modelcontextprotocol/sdk`) nominally
|
|
7
|
+
* incompatible even at identical versions. Method-parameter bivariance makes
|
|
8
|
+
* any real `McpServer` assignable to this shape; nothing else plausibly is.
|
|
9
|
+
*
|
|
10
|
+
* The zod schemas behind `inputSchema` stay internal to this package, so
|
|
11
|
+
* consumers never mix zod instances at the type level (they may bundle zod
|
|
12
|
+
* v4 while this package uses v3 — the MCP SDK detects the flavor per call).
|
|
13
|
+
*/
|
|
14
|
+
export interface AffsetToolServer {
|
|
15
|
+
registerTool(name: string, config: object, callback: (...args: never[]) => unknown): unknown;
|
|
16
|
+
registerResource(name: string, uri: string, metadata: object, callback: never): unknown;
|
|
17
|
+
}
|
|
18
|
+
export interface ToolCallEvent {
|
|
19
|
+
toolName: string;
|
|
20
|
+
durationMs: number;
|
|
21
|
+
status: "ok" | "error";
|
|
22
|
+
}
|
|
23
|
+
export interface RegisterAffsetToolsOptions {
|
|
24
|
+
/**
|
|
25
|
+
* Optional transport-owned audit hook. It receives metadata only, never tool
|
|
26
|
+
* arguments or output. Hook failures are isolated from the tool result.
|
|
27
|
+
*/
|
|
28
|
+
onToolCall?: (event: ToolCallEvent) => void | Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Register the full affset tool roster and docs resources against `config`.
|
|
32
|
+
* The single source of truth for the roster: the stdio entrypoint
|
|
33
|
+
* (`createServer`) and the remote gateway's `McpAgent` both call exactly this,
|
|
34
|
+
* so the two transports cannot drift (REMOTE-MCP-PRD.md §5.6).
|
|
35
|
+
*
|
|
36
|
+
* When `config.readOnly` is set, every tool that is not `readOnlyHint: true`
|
|
37
|
+
* is skipped — identical semantics to AFFSET_READ_ONLY on stdio and to a
|
|
38
|
+
* `read`-scoped OAuth grant on the gateway.
|
|
39
|
+
*/
|
|
40
|
+
export declare function registerAffsetTools(toolServer: AffsetToolServer, config: Config, options?: RegisterAffsetToolsOptions): void;
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import { normalizeRuntimeConfig } from "./runtimeConfig.js";
|
|
2
|
+
import { AffsetClient } from "./client.js";
|
|
3
|
+
import { DOCS_FEEDS, fetchDocsFeed } from "./docs.js";
|
|
4
|
+
import { getStats, getStatsInputSchema, GET_STATS_DESCRIPTION } from "./tools/getStats.js";
|
|
5
|
+
import { cutZones, cutZonesInputSchema, CUT_ZONES_DESCRIPTION } from "./tools/cutZones.js";
|
|
6
|
+
import { createCampaign, createCampaignInputSchema, CREATE_CAMPAIGN_DESCRIPTION, } from "./tools/createCampaign.js";
|
|
7
|
+
import { listCampaigns, listCampaignsInputSchema, LIST_CAMPAIGNS_DESCRIPTION, } from "./tools/listCampaigns.js";
|
|
8
|
+
import { getCampaign, getCampaignInputSchema, GET_CAMPAIGN_DESCRIPTION, } from "./tools/getCampaign.js";
|
|
9
|
+
import { listZones, listZonesInputSchema, LIST_ZONES_DESCRIPTION } from "./tools/listZones.js";
|
|
10
|
+
import { listTeam, listTeamInputSchema, LIST_TEAM_DESCRIPTION } from "./tools/listTeam.js";
|
|
11
|
+
import { createTeamMember, createTeamMemberInputSchema, CREATE_TEAM_MEMBER_DESCRIPTION, } from "./tools/createTeamMember.js";
|
|
12
|
+
import { createZone, createZoneInputSchema, CREATE_ZONE_DESCRIPTION } from "./tools/createZone.js";
|
|
13
|
+
import { getZoneUrl, getZoneUrlInputSchema, GET_ZONE_URL_DESCRIPTION } from "./tools/getZoneUrl.js";
|
|
14
|
+
import { getTrackingLink, getTrackingLinkInputSchema, GET_TRACKING_LINK_DESCRIPTION, } from "./tools/getTrackingLink.js";
|
|
15
|
+
import { updateZone, updateZoneInputSchema, UPDATE_ZONE_DESCRIPTION } from "./tools/updateZone.js";
|
|
16
|
+
import { listTrafficSources, listTrafficSourcesInputSchema, LIST_TRAFFIC_SOURCES_DESCRIPTION, } from "./tools/listTrafficSources.js";
|
|
17
|
+
import { createTrafficSource, createTrafficSourceInputSchema, CREATE_TRAFFIC_SOURCE_DESCRIPTION, } from "./tools/createTrafficSource.js";
|
|
18
|
+
import { updateTrafficSource, updateTrafficSourceInputSchema, UPDATE_TRAFFIC_SOURCE_DESCRIPTION, } from "./tools/updateTrafficSource.js";
|
|
19
|
+
import { updateCampaign, updateCampaignInputSchema, UPDATE_CAMPAIGN_DESCRIPTION, } from "./tools/updateCampaign.js";
|
|
20
|
+
import { setCampaignStatus, setCampaignStatusInputSchema, SET_CAMPAIGN_STATUS_DESCRIPTION, } from "./tools/setCampaignStatus.js";
|
|
21
|
+
import { listPayoutRules, listPayoutRulesInputSchema, LIST_PAYOUT_RULES_DESCRIPTION, } from "./tools/listPayoutRules.js";
|
|
22
|
+
import { setPayoutRule, setPayoutRuleInputSchema, SET_PAYOUT_RULE_DESCRIPTION, } from "./tools/setPayoutRule.js";
|
|
23
|
+
import { deletePayoutRule, deletePayoutRuleInputSchema, DELETE_PAYOUT_RULE_DESCRIPTION, } from "./tools/deletePayoutRule.js";
|
|
24
|
+
import { setPayoutGoal, setPayoutGoalInputSchema, SET_PAYOUT_GOAL_DESCRIPTION, } from "./tools/setPayoutGoal.js";
|
|
25
|
+
import { listTargetingTypes, listTargetingTypesInputSchema, LIST_TARGETING_TYPES_DESCRIPTION, } from "./tools/listTargetingTypes.js";
|
|
26
|
+
import { listTargetingRules, listTargetingRulesInputSchema, LIST_TARGETING_RULES_DESCRIPTION, } from "./tools/listTargetingRules.js";
|
|
27
|
+
import { setTargetingRule, setTargetingRuleInputSchema, SET_TARGETING_RULE_DESCRIPTION, } from "./tools/setTargetingRule.js";
|
|
28
|
+
import { removeTargetingRule, removeTargetingRuleInputSchema, REMOVE_TARGETING_RULE_DESCRIPTION, } from "./tools/removeTargetingRule.js";
|
|
29
|
+
import { listSubLabels, listSubLabelsInputSchema, LIST_SUB_LABELS_DESCRIPTION, } from "./tools/listSubLabels.js";
|
|
30
|
+
import { setSubLabels, setSubLabelsInputSchema, SET_SUB_LABELS_DESCRIPTION, } from "./tools/setSubLabels.js";
|
|
31
|
+
import { listConversions, listConversionsInputSchema, LIST_CONVERSIONS_DESCRIPTION, } from "./tools/listConversions.js";
|
|
32
|
+
import { whoami, whoamiInputSchema, WHOAMI_DESCRIPTION } from "./tools/whoami.js";
|
|
33
|
+
function resultIsError(result) {
|
|
34
|
+
return (typeof result === "object" && result !== null && "isError" in result && result.isError === true);
|
|
35
|
+
}
|
|
36
|
+
function instrumentToolCallback(name, callback, onToolCall) {
|
|
37
|
+
const instrumented = async (...args) => {
|
|
38
|
+
const startedAt = Date.now();
|
|
39
|
+
let status = "ok";
|
|
40
|
+
try {
|
|
41
|
+
const result = await callback(...args);
|
|
42
|
+
if (resultIsError(result))
|
|
43
|
+
status = "error";
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
status = "error";
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
try {
|
|
52
|
+
await onToolCall({
|
|
53
|
+
toolName: name,
|
|
54
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
55
|
+
status,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Audit/telemetry must never alter the MCP tool's result.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
return instrumented;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Register the full affset tool roster and docs resources against `config`.
|
|
67
|
+
* The single source of truth for the roster: the stdio entrypoint
|
|
68
|
+
* (`createServer`) and the remote gateway's `McpAgent` both call exactly this,
|
|
69
|
+
* so the two transports cannot drift (REMOTE-MCP-PRD.md §5.6).
|
|
70
|
+
*
|
|
71
|
+
* When `config.readOnly` is set, every tool that is not `readOnlyHint: true`
|
|
72
|
+
* is skipped — identical semantics to AFFSET_READ_ONLY on stdio and to a
|
|
73
|
+
* `read`-scoped OAuth grant on the gateway.
|
|
74
|
+
*/
|
|
75
|
+
export function registerAffsetTools(toolServer, config, options = {}) {
|
|
76
|
+
const runtimeConfig = normalizeRuntimeConfig(config);
|
|
77
|
+
const client = new AffsetClient(runtimeConfig);
|
|
78
|
+
// One cast at the boundary; everything below is the SDK's real surface.
|
|
79
|
+
const server = toolServer;
|
|
80
|
+
// Skip mutating tools entirely when read-only is set — they never appear in
|
|
81
|
+
// tools/list and cannot be called. Skipping is fail-closed: we do not depend
|
|
82
|
+
// on the SDK's remove() to unlist a tool that was already registered. See
|
|
83
|
+
// AFFSET_READ_ONLY in the README for why this exists (untrusted third-party
|
|
84
|
+
// data can reach model context via list_conversions/get_stats, so mutation
|
|
85
|
+
// tools are an injection blast-radius control, not just a UI nicety).
|
|
86
|
+
const registerTool = (name, toolConfig, cb) => {
|
|
87
|
+
if (runtimeConfig.readOnly && toolConfig.annotations?.readOnlyHint !== true) {
|
|
88
|
+
return {
|
|
89
|
+
enabled: false,
|
|
90
|
+
enable() { },
|
|
91
|
+
disable() { },
|
|
92
|
+
remove() { },
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const callback = options.onToolCall ? instrumentToolCallback(name, cb, options.onToolCall) : cb;
|
|
96
|
+
return server.registerTool(name, toolConfig, callback);
|
|
97
|
+
};
|
|
98
|
+
registerTool("whoami", {
|
|
99
|
+
title: "Show the tenant this MCP is bound to",
|
|
100
|
+
description: WHOAMI_DESCRIPTION,
|
|
101
|
+
inputSchema: whoamiInputSchema,
|
|
102
|
+
annotations: {
|
|
103
|
+
readOnlyHint: true,
|
|
104
|
+
destructiveHint: false,
|
|
105
|
+
idempotentHint: true,
|
|
106
|
+
openWorldHint: true,
|
|
107
|
+
},
|
|
108
|
+
}, () => whoami(client, runtimeConfig));
|
|
109
|
+
registerTool("get_stats", {
|
|
110
|
+
title: "Get affset stats",
|
|
111
|
+
description: GET_STATS_DESCRIPTION,
|
|
112
|
+
inputSchema: getStatsInputSchema,
|
|
113
|
+
annotations: {
|
|
114
|
+
readOnlyHint: true,
|
|
115
|
+
destructiveHint: false,
|
|
116
|
+
idempotentHint: true,
|
|
117
|
+
openWorldHint: true,
|
|
118
|
+
},
|
|
119
|
+
}, (args) => getStats(client, args));
|
|
120
|
+
registerTool("list_campaigns", {
|
|
121
|
+
title: "List campaigns",
|
|
122
|
+
description: LIST_CAMPAIGNS_DESCRIPTION,
|
|
123
|
+
inputSchema: listCampaignsInputSchema,
|
|
124
|
+
annotations: {
|
|
125
|
+
readOnlyHint: true,
|
|
126
|
+
destructiveHint: false,
|
|
127
|
+
idempotentHint: true,
|
|
128
|
+
openWorldHint: true,
|
|
129
|
+
},
|
|
130
|
+
}, (args) => listCampaigns(client, args));
|
|
131
|
+
registerTool("get_campaign", {
|
|
132
|
+
title: "Get a campaign's full record",
|
|
133
|
+
description: GET_CAMPAIGN_DESCRIPTION,
|
|
134
|
+
inputSchema: getCampaignInputSchema,
|
|
135
|
+
annotations: {
|
|
136
|
+
readOnlyHint: true,
|
|
137
|
+
destructiveHint: false,
|
|
138
|
+
idempotentHint: true,
|
|
139
|
+
openWorldHint: true,
|
|
140
|
+
},
|
|
141
|
+
}, (args) => getCampaign(client, args));
|
|
142
|
+
registerTool("list_zones", {
|
|
143
|
+
title: "List zones",
|
|
144
|
+
description: LIST_ZONES_DESCRIPTION,
|
|
145
|
+
inputSchema: listZonesInputSchema,
|
|
146
|
+
annotations: {
|
|
147
|
+
readOnlyHint: true,
|
|
148
|
+
destructiveHint: false,
|
|
149
|
+
idempotentHint: true,
|
|
150
|
+
openWorldHint: true,
|
|
151
|
+
},
|
|
152
|
+
}, (args) => listZones(client, args));
|
|
153
|
+
registerTool("list_team", {
|
|
154
|
+
title: "List team members",
|
|
155
|
+
description: LIST_TEAM_DESCRIPTION,
|
|
156
|
+
inputSchema: listTeamInputSchema,
|
|
157
|
+
annotations: {
|
|
158
|
+
readOnlyHint: true,
|
|
159
|
+
destructiveHint: false,
|
|
160
|
+
idempotentHint: true,
|
|
161
|
+
openWorldHint: true,
|
|
162
|
+
},
|
|
163
|
+
}, (args) => listTeam(client, args));
|
|
164
|
+
registerTool("create_team_member", {
|
|
165
|
+
title: "Invite a team member",
|
|
166
|
+
description: CREATE_TEAM_MEMBER_DESCRIPTION,
|
|
167
|
+
inputSchema: createTeamMemberInputSchema,
|
|
168
|
+
annotations: {
|
|
169
|
+
readOnlyHint: false,
|
|
170
|
+
destructiveHint: false,
|
|
171
|
+
idempotentHint: false,
|
|
172
|
+
openWorldHint: true,
|
|
173
|
+
},
|
|
174
|
+
}, (args) => createTeamMember(client, args));
|
|
175
|
+
registerTool("get_zone_url", {
|
|
176
|
+
title: "Get the zone URL for a traffic source",
|
|
177
|
+
description: GET_ZONE_URL_DESCRIPTION,
|
|
178
|
+
inputSchema: getZoneUrlInputSchema,
|
|
179
|
+
annotations: {
|
|
180
|
+
readOnlyHint: true,
|
|
181
|
+
destructiveHint: false,
|
|
182
|
+
idempotentHint: true,
|
|
183
|
+
openWorldHint: true,
|
|
184
|
+
},
|
|
185
|
+
}, (args) => getZoneUrl(client, runtimeConfig, args));
|
|
186
|
+
registerTool("get_tracking_link", {
|
|
187
|
+
title: "Get a campaign's tracking link",
|
|
188
|
+
description: GET_TRACKING_LINK_DESCRIPTION,
|
|
189
|
+
inputSchema: getTrackingLinkInputSchema,
|
|
190
|
+
annotations: {
|
|
191
|
+
readOnlyHint: true,
|
|
192
|
+
destructiveHint: false,
|
|
193
|
+
idempotentHint: true,
|
|
194
|
+
openWorldHint: true,
|
|
195
|
+
},
|
|
196
|
+
}, (args) => getTrackingLink(client, runtimeConfig, args));
|
|
197
|
+
registerTool("create_campaign", {
|
|
198
|
+
title: "Create a campaign",
|
|
199
|
+
description: CREATE_CAMPAIGN_DESCRIPTION,
|
|
200
|
+
inputSchema: createCampaignInputSchema,
|
|
201
|
+
annotations: {
|
|
202
|
+
readOnlyHint: false,
|
|
203
|
+
destructiveHint: false,
|
|
204
|
+
idempotentHint: false,
|
|
205
|
+
openWorldHint: true,
|
|
206
|
+
},
|
|
207
|
+
}, (args) => createCampaign(client, runtimeConfig, args));
|
|
208
|
+
registerTool("update_campaign", {
|
|
209
|
+
title: "Update a campaign",
|
|
210
|
+
description: UPDATE_CAMPAIGN_DESCRIPTION,
|
|
211
|
+
inputSchema: updateCampaignInputSchema,
|
|
212
|
+
annotations: {
|
|
213
|
+
readOnlyHint: false,
|
|
214
|
+
destructiveHint: true,
|
|
215
|
+
idempotentHint: false,
|
|
216
|
+
openWorldHint: true,
|
|
217
|
+
},
|
|
218
|
+
}, (args) => updateCampaign(client, args));
|
|
219
|
+
registerTool("set_campaign_status", {
|
|
220
|
+
title: "Run or pause a campaign",
|
|
221
|
+
description: SET_CAMPAIGN_STATUS_DESCRIPTION,
|
|
222
|
+
inputSchema: setCampaignStatusInputSchema,
|
|
223
|
+
annotations: {
|
|
224
|
+
readOnlyHint: false,
|
|
225
|
+
destructiveHint: true,
|
|
226
|
+
idempotentHint: false,
|
|
227
|
+
openWorldHint: true,
|
|
228
|
+
},
|
|
229
|
+
}, (args) => setCampaignStatus(client, args));
|
|
230
|
+
registerTool("create_zone", {
|
|
231
|
+
title: "Create a zone",
|
|
232
|
+
description: CREATE_ZONE_DESCRIPTION,
|
|
233
|
+
inputSchema: createZoneInputSchema,
|
|
234
|
+
annotations: {
|
|
235
|
+
readOnlyHint: false,
|
|
236
|
+
destructiveHint: false,
|
|
237
|
+
idempotentHint: false,
|
|
238
|
+
openWorldHint: true,
|
|
239
|
+
},
|
|
240
|
+
}, (args) => createZone(client, args));
|
|
241
|
+
registerTool("update_zone", {
|
|
242
|
+
title: "Update a zone",
|
|
243
|
+
description: UPDATE_ZONE_DESCRIPTION,
|
|
244
|
+
inputSchema: updateZoneInputSchema,
|
|
245
|
+
annotations: {
|
|
246
|
+
readOnlyHint: false,
|
|
247
|
+
destructiveHint: true,
|
|
248
|
+
idempotentHint: false,
|
|
249
|
+
openWorldHint: true,
|
|
250
|
+
},
|
|
251
|
+
}, (args) => updateZone(client, args));
|
|
252
|
+
registerTool("list_traffic_sources", {
|
|
253
|
+
title: "List traffic sources",
|
|
254
|
+
description: LIST_TRAFFIC_SOURCES_DESCRIPTION,
|
|
255
|
+
inputSchema: listTrafficSourcesInputSchema,
|
|
256
|
+
annotations: {
|
|
257
|
+
readOnlyHint: true,
|
|
258
|
+
destructiveHint: false,
|
|
259
|
+
idempotentHint: true,
|
|
260
|
+
openWorldHint: true,
|
|
261
|
+
},
|
|
262
|
+
}, (args) => listTrafficSources(client, args));
|
|
263
|
+
registerTool("create_traffic_source", {
|
|
264
|
+
title: "Create a traffic source",
|
|
265
|
+
description: CREATE_TRAFFIC_SOURCE_DESCRIPTION,
|
|
266
|
+
inputSchema: createTrafficSourceInputSchema,
|
|
267
|
+
annotations: {
|
|
268
|
+
readOnlyHint: false,
|
|
269
|
+
destructiveHint: false,
|
|
270
|
+
idempotentHint: false,
|
|
271
|
+
openWorldHint: true,
|
|
272
|
+
},
|
|
273
|
+
}, (args) => createTrafficSource(client, args));
|
|
274
|
+
registerTool("update_traffic_source", {
|
|
275
|
+
title: "Update a traffic source",
|
|
276
|
+
description: UPDATE_TRAFFIC_SOURCE_DESCRIPTION,
|
|
277
|
+
inputSchema: updateTrafficSourceInputSchema,
|
|
278
|
+
annotations: {
|
|
279
|
+
readOnlyHint: false,
|
|
280
|
+
destructiveHint: true,
|
|
281
|
+
idempotentHint: false,
|
|
282
|
+
openWorldHint: true,
|
|
283
|
+
},
|
|
284
|
+
}, (args) => updateTrafficSource(client, args));
|
|
285
|
+
registerTool("cut_zones", {
|
|
286
|
+
title: "Cut underperforming zones",
|
|
287
|
+
description: CUT_ZONES_DESCRIPTION,
|
|
288
|
+
inputSchema: cutZonesInputSchema,
|
|
289
|
+
annotations: {
|
|
290
|
+
readOnlyHint: false,
|
|
291
|
+
destructiveHint: true,
|
|
292
|
+
idempotentHint: false,
|
|
293
|
+
openWorldHint: true,
|
|
294
|
+
},
|
|
295
|
+
}, (args) => cutZones(client, args));
|
|
296
|
+
registerTool("list_payout_rules", {
|
|
297
|
+
title: "List payout rules",
|
|
298
|
+
description: LIST_PAYOUT_RULES_DESCRIPTION,
|
|
299
|
+
inputSchema: listPayoutRulesInputSchema,
|
|
300
|
+
annotations: {
|
|
301
|
+
readOnlyHint: true,
|
|
302
|
+
destructiveHint: false,
|
|
303
|
+
idempotentHint: true,
|
|
304
|
+
openWorldHint: true,
|
|
305
|
+
},
|
|
306
|
+
}, (args) => listPayoutRules(client, args));
|
|
307
|
+
registerTool("set_payout_rule", {
|
|
308
|
+
title: "Set a payout rule",
|
|
309
|
+
description: SET_PAYOUT_RULE_DESCRIPTION,
|
|
310
|
+
inputSchema: setPayoutRuleInputSchema,
|
|
311
|
+
annotations: {
|
|
312
|
+
readOnlyHint: false,
|
|
313
|
+
destructiveHint: true,
|
|
314
|
+
idempotentHint: false,
|
|
315
|
+
openWorldHint: true,
|
|
316
|
+
},
|
|
317
|
+
}, (args) => setPayoutRule(client, args));
|
|
318
|
+
registerTool("delete_payout_rule", {
|
|
319
|
+
title: "Delete a payout rule",
|
|
320
|
+
description: DELETE_PAYOUT_RULE_DESCRIPTION,
|
|
321
|
+
inputSchema: deletePayoutRuleInputSchema,
|
|
322
|
+
annotations: {
|
|
323
|
+
readOnlyHint: false,
|
|
324
|
+
destructiveHint: true,
|
|
325
|
+
idempotentHint: false,
|
|
326
|
+
openWorldHint: true,
|
|
327
|
+
},
|
|
328
|
+
}, (args) => deletePayoutRule(client, args));
|
|
329
|
+
registerTool("set_payout_goal", {
|
|
330
|
+
title: "Set payout goal type",
|
|
331
|
+
description: SET_PAYOUT_GOAL_DESCRIPTION,
|
|
332
|
+
inputSchema: setPayoutGoalInputSchema,
|
|
333
|
+
annotations: {
|
|
334
|
+
readOnlyHint: false,
|
|
335
|
+
destructiveHint: true,
|
|
336
|
+
idempotentHint: false,
|
|
337
|
+
openWorldHint: true,
|
|
338
|
+
},
|
|
339
|
+
}, (args) => setPayoutGoal(client, args));
|
|
340
|
+
registerTool("list_targeting_types", {
|
|
341
|
+
title: "List targeting rule types",
|
|
342
|
+
description: LIST_TARGETING_TYPES_DESCRIPTION,
|
|
343
|
+
inputSchema: listTargetingTypesInputSchema,
|
|
344
|
+
annotations: {
|
|
345
|
+
readOnlyHint: true,
|
|
346
|
+
destructiveHint: false,
|
|
347
|
+
idempotentHint: true,
|
|
348
|
+
openWorldHint: true,
|
|
349
|
+
},
|
|
350
|
+
}, () => listTargetingTypes(client));
|
|
351
|
+
registerTool("list_targeting_rules", {
|
|
352
|
+
title: "List campaign targeting rules",
|
|
353
|
+
description: LIST_TARGETING_RULES_DESCRIPTION,
|
|
354
|
+
inputSchema: listTargetingRulesInputSchema,
|
|
355
|
+
annotations: {
|
|
356
|
+
readOnlyHint: true,
|
|
357
|
+
destructiveHint: false,
|
|
358
|
+
idempotentHint: true,
|
|
359
|
+
openWorldHint: true,
|
|
360
|
+
},
|
|
361
|
+
}, (args) => listTargetingRules(client, args));
|
|
362
|
+
registerTool("set_targeting_rule", {
|
|
363
|
+
title: "Set a targeting rule",
|
|
364
|
+
description: SET_TARGETING_RULE_DESCRIPTION,
|
|
365
|
+
inputSchema: setTargetingRuleInputSchema,
|
|
366
|
+
annotations: {
|
|
367
|
+
readOnlyHint: false,
|
|
368
|
+
destructiveHint: true,
|
|
369
|
+
idempotentHint: false,
|
|
370
|
+
openWorldHint: true,
|
|
371
|
+
},
|
|
372
|
+
}, (args) => setTargetingRule(client, args));
|
|
373
|
+
registerTool("remove_targeting_rule", {
|
|
374
|
+
title: "Remove a targeting rule",
|
|
375
|
+
description: REMOVE_TARGETING_RULE_DESCRIPTION,
|
|
376
|
+
inputSchema: removeTargetingRuleInputSchema,
|
|
377
|
+
annotations: {
|
|
378
|
+
readOnlyHint: false,
|
|
379
|
+
destructiveHint: true,
|
|
380
|
+
idempotentHint: false,
|
|
381
|
+
openWorldHint: true,
|
|
382
|
+
},
|
|
383
|
+
}, (args) => removeTargetingRule(client, args));
|
|
384
|
+
registerTool("list_sub_labels", {
|
|
385
|
+
title: "List sub labels",
|
|
386
|
+
description: LIST_SUB_LABELS_DESCRIPTION,
|
|
387
|
+
inputSchema: listSubLabelsInputSchema,
|
|
388
|
+
annotations: {
|
|
389
|
+
readOnlyHint: true,
|
|
390
|
+
destructiveHint: false,
|
|
391
|
+
idempotentHint: true,
|
|
392
|
+
openWorldHint: true,
|
|
393
|
+
},
|
|
394
|
+
}, () => listSubLabels(client));
|
|
395
|
+
registerTool("set_sub_labels", {
|
|
396
|
+
title: "Set sub labels",
|
|
397
|
+
description: SET_SUB_LABELS_DESCRIPTION,
|
|
398
|
+
inputSchema: setSubLabelsInputSchema,
|
|
399
|
+
annotations: {
|
|
400
|
+
readOnlyHint: false,
|
|
401
|
+
destructiveHint: true,
|
|
402
|
+
idempotentHint: false,
|
|
403
|
+
openWorldHint: true,
|
|
404
|
+
},
|
|
405
|
+
}, (args) => setSubLabels(client, args));
|
|
406
|
+
registerTool("list_conversions", {
|
|
407
|
+
title: "List conversions",
|
|
408
|
+
description: LIST_CONVERSIONS_DESCRIPTION,
|
|
409
|
+
inputSchema: listConversionsInputSchema,
|
|
410
|
+
annotations: {
|
|
411
|
+
readOnlyHint: true,
|
|
412
|
+
destructiveHint: false,
|
|
413
|
+
idempotentHint: true,
|
|
414
|
+
openWorldHint: true,
|
|
415
|
+
},
|
|
416
|
+
}, (args) => listConversions(client, args));
|
|
417
|
+
// Documentation resources. The affset API reference (the same content as the
|
|
418
|
+
// /docs page) is exposed so an assistant can answer "how does X work / how do
|
|
419
|
+
// I call Y" from the docs themselves, not just from the tool schemas above.
|
|
420
|
+
// Fetched from config.docsBaseUrl at read time, so it always reflects the
|
|
421
|
+
// currently published docs. Always registered — read-only by nature, so
|
|
422
|
+
// AFFSET_READ_ONLY doesn't gate them.
|
|
423
|
+
const registerDocsResource = (name, uri, description, feed) => {
|
|
424
|
+
server.registerResource(name, uri, { title: name, description, mimeType: feed.mimeType }, async (resourceUri) => ({
|
|
425
|
+
contents: [
|
|
426
|
+
{
|
|
427
|
+
uri: resourceUri.href,
|
|
428
|
+
mimeType: feed.mimeType,
|
|
429
|
+
text: await fetchDocsFeed(runtimeConfig, feed),
|
|
430
|
+
},
|
|
431
|
+
],
|
|
432
|
+
}));
|
|
433
|
+
};
|
|
434
|
+
registerDocsResource("affset-api-reference", "affset://docs/api-reference", "The affset HTTP API reference (endpoints, auth, roles, examples) as Markdown — the same content as the /docs page.", DOCS_FEEDS.markdown);
|
|
435
|
+
registerDocsResource("affset-api-reference-json", "affset://docs/api-reference.json", "The affset HTTP API reference as structured JSON, for programmatic use.", DOCS_FEEDS.json);
|
|
436
|
+
}
|
|
437
|
+
//# sourceMappingURL=registerTools.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime configuration shared by the stdio entrypoint and library consumers.
|
|
3
|
+
*
|
|
4
|
+
* Keep this module free of Node-specific globals and types: it is part of the
|
|
5
|
+
* public `@affset/mcp/core` declaration graph and is consumed by Workers.
|
|
6
|
+
*/
|
|
7
|
+
export interface Config {
|
|
8
|
+
/** Base URL of the affset API. Normalized to a bare origin. */
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
/** Public origin that serves the documentation feeds. */
|
|
11
|
+
docsBaseUrl: string;
|
|
12
|
+
/** Tenant API key sent as `Authorization: Bearer <key>`. */
|
|
13
|
+
apiKey: string;
|
|
14
|
+
/** Tenant namespace sent as `X-Namespace`. */
|
|
15
|
+
namespace: string;
|
|
16
|
+
/** Per-request timeout in milliseconds. */
|
|
17
|
+
requestTimeoutMs: number;
|
|
18
|
+
/** When true, only tools annotated as read-only are registered. */
|
|
19
|
+
readOnly: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare const MIN_REQUEST_TIMEOUT_MS = 1000;
|
|
22
|
+
export declare const MAX_REQUEST_TIMEOUT_MS = 300000;
|
|
23
|
+
/** Parse and normalize an HTTP(S) origin, refusing cleartext remote hosts. */
|
|
24
|
+
export declare function parseOriginUrl(raw: string, name: string, carriesApiKey?: boolean): URL;
|
|
25
|
+
/** Validate a namespace using the same rules as tenant signup. */
|
|
26
|
+
export declare function validateNamespace(namespace: string, name: string): void;
|
|
27
|
+
/** Reject empty keys and CR/LF so a value cannot split HTTP headers. */
|
|
28
|
+
export declare function validateApiKey(value: unknown, name: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Validate and normalize config supplied through the public library API.
|
|
31
|
+
*
|
|
32
|
+
* TypeScript types are not a runtime security boundary. Hosted transports can
|
|
33
|
+
* source these values from deployment config or grant records, so fail before
|
|
34
|
+
* registering tools if a malformed value could leak a bearer key or silently
|
|
35
|
+
* disable the read-only boundary.
|
|
36
|
+
*/
|
|
37
|
+
export declare function normalizeRuntimeConfig(config: Config): Config;
|