@affset/mcp 0.1.0 → 0.2.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 +158 -37
- 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/index.js +0 -0
- package/dist/lib/format.d.ts +46 -0
- package/dist/lib/format.js +3 -0
- package/dist/lib/integrationUrls.d.ts +57 -0
- package/dist/lib/linkArgs.d.ts +27 -0
- 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/registerTools.d.ts +40 -0
- package/dist/registerTools.js +401 -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 -306
- package/dist/tools/createCampaign.d.ts +25 -0
- package/dist/tools/createCampaign.js +3 -2
- package/dist/tools/createTeamMember.d.ts +26 -0
- package/dist/tools/createTeamMember.js +204 -0
- package/dist/tools/createZone.d.ts +22 -0
- 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 +40 -0
- package/dist/tools/getStats.js +37 -2
- package/dist/tools/getTrackingLink.d.ts +23 -0
- package/dist/tools/getZoneUrl.d.ts +21 -0
- package/dist/tools/listCampaigns.d.ts +23 -0
- package/dist/tools/listConversions.d.ts +29 -0
- 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/listZones.d.ts +23 -0
- 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/updateZone.d.ts +25 -0
- package/dist/tools/whoami.d.ts +6 -0
- package/dist/types.d.ts +210 -0
- package/dist/types.js +1 -0
- package/dist/version.d.ts +7 -0
- package/dist/version.js +8 -0
- package/package.json +16 -2
|
@@ -0,0 +1,401 @@
|
|
|
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 { updateCampaign, updateCampaignInputSchema, UPDATE_CAMPAIGN_DESCRIPTION, } from "./tools/updateCampaign.js";
|
|
17
|
+
import { setCampaignStatus, setCampaignStatusInputSchema, SET_CAMPAIGN_STATUS_DESCRIPTION, } from "./tools/setCampaignStatus.js";
|
|
18
|
+
import { listPayoutRules, listPayoutRulesInputSchema, LIST_PAYOUT_RULES_DESCRIPTION, } from "./tools/listPayoutRules.js";
|
|
19
|
+
import { setPayoutRule, setPayoutRuleInputSchema, SET_PAYOUT_RULE_DESCRIPTION, } from "./tools/setPayoutRule.js";
|
|
20
|
+
import { deletePayoutRule, deletePayoutRuleInputSchema, DELETE_PAYOUT_RULE_DESCRIPTION, } from "./tools/deletePayoutRule.js";
|
|
21
|
+
import { setPayoutGoal, setPayoutGoalInputSchema, SET_PAYOUT_GOAL_DESCRIPTION, } from "./tools/setPayoutGoal.js";
|
|
22
|
+
import { listTargetingTypes, listTargetingTypesInputSchema, LIST_TARGETING_TYPES_DESCRIPTION, } from "./tools/listTargetingTypes.js";
|
|
23
|
+
import { listTargetingRules, listTargetingRulesInputSchema, LIST_TARGETING_RULES_DESCRIPTION, } from "./tools/listTargetingRules.js";
|
|
24
|
+
import { setTargetingRule, setTargetingRuleInputSchema, SET_TARGETING_RULE_DESCRIPTION, } from "./tools/setTargetingRule.js";
|
|
25
|
+
import { removeTargetingRule, removeTargetingRuleInputSchema, REMOVE_TARGETING_RULE_DESCRIPTION, } from "./tools/removeTargetingRule.js";
|
|
26
|
+
import { listSubLabels, listSubLabelsInputSchema, LIST_SUB_LABELS_DESCRIPTION, } from "./tools/listSubLabels.js";
|
|
27
|
+
import { setSubLabels, setSubLabelsInputSchema, SET_SUB_LABELS_DESCRIPTION, } from "./tools/setSubLabels.js";
|
|
28
|
+
import { listConversions, listConversionsInputSchema, LIST_CONVERSIONS_DESCRIPTION, } from "./tools/listConversions.js";
|
|
29
|
+
import { whoami, whoamiInputSchema, WHOAMI_DESCRIPTION } from "./tools/whoami.js";
|
|
30
|
+
function resultIsError(result) {
|
|
31
|
+
return (typeof result === "object" && result !== null && "isError" in result && result.isError === true);
|
|
32
|
+
}
|
|
33
|
+
function instrumentToolCallback(name, callback, onToolCall) {
|
|
34
|
+
const instrumented = async (...args) => {
|
|
35
|
+
const startedAt = Date.now();
|
|
36
|
+
let status = "ok";
|
|
37
|
+
try {
|
|
38
|
+
const result = await callback(...args);
|
|
39
|
+
if (resultIsError(result))
|
|
40
|
+
status = "error";
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
status = "error";
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
try {
|
|
49
|
+
await onToolCall({
|
|
50
|
+
toolName: name,
|
|
51
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
52
|
+
status,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Audit/telemetry must never alter the MCP tool's result.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
return instrumented;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Register the full affset tool roster and docs resources against `config`.
|
|
64
|
+
* The single source of truth for the roster: the stdio entrypoint
|
|
65
|
+
* (`createServer`) and the remote gateway's `McpAgent` both call exactly this,
|
|
66
|
+
* so the two transports cannot drift (REMOTE-MCP-PRD.md §5.6).
|
|
67
|
+
*
|
|
68
|
+
* When `config.readOnly` is set, every tool that is not `readOnlyHint: true`
|
|
69
|
+
* is skipped — identical semantics to AFFSET_READ_ONLY on stdio and to a
|
|
70
|
+
* `read`-scoped OAuth grant on the gateway.
|
|
71
|
+
*/
|
|
72
|
+
export function registerAffsetTools(toolServer, config, options = {}) {
|
|
73
|
+
const runtimeConfig = normalizeRuntimeConfig(config);
|
|
74
|
+
const client = new AffsetClient(runtimeConfig);
|
|
75
|
+
// One cast at the boundary; everything below is the SDK's real surface.
|
|
76
|
+
const server = toolServer;
|
|
77
|
+
// Skip mutating tools entirely when read-only is set — they never appear in
|
|
78
|
+
// tools/list and cannot be called. Skipping is fail-closed: we do not depend
|
|
79
|
+
// on the SDK's remove() to unlist a tool that was already registered. See
|
|
80
|
+
// AFFSET_READ_ONLY in the README for why this exists (untrusted third-party
|
|
81
|
+
// data can reach model context via list_conversions/get_stats, so mutation
|
|
82
|
+
// tools are an injection blast-radius control, not just a UI nicety).
|
|
83
|
+
const registerTool = (name, toolConfig, cb) => {
|
|
84
|
+
if (runtimeConfig.readOnly && toolConfig.annotations?.readOnlyHint !== true) {
|
|
85
|
+
return {
|
|
86
|
+
enabled: false,
|
|
87
|
+
enable() { },
|
|
88
|
+
disable() { },
|
|
89
|
+
remove() { },
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const callback = options.onToolCall ? instrumentToolCallback(name, cb, options.onToolCall) : cb;
|
|
93
|
+
return server.registerTool(name, toolConfig, callback);
|
|
94
|
+
};
|
|
95
|
+
registerTool("whoami", {
|
|
96
|
+
title: "Show the tenant this MCP is bound to",
|
|
97
|
+
description: WHOAMI_DESCRIPTION,
|
|
98
|
+
inputSchema: whoamiInputSchema,
|
|
99
|
+
annotations: {
|
|
100
|
+
readOnlyHint: true,
|
|
101
|
+
destructiveHint: false,
|
|
102
|
+
idempotentHint: true,
|
|
103
|
+
openWorldHint: true,
|
|
104
|
+
},
|
|
105
|
+
}, () => whoami(client, runtimeConfig));
|
|
106
|
+
registerTool("get_stats", {
|
|
107
|
+
title: "Get affset stats",
|
|
108
|
+
description: GET_STATS_DESCRIPTION,
|
|
109
|
+
inputSchema: getStatsInputSchema,
|
|
110
|
+
annotations: {
|
|
111
|
+
readOnlyHint: true,
|
|
112
|
+
destructiveHint: false,
|
|
113
|
+
idempotentHint: true,
|
|
114
|
+
openWorldHint: true,
|
|
115
|
+
},
|
|
116
|
+
}, (args) => getStats(client, args));
|
|
117
|
+
registerTool("list_campaigns", {
|
|
118
|
+
title: "List campaigns",
|
|
119
|
+
description: LIST_CAMPAIGNS_DESCRIPTION,
|
|
120
|
+
inputSchema: listCampaignsInputSchema,
|
|
121
|
+
annotations: {
|
|
122
|
+
readOnlyHint: true,
|
|
123
|
+
destructiveHint: false,
|
|
124
|
+
idempotentHint: true,
|
|
125
|
+
openWorldHint: true,
|
|
126
|
+
},
|
|
127
|
+
}, (args) => listCampaigns(client, args));
|
|
128
|
+
registerTool("get_campaign", {
|
|
129
|
+
title: "Get a campaign's full record",
|
|
130
|
+
description: GET_CAMPAIGN_DESCRIPTION,
|
|
131
|
+
inputSchema: getCampaignInputSchema,
|
|
132
|
+
annotations: {
|
|
133
|
+
readOnlyHint: true,
|
|
134
|
+
destructiveHint: false,
|
|
135
|
+
idempotentHint: true,
|
|
136
|
+
openWorldHint: true,
|
|
137
|
+
},
|
|
138
|
+
}, (args) => getCampaign(client, args));
|
|
139
|
+
registerTool("list_zones", {
|
|
140
|
+
title: "List zones",
|
|
141
|
+
description: LIST_ZONES_DESCRIPTION,
|
|
142
|
+
inputSchema: listZonesInputSchema,
|
|
143
|
+
annotations: {
|
|
144
|
+
readOnlyHint: true,
|
|
145
|
+
destructiveHint: false,
|
|
146
|
+
idempotentHint: true,
|
|
147
|
+
openWorldHint: true,
|
|
148
|
+
},
|
|
149
|
+
}, (args) => listZones(client, args));
|
|
150
|
+
registerTool("list_team", {
|
|
151
|
+
title: "List team members",
|
|
152
|
+
description: LIST_TEAM_DESCRIPTION,
|
|
153
|
+
inputSchema: listTeamInputSchema,
|
|
154
|
+
annotations: {
|
|
155
|
+
readOnlyHint: true,
|
|
156
|
+
destructiveHint: false,
|
|
157
|
+
idempotentHint: true,
|
|
158
|
+
openWorldHint: true,
|
|
159
|
+
},
|
|
160
|
+
}, (args) => listTeam(client, args));
|
|
161
|
+
registerTool("create_team_member", {
|
|
162
|
+
title: "Invite a team member",
|
|
163
|
+
description: CREATE_TEAM_MEMBER_DESCRIPTION,
|
|
164
|
+
inputSchema: createTeamMemberInputSchema,
|
|
165
|
+
annotations: {
|
|
166
|
+
readOnlyHint: false,
|
|
167
|
+
destructiveHint: false,
|
|
168
|
+
idempotentHint: false,
|
|
169
|
+
openWorldHint: true,
|
|
170
|
+
},
|
|
171
|
+
}, (args) => createTeamMember(client, args));
|
|
172
|
+
registerTool("get_zone_url", {
|
|
173
|
+
title: "Get the zone URL for a traffic source",
|
|
174
|
+
description: GET_ZONE_URL_DESCRIPTION,
|
|
175
|
+
inputSchema: getZoneUrlInputSchema,
|
|
176
|
+
annotations: {
|
|
177
|
+
readOnlyHint: true,
|
|
178
|
+
destructiveHint: false,
|
|
179
|
+
idempotentHint: true,
|
|
180
|
+
openWorldHint: true,
|
|
181
|
+
},
|
|
182
|
+
}, (args) => getZoneUrl(client, runtimeConfig, args));
|
|
183
|
+
registerTool("get_tracking_link", {
|
|
184
|
+
title: "Get a campaign's tracking link",
|
|
185
|
+
description: GET_TRACKING_LINK_DESCRIPTION,
|
|
186
|
+
inputSchema: getTrackingLinkInputSchema,
|
|
187
|
+
annotations: {
|
|
188
|
+
readOnlyHint: true,
|
|
189
|
+
destructiveHint: false,
|
|
190
|
+
idempotentHint: true,
|
|
191
|
+
openWorldHint: true,
|
|
192
|
+
},
|
|
193
|
+
}, (args) => getTrackingLink(client, runtimeConfig, args));
|
|
194
|
+
registerTool("create_campaign", {
|
|
195
|
+
title: "Create a campaign",
|
|
196
|
+
description: CREATE_CAMPAIGN_DESCRIPTION,
|
|
197
|
+
inputSchema: createCampaignInputSchema,
|
|
198
|
+
annotations: {
|
|
199
|
+
readOnlyHint: false,
|
|
200
|
+
destructiveHint: false,
|
|
201
|
+
idempotentHint: false,
|
|
202
|
+
openWorldHint: true,
|
|
203
|
+
},
|
|
204
|
+
}, (args) => createCampaign(client, runtimeConfig, args));
|
|
205
|
+
registerTool("update_campaign", {
|
|
206
|
+
title: "Update a campaign",
|
|
207
|
+
description: UPDATE_CAMPAIGN_DESCRIPTION,
|
|
208
|
+
inputSchema: updateCampaignInputSchema,
|
|
209
|
+
annotations: {
|
|
210
|
+
readOnlyHint: false,
|
|
211
|
+
destructiveHint: true,
|
|
212
|
+
idempotentHint: false,
|
|
213
|
+
openWorldHint: true,
|
|
214
|
+
},
|
|
215
|
+
}, (args) => updateCampaign(client, args));
|
|
216
|
+
registerTool("set_campaign_status", {
|
|
217
|
+
title: "Run or pause a campaign",
|
|
218
|
+
description: SET_CAMPAIGN_STATUS_DESCRIPTION,
|
|
219
|
+
inputSchema: setCampaignStatusInputSchema,
|
|
220
|
+
annotations: {
|
|
221
|
+
readOnlyHint: false,
|
|
222
|
+
destructiveHint: true,
|
|
223
|
+
idempotentHint: false,
|
|
224
|
+
openWorldHint: true,
|
|
225
|
+
},
|
|
226
|
+
}, (args) => setCampaignStatus(client, args));
|
|
227
|
+
registerTool("create_zone", {
|
|
228
|
+
title: "Create a zone",
|
|
229
|
+
description: CREATE_ZONE_DESCRIPTION,
|
|
230
|
+
inputSchema: createZoneInputSchema,
|
|
231
|
+
annotations: {
|
|
232
|
+
readOnlyHint: false,
|
|
233
|
+
destructiveHint: false,
|
|
234
|
+
idempotentHint: false,
|
|
235
|
+
openWorldHint: true,
|
|
236
|
+
},
|
|
237
|
+
}, (args) => createZone(client, args));
|
|
238
|
+
registerTool("update_zone", {
|
|
239
|
+
title: "Update a zone",
|
|
240
|
+
description: UPDATE_ZONE_DESCRIPTION,
|
|
241
|
+
inputSchema: updateZoneInputSchema,
|
|
242
|
+
annotations: {
|
|
243
|
+
readOnlyHint: false,
|
|
244
|
+
destructiveHint: true,
|
|
245
|
+
idempotentHint: false,
|
|
246
|
+
openWorldHint: true,
|
|
247
|
+
},
|
|
248
|
+
}, (args) => updateZone(client, args));
|
|
249
|
+
registerTool("cut_zones", {
|
|
250
|
+
title: "Cut underperforming zones",
|
|
251
|
+
description: CUT_ZONES_DESCRIPTION,
|
|
252
|
+
inputSchema: cutZonesInputSchema,
|
|
253
|
+
annotations: {
|
|
254
|
+
readOnlyHint: false,
|
|
255
|
+
destructiveHint: true,
|
|
256
|
+
idempotentHint: false,
|
|
257
|
+
openWorldHint: true,
|
|
258
|
+
},
|
|
259
|
+
}, (args) => cutZones(client, args));
|
|
260
|
+
registerTool("list_payout_rules", {
|
|
261
|
+
title: "List payout rules",
|
|
262
|
+
description: LIST_PAYOUT_RULES_DESCRIPTION,
|
|
263
|
+
inputSchema: listPayoutRulesInputSchema,
|
|
264
|
+
annotations: {
|
|
265
|
+
readOnlyHint: true,
|
|
266
|
+
destructiveHint: false,
|
|
267
|
+
idempotentHint: true,
|
|
268
|
+
openWorldHint: true,
|
|
269
|
+
},
|
|
270
|
+
}, (args) => listPayoutRules(client, args));
|
|
271
|
+
registerTool("set_payout_rule", {
|
|
272
|
+
title: "Set a payout rule",
|
|
273
|
+
description: SET_PAYOUT_RULE_DESCRIPTION,
|
|
274
|
+
inputSchema: setPayoutRuleInputSchema,
|
|
275
|
+
annotations: {
|
|
276
|
+
readOnlyHint: false,
|
|
277
|
+
destructiveHint: true,
|
|
278
|
+
idempotentHint: false,
|
|
279
|
+
openWorldHint: true,
|
|
280
|
+
},
|
|
281
|
+
}, (args) => setPayoutRule(client, args));
|
|
282
|
+
registerTool("delete_payout_rule", {
|
|
283
|
+
title: "Delete a payout rule",
|
|
284
|
+
description: DELETE_PAYOUT_RULE_DESCRIPTION,
|
|
285
|
+
inputSchema: deletePayoutRuleInputSchema,
|
|
286
|
+
annotations: {
|
|
287
|
+
readOnlyHint: false,
|
|
288
|
+
destructiveHint: true,
|
|
289
|
+
idempotentHint: false,
|
|
290
|
+
openWorldHint: true,
|
|
291
|
+
},
|
|
292
|
+
}, (args) => deletePayoutRule(client, args));
|
|
293
|
+
registerTool("set_payout_goal", {
|
|
294
|
+
title: "Set payout goal type",
|
|
295
|
+
description: SET_PAYOUT_GOAL_DESCRIPTION,
|
|
296
|
+
inputSchema: setPayoutGoalInputSchema,
|
|
297
|
+
annotations: {
|
|
298
|
+
readOnlyHint: false,
|
|
299
|
+
destructiveHint: true,
|
|
300
|
+
idempotentHint: false,
|
|
301
|
+
openWorldHint: true,
|
|
302
|
+
},
|
|
303
|
+
}, (args) => setPayoutGoal(client, args));
|
|
304
|
+
registerTool("list_targeting_types", {
|
|
305
|
+
title: "List targeting rule types",
|
|
306
|
+
description: LIST_TARGETING_TYPES_DESCRIPTION,
|
|
307
|
+
inputSchema: listTargetingTypesInputSchema,
|
|
308
|
+
annotations: {
|
|
309
|
+
readOnlyHint: true,
|
|
310
|
+
destructiveHint: false,
|
|
311
|
+
idempotentHint: true,
|
|
312
|
+
openWorldHint: true,
|
|
313
|
+
},
|
|
314
|
+
}, () => listTargetingTypes(client));
|
|
315
|
+
registerTool("list_targeting_rules", {
|
|
316
|
+
title: "List campaign targeting rules",
|
|
317
|
+
description: LIST_TARGETING_RULES_DESCRIPTION,
|
|
318
|
+
inputSchema: listTargetingRulesInputSchema,
|
|
319
|
+
annotations: {
|
|
320
|
+
readOnlyHint: true,
|
|
321
|
+
destructiveHint: false,
|
|
322
|
+
idempotentHint: true,
|
|
323
|
+
openWorldHint: true,
|
|
324
|
+
},
|
|
325
|
+
}, (args) => listTargetingRules(client, args));
|
|
326
|
+
registerTool("set_targeting_rule", {
|
|
327
|
+
title: "Set a targeting rule",
|
|
328
|
+
description: SET_TARGETING_RULE_DESCRIPTION,
|
|
329
|
+
inputSchema: setTargetingRuleInputSchema,
|
|
330
|
+
annotations: {
|
|
331
|
+
readOnlyHint: false,
|
|
332
|
+
destructiveHint: true,
|
|
333
|
+
idempotentHint: false,
|
|
334
|
+
openWorldHint: true,
|
|
335
|
+
},
|
|
336
|
+
}, (args) => setTargetingRule(client, args));
|
|
337
|
+
registerTool("remove_targeting_rule", {
|
|
338
|
+
title: "Remove a targeting rule",
|
|
339
|
+
description: REMOVE_TARGETING_RULE_DESCRIPTION,
|
|
340
|
+
inputSchema: removeTargetingRuleInputSchema,
|
|
341
|
+
annotations: {
|
|
342
|
+
readOnlyHint: false,
|
|
343
|
+
destructiveHint: true,
|
|
344
|
+
idempotentHint: false,
|
|
345
|
+
openWorldHint: true,
|
|
346
|
+
},
|
|
347
|
+
}, (args) => removeTargetingRule(client, args));
|
|
348
|
+
registerTool("list_sub_labels", {
|
|
349
|
+
title: "List sub labels",
|
|
350
|
+
description: LIST_SUB_LABELS_DESCRIPTION,
|
|
351
|
+
inputSchema: listSubLabelsInputSchema,
|
|
352
|
+
annotations: {
|
|
353
|
+
readOnlyHint: true,
|
|
354
|
+
destructiveHint: false,
|
|
355
|
+
idempotentHint: true,
|
|
356
|
+
openWorldHint: true,
|
|
357
|
+
},
|
|
358
|
+
}, () => listSubLabels(client));
|
|
359
|
+
registerTool("set_sub_labels", {
|
|
360
|
+
title: "Set sub labels",
|
|
361
|
+
description: SET_SUB_LABELS_DESCRIPTION,
|
|
362
|
+
inputSchema: setSubLabelsInputSchema,
|
|
363
|
+
annotations: {
|
|
364
|
+
readOnlyHint: false,
|
|
365
|
+
destructiveHint: true,
|
|
366
|
+
idempotentHint: false,
|
|
367
|
+
openWorldHint: true,
|
|
368
|
+
},
|
|
369
|
+
}, (args) => setSubLabels(client, args));
|
|
370
|
+
registerTool("list_conversions", {
|
|
371
|
+
title: "List conversions",
|
|
372
|
+
description: LIST_CONVERSIONS_DESCRIPTION,
|
|
373
|
+
inputSchema: listConversionsInputSchema,
|
|
374
|
+
annotations: {
|
|
375
|
+
readOnlyHint: true,
|
|
376
|
+
destructiveHint: false,
|
|
377
|
+
idempotentHint: true,
|
|
378
|
+
openWorldHint: true,
|
|
379
|
+
},
|
|
380
|
+
}, (args) => listConversions(client, args));
|
|
381
|
+
// Documentation resources. The affset API reference (the same content as the
|
|
382
|
+
// /docs page) is exposed so an assistant can answer "how does X work / how do
|
|
383
|
+
// I call Y" from the docs themselves, not just from the tool schemas above.
|
|
384
|
+
// Fetched from config.docsBaseUrl at read time, so it always reflects the
|
|
385
|
+
// currently published docs. Always registered — read-only by nature, so
|
|
386
|
+
// AFFSET_READ_ONLY doesn't gate them.
|
|
387
|
+
const registerDocsResource = (name, uri, description, feed) => {
|
|
388
|
+
server.registerResource(name, uri, { title: name, description, mimeType: feed.mimeType }, async (resourceUri) => ({
|
|
389
|
+
contents: [
|
|
390
|
+
{
|
|
391
|
+
uri: resourceUri.href,
|
|
392
|
+
mimeType: feed.mimeType,
|
|
393
|
+
text: await fetchDocsFeed(runtimeConfig, feed),
|
|
394
|
+
},
|
|
395
|
+
],
|
|
396
|
+
}));
|
|
397
|
+
};
|
|
398
|
+
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);
|
|
399
|
+
registerDocsResource("affset-api-reference-json", "affset://docs/api-reference.json", "The affset HTTP API reference as structured JSON, for programmatic use.", DOCS_FEEDS.json);
|
|
400
|
+
}
|
|
401
|
+
//# 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;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export const MIN_REQUEST_TIMEOUT_MS = 1_000;
|
|
2
|
+
export const MAX_REQUEST_TIMEOUT_MS = 300_000;
|
|
3
|
+
const NAMESPACE_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
4
|
+
function isLoopbackHost(hostname) {
|
|
5
|
+
return (hostname === "localhost" ||
|
|
6
|
+
hostname === "127.0.0.1" ||
|
|
7
|
+
hostname === "::1" ||
|
|
8
|
+
hostname.endsWith(".localhost"));
|
|
9
|
+
}
|
|
10
|
+
/** Parse and normalize an HTTP(S) origin, refusing cleartext remote hosts. */
|
|
11
|
+
export function parseOriginUrl(raw, name, carriesApiKey = false) {
|
|
12
|
+
const normalized = raw.replace(/\/+$/, "");
|
|
13
|
+
let parsed;
|
|
14
|
+
try {
|
|
15
|
+
parsed = new URL(normalized);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
throw new Error(`${name} is not a valid URL: ${raw}`);
|
|
19
|
+
}
|
|
20
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
21
|
+
throw new Error(`${name} must be http(s), got ${parsed.protocol}`);
|
|
22
|
+
}
|
|
23
|
+
if (parsed.username || parsed.password) {
|
|
24
|
+
throw new Error(`${name} must not contain embedded credentials.`);
|
|
25
|
+
}
|
|
26
|
+
if ((parsed.pathname !== "/" && parsed.pathname !== "") || parsed.search || parsed.hash) {
|
|
27
|
+
throw new Error(`${name} must be an origin only (scheme, host, and optional port; no path, query, or fragment).`);
|
|
28
|
+
}
|
|
29
|
+
if (parsed.protocol === "http:" && !isLoopbackHost(parsed.hostname)) {
|
|
30
|
+
const cleartextHint = carriesApiKey ? " — plain http would send the API key in cleartext" : "";
|
|
31
|
+
throw new Error(`${name} must be https for a non-local host (got http://${parsed.hostname})${cleartextHint}.`);
|
|
32
|
+
}
|
|
33
|
+
return parsed;
|
|
34
|
+
}
|
|
35
|
+
/** Validate a namespace using the same rules as tenant signup. */
|
|
36
|
+
export function validateNamespace(namespace, name) {
|
|
37
|
+
if (namespace.length < 3 || namespace.length > 63) {
|
|
38
|
+
throw new Error(`${name} must be 3–63 characters.`);
|
|
39
|
+
}
|
|
40
|
+
if (!NAMESPACE_RE.test(namespace) || namespace.includes("--")) {
|
|
41
|
+
throw new Error(`${name} must be lowercase letters, numbers, and hyphens only ` +
|
|
42
|
+
"(no leading/trailing or consecutive hyphens).");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function requiredString(value, name) {
|
|
46
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
47
|
+
throw new Error(`${name} must be a non-empty string.`);
|
|
48
|
+
}
|
|
49
|
+
return value.trim();
|
|
50
|
+
}
|
|
51
|
+
/** Reject empty keys and CR/LF so a value cannot split HTTP headers. */
|
|
52
|
+
export function validateApiKey(value, name) {
|
|
53
|
+
const apiKey = requiredString(value, name);
|
|
54
|
+
if (/\r|\n/.test(apiKey)) {
|
|
55
|
+
throw new Error(`${name} must not contain line breaks.`);
|
|
56
|
+
}
|
|
57
|
+
return apiKey;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Validate and normalize config supplied through the public library API.
|
|
61
|
+
*
|
|
62
|
+
* TypeScript types are not a runtime security boundary. Hosted transports can
|
|
63
|
+
* source these values from deployment config or grant records, so fail before
|
|
64
|
+
* registering tools if a malformed value could leak a bearer key or silently
|
|
65
|
+
* disable the read-only boundary.
|
|
66
|
+
*/
|
|
67
|
+
export function normalizeRuntimeConfig(config) {
|
|
68
|
+
if (config === null || typeof config !== "object") {
|
|
69
|
+
throw new Error("config must be an object.");
|
|
70
|
+
}
|
|
71
|
+
const baseUrl = parseOriginUrl(requiredString(config.baseUrl, "config.baseUrl"), "config.baseUrl", true).origin;
|
|
72
|
+
const docsBaseUrl = parseOriginUrl(requiredString(config.docsBaseUrl, "config.docsBaseUrl"), "config.docsBaseUrl").origin;
|
|
73
|
+
const apiKey = validateApiKey(config.apiKey, "config.apiKey");
|
|
74
|
+
const namespace = requiredString(config.namespace, "config.namespace");
|
|
75
|
+
validateNamespace(namespace, "config.namespace");
|
|
76
|
+
if (!Number.isSafeInteger(config.requestTimeoutMs) ||
|
|
77
|
+
config.requestTimeoutMs < MIN_REQUEST_TIMEOUT_MS ||
|
|
78
|
+
config.requestTimeoutMs > MAX_REQUEST_TIMEOUT_MS) {
|
|
79
|
+
throw new Error(`config.requestTimeoutMs must be an integer between ${MIN_REQUEST_TIMEOUT_MS} and ${MAX_REQUEST_TIMEOUT_MS}.`);
|
|
80
|
+
}
|
|
81
|
+
if (typeof config.readOnly !== "boolean") {
|
|
82
|
+
throw new Error("config.readOnly must be a boolean.");
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
baseUrl,
|
|
86
|
+
docsBaseUrl,
|
|
87
|
+
apiKey,
|
|
88
|
+
namespace,
|
|
89
|
+
requestTimeoutMs: config.requestTimeoutMs,
|
|
90
|
+
readOnly: config.readOnly,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=runtimeConfig.js.map
|
package/dist/server.d.ts
ADDED