@canonry/canonry 5.17.0 → 5.19.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.
Files changed (34) hide show
  1. package/assets/assets/{AuditHistoryPanel-CZUnfysJ.js → AuditHistoryPanel-C_p8jX7Q.js} +1 -1
  2. package/assets/assets/{BacklinksPage-C9qSkTUW.js → BacklinksPage-jD4nOGw1.js} +1 -1
  3. package/assets/assets/{HistoryPage-CYMWnCta.js → HistoryPage-DJGBbtBY.js} +1 -1
  4. package/assets/assets/MeasurementPropertyPage-Dmr3TVlo.js +1 -0
  5. package/assets/assets/ProjectPage-B_haCKiA.js +8 -0
  6. package/assets/assets/{RunRow-CwMUW3rM.js → RunRow-BMbwR2yk.js} +1 -1
  7. package/assets/assets/{RunsPage-DbzSmrnc.js → RunsPage-D8hHakq2.js} +1 -1
  8. package/assets/assets/{SettingsPage-DM-G5WzL.js → SettingsPage-QKnRcFA9.js} +1 -1
  9. package/assets/assets/{SiteHealthSection-BwmQJ9Z9.js → SiteHealthSection-CDkUXqmb.js} +3 -3
  10. package/assets/assets/{TrafficPage-DSumJzUk.js → TrafficPage-CMif8RlM.js} +2 -2
  11. package/assets/assets/{TrafficSourceDetailPage-D5BjIBkN.js → TrafficSourceDetailPage-DOCTP_4-.js} +1 -1
  12. package/assets/assets/{extract-error-message-DbpR8Tzb.js → extract-error-message-Dm8RvYE5.js} +1 -1
  13. package/assets/assets/index-lj0fuFl4.js +86 -0
  14. package/assets/assets/{react-sigma_core.esm.min-C3cM2I3z.js → react-sigma_core.esm.min-DOPuwLtN.js} +1 -1
  15. package/assets/assets/v2-overview-adapter-C19tUcDS.js +1 -0
  16. package/assets/index.html +1 -1
  17. package/dist/{chunk-2EE6RPWV.js → chunk-ABUO27BV.js} +3 -3
  18. package/dist/{chunk-GYX277BV.js → chunk-GUJY3RIM.js} +12 -12
  19. package/dist/{chunk-H6COEFVL.js → chunk-P2EFPLIO.js} +55 -21
  20. package/dist/{chunk-3OYWLQEE.js → chunk-QJWIOCEO.js} +2 -2
  21. package/dist/{chunk-XC24KCTL.js → chunk-SPCEXW3B.js} +14 -3969
  22. package/dist/{chunk-L36TC6LB.js → chunk-WLL7Q7JD.js} +57 -3
  23. package/dist/chunk-YHKKCXGY.js +3975 -0
  24. package/dist/cli.js +31 -20
  25. package/dist/{demo-server-CUBR2NVR.js → demo-server-BOIBS74F.js} +1071 -187
  26. package/dist/index.js +6 -6
  27. package/dist/{intelligence-service-6N7SIWLD.js → intelligence-service-IWR2H6OW.js} +2 -2
  28. package/dist/mcp.js +4 -4
  29. package/package.json +14 -14
  30. package/assets/assets/MeasurementPropertyPage-Bu4KDAdf.js +0 -1
  31. package/assets/assets/ProjectPage-Bg87LPit.js +0 -8
  32. package/assets/assets/index-BtHRel6S.js +0 -86
  33. package/assets/assets/v2-overview-adapter-DllH2EQb.js +0 -1
  34. package/dist/chunk-AOJU5LKX.js +0 -8
@@ -0,0 +1,3975 @@
1
+ import {
2
+ AGENT_MEMORY_KEY_MAX_LENGTH,
3
+ AGENT_MEMORY_VALUE_MAX_BYTES,
4
+ DISCOVERY_MAX_PROBES_CAP,
5
+ DISCOVERY_PROBE_CONCURRENCY_CAP,
6
+ GOOGLE_MARKETING_STORED_SNAPSHOT_PAGE_MAX,
7
+ RESEARCH_RUN_SCOPE,
8
+ adsActivateTreeRequestSchema,
9
+ adsAdCreateRequestSchema,
10
+ adsAdGroupCreateRequestSchema,
11
+ adsAdGroupUpdateRequestSchema,
12
+ adsAdUpdateRequestSchema,
13
+ adsCampaignCreateRequestSchema,
14
+ adsCampaignUpdateRequestSchema,
15
+ adsGeoSearchQuerySchema,
16
+ adsImageUploadRequestSchema,
17
+ adsOperationReconcileRequestSchema,
18
+ adsPauseRequestSchema,
19
+ adsUnresolvedOperationListQuerySchema,
20
+ agentConversationCreateSchema,
21
+ apiKeyDtoSchema,
22
+ backlinkSourceSchema,
23
+ canonicalizeGtmAccountId,
24
+ canonicalizeGtmResourceSelection,
25
+ competitorBatchRequestSchema,
26
+ competitorLandscapeQuerySchema,
27
+ describeError,
28
+ discoveryBucketSchema,
29
+ discoveryCompetitorTypeSchema,
30
+ discoveryPromoteRequestSchema,
31
+ discoveryRunRequestSchema,
32
+ gaMeasurementAnalysisWindowSchema,
33
+ gaMeasurementHostScopeSchema,
34
+ googleAdsMetricsWindowSchema,
35
+ gscPerformanceOrderBySchema,
36
+ keywordBatchRequestSchema,
37
+ keywordGenerateRequestSchema,
38
+ logQuerySchema,
39
+ measurementChangesQuerySchema,
40
+ measurementDataQualityQuerySchema,
41
+ measurementDiscoveryRequestSchema,
42
+ measurementDraftApplyAssignmentsRequestSchema,
43
+ measurementDraftApplyGroupMembershipRequestSchema,
44
+ measurementDraftApplyPairedAssignmentsRequestSchema,
45
+ measurementDraftApplySitemapSelectionRequestSchema,
46
+ measurementDraftClassifyAssignmentsRequestSchema,
47
+ measurementDraftClearAssignmentsRequestSchema,
48
+ measurementDraftCollectionQuerySchema,
49
+ measurementDraftCreateRequestSchema,
50
+ measurementDraftExcludeTargetRequestSchema,
51
+ measurementDraftImportSitemapRequestSchema,
52
+ measurementDraftMergeTargetsRequestSchema,
53
+ measurementDraftPreviewAssignmentsRequestSchema,
54
+ measurementDraftPreviewGroupMembershipRequestSchema,
55
+ measurementDraftPublishRequestSchema,
56
+ measurementDraftRebindTargetRequestSchema,
57
+ measurementDraftRemoveAssignmentRequestSchema,
58
+ measurementDraftRemoveCompetitorRequestSchema,
59
+ measurementDraftRemoveGroupRequestSchema,
60
+ measurementDraftRenameTargetRequestSchema,
61
+ measurementDraftReplaceAssignmentsRequestSchema,
62
+ measurementDraftUpsertCompetitorRequestSchema,
63
+ measurementDraftUpsertGroupRequestSchema,
64
+ measurementDraftUpsertMarketRequestSchema,
65
+ measurementDraftUpsertTargetRequestSchema,
66
+ measurementOverviewQuerySchema,
67
+ measurementPlanAuthoringSchema,
68
+ measurementPlanDeactivateRequestSchema,
69
+ measurementPlanPublishRequestSchema,
70
+ measurementPortfolioSummaryQuerySchema,
71
+ measurementPropertyCompetitorsQuerySchema,
72
+ measurementPropertyEvidenceQuerySchema,
73
+ measurementPropertyQuestionsQuerySchema,
74
+ measurementQuerySetUpsertRequestSchema,
75
+ measurementQueryTemplateApplyRequestSchema,
76
+ measurementQueryTemplateUpsertRequestSchema,
77
+ measurementQuestionResultQuerySchema,
78
+ notificationCreateRequestSchema,
79
+ notificationEventSchema,
80
+ operationalLogListDtoSchema,
81
+ organicEvidencePeriodSchema,
82
+ projectConfigSchema,
83
+ projectUpsertRequestSchema,
84
+ providerQuotaPolicySchema,
85
+ providerSummaryEntryDtoSchema,
86
+ queryBatchRequestSchema,
87
+ queryGenerateRequestSchema,
88
+ queryTrackingCommitRequestSchema,
89
+ queryTrackingPreviewRequestSchema,
90
+ reportPeriodSchema,
91
+ researchBatchCreateSchema,
92
+ researchRunCreateSchema,
93
+ resultsClearRequestSchema,
94
+ runListFilterQuerySchema,
95
+ runTriggerRequestSchema,
96
+ schedulableRunKindSchema,
97
+ scheduleDtoSchema,
98
+ scheduleUpsertRequestSchema,
99
+ settingsDtoSchema,
100
+ snapshotRequestInputSchema,
101
+ telemetryStatusDtoSchema,
102
+ trafficConnectCloudRunRequestSchema,
103
+ trafficConnectVercelRequestSchema,
104
+ trafficConnectWordpressRequestSchema,
105
+ trafficEventKindSchema,
106
+ trafficSeriesGranularitySchema,
107
+ visibilityReportRequestSchema
108
+ } from "./chunk-WLL7Q7JD.js";
109
+
110
+ // src/cli-error.ts
111
+ function isMachineFormat(format) {
112
+ return format === "json" || format === "jsonl";
113
+ }
114
+ var EXIT_USER_ERROR = 1;
115
+ var EXIT_SYSTEM_ERROR = 2;
116
+ var CliError = class extends Error {
117
+ code;
118
+ displayMessage;
119
+ details;
120
+ exitCode;
121
+ constructor(options) {
122
+ super(options.message);
123
+ this.name = "CliError";
124
+ this.code = options.code;
125
+ this.displayMessage = options.displayMessage;
126
+ this.details = options.details;
127
+ this.exitCode = options.exitCode ?? EXIT_USER_ERROR;
128
+ }
129
+ };
130
+ function usageError(displayMessage, options) {
131
+ const firstLine = displayMessage.split("\n", 1)[0] ?? "Error: invalid command usage";
132
+ return new CliError({
133
+ code: "CLI_USAGE_ERROR",
134
+ message: options?.message ?? firstLine.replace(/^Error:\s*/, ""),
135
+ displayMessage,
136
+ details: options?.details
137
+ });
138
+ }
139
+ function isEndpointMissing(err) {
140
+ if (!(err instanceof CliError)) return false;
141
+ const status = err.details?.httpStatus;
142
+ return status === 404 || status === 405;
143
+ }
144
+ function systemError(message, options) {
145
+ return new CliError({
146
+ code: "CLI_SYSTEM_ERROR",
147
+ message,
148
+ displayMessage: options?.displayMessage,
149
+ details: options?.details,
150
+ exitCode: EXIT_SYSTEM_ERROR
151
+ });
152
+ }
153
+ function printCliError(err, format) {
154
+ if (isMachineFormat(format)) {
155
+ const envelope = err instanceof CliError ? { error: { code: err.code, message: err.message, ...err.details ? { details: err.details } : {} } } : { error: { code: "CLI_ERROR", message: err instanceof Error ? err.message : "An unexpected error occurred" } };
156
+ console.error(JSON.stringify(envelope, null, format === "jsonl" ? 0 : 2));
157
+ return;
158
+ }
159
+ if (err instanceof CliError && err.displayMessage) {
160
+ console.error(err.displayMessage);
161
+ return;
162
+ }
163
+ if (err instanceof Error) {
164
+ console.error(`Error: ${err.message}`);
165
+ return;
166
+ }
167
+ console.error("An unexpected error occurred");
168
+ }
169
+
170
+ // src/package-version.ts
171
+ import { createRequire } from "module";
172
+ var _require = createRequire(import.meta.url);
173
+ var PACKAGE_VERSION = _require("../package.json").version;
174
+
175
+ // src/mcp/tool-registry.ts
176
+ import { z as z3 } from "zod";
177
+
178
+ // src/measurement-draft-actions.ts
179
+ import { z } from "zod";
180
+ var idempotencyKeySchema = z.string().trim().min(1).describe(
181
+ "A fresh request key. Reuse it only when retrying the identical request."
182
+ );
183
+ var draftEtagSchema = z.string().trim().min(1).optional().describe(
184
+ "Current draft ETag from canonry_measurement_draft_get. The API requires it for draft edits, publish, and discard; omit it only to receive the API\u2019s actionable 428 response."
185
+ );
186
+ function mutationOperationSchema(action, request) {
187
+ return z.object({
188
+ action: z.literal(action),
189
+ request,
190
+ etag: draftEtagSchema,
191
+ idempotencyKey: idempotencyKeySchema
192
+ }).strict().describe(`Operation for ${action}.`);
193
+ }
194
+ var measurementDraftOperationSchema = z.discriminatedUnion("action", [
195
+ z.object({
196
+ action: z.literal("create"),
197
+ request: measurementDraftCreateRequestSchema,
198
+ idempotencyKey: idempotencyKeySchema
199
+ }).strict().describe("Operation for create."),
200
+ mutationOperationSchema("import-sitemap", measurementDraftImportSitemapRequestSchema),
201
+ mutationOperationSchema("apply-sitemap-selection", measurementDraftApplySitemapSelectionRequestSchema),
202
+ mutationOperationSchema("upsert-target", measurementDraftUpsertTargetRequestSchema),
203
+ mutationOperationSchema("rename-target", measurementDraftRenameTargetRequestSchema),
204
+ mutationOperationSchema("merge-targets", measurementDraftMergeTargetsRequestSchema),
205
+ mutationOperationSchema("exclude-target", measurementDraftExcludeTargetRequestSchema),
206
+ mutationOperationSchema("rebind-target", measurementDraftRebindTargetRequestSchema),
207
+ mutationOperationSchema("apply-assignments", measurementDraftApplyAssignmentsRequestSchema),
208
+ z.object({
209
+ action: z.literal("preview-assignments"),
210
+ request: measurementDraftPreviewAssignmentsRequestSchema
211
+ }).strict().describe("Read-semantic assignment impact preview."),
212
+ mutationOperationSchema("replace-assignments", measurementDraftReplaceAssignmentsRequestSchema),
213
+ mutationOperationSchema("apply-paired-assignments", measurementDraftApplyPairedAssignmentsRequestSchema),
214
+ mutationOperationSchema("remove-assignment", measurementDraftRemoveAssignmentRequestSchema),
215
+ mutationOperationSchema("clear-assignments", measurementDraftClearAssignmentsRequestSchema),
216
+ mutationOperationSchema("classify-assignments", measurementDraftClassifyAssignmentsRequestSchema),
217
+ mutationOperationSchema("upsert-group", measurementDraftUpsertGroupRequestSchema),
218
+ mutationOperationSchema("upsert-market", measurementDraftUpsertMarketRequestSchema),
219
+ mutationOperationSchema("remove-group", measurementDraftRemoveGroupRequestSchema),
220
+ z.object({
221
+ action: z.literal("preview-group-membership"),
222
+ request: measurementDraftPreviewGroupMembershipRequestSchema
223
+ }).strict().describe("Read-semantic CSV group-membership preview."),
224
+ mutationOperationSchema("apply-group-membership", measurementDraftApplyGroupMembershipRequestSchema),
225
+ mutationOperationSchema("upsert-competitor", measurementDraftUpsertCompetitorRequestSchema),
226
+ mutationOperationSchema("remove-competitor", measurementDraftRemoveCompetitorRequestSchema),
227
+ z.object({ action: z.literal("compile-preview") }).strict().describe("Operation for compile-preview."),
228
+ z.object({ action: z.literal("diff-preview") }).strict().describe("Operation for diff-preview."),
229
+ mutationOperationSchema("publish", measurementDraftPublishRequestSchema),
230
+ z.object({
231
+ action: z.literal("discard"),
232
+ etag: draftEtagSchema,
233
+ idempotencyKey: idempotencyKeySchema
234
+ }).strict().describe("Operation for discard.")
235
+ ]);
236
+ function runMeasurementDraftAction(client, project, actionInput) {
237
+ switch (actionInput.action) {
238
+ case "create":
239
+ return client.createMeasurementPlanDraft(project, actionInput.request, actionInput.idempotencyKey);
240
+ case "import-sitemap":
241
+ return client.importMeasurementDraftSitemap(project, actionInput.request, actionInput.idempotencyKey, actionInput.etag);
242
+ case "apply-sitemap-selection":
243
+ return client.applyMeasurementDraftSitemapSelection(project, actionInput.request, actionInput.idempotencyKey, actionInput.etag);
244
+ case "upsert-target":
245
+ return client.upsertMeasurementDraftTarget(project, actionInput.request, actionInput.idempotencyKey, actionInput.etag);
246
+ case "rename-target":
247
+ return client.renameMeasurementDraftTarget(project, actionInput.request, actionInput.idempotencyKey, actionInput.etag);
248
+ case "merge-targets":
249
+ return client.mergeMeasurementDraftTargets(project, actionInput.request, actionInput.idempotencyKey, actionInput.etag);
250
+ case "exclude-target":
251
+ return client.excludeMeasurementDraftTarget(project, actionInput.request, actionInput.idempotencyKey, actionInput.etag);
252
+ case "rebind-target":
253
+ return client.rebindMeasurementDraftTarget(project, actionInput.request, actionInput.idempotencyKey, actionInput.etag);
254
+ case "apply-assignments":
255
+ return client.applyMeasurementDraftAssignments(project, actionInput.request, actionInput.idempotencyKey, actionInput.etag);
256
+ case "preview-assignments":
257
+ return client.previewMeasurementDraftAssignments(project, actionInput.request);
258
+ case "replace-assignments":
259
+ return client.replaceMeasurementDraftAssignments(project, actionInput.request, actionInput.idempotencyKey, actionInput.etag);
260
+ case "apply-paired-assignments":
261
+ return client.applyPairedMeasurementDraftAssignments(project, actionInput.request, actionInput.idempotencyKey, actionInput.etag);
262
+ case "remove-assignment":
263
+ return client.removeMeasurementDraftAssignment(project, actionInput.request, actionInput.idempotencyKey, actionInput.etag);
264
+ case "clear-assignments":
265
+ return client.clearMeasurementDraftAssignments(project, actionInput.request, actionInput.idempotencyKey, actionInput.etag);
266
+ case "classify-assignments":
267
+ return client.classifyMeasurementDraftAssignments(project, actionInput.request, actionInput.idempotencyKey, actionInput.etag);
268
+ case "upsert-group":
269
+ return client.upsertMeasurementDraftGroup(project, actionInput.request, actionInput.idempotencyKey, actionInput.etag);
270
+ case "upsert-market":
271
+ return client.upsertMeasurementDraftMarket(project, actionInput.request, actionInput.idempotencyKey, actionInput.etag);
272
+ case "remove-group":
273
+ return client.removeMeasurementDraftGroup(project, actionInput.request, actionInput.idempotencyKey, actionInput.etag);
274
+ case "preview-group-membership":
275
+ return client.previewMeasurementDraftGroupMembership(project, actionInput.request);
276
+ case "apply-group-membership":
277
+ return client.applyMeasurementDraftGroupMembership(project, actionInput.request, actionInput.idempotencyKey, actionInput.etag);
278
+ case "upsert-competitor":
279
+ return client.upsertMeasurementDraftCompetitor(project, actionInput.request, actionInput.idempotencyKey, actionInput.etag);
280
+ case "remove-competitor":
281
+ return client.removeMeasurementDraftCompetitor(project, actionInput.request, actionInput.idempotencyKey, actionInput.etag);
282
+ case "compile-preview":
283
+ return client.compileMeasurementDraftPreview(project);
284
+ case "diff-preview":
285
+ return client.diffMeasurementDraftPreview(project);
286
+ case "publish":
287
+ return client.publishMeasurementDraft(project, actionInput.request, actionInput.idempotencyKey, actionInput.etag);
288
+ case "discard":
289
+ return client.discardMeasurementDraft(project, actionInput.idempotencyKey, actionInput.etag);
290
+ }
291
+ }
292
+
293
+ // src/mcp/schema.ts
294
+ import { z as z2 } from "zod";
295
+ var projectNameSchema = z2.string().min(1).describe("Canonry project name.");
296
+ var runIdSchema = z2.string().min(1).describe("Canonry run ID.");
297
+ var insightIdSchema = z2.string().min(1).describe("Canonry insight ID.");
298
+ var analyticsWindowSchema = z2.enum(["7d", "30d", "90d", "all"]).describe("Analytics time window.");
299
+ var emptyInputSchema = z2.object({});
300
+ var projectInputSchema = z2.object({
301
+ project: projectNameSchema
302
+ });
303
+ function toJsonSchema(schema, name) {
304
+ return {
305
+ ...z2.toJSONSchema(schema, { target: "draft-7" }),
306
+ title: name
307
+ };
308
+ }
309
+ function compactStringParams(values, keys) {
310
+ const params = {};
311
+ for (const key of keys) {
312
+ const value = values[key];
313
+ if (value === void 0 || value === null || value === "") continue;
314
+ if (typeof value === "string") {
315
+ params[key] = value;
316
+ } else if (typeof value === "number" || typeof value === "boolean") {
317
+ params[key] = String(value);
318
+ }
319
+ }
320
+ return Object.keys(params).length ? params : void 0;
321
+ }
322
+ function uniqueStrings(values) {
323
+ const seen = /* @__PURE__ */ new Set();
324
+ const result = [];
325
+ for (const value of values) {
326
+ const trimmed = value.trim();
327
+ if (!trimmed || seen.has(trimmed)) continue;
328
+ seen.add(trimmed);
329
+ result.push(trimmed);
330
+ }
331
+ return result;
332
+ }
333
+
334
+ // src/mcp/tool-registry.ts
335
+ var readAnnotations = (openWorldHint) => ({
336
+ readOnlyHint: true,
337
+ ...openWorldHint ? { openWorldHint } : {}
338
+ });
339
+ var writeAnnotations = (opts) => ({
340
+ readOnlyHint: false,
341
+ idempotentHint: opts.idempotentHint,
342
+ destructiveHint: Boolean(opts.destructiveHint),
343
+ ...opts.openWorldHint ? { openWorldHint: opts.openWorldHint } : {}
344
+ });
345
+ function defineTool(tool) {
346
+ return {
347
+ ...tool,
348
+ inputJsonSchema: toJsonSchema(tool.inputSchema, tool.name)
349
+ };
350
+ }
351
+ var runTriggerInputSchema = z3.object({
352
+ project: projectNameSchema,
353
+ request: runTriggerRequestSchema.optional()
354
+ });
355
+ var measurementPlanVersionInputSchema = z3.object({ project: projectNameSchema, revision: z3.number().int().positive() });
356
+ var measurementReportInputSchema = measurementPlanVersionInputSchema.extend({
357
+ runId: runIdSchema.optional().describe("Exact eligible full measurement run to reconstruct. Omit for the latest run in the revision.")
358
+ });
359
+ var measurementPlanPreviewInputSchema = z3.object({ project: projectNameSchema, plan: measurementPlanAuthoringSchema });
360
+ var measurementPlanPublishInputSchema = measurementPlanPublishRequestSchema.extend({ project: projectNameSchema });
361
+ var measurementPlanRetireInputSchema = z3.object({ project: projectNameSchema, stableKey: z3.string().min(1) });
362
+ var measurementDiscoveryInputSchema = measurementDiscoveryRequestSchema.extend({ project: projectNameSchema });
363
+ var idempotencyKeyInputSchema = z3.string().trim().min(1).describe("A fresh request key. Reuse it only when retrying the identical request.");
364
+ var measurementOverviewInputSchema = z3.object({
365
+ project: projectNameSchema,
366
+ scope: measurementOverviewQuerySchema.shape.scope.describe("Read all Properties, one reporting group, or one Property."),
367
+ groupKey: measurementOverviewQuerySchema.shape.groupKey.describe("Group stable key. Required only for group scope."),
368
+ targetKey: measurementOverviewQuerySchema.shape.targetKey.describe("Property stable key. Required only for property scope."),
369
+ queryClass: measurementOverviewQuerySchema.shape.queryClass.describe("Select non-brand for acquisition performance or branded for brand awareness. Omitted means all classes combined; always name the class in the report."),
370
+ provider: measurementOverviewQuerySchema.shape.provider,
371
+ location: measurementOverviewQuerySchema.shape.location,
372
+ from: measurementOverviewQuerySchema.shape.from,
373
+ to: measurementOverviewQuerySchema.shape.to,
374
+ runId: measurementOverviewQuerySchema.shape.runId,
375
+ search: measurementOverviewQuerySchema.shape.search,
376
+ sort: measurementOverviewQuerySchema.shape.sort,
377
+ cursor: measurementOverviewQuerySchema.shape.cursor,
378
+ limit: measurementOverviewQuerySchema.shape.limit
379
+ }).strict().superRefine((input, context) => {
380
+ if (input.scope === "group" && !input.groupKey) {
381
+ context.addIssue({ code: "custom", path: ["groupKey"], message: "Group scope requires groupKey." });
382
+ }
383
+ if (input.scope === "property" && !input.targetKey) {
384
+ context.addIssue({ code: "custom", path: ["targetKey"], message: "Property scope requires targetKey." });
385
+ }
386
+ if (input.scope !== "group" && input.groupKey) {
387
+ context.addIssue({ code: "custom", path: ["groupKey"], message: `${input.scope} scope does not accept groupKey.` });
388
+ }
389
+ if (input.scope !== "property" && input.targetKey) {
390
+ context.addIssue({ code: "custom", path: ["targetKey"], message: `${input.scope} scope does not accept targetKey.` });
391
+ }
392
+ });
393
+ var measurementPropertyEvidenceInputSchema = measurementPropertyEvidenceQuerySchema.extend({
394
+ project: projectNameSchema,
395
+ targetKey: measurementPropertyEvidenceQuerySchema.shape.targetKey.describe("Property stable key. Required \u2014 this read is scoped to exactly one Property."),
396
+ shape: measurementPropertyEvidenceQuerySchema.shape.shape.describe(
397
+ "What one row is. Omit for sources (one row per cited URL). answers gives one row per measured answer with its cited URLs nested, including the answers that cited nothing."
398
+ )
399
+ }).strict();
400
+ var measurementPortfolioSummaryInputSchema = measurementPortfolioSummaryQuerySchema.extend({
401
+ project: projectNameSchema
402
+ }).strict();
403
+ var measurementPropertyQuestionsInputSchema = measurementPropertyQuestionsQuerySchema.extend({
404
+ project: projectNameSchema
405
+ }).strict();
406
+ var measurementQuestionResultInputSchema = measurementQuestionResultQuerySchema.extend({
407
+ project: projectNameSchema
408
+ }).strict();
409
+ var measurementPropertyCompetitorsInputSchema = measurementPropertyCompetitorsQuerySchema.extend({
410
+ project: projectNameSchema
411
+ }).strict();
412
+ var measurementChangesInputSchema = measurementChangesQuerySchema.extend({
413
+ project: projectNameSchema
414
+ }).strict();
415
+ var measurementDataQualityInputSchema = measurementDataQualityQuerySchema.extend({
416
+ project: projectNameSchema
417
+ }).strict();
418
+ var measurementDraftCollectionInputSchema = measurementDraftCollectionQuerySchema.extend({ project: projectNameSchema });
419
+ var measurementQuerySetInputSchema = z3.object({
420
+ project: projectNameSchema,
421
+ setId: z3.string().trim().min(1)
422
+ }).strict();
423
+ var measurementQuerySetUpsertInputSchema = measurementQuerySetInputSchema.extend({
424
+ request: measurementQuerySetUpsertRequestSchema
425
+ }).strict();
426
+ var measurementQueryTemplateInputSchema = z3.object({
427
+ project: projectNameSchema,
428
+ templateId: z3.string().trim().min(1)
429
+ }).strict();
430
+ var measurementQueryTemplateUpsertInputSchema = measurementQueryTemplateInputSchema.extend({
431
+ request: measurementQueryTemplateUpsertRequestSchema
432
+ }).strict();
433
+ var measurementQueryTemplateApplyInputSchema = measurementQueryTemplateInputSchema.extend({
434
+ request: measurementQueryTemplateApplyRequestSchema,
435
+ idempotencyKey: idempotencyKeyInputSchema
436
+ }).strict();
437
+ var measurementPlanDeactivateInputSchema = measurementPlanDeactivateRequestSchema.extend({
438
+ project: projectNameSchema,
439
+ idempotencyKey: idempotencyKeyInputSchema
440
+ }).strict();
441
+ var measurementDraftActionInputSchema = z3.object({
442
+ project: projectNameSchema,
443
+ operation: measurementDraftOperationSchema.describe("Typed draft operation. Select exactly one action branch.")
444
+ }).strict();
445
+ var measurementDraftActionOpenApiOperations = [
446
+ "POST /api/v1/projects/{name}/measurement-plan/draft/actions/create",
447
+ "POST /api/v1/projects/{name}/measurement-plan/draft/actions/import-sitemap",
448
+ "POST /api/v1/projects/{name}/measurement-plan/draft/actions/apply-sitemap-selection",
449
+ "POST /api/v1/projects/{name}/measurement-plan/draft/actions/upsert-target",
450
+ "POST /api/v1/projects/{name}/measurement-plan/draft/actions/rename-target",
451
+ "POST /api/v1/projects/{name}/measurement-plan/draft/actions/merge-targets",
452
+ "POST /api/v1/projects/{name}/measurement-plan/draft/actions/exclude-target",
453
+ "POST /api/v1/projects/{name}/measurement-plan/draft/actions/rebind-target",
454
+ "POST /api/v1/projects/{name}/measurement-plan/draft/actions/apply-assignments",
455
+ "POST /api/v1/projects/{name}/measurement-plan/draft/actions/preview-assignments",
456
+ "POST /api/v1/projects/{name}/measurement-plan/draft/actions/replace-assignments",
457
+ "POST /api/v1/projects/{name}/measurement-plan/draft/actions/apply-paired-assignments",
458
+ "POST /api/v1/projects/{name}/measurement-plan/draft/actions/remove-assignment",
459
+ "POST /api/v1/projects/{name}/measurement-plan/draft/actions/clear-assignments",
460
+ "POST /api/v1/projects/{name}/measurement-plan/draft/actions/classify-assignments",
461
+ "POST /api/v1/projects/{name}/measurement-plan/draft/actions/upsert-group",
462
+ "POST /api/v1/projects/{name}/measurement-plan/draft/actions/upsert-market",
463
+ "POST /api/v1/projects/{name}/measurement-plan/draft/actions/remove-group",
464
+ "POST /api/v1/projects/{name}/measurement-plan/draft/actions/preview-group-membership",
465
+ "POST /api/v1/projects/{name}/measurement-plan/draft/actions/apply-group-membership",
466
+ "POST /api/v1/projects/{name}/measurement-plan/draft/actions/upsert-competitor",
467
+ "POST /api/v1/projects/{name}/measurement-plan/draft/actions/remove-competitor",
468
+ "POST /api/v1/projects/{name}/measurement-plan/draft/actions/compile-preview",
469
+ "POST /api/v1/projects/{name}/measurement-plan/draft/actions/diff-preview",
470
+ "POST /api/v1/projects/{name}/measurement-plan/draft/actions/publish",
471
+ "POST /api/v1/projects/{name}/measurement-plan/draft/actions/discard"
472
+ ];
473
+ var runsListInputSchema = z3.object({
474
+ project: projectNameSchema,
475
+ limit: z3.number().int().positive().max(500).optional(),
476
+ kind: runListFilterQuerySchema.shape.kind.describe('Restrict to a single run kind (e.g. "answer-visibility"). Unknown values are rejected.'),
477
+ status: runListFilterQuerySchema.shape.status.describe('Restrict to a single run status (e.g. "running" for in-flight work, "failed" to triage). Unknown values are rejected.')
478
+ });
479
+ var runGetInputSchema = z3.object({
480
+ runId: runIdSchema
481
+ });
482
+ var runFillInputSchema = z3.object({
483
+ runId: runIdSchema,
484
+ providers: z3.array(z3.string().min(1)).min(1).optional().describe("Fill only these providers. Omit for every provider with missing answers."),
485
+ dryRun: z3.boolean().optional().describe("Evaluate every rule and report what would run, without queueing or spending quota.")
486
+ });
487
+ var timelineInputSchema = z3.object({
488
+ project: projectNameSchema,
489
+ location: z3.string().optional().describe("Location label. Use an empty string for locationless results."),
490
+ limit: z3.number().int().positive().max(100).optional().describe("Restrict history to the most recent N project runs.")
491
+ });
492
+ var historyFilterShape = {
493
+ limit: z3.number().int().positive().max(500).optional(),
494
+ offset: z3.number().int().nonnegative().optional(),
495
+ since: z3.string().optional().describe("ISO 8601 lower bound."),
496
+ action: z3.string().optional().describe("Exact audit action filter."),
497
+ actor: z3.string().optional().describe("Exact actor filter."),
498
+ entityType: z3.string().optional().describe("Exact entity type filter.")
499
+ };
500
+ var projectHistoryInputSchema = z3.object({ project: projectNameSchema, ...historyFilterShape });
501
+ var globalHistoryInputSchema = z3.object(historyFilterShape);
502
+ var snapshotsListInputSchema = z3.object({
503
+ project: projectNameSchema,
504
+ limit: z3.number().int().positive().max(500).optional(),
505
+ offset: z3.number().int().nonnegative().optional(),
506
+ location: z3.string().optional().describe("Location label. Use an empty string for locationless results.")
507
+ });
508
+ var snapshotsDiffInputSchema = z3.object({
509
+ project: projectNameSchema,
510
+ run1: runIdSchema,
511
+ run2: runIdSchema
512
+ });
513
+ var insightsListInputSchema = z3.object({
514
+ project: projectNameSchema,
515
+ dismissed: z3.boolean().optional(),
516
+ runId: runIdSchema.optional()
517
+ });
518
+ var insightInputSchema = z3.object({
519
+ project: projectNameSchema,
520
+ insightId: insightIdSchema
521
+ });
522
+ var healthHistoryInputSchema = z3.object({
523
+ project: projectNameSchema,
524
+ limit: z3.number().int().positive().max(100).optional()
525
+ });
526
+ var gscPerformanceInputSchema = z3.object({
527
+ project: projectNameSchema,
528
+ startDate: z3.string().optional(),
529
+ endDate: z3.string().optional(),
530
+ query: z3.string().optional(),
531
+ page: z3.string().optional(),
532
+ limit: z3.number().int().positive().max(5e3).optional(),
533
+ offset: z3.number().int().nonnegative().optional(),
534
+ orderBy: gscPerformanceOrderBySchema.optional(),
535
+ window: analyticsWindowSchema.optional()
536
+ });
537
+ var gscPerformanceDailyInputSchema = z3.object({
538
+ project: projectNameSchema,
539
+ startDate: z3.string().optional(),
540
+ endDate: z3.string().optional(),
541
+ window: analyticsWindowSchema.optional()
542
+ });
543
+ var gscTopPagesInputSchema = z3.object({
544
+ project: projectNameSchema,
545
+ startDate: z3.string().optional(),
546
+ endDate: z3.string().optional(),
547
+ limit: z3.number().int().positive().max(500).optional(),
548
+ window: analyticsWindowSchema.optional()
549
+ });
550
+ var gscInspectionsInputSchema = z3.object({
551
+ project: projectNameSchema,
552
+ url: z3.string().optional(),
553
+ limit: z3.number().int().positive().max(500).optional()
554
+ });
555
+ var gscCoverageHistoryInputSchema = z3.object({
556
+ project: projectNameSchema,
557
+ limit: z3.number().int().positive().max(500).optional()
558
+ });
559
+ var gscSitemapsInputSchema = z3.object({
560
+ project: projectNameSchema,
561
+ sitemapIndex: z3.string().url().optional()
562
+ });
563
+ var gscSitemapsSubmitInputSchema = z3.union([
564
+ z3.object({
565
+ project: projectNameSchema,
566
+ sitemapUrls: z3.array(z3.string().url()).min(1).max(50)
567
+ }).strict(),
568
+ z3.object({
569
+ project: projectNameSchema,
570
+ mode: z3.enum(["indexes", "all-files"])
571
+ }).strict()
572
+ ]);
573
+ async function submitGscSitemapsFromMcp(client, input) {
574
+ let sitemapUrls;
575
+ if ("sitemapUrls" in input) {
576
+ sitemapUrls = uniqueStrings(input.sitemapUrls);
577
+ } else {
578
+ const topLevel = await client.gscSitemaps(input.project);
579
+ if (input.mode === "indexes") {
580
+ sitemapUrls = uniqueStrings(
581
+ topLevel.preferredSubmissionUrls.length > 0 ? topLevel.preferredSubmissionUrls : topLevel.sitemaps.map((sitemap) => sitemap.path)
582
+ );
583
+ } else {
584
+ const indexes = topLevel.sitemaps.filter((sitemap) => sitemap.isSitemapsIndex).map((sitemap) => sitemap.path);
585
+ const expandedIndexUrls = [];
586
+ for (let offset = 0; offset < indexes.length; offset += 4) {
587
+ const children = await Promise.all(
588
+ indexes.slice(offset, offset + 4).map(
589
+ (sitemapIndex) => client.gscSitemaps(input.project, { sitemapIndex })
590
+ )
591
+ );
592
+ children.forEach((result, index) => {
593
+ const sitemapIndex = indexes[offset + index];
594
+ expandedIndexUrls.push(...result.sitemaps.length > 0 ? result.sitemaps.map((sitemap) => sitemap.path) : [sitemapIndex]);
595
+ });
596
+ }
597
+ sitemapUrls = uniqueStrings([
598
+ ...topLevel.sitemaps.filter((sitemap) => !sitemap.isSitemapsIndex).map((sitemap) => sitemap.path),
599
+ ...expandedIndexUrls
600
+ ]);
601
+ }
602
+ }
603
+ if (sitemapUrls.length === 0) {
604
+ throw new Error("No GSC sitemaps found. Submit an explicit sitemap URL first.");
605
+ }
606
+ const aggregate = {
607
+ summary: { total: 0, accepted: 0, failed: 0 },
608
+ results: []
609
+ };
610
+ for (let offset = 0; offset < sitemapUrls.length; offset += 50) {
611
+ const batchSitemapUrls = sitemapUrls.slice(offset, offset + 50);
612
+ try {
613
+ const result = await client.gscSubmitSitemaps(input.project, {
614
+ sitemapUrls: batchSitemapUrls
615
+ });
616
+ aggregate.summary.total += result.summary.total;
617
+ aggregate.summary.accepted += result.summary.accepted;
618
+ aggregate.summary.failed += result.summary.failed;
619
+ aggregate.results.push(...result.results);
620
+ } catch (cause) {
621
+ if (offset === 0) throw cause;
622
+ const attempted = aggregate.summary.total + batchSitemapUrls.length;
623
+ const remaining = sitemapUrls.length - attempted;
624
+ throw new CliError({
625
+ code: "GOOGLE_SITEMAP_SUBMISSION_PARTIAL",
626
+ message: `Sitemap submission stopped after ${aggregate.summary.total} completed results; ${batchSitemapUrls.length} are unconfirmed and ${remaining} were not attempted.`,
627
+ exitCode: cause instanceof CliError && cause.exitCode === 1 ? 1 : EXIT_SYSTEM_ERROR,
628
+ details: {
629
+ project: input.project,
630
+ accepted: aggregate.summary.accepted,
631
+ failed: aggregate.summary.failed,
632
+ completed: aggregate.summary.total,
633
+ attempted,
634
+ unconfirmed: batchSitemapUrls.length,
635
+ remaining,
636
+ unconfirmedBatch: { index: Math.floor(offset / 50) + 1, sitemapUrls: batchSitemapUrls },
637
+ partialResult: aggregate,
638
+ cause: cause instanceof CliError ? { code: cause.code, message: cause.message, details: cause.details } : { message: describeError(cause) }
639
+ }
640
+ });
641
+ }
642
+ }
643
+ return aggregate;
644
+ }
645
+ var gaWindowInputSchema = z3.object({
646
+ project: projectNameSchema,
647
+ window: analyticsWindowSchema.optional(),
648
+ startDate: z3.string().optional(),
649
+ endDate: z3.string().optional()
650
+ });
651
+ var GA_RANGE_PARAMS = ["window", "startDate", "endDate"];
652
+ var gaTrafficInputSchema = gaWindowInputSchema.extend({
653
+ limit: z3.number().int().positive().max(500).optional()
654
+ });
655
+ var gaMeasurementAnalysisInputSchema = z3.object({
656
+ project: projectNameSchema,
657
+ window: gaMeasurementAnalysisWindowSchema.optional(),
658
+ hostScope: gaMeasurementHostScopeSchema.optional(),
659
+ pathPrefix: z3.string().min(1).optional(),
660
+ limit: z3.number().int().positive().max(100).optional()
661
+ });
662
+ var queriesInputSchema = z3.object({
663
+ project: projectNameSchema,
664
+ request: queryBatchRequestSchema
665
+ });
666
+ var queryGenerateInputSchema = z3.object({
667
+ project: projectNameSchema,
668
+ request: queryGenerateRequestSchema
669
+ });
670
+ var gbpListLocationsInputSchema = z3.object({
671
+ project: projectNameSchema,
672
+ selected: z3.boolean().optional()
673
+ });
674
+ var gbpDiscoverInputSchema = z3.object({
675
+ project: projectNameSchema,
676
+ selectAllNew: z3.boolean().optional().default(true),
677
+ accountName: z3.string().regex(/^accounts\//, 'accountName must be a Google resource name like "accounts/12345"').optional(),
678
+ switchAccount: z3.boolean().optional().default(false)
679
+ });
680
+ var gbpLocationSelectionInputSchema = z3.object({
681
+ project: projectNameSchema,
682
+ locationName: z3.string().min(1).regex(/^locations\//, 'locationName must be a Google resource name like "locations/12345"'),
683
+ selected: z3.boolean()
684
+ });
685
+ var gbpSyncInputSchema = z3.object({
686
+ project: projectNameSchema,
687
+ locationNames: z3.array(z3.string()).optional(),
688
+ daysOfMetrics: z3.number().int().positive().max(540).optional(),
689
+ monthsOfKeywords: z3.number().int().positive().max(18).optional()
690
+ });
691
+ var gbpMetricsInputSchema = z3.object({
692
+ project: projectNameSchema,
693
+ locationName: z3.string().optional(),
694
+ metric: z3.string().optional()
695
+ });
696
+ var gbpLocationScopedInputSchema = z3.object({
697
+ project: projectNameSchema,
698
+ locationName: z3.string().optional()
699
+ });
700
+ var gbpAccountsInputSchema = z3.object({
701
+ project: projectNameSchema
702
+ });
703
+ var adsInsightsInputSchema = z3.object({
704
+ project: projectNameSchema,
705
+ level: z3.enum(["campaign", "ad_group"]).optional(),
706
+ entityId: z3.string().optional(),
707
+ from: z3.string().optional(),
708
+ to: z3.string().optional()
709
+ });
710
+ var adsGeoSearchInputSchema = adsGeoSearchQuerySchema.extend({
711
+ project: projectNameSchema
712
+ });
713
+ var adsLiveDeliveryInputSchema = z3.object({
714
+ project: projectNameSchema,
715
+ campaignId: z3.string().min(1).max(200).optional(),
716
+ lookbackDays: z3.number().int().min(1).max(30).optional()
717
+ });
718
+ var adsOperationInputSchema = z3.object({
719
+ project: projectNameSchema,
720
+ operationKey: z3.string().min(8).max(128)
721
+ });
722
+ var adsOperationResumeActivationInputSchema = adsOperationInputSchema.strict();
723
+ var adsUnresolvedOperationsInputSchema = adsUnresolvedOperationListQuerySchema.extend({
724
+ project: projectNameSchema
725
+ });
726
+ var adsOperationReconcileInputSchema = adsOperationReconcileRequestSchema.extend({
727
+ project: projectNameSchema,
728
+ operationKey: z3.string().min(8).max(128)
729
+ });
730
+ var adsImageUploadInputSchema = z3.object({
731
+ project: projectNameSchema,
732
+ request: adsImageUploadRequestSchema
733
+ });
734
+ var adsCampaignCreateInputSchema = z3.object({
735
+ project: projectNameSchema,
736
+ request: adsCampaignCreateRequestSchema
737
+ });
738
+ var adsCampaignUpdateInputSchema = z3.object({
739
+ project: projectNameSchema,
740
+ campaignId: z3.string().min(1),
741
+ request: adsCampaignUpdateRequestSchema
742
+ });
743
+ var adsCampaignActivateTreeInputSchema = z3.object({
744
+ project: projectNameSchema,
745
+ campaignId: z3.string().min(1),
746
+ request: adsActivateTreeRequestSchema
747
+ });
748
+ var adsCampaignPauseInputSchema = z3.object({
749
+ project: projectNameSchema,
750
+ campaignId: z3.string().min(1),
751
+ request: adsPauseRequestSchema
752
+ });
753
+ var adsAdGroupCreateInputSchema = z3.object({
754
+ project: projectNameSchema,
755
+ request: adsAdGroupCreateRequestSchema
756
+ });
757
+ var adsAdGroupUpdateInputSchema = z3.object({
758
+ project: projectNameSchema,
759
+ adGroupId: z3.string().min(1),
760
+ request: adsAdGroupUpdateRequestSchema
761
+ });
762
+ var adsAdGroupPauseInputSchema = z3.object({
763
+ project: projectNameSchema,
764
+ adGroupId: z3.string().min(1),
765
+ request: adsPauseRequestSchema
766
+ });
767
+ var adsAdCreateInputSchema = z3.object({
768
+ project: projectNameSchema,
769
+ request: adsAdCreateRequestSchema
770
+ });
771
+ var adsAdUpdateInputSchema = z3.object({
772
+ project: projectNameSchema,
773
+ adId: z3.string().min(1),
774
+ request: adsAdUpdateRequestSchema
775
+ });
776
+ var adsAdPauseInputSchema = z3.object({
777
+ project: projectNameSchema,
778
+ adId: z3.string().min(1),
779
+ request: adsPauseRequestSchema
780
+ });
781
+ var googleMarketingSnapshotPageInputSchema = z3.object({
782
+ project: projectNameSchema,
783
+ limit: z3.number().int().min(1).max(GOOGLE_MARKETING_STORED_SNAPSHOT_PAGE_MAX).optional(),
784
+ cursor: z3.string().trim().min(1).optional()
785
+ }).strict();
786
+ var googleAdsPerformanceInputSchema = z3.object({
787
+ project: projectNameSchema,
788
+ window: googleAdsMetricsWindowSchema.optional()
789
+ }).strict();
790
+ var googleMarketingSnapshotInputSchema = z3.object({
791
+ project: projectNameSchema,
792
+ snapshotId: z3.string().trim().min(1)
793
+ }).strict();
794
+ var gtmAccountInputSchema = z3.object({
795
+ project: projectNameSchema,
796
+ accountId: z3.string().trim().min(1)
797
+ }).strict().superRefine((input, context) => {
798
+ if (!canonicalizeGtmAccountId(input.accountId)) {
799
+ context.addIssue({
800
+ code: z3.ZodIssueCode.custom,
801
+ path: ["accountId"],
802
+ message: "Expected a safe GTM account ID or accounts/{id} resource path."
803
+ });
804
+ }
805
+ });
806
+ var gtmContainerInputSchema = z3.object({
807
+ project: projectNameSchema,
808
+ accountId: z3.string().trim().min(1),
809
+ containerId: z3.string().trim().min(1)
810
+ }).strict().superRefine((input, context) => {
811
+ if (!canonicalizeGtmResourceSelection(input)) {
812
+ context.addIssue({
813
+ code: z3.ZodIssueCode.custom,
814
+ path: ["containerId"],
815
+ message: "Expected matching safe GTM account/container IDs or resource paths."
816
+ });
817
+ }
818
+ });
819
+ function canonicalGtmMcpAccountId(accountId) {
820
+ const canonical = canonicalizeGtmAccountId(accountId);
821
+ if (!canonical) throw new Error("Invalid GTM account input.");
822
+ return canonical;
823
+ }
824
+ function canonicalGtmMcpSelection(accountId, containerId) {
825
+ const canonical = canonicalizeGtmResourceSelection({ accountId, containerId });
826
+ if (!canonical) throw new Error("Invalid GTM account/container input.");
827
+ return canonical;
828
+ }
829
+ var conversionTrackingContractInputSchema = z3.object({
830
+ project: projectNameSchema,
831
+ contractId: z3.string().trim().min(1)
832
+ }).strict();
833
+ var keywordsInputSchema = z3.object({
834
+ project: projectNameSchema,
835
+ request: keywordBatchRequestSchema
836
+ });
837
+ var keywordGenerateInputSchema = z3.object({
838
+ project: projectNameSchema,
839
+ request: keywordGenerateRequestSchema
840
+ });
841
+ var competitorsInputSchema = z3.object({
842
+ project: projectNameSchema,
843
+ request: competitorBatchRequestSchema
844
+ });
845
+ var competitorLandscapeInputSchema = competitorLandscapeQuerySchema.safeExtend({
846
+ project: projectNameSchema
847
+ }).strict();
848
+ var projectUpsertInputSchema = z3.object({
849
+ project: projectNameSchema,
850
+ request: projectUpsertRequestSchema
851
+ });
852
+ var applyConfigInputSchema = z3.object({
853
+ config: projectConfigSchema
854
+ });
855
+ var scheduleSetInputSchema = z3.object({
856
+ project: projectNameSchema,
857
+ schedule: scheduleUpsertRequestSchema
858
+ });
859
+ var scheduleReadInputSchema = z3.object({
860
+ project: projectNameSchema,
861
+ kind: schedulableRunKindSchema.optional().describe('Schedulable run kind. Defaults to "answer-visibility" if omitted.')
862
+ });
863
+ var scheduleListOutputSchema = z3.object({ items: z3.array(scheduleDtoSchema) });
864
+ var notificationEventsOutputSchema = z3.object({ items: z3.array(notificationEventSchema) });
865
+ var agentWebhookAttachInputSchema = z3.object({
866
+ project: projectNameSchema,
867
+ url: z3.string().url()
868
+ });
869
+ var doctorInputSchema = z3.object({
870
+ project: projectNameSchema.optional().describe("Project name to scope project-level checks. Omit to run global checks (provider keys, config, etc.)."),
871
+ checks: z3.array(z3.string().min(1)).optional().describe('Optional check IDs or wildcard prefixes (e.g. "google.auth.*", "config.providers"). Empty/omitted runs all matching checks for the chosen scope.')
872
+ });
873
+ var contentTargetsInputSchema = z3.object({
874
+ project: projectNameSchema,
875
+ limit: z3.number().int().positive().max(500).optional().describe("Max rows. Defaults to all. Use a small number (3-10) when summarizing for the user."),
876
+ includeInProgress: z3.boolean().optional().describe("Include rows that already have an in-flight tracked action. Default false."),
877
+ winnabilityClass: z3.enum(["ownable", "ceded"]).optional().describe('Filter by winnability: "ownable" (worth a brief) or "ceded" (aggregator/editorial head term to skip).'),
878
+ ownable: z3.boolean().optional().describe('Convenience: when true, return only ownable targets (same as winnabilityClass="ownable").')
879
+ });
880
+ var contentBriefInputSchema = z3.object({
881
+ project: projectNameSchema,
882
+ targetRef: z3.string().min(1).describe("Stable target ref from canonry_content_targets. The target must be ownable; ceded targets are rejected."),
883
+ provider: z3.string().optional().describe("Optional provider override (claude|openai|gemini|zai|deepinfra)."),
884
+ model: z3.string().optional().describe("Optional model override within the chosen provider."),
885
+ forceRefresh: z3.boolean().optional().describe("Force a fresh synthesis even if a cached brief exists.")
886
+ });
887
+ var contentMapInputSchema = z3.object({
888
+ project: projectNameSchema
889
+ });
890
+ var backlinksDomainsInputSchema = z3.object({
891
+ project: projectNameSchema,
892
+ limit: z3.number().int().positive().max(200).optional().describe("Max linking-domain rows. Default 50, max 200."),
893
+ release: z3.string().optional().describe("Common Crawl release id, e.g. cc-main-2026-jan-feb-mar. Omit for the most recent release with data."),
894
+ source: backlinkSourceSchema.optional().describe("Stored source. Common Crawl is active; bing-webmaster is historical-only.")
895
+ });
896
+ var backlinksSourcesInputSchema = z3.object({
897
+ project: projectNameSchema
898
+ });
899
+ var memoryUpsertInputSchema = z3.object({
900
+ project: projectNameSchema,
901
+ key: z3.string().min(1).max(AGENT_MEMORY_KEY_MAX_LENGTH).describe(`Stable identifier for the note (max ${AGENT_MEMORY_KEY_MAX_LENGTH} chars). Writing the same key overwrites the prior value.`),
902
+ value: z3.string().min(1).describe(`Plain-text note body (max ${AGENT_MEMORY_VALUE_MAX_BYTES} bytes). Use for durable operator preferences, migration context, or non-obvious reasoning that should survive future sessions.`)
903
+ });
904
+ var memoryForgetInputSchema = z3.object({
905
+ project: projectNameSchema,
906
+ key: z3.string().min(1).max(AGENT_MEMORY_KEY_MAX_LENGTH).describe("Exact key of the note to remove. No-op (status=missing) when no note exists for that key.")
907
+ });
908
+ var trafficConnectCloudRunInputSchema = z3.object({
909
+ project: projectNameSchema,
910
+ request: trafficConnectCloudRunRequestSchema
911
+ });
912
+ var trafficConnectWordpressInputSchema = z3.object({
913
+ project: projectNameSchema,
914
+ request: trafficConnectWordpressRequestSchema
915
+ });
916
+ var trafficConnectVercelInputSchema = z3.object({
917
+ project: projectNameSchema,
918
+ request: trafficConnectVercelRequestSchema
919
+ });
920
+ var trafficSyncInputSchema = z3.object({
921
+ project: projectNameSchema,
922
+ sourceId: z3.string().min(1).describe("Traffic source ID returned by canonry_traffic_connect_cloud_run or canonry_traffic_sources_list."),
923
+ sinceMinutes: z3.number().int().positive().max(365 * 24 * 60).optional().describe("Optional lookback in minutes. Defaults are adapter-specific and clamp forward to lastSyncedAt; a new or idle WordPress source uses 365d to cover the plugin\u2019s maximum configurable retention.")
924
+ });
925
+ var trafficBackfillInputSchema = z3.object({
926
+ project: projectNameSchema,
927
+ sourceId: z3.string().min(1).describe("Traffic source ID returned by canonry_traffic_sources_list."),
928
+ days: z3.number().int().positive().max(90).optional().describe("Lookback window in days. Default 30, capped by the adapter at 90d. Generic WordPress replace backfill is unavailable because retained coverage is unproven.")
929
+ });
930
+ var trafficResetInputSchema = z3.object({
931
+ project: projectNameSchema,
932
+ sourceId: z3.string().min(1).describe("Traffic source ID returned by canonry_traffic_sources_list."),
933
+ advanceToNow: z3.literal(true).describe("Must be `true`. Explicit gate against accidental resets. Advances lastSyncedAt to NOW and clears the source's error state; WordPress also clears its continuation state and records an unrecovered span that needs retention-aware repair.")
934
+ });
935
+ var trafficEventsInputSchema = z3.object({
936
+ project: projectNameSchema,
937
+ since: z3.string().optional().describe("ISO 8601 lower bound. Defaults to 24h ago when omitted."),
938
+ until: z3.string().optional().describe("ISO 8601 upper bound. Defaults to now when omitted."),
939
+ kind: z3.union([trafficEventKindSchema, z3.literal("all")]).optional().describe('Filter to one traffic kind; "all" (default) returns every kind.'),
940
+ sourceId: z3.string().min(1).optional().describe("Restrict to a single traffic source ID."),
941
+ limit: z3.number().int().positive().max(5e3).optional().describe("Max combined rows. Defaults to 500, max 5000. Totals always reflect the full window."),
942
+ granularity: trafficSeriesGranularitySchema.optional().describe("Full-window chart series bucket size: hour (default) or day.")
943
+ });
944
+ var trafficSourceIdInputSchema = z3.object({
945
+ project: projectNameSchema,
946
+ sourceId: z3.string().min(1).describe("Traffic source ID.")
947
+ });
948
+ var discoveryRunInputSchema = z3.object({
949
+ project: projectNameSchema,
950
+ request: discoveryRunRequestSchema.extend({
951
+ // Stronger descriptions for the LLM. The base Zod schema enforces the
952
+ // upper bound; this just clarifies the meaning of each knob.
953
+ icpDescription: z3.string().min(1).optional().describe("Free-text ICP description. If omitted, the project must already have spec.icpDescription stored."),
954
+ buyerDescription: z3.string().min(1).optional().describe("Who evaluates or buys the offering, separate from the ICP. When present, every generated query is anchored on this buyer."),
955
+ seedProviders: z3.array(z3.enum(["gemini", "openai"])).min(1).optional().describe('Which providers generate seed candidates. Omit for the Gemini-only default; ["gemini","openai"] merges both phrasing distributions before dedup.'),
956
+ dedupThreshold: z3.number().min(0).max(1).optional().describe("Cosine similarity threshold for clustering seed candidates. Defaults to 0.85. Lower values dedupe more aggressively."),
957
+ maxProbes: z3.number().int().positive().max(DISCOVERY_MAX_PROBES_CAP).optional().describe(`Max canonical queries to probe in this session. Default 100, hard cap ${DISCOVERY_MAX_PROBES_CAP}.`),
958
+ probeConcurrency: z3.number().int().min(1).max(DISCOVERY_PROBE_CONCURRENCY_CAP).optional().describe(`How many probes may run in parallel. Default 1 (strictly serial), hard cap ${DISCOVERY_PROBE_CONCURRENCY_CAP}. Probe rows are persisted in canonical order regardless of concurrency, so this only shortens wall-clock time.`)
959
+ }).optional()
960
+ });
961
+ var discoverySessionsListInputSchema = z3.object({
962
+ project: projectNameSchema,
963
+ limit: z3.number().int().positive().max(200).optional().describe("Max sessions returned. Default 50.")
964
+ });
965
+ var discoverySessionIdInputSchema = z3.object({
966
+ project: projectNameSchema,
967
+ sessionId: z3.string().min(1).describe("Discovery session ID returned by canonry_discover_run_start.")
968
+ });
969
+ var researchRunStartInputSchema = z3.object({
970
+ project: projectNameSchema,
971
+ request: researchRunCreateSchema.describe("One shared provider/model/location context for every final free-form query in this saved research batch. An optional market or Property destination is saved with the batch. Template provenance records a template ID and version only; it never expands or alters queries.")
972
+ });
973
+ var researchRunsListInputSchema = z3.object({
974
+ project: projectNameSchema,
975
+ limit: z3.number().int().positive().max(100).optional().describe("Max saved research runs returned. Default 20."),
976
+ cursor: z3.string().min(1).optional().describe("Opaque cursor from the previous page.")
977
+ });
978
+ var researchBatchStartInputSchema = z3.object({
979
+ project: projectNameSchema,
980
+ request: researchBatchCreateSchema.describe("Reviewed concrete runs, at most 20 destinations and 50 total queries. Specify provider, model and location (null means none) for every run. Pin the plan revision for each market or Property. Reuse the same idempotencyKey and unchanged request after an uncertain response.")
981
+ });
982
+ var researchRunIdInputSchema = z3.object({
983
+ project: projectNameSchema,
984
+ runId: z3.string().min(1).describe("Research run ID returned by canonry_research_run_start.")
985
+ });
986
+ var discoveryHarvestInputSchema = z3.object({
987
+ project: projectNameSchema,
988
+ sessionId: z3.string().min(1).describe("Discovery session ID returned by canonry_discover_run_start."),
989
+ minProbeHits: z3.number().int().positive().optional().describe("Recurrence floor \u2014 a candidate must have appeared in at least this many distinct probes to be admitted. Default 1."),
990
+ anchor: z3.boolean().optional().describe("Apply the subject-anchor filter that drops off-topic acronym collisions. Default true; pass false for new-subject discovery on a well-scoped project.")
991
+ });
992
+ var discoveryPromoteInputSchema = z3.object({
993
+ project: projectNameSchema,
994
+ sessionId: z3.string().min(1).describe("Discovery session ID returned by canonry_discover_run_start."),
995
+ request: discoveryPromoteRequestSchema.extend({
996
+ // Stronger descriptions for the LLM. The base Zod schema enforces the shape.
997
+ buckets: z3.array(discoveryBucketSchema).min(1).optional().describe("Which probe buckets to adopt into the tracked basket. Omitted promotes cited + aspirational; include wasted-surface explicitly for off-ICP competitor gaps."),
998
+ includeCompetitors: z3.boolean().optional().describe("Whether to also merge recurring discovered competitor domains into the project. Defaults to true."),
999
+ competitorTypes: z3.array(discoveryCompetitorTypeSchema).min(1).optional().describe("Which classified competitor types to merge. Omitted promotes direct-competitor only; pass an explicit list to also adopt editorial-media channels or to recover legacy unknown entries. Ignored when includeCompetitors is false.")
1000
+ }).optional()
1001
+ });
1002
+ var technicalAeoScoreInputSchema = z3.object({
1003
+ project: projectNameSchema,
1004
+ runId: runIdSchema.optional().describe("Historical site-audit run ID. Omit for the latest audit.")
1005
+ });
1006
+ var technicalAeoPagesInputSchema = z3.object({
1007
+ project: projectNameSchema,
1008
+ runId: runIdSchema.optional().describe("Historical site-audit run ID. Omit for the latest audit."),
1009
+ status: z3.enum(["success", "error"]).optional().describe("Filter to successfully-audited or errored pages."),
1010
+ sort: z3.enum(["score-asc", "score-desc", "url"]).optional().describe("Sort order. Defaults to score-asc (worst pages first)."),
1011
+ limit: z3.number().int().positive().max(500).optional(),
1012
+ offset: z3.number().int().nonnegative().optional()
1013
+ });
1014
+ var technicalAeoTrendInputSchema = z3.object({
1015
+ project: projectNameSchema,
1016
+ limit: z3.number().int().positive().max(365).optional()
1017
+ });
1018
+ var technicalAeoCrawlInputSchema = z3.object({
1019
+ project: projectNameSchema,
1020
+ runId: runIdSchema.optional().describe("Historical crawl-bearing site-audit run ID. Omit for the latest persisted crawl.")
1021
+ });
1022
+ var siteHealthPageAuditInputSchema = z3.object({
1023
+ project: projectNameSchema,
1024
+ runId: runIdSchema.optional().describe("Historical crawl-bearing site-audit run ID. Omit for the latest persisted crawl."),
1025
+ nodeKey: z3.string().min(1).optional().describe("Exact crawl node key, as returned by Site Health page or subgraph reads."),
1026
+ url: z3.string().url().optional().describe("Exact page URL. Use this only when a crawl node key is unavailable.")
1027
+ }).refine((value) => Boolean(value.nodeKey || value.url), {
1028
+ message: "Provide nodeKey or url.",
1029
+ path: ["nodeKey"]
1030
+ }).refine((value) => !(value.nodeKey && value.url), {
1031
+ message: "Provide nodeKey or url, not both.",
1032
+ path: ["nodeKey"]
1033
+ });
1034
+ var SITE_HEALTH_MCP_MAX_NODES = 25;
1035
+ var SITE_HEALTH_MCP_MAX_EDGES = 50;
1036
+ var siteHealthSubgraphInputSchema = z3.object({
1037
+ project: projectNameSchema,
1038
+ runId: runIdSchema.optional().describe("Historical crawl-bearing site-audit run ID. Omit for the latest complete crawl."),
1039
+ nodeKey: z3.string().min(1).optional().describe("Focus crawl node key. Omit with url to focus the crawl root."),
1040
+ url: z3.string().url().optional().describe("Focus canonical URL. Omit with nodeKey to focus the crawl root."),
1041
+ hops: z3.number().int().min(0).max(3).optional().describe("Neighborhood depth from the focus node. Keep this small."),
1042
+ maxNodes: z3.number().int().positive().max(SITE_HEALTH_MCP_MAX_NODES).default(SITE_HEALTH_MCP_MAX_NODES).describe("Hard MCP cap: at most 25 nodes. Narrow or refocus instead of loading the site."),
1043
+ maxEdges: z3.number().int().positive().max(SITE_HEALTH_MCP_MAX_EDGES).default(SITE_HEALTH_MCP_MAX_EDGES).describe("Hard MCP cap: at most 50 edges. Narrow or refocus instead of loading the site.")
1044
+ }).refine((value) => !(value.nodeKey && value.url), {
1045
+ message: "Provide nodeKey or url, not both.",
1046
+ path: ["nodeKey"]
1047
+ });
1048
+ var siteHealthPathInputSchema = z3.object({
1049
+ project: projectNameSchema,
1050
+ runId: runIdSchema.optional().describe("Historical crawl-bearing site-audit run ID. Omit for the latest complete crawl."),
1051
+ fromNodeKey: z3.string().min(1).optional().describe("Origin node key. Omit with fromUrl to start at the crawl root."),
1052
+ fromUrl: z3.string().url().optional().describe("Origin URL. Omit with fromNodeKey to start at the crawl root."),
1053
+ toNodeKey: z3.string().min(1).optional().describe("Required destination node key."),
1054
+ toUrl: z3.string().url().optional().describe("Required destination URL."),
1055
+ maxDepth: z3.number().int().positive().max(24).optional().describe("Maximum directed-link depth to search.")
1056
+ }).refine((value) => !(value.fromNodeKey && value.fromUrl), {
1057
+ message: "Provide fromNodeKey or fromUrl, not both.",
1058
+ path: ["fromNodeKey"]
1059
+ }).refine((value) => Boolean(value.toNodeKey || value.toUrl), {
1060
+ message: "Provide toNodeKey or toUrl.",
1061
+ path: ["toNodeKey"]
1062
+ }).refine((value) => !(value.toNodeKey && value.toUrl), {
1063
+ message: "Provide toNodeKey or toUrl, not both.",
1064
+ path: ["toNodeKey"]
1065
+ });
1066
+ var siteHealthChangesInputSchema = z3.object({
1067
+ project: projectNameSchema,
1068
+ fromRunId: runIdSchema.optional().describe("Earlier complete crawl run ID. Omit to compare the previous complete crawl."),
1069
+ toRunId: runIdSchema.optional().describe("Later complete crawl run ID. Omit to compare the latest complete crawl."),
1070
+ scope: z3.enum(["all", "pages", "links"]).optional().describe("Limit the diff to page or link changes. Omit or use all for both."),
1071
+ change: z3.enum(["all", "added", "removed", "changed"]).optional().describe("Limit the diff to one change kind. Omit or use all for every kind."),
1072
+ cursor: z3.string().min(1).optional().describe("Opaque cursor from the previous Site Health changes result."),
1073
+ limit: z3.number().int().positive().max(25).default(25).describe("Hard MCP cap: 25 records, because each change carries before and after DTOs.")
1074
+ });
1075
+ var technicalAeoCrawlPagesInputSchema = z3.object({
1076
+ project: projectNameSchema,
1077
+ runId: runIdSchema.optional(),
1078
+ inventoryEligible: z3.boolean().optional().describe("Filter Canonry technical-inventory eligibility. This is not actual Google index coverage."),
1079
+ fetchState: z3.string().min(1).optional().describe("Filter crawler fetch state, for example html, redirect, non-html, or fetch-error."),
1080
+ indexabilityState: z3.string().min(1).optional().describe("Filter crawler-derived indexability state. This is not Google index coverage."),
1081
+ auditState: z3.string().min(1).optional().describe("Filter audit state."),
1082
+ sort: z3.enum(["url", "path", "score-asc", "score-desc"]).optional(),
1083
+ cursor: z3.string().min(1).optional().describe("Opaque cursor from the previous crawl-pages result."),
1084
+ limit: z3.number().int().positive().max(200).optional()
1085
+ });
1086
+ var technicalAeoStructureInputSchema = z3.object({
1087
+ project: projectNameSchema,
1088
+ runId: runIdSchema.optional(),
1089
+ parentPath: z3.string().min(1).optional().describe("Path whose immediate children to list. Defaults to /. This never returns a whole site tree."),
1090
+ cursor: z3.string().min(1).optional().describe("Opaque cursor from the previous structure result."),
1091
+ limit: z3.number().int().positive().max(100).optional()
1092
+ });
1093
+ var linkKindSchema = z3.enum(["all", "content", "template"]).optional().describe(
1094
+ "Restrict to content links (excludes nav, header, and footer links) or to template links only. Defaults to all. Check templateDetection before reading an empty content list as a real zero, and before comparing a count with an older scan: it says whether the split came from where each link sits in the page or from how many pages repeat it."
1095
+ );
1096
+ var technicalAeoInternalLinksInputSchema = z3.object({
1097
+ project: projectNameSchema,
1098
+ runId: runIdSchema.optional(),
1099
+ sourceUrl: z3.string().url().optional(),
1100
+ targetUrl: z3.string().url().optional(),
1101
+ followable: z3.boolean().optional(),
1102
+ linkKind: linkKindSchema,
1103
+ cursor: z3.string().min(1).optional().describe("Opaque cursor from the previous internal-links result."),
1104
+ limit: z3.number().int().positive().max(200).optional()
1105
+ });
1106
+ var technicalAeoLinkNeighborsInputSchema = z3.object({
1107
+ project: projectNameSchema,
1108
+ runId: runIdSchema.optional(),
1109
+ nodeKey: z3.string().min(1).optional(),
1110
+ url: z3.string().url().optional(),
1111
+ linkKind: linkKindSchema,
1112
+ limit: z3.number().int().positive().max(100).optional()
1113
+ }).refine((value) => Boolean(value.nodeKey || value.url), {
1114
+ message: "Provide nodeKey or url.",
1115
+ path: ["nodeKey"]
1116
+ });
1117
+ var technicalAeoDeadLinksInputSchema = z3.object({
1118
+ project: projectNameSchema,
1119
+ runId: runIdSchema.optional(),
1120
+ cursor: z3.string().min(1).optional().describe("Opaque cursor from the previous dead-links result."),
1121
+ limit: z3.number().int().positive().max(200).optional()
1122
+ });
1123
+ var technicalAeoRunInputSchema = z3.object({
1124
+ project: projectNameSchema,
1125
+ sitemapUrl: z3.string().url().optional().describe("Override the sitemap URL. Defaults to https://<canonicalDomain>/sitemap.xml."),
1126
+ limit: z3.number().int().positive().max(2e3).optional().describe("Deprecated compatibility alias for maxPages."),
1127
+ maxPages: z3.number().int().positive().max(5e4).optional().describe("Maximum pages crawled and audited. Defaults to 1,000; hard maximum 50,000."),
1128
+ maxEdges: z3.number().int().positive().max(1e6).optional().describe("Maximum link observations retained for this crawl. When omitted the crawl engine derives the budget from the page count; hard maximum 1,000,000."),
1129
+ maxDepth: z3.number().int().min(0).max(100).optional().describe("Maximum internal-link depth from the root page."),
1130
+ checkDeadLinks: z3.boolean().optional().describe("Opt in to internal dead-link checks. Omitted and false both disable checks.")
1131
+ });
1132
+ var AGENT_WEBHOOK_EVENTS = [
1133
+ notificationEventSchema.enum["run.completed"],
1134
+ notificationEventSchema.enum["insight.critical"],
1135
+ notificationEventSchema.enum["insight.high"],
1136
+ notificationEventSchema.enum["citation.gained"]
1137
+ ];
1138
+ var canonryMcpTools = [
1139
+ defineTool({
1140
+ name: "canonry_visibility_report",
1141
+ title: "Read scoped AI visibility",
1142
+ description: "Read stored visibility for a site, group, market or property. Branded, non-brand and unclassified answers remain separate populations. The response owns rates, trends, query performance, answers and competitors under one frozen measured definition. Each population carries `comparison`, the change versus the previous eligible whole-project sweep when both are complete and comparable; it is absent when that sweep cannot be read. Material plan changes retain the prior measured revision; pending assignments are explicit. Search filters the query list only. Reuse cursors with identical selection. Never starts a sweep.",
1143
+ access: "read",
1144
+ tier: "monitoring",
1145
+ inputSchema: visibilityReportRequestSchema.safeExtend({ project: projectNameSchema }),
1146
+ annotations: readAnnotations(),
1147
+ openApiOperations: ["GET /api/v1/projects/{name}/visibility-report"],
1148
+ handler: (client, input) => {
1149
+ const { project, ...selection } = input;
1150
+ return client.getVisibilityReport(project, selection);
1151
+ }
1152
+ }),
1153
+ defineTool({
1154
+ name: "canonry_query_tracking_workspace",
1155
+ title: "Read query assignments",
1156
+ description: "Read tracked queries, exact assignments, saved research sources and the current workspace version for a simple site or advanced portfolio. Also returns scopeOptions: server-built project, group, market and property choices with distinct property counts.",
1157
+ access: "read",
1158
+ tier: "setup",
1159
+ inputSchema: projectInputSchema,
1160
+ annotations: readAnnotations(),
1161
+ openApiOperations: ["GET /api/v1/projects/{name}/query-tracking"],
1162
+ handler: (client, input) => client.getQueryTrackingWorkspace(input.project)
1163
+ }),
1164
+ defineTool({
1165
+ name: "canonry_query_tracking_preview",
1166
+ title: "Preview query assignments",
1167
+ description: "Preview manual, template or saved research additions and assignment removals against the exact workspace version. Returns a review token, deduplicated change and next-sweep workload. Does not publish or start provider work. This POST requires write access.",
1168
+ access: "write",
1169
+ tier: "setup",
1170
+ inputSchema: z3.object({ project: projectNameSchema, request: queryTrackingPreviewRequestSchema }).strict(),
1171
+ annotations: readAnnotations(),
1172
+ openApiOperations: ["POST /api/v1/projects/{name}/query-tracking/preview"],
1173
+ handler: (client, input) => client.previewQueryTracking(input.project, input.request)
1174
+ }),
1175
+ defineTool({
1176
+ name: "canonry_query_tracking_commit",
1177
+ title: "Publish reviewed query assignments",
1178
+ description: "Commit the exact reviewed mutation using its workspace version and preview token. No-op changes do not publish a revision. A successful publication starts zero provider calls; new assignments await the next project-wide sweep. Use the returned revision rather than predicting one.",
1179
+ access: "write",
1180
+ tier: "setup",
1181
+ inputSchema: z3.object({ project: projectNameSchema, request: queryTrackingCommitRequestSchema }).strict(),
1182
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
1183
+ openApiOperations: ["POST /api/v1/projects/{name}/query-tracking/commit"],
1184
+ handler: (client, input) => client.commitQueryTracking(input.project, input.request)
1185
+ }),
1186
+ defineTool({
1187
+ name: "canonry_projects_list",
1188
+ title: "List Canonry projects",
1189
+ description: "List all Canonry projects available through the configured API.",
1190
+ access: "read",
1191
+ tier: "core",
1192
+ inputSchema: emptyInputSchema,
1193
+ annotations: readAnnotations(),
1194
+ openApiOperations: ["GET /api/v1/projects"],
1195
+ handler: (client) => client.listProjects()
1196
+ }),
1197
+ defineTool({
1198
+ name: "canonry_project_get",
1199
+ title: "Get project",
1200
+ description: "Get a Canonry project by name.",
1201
+ access: "read",
1202
+ tier: "core",
1203
+ inputSchema: projectInputSchema,
1204
+ annotations: readAnnotations(),
1205
+ openApiOperations: ["GET /api/v1/projects/{name}"],
1206
+ handler: (client, input) => client.getProject(input.project)
1207
+ }),
1208
+ defineTool({
1209
+ name: "canonry_project_delete_preview",
1210
+ title: "Preview project delete impact",
1211
+ description: "Returns the cascade impact of deleting a project \u2014 how many queries, competitors, runs, snapshots, and insights would be removed, plus how many audit_log rows would be detached (project_id set NULL). Read-only. Use this BEFORE invoking project delete on any project you didn't create yourself; the underlying delete is irreversible.",
1212
+ access: "read",
1213
+ tier: "setup",
1214
+ inputSchema: projectInputSchema,
1215
+ annotations: readAnnotations(),
1216
+ openApiOperations: ["GET /api/v1/projects/{name}/delete-preview"],
1217
+ handler: (client, input) => client.previewProjectDelete(input.project)
1218
+ }),
1219
+ defineTool({
1220
+ name: "canonry_project_overview",
1221
+ title: "Get project overview (composite)",
1222
+ description: 'One-call summary for "how is project X doing?". Returns independent mention and citation coverage, separate query-level movement for each signal, query-basket comparability with added/removed counts, latest run and health, insights, provider/model breakdowns, competitors, attention items, and recent history. Movement excludes queries not shared by both sweeps. Filterable by location and time window. Prefer this over fanning out to separate read tools for the same question. SCOPE LIMIT: this returns nothing about Advanced Measurement: no plan, Targets, Properties, groups, or segments. Its silence is not evidence that a project lacks them. Before stating that a project has no Properties, no Advanced plan, or cannot break out per-Property performance, call canonry_measurement_plan_get to establish the portfolio type, and canonry_measurement_portfolio_summary to rank Properties.',
1223
+ access: "read",
1224
+ tier: "core",
1225
+ inputSchema: z3.object({
1226
+ project: projectNameSchema,
1227
+ location: z3.string().optional().describe('Filter to runs from this location label (e.g. "Boston, MA, US"). Omit for all locations.'),
1228
+ since: z3.string().optional().describe("ISO 8601 datetime \u2014 only include runs at or after this time. Omit for full history.")
1229
+ }),
1230
+ annotations: readAnnotations(),
1231
+ openApiOperations: ["GET /api/v1/projects/{name}/overview"],
1232
+ handler: (client, input) => client.getProjectOverview(input.project, {
1233
+ location: input.location,
1234
+ since: input.since
1235
+ })
1236
+ }),
1237
+ defineTool({
1238
+ name: "canonry_report",
1239
+ title: "Get aggregated AEO report",
1240
+ description: "Returns the full canonical AEO report bundle for a project \u2014 executive summary, client summary, agency diagnostics, action plan, per-query \xD7 per-provider citation matrix, competitor landscape, AI citation sources, GSC/GA4 performance, social and AI referrals, indexing health, citations trend, prioritized insights, and recommended next steps. Same payload `canonry report <project>` consumes to render audience-specific HTML. Pass `period` (7/14/30/90 days, default 30) to scope the GSC/GA4/server-activity sections and the period-over-period comparisons.",
1241
+ access: "read",
1242
+ tier: "monitoring",
1243
+ inputSchema: z3.object({
1244
+ project: projectNameSchema,
1245
+ period: reportPeriodSchema.optional()
1246
+ }),
1247
+ annotations: readAnnotations(),
1248
+ openApiOperations: ["GET /api/v1/projects/{name}/report"],
1249
+ handler: (client, input) => client.getReport(input.project, input.period !== void 0 ? { period: input.period } : void 0)
1250
+ }),
1251
+ defineTool({
1252
+ name: "canonry_organic_evidence",
1253
+ title: "Reconcile organic and AI evidence",
1254
+ description: "One-call investigation of whether organic work is gaining visibility, traffic, or AI attention. Returns source-specific 30-day GSC and GA4 cohorts, URL-agnostic page evidence, available GA4 lead-event evidence (not lead attribution), server-observed AI crawling/user-fetch/referral evidence, the latest answer-visibility sweep, source coverage, findings, and limitations. It preserves native units. Prefer this over fanning out across GSC, GA, traffic, and visibility tools.",
1255
+ access: "read",
1256
+ tier: "monitoring",
1257
+ inputSchema: z3.object({
1258
+ project: projectNameSchema,
1259
+ period: organicEvidencePeriodSchema.optional().describe("Evidence window: 60 or 90 days (default 90).")
1260
+ }),
1261
+ annotations: readAnnotations(),
1262
+ openApiOperations: ["GET /api/v1/projects/{name}/organic-evidence"],
1263
+ handler: (client, input) => client.getOrganicEvidence(input.project, input.period)
1264
+ }),
1265
+ defineTool({
1266
+ name: "canonry_analytics_metrics",
1267
+ title: "Get citation & mention trend",
1268
+ description: "Citation and mention rates over time for a project, bucketed adaptively (daily \u2192 monthly by span) and probe-excluded. Returns overall + per-provider window aggregates AND a per-bucket `byProvider` breakdown so you can read how each engine's cited/mentioned rate moved run-over-run \u2014 the same data the dashboard's \"Citations & mentions over time\" chart plots. Includes trend direction (improving/declining/stable) for both signals and query-set-change annotations. Filter the range with `window` (7d/30d/90d/all).",
1269
+ access: "read",
1270
+ tier: "monitoring",
1271
+ inputSchema: z3.object({
1272
+ project: projectNameSchema,
1273
+ window: analyticsWindowSchema.optional().describe("Time range: 7d, 30d, 90d, or all (default all).")
1274
+ }),
1275
+ annotations: readAnnotations(),
1276
+ openApiOperations: ["GET /api/v1/projects/{name}/analytics/metrics"],
1277
+ handler: (client, input) => client.getAnalyticsMetrics(input.project, input.window)
1278
+ }),
1279
+ defineTool({
1280
+ name: "canonry_analytics_sources",
1281
+ title: "Get cited-source rankings",
1282
+ description: "Where AI engines get the facts they cite for a project. Returns the FULL ranked list of cited domains (not truncated) \u2014 each tagged with a category and an actionable surface class (own / direct-competitor / ota-aggregator / editorial-media / other) \u2014 plus a surface-class roll-up and a per-provider breakdown (each provider's cited-domain mix + total cited slots). The surface class is deterministic (own/competitor from project data, the rest from the source allow-list) and enriched by discovery's stored per-domain classifications when present \u2014 no new LLM calls. Probe-excluded, window-filterable (7d/30d/90d/all). Use `limit` to cap each ranked list to the top N domains (an explicit long-tail rollup preserves the totals). All counts/shares/classification are computed server-side.",
1283
+ access: "read",
1284
+ tier: "monitoring",
1285
+ inputSchema: z3.object({
1286
+ project: projectNameSchema,
1287
+ window: analyticsWindowSchema.optional().describe("Time range: 7d, 30d, 90d, or all (default all)."),
1288
+ limit: z3.number().int().positive().optional().describe("Cap each ranked list to the top N domains. Omit for the full list.")
1289
+ }),
1290
+ annotations: readAnnotations(),
1291
+ openApiOperations: ["GET /api/v1/projects/{name}/analytics/sources"],
1292
+ handler: (client, input) => client.getAnalyticsSources(input.project, { window: input.window, limit: input.limit })
1293
+ }),
1294
+ defineTool({
1295
+ name: "canonry_competitor_landscape",
1296
+ title: "Get historical competitor landscape",
1297
+ description: "Returns pinned competitors first, then observed direct competitors and other cited sources from stored answer/source evidence only. Mention share is percentage points (0..100) from answer text; citations are independent. The response carries basis (tracked or observed), availability, and reason. Pins exclusively define the comparison set when present. Otherwise at least 3 stored direct competitors must each be mentioned in 3 answers in the selected scope; raw names and platforms never enter that denominator. Without a comparison set the ratio is null, never 100%. The full comparison array is uncapped. Share of voice needs ONE query class: `queryClass=all`, and omitting it, pool branded and non-brand, so every `shareOfVoice` comes back null and only the counts are published. Pass `queryClass=non-brand` (or `branded`) for a ratio, exactly as visibility-stats does. A project with no brand name or alias cannot be split by class, so a class-scoped read on one is refused rather than answered empty. Optional groupBy: model adds provider/requested-model groups with separate served-model evidence and sample counts. Optional model filters one exact requested model ID and requires provider. Unknown historical models remain explicit. Groups are not a matched-query or equal-weight comparison. Advanced reads support one market group or explicit scope: all-markets. No provider, discovery, or classifier work runs. Ranked lists are capped at 100 observed/other-source rows per group; pins remain complete. Model groups are capped at 50 and disclose truncation.",
1298
+ access: "read",
1299
+ tier: "monitoring",
1300
+ inputSchema: competitorLandscapeInputSchema,
1301
+ annotations: readAnnotations(),
1302
+ openApiOperations: ["GET /api/v1/projects/{name}/analytics/competitors"],
1303
+ handler: (client, input) => {
1304
+ const { project, ...query } = input;
1305
+ return client.getCompetitorLandscape(project, query);
1306
+ }
1307
+ }),
1308
+ defineTool({
1309
+ name: "canonry_search",
1310
+ title: "Search project (composite)",
1311
+ description: "Search query snapshots and intelligence insights for the given text. Looks at snapshot answer text, cited domains, raw provider responses, and insight title/query/recommendation/cause. Returns ranked hits with snippets \u2014 use it instead of paginating snapshots when you need to find a competitor mention or term.",
1312
+ access: "read",
1313
+ tier: "core",
1314
+ inputSchema: z3.object({
1315
+ project: projectNameSchema,
1316
+ q: z3.string().min(2).describe("Search term, at least 2 characters."),
1317
+ limit: z3.number().int().positive().max(50).optional().describe("Max combined hits (1-50, default 25).")
1318
+ }),
1319
+ annotations: readAnnotations(),
1320
+ openApiOperations: ["GET /api/v1/projects/{name}/search"],
1321
+ handler: (client, input) => client.searchProject(input.project, { q: input.q, limit: input.limit })
1322
+ }),
1323
+ defineTool({
1324
+ name: "canonry_doctor",
1325
+ title: "Run health checks",
1326
+ description: 'Run canonry health checks. With `project`, runs project-scoped checks (Google/GA auth, redirect URI, scopes, property access). Without `project`, runs global checks (provider keys, etc.). Use `checks` to filter by exact ID or wildcard prefix (e.g. ["google.auth.*"]). Returns a structured DoctorReport with per-check status, code, summary, remediation, and details \u2014 use this to diagnose Google auth failures (401/403/redirect-mismatch/principal-mismatch) without parsing logs.',
1327
+ access: "read",
1328
+ tier: "core",
1329
+ inputSchema: doctorInputSchema,
1330
+ annotations: readAnnotations(true),
1331
+ openApiOperations: ["GET /api/v1/doctor", "GET /api/v1/projects/{name}/doctor"],
1332
+ handler: (client, input) => client.runDoctor({ project: input.project, checkIds: input.checks })
1333
+ }),
1334
+ defineTool({
1335
+ name: "canonry_project_export",
1336
+ title: "Export project config",
1337
+ description: "Export a Canonry project in config-as-code format.",
1338
+ access: "read",
1339
+ tier: "setup",
1340
+ inputSchema: projectInputSchema,
1341
+ annotations: readAnnotations(),
1342
+ openApiOperations: ["GET /api/v1/projects/{name}/export"],
1343
+ handler: (client, input) => client.getExport(input.project)
1344
+ }),
1345
+ defineTool({
1346
+ name: "canonry_results_clear",
1347
+ title: "Preview or clear saved results",
1348
+ description: "Clear only explicitly selected visibility runs and research batches. Preview is the default; back up evidence and set confirm=true to delete. Refuses active work and other run kinds. Preserves queries, plans, schedules, Site Health, backlinks, usage, and the audit trail.",
1349
+ access: "write",
1350
+ tier: "monitoring",
1351
+ inputSchema: resultsClearRequestSchema.safeExtend({ project: projectNameSchema }),
1352
+ annotations: writeAnnotations({ idempotentHint: false, destructiveHint: true }),
1353
+ openApiOperations: ["POST /api/v1/projects/{name}/results/clear"],
1354
+ handler: (client, input) => client.clearResults(input.project, { runIds: input.runIds, researchRunIds: input.researchRunIds, confirm: input.confirm })
1355
+ }),
1356
+ defineTool({
1357
+ name: "canonry_project_history",
1358
+ title: "Get project history",
1359
+ description: "Get audit history for a Canonry project.",
1360
+ access: "read",
1361
+ tier: "monitoring",
1362
+ inputSchema: projectHistoryInputSchema,
1363
+ annotations: readAnnotations(),
1364
+ openApiOperations: ["GET /api/v1/projects/{name}/history"],
1365
+ handler: (client, input) => client.getHistory(input.project, {
1366
+ limit: input.limit,
1367
+ offset: input.offset,
1368
+ since: input.since,
1369
+ action: input.action,
1370
+ actor: input.actor,
1371
+ entityType: input.entityType
1372
+ })
1373
+ }),
1374
+ defineTool({
1375
+ name: "canonry_history_global",
1376
+ title: "Get instance history",
1377
+ description: "Get the instance-wide audit trail, including retained entries whose project was deleted. Full-instance keys only; project-scoped keys remain limited to their project.",
1378
+ access: "read",
1379
+ tier: "monitoring",
1380
+ inputSchema: globalHistoryInputSchema,
1381
+ annotations: readAnnotations(),
1382
+ openApiOperations: ["GET /api/v1/history"],
1383
+ handler: (client, input) => client.getGlobalHistory(input)
1384
+ }),
1385
+ defineTool({
1386
+ name: "canonry_runs_list",
1387
+ title: "List project runs",
1388
+ description: "List runs for a Canonry project. Includes both real runs (trigger='manual'/'scheduled'/'config-apply'/'backfill') AND probe runs (trigger='probe'). Probe runs are operator/agent test runs that don't influence dashboard, analytics, intelligence, or notifications \u2014 filter by `trigger !== 'probe'` if you only want runs that feed project metrics. Narrow with `kind` and/or `status` (e.g. status='running' lists only in-flight work).",
1389
+ access: "read",
1390
+ tier: "monitoring",
1391
+ inputSchema: runsListInputSchema,
1392
+ annotations: readAnnotations(),
1393
+ openApiOperations: ["GET /api/v1/projects/{name}/runs"],
1394
+ handler: (client, input) => client.listRuns(input.project, input.limit, input.kind, input.status)
1395
+ }),
1396
+ defineTool({
1397
+ name: "canonry_runs_latest",
1398
+ title: "Get latest project run",
1399
+ description: "Get the latest run and total run count for a Canonry project.",
1400
+ access: "read",
1401
+ tier: "monitoring",
1402
+ inputSchema: projectInputSchema,
1403
+ annotations: readAnnotations(),
1404
+ openApiOperations: ["GET /api/v1/projects/{name}/runs/latest"],
1405
+ handler: (client, input) => client.getLatestRun(input.project)
1406
+ }),
1407
+ defineTool({
1408
+ name: "canonry_run_get",
1409
+ title: "Get run",
1410
+ description: "Get a Canonry run with its snapshots.",
1411
+ access: "read",
1412
+ tier: "monitoring",
1413
+ inputSchema: runGetInputSchema,
1414
+ annotations: readAnnotations(),
1415
+ openApiOperations: ["GET /api/v1/runs/{id}"],
1416
+ handler: (client, input) => client.getRun(input.runId)
1417
+ }),
1418
+ defineTool({
1419
+ name: "canonry_run_completeness",
1420
+ title: "Get run completeness",
1421
+ description: "Expected, answered and missing measurement slots per provider for a plan run, whether canonry_run_fill would be admitted now and why not, and the latest fill.",
1422
+ access: "read",
1423
+ tier: "monitoring",
1424
+ inputSchema: runGetInputSchema,
1425
+ annotations: readAnnotations(),
1426
+ openApiOperations: ["GET /api/v1/runs/{id}/completeness"],
1427
+ handler: (client, input) => client.getRunCompleteness(input.runId)
1428
+ }),
1429
+ defineTool({
1430
+ name: "canonry_run_fill",
1431
+ title: "Fill a partial run",
1432
+ description: "Record a partial plan run's missing answers under the SAME run id, so it stays one run in every report. Spends provider quota for each missing answer, so call with dryRun first. Refused when the plan was republished, the run is over 24 hours old, or a newer sweep exists. Returns the queued fill; poll canonry_run_completeness for the outcome.",
1433
+ access: "write",
1434
+ tier: "monitoring",
1435
+ inputSchema: runFillInputSchema,
1436
+ annotations: writeAnnotations({ idempotentHint: false, openWorldHint: true }),
1437
+ openApiOperations: ["POST /api/v1/runs/{id}/fill"],
1438
+ handler: (client, input) => client.fillRun(input.runId, {
1439
+ ...input.providers ? { providers: input.providers } : {},
1440
+ ...input.dryRun ? { dryRun: true } : {}
1441
+ })
1442
+ }),
1443
+ defineTool({
1444
+ name: "canonry_timeline_get",
1445
+ title: "Get project timeline",
1446
+ description: "Get per-query citation history for a Canonry project.",
1447
+ access: "read",
1448
+ tier: "monitoring",
1449
+ inputSchema: timelineInputSchema,
1450
+ annotations: readAnnotations(),
1451
+ openApiOperations: ["GET /api/v1/projects/{name}/timeline"],
1452
+ handler: (client, input) => client.getTimeline(input.project, input.location, input.limit)
1453
+ }),
1454
+ defineTool({
1455
+ name: "canonry_snapshots_list",
1456
+ title: "List query snapshots",
1457
+ description: "List paginated query snapshots for a Canonry project.",
1458
+ access: "read",
1459
+ tier: "monitoring",
1460
+ inputSchema: snapshotsListInputSchema,
1461
+ annotations: readAnnotations(),
1462
+ openApiOperations: ["GET /api/v1/projects/{name}/snapshots"],
1463
+ handler: (client, input) => client.getSnapshots(input.project, {
1464
+ limit: input.limit,
1465
+ offset: input.offset,
1466
+ location: input.location
1467
+ })
1468
+ }),
1469
+ defineTool({
1470
+ name: "canonry_snapshots_diff",
1471
+ title: "Diff snapshots",
1472
+ description: "Compare query snapshot states between two Canonry runs.",
1473
+ access: "read",
1474
+ tier: "monitoring",
1475
+ inputSchema: snapshotsDiffInputSchema,
1476
+ annotations: readAnnotations(),
1477
+ openApiOperations: ["GET /api/v1/projects/{name}/snapshots/diff"],
1478
+ handler: (client, input) => client.getSnapshotDiff(input.project, input.run1, input.run2)
1479
+ }),
1480
+ defineTool({
1481
+ name: "canonry_insights_list",
1482
+ title: "List insights",
1483
+ description: "List intelligence insights for a Canonry project.",
1484
+ access: "read",
1485
+ tier: "monitoring",
1486
+ inputSchema: insightsListInputSchema,
1487
+ annotations: readAnnotations(),
1488
+ openApiOperations: ["GET /api/v1/projects/{name}/insights"],
1489
+ handler: (client, input) => client.getInsights(input.project, { dismissed: input.dismissed, runId: input.runId })
1490
+ }),
1491
+ defineTool({
1492
+ name: "canonry_insight_get",
1493
+ title: "Get insight",
1494
+ description: "Get one intelligence insight for a Canonry project.",
1495
+ access: "read",
1496
+ tier: "monitoring",
1497
+ inputSchema: insightInputSchema,
1498
+ annotations: readAnnotations(),
1499
+ openApiOperations: ["GET /api/v1/projects/{name}/insights/{id}"],
1500
+ handler: (client, input) => client.getInsight(input.project, input.insightId)
1501
+ }),
1502
+ defineTool({
1503
+ name: "canonry_health_latest",
1504
+ title: "Get latest health",
1505
+ description: 'Get the latest health snapshot for a Canonry project. Always returns a snapshot once the project exists: real data carries `status: "ready"`; newly-created projects (or projects with only failed runs) carry `status: "no-data"` with `reason: "no-runs-yet"` and zeroed metrics.',
1506
+ access: "read",
1507
+ tier: "monitoring",
1508
+ inputSchema: projectInputSchema,
1509
+ annotations: readAnnotations(),
1510
+ openApiOperations: ["GET /api/v1/projects/{name}/health/latest"],
1511
+ handler: (client, input) => client.getHealth(input.project)
1512
+ }),
1513
+ defineTool({
1514
+ name: "canonry_health_history",
1515
+ title: "Get health history",
1516
+ description: "Get health snapshot history for a Canonry project.",
1517
+ access: "read",
1518
+ tier: "monitoring",
1519
+ inputSchema: healthHistoryInputSchema,
1520
+ annotations: readAnnotations(),
1521
+ openApiOperations: ["GET /api/v1/projects/{name}/health/history"],
1522
+ handler: (client, input) => client.getHealthHistory(input.project, input.limit)
1523
+ }),
1524
+ defineTool({
1525
+ name: "canonry_citations_visibility",
1526
+ title: "Get citation visibility",
1527
+ description: 'Single-call AI citation surface for a Canonry project. Returns the project headline (cited by N of M engines), per-query engine coverage rows from the latest snapshot per (query \xD7 provider), and a competitor-gap list (queries where a configured competitor is cited but the project is not). Carries `status: "no-data"` with `reason: "no-queries"` or `"no-runs-yet"` when inputs are missing.',
1528
+ access: "read",
1529
+ tier: "monitoring",
1530
+ inputSchema: projectInputSchema,
1531
+ annotations: readAnnotations(),
1532
+ openApiOperations: ["GET /api/v1/projects/{name}/citations/visibility"],
1533
+ handler: (client, input) => client.getCitationVisibility(input.project)
1534
+ }),
1535
+ defineTool({
1536
+ name: "canonry_visibility_stats",
1537
+ title: "Get aggregated mention/citation stats",
1538
+ description: 'Per-query mention (answer-text) and citation (source-list) counts WITH a sample size, pooled across many answer-visibility runs (probe-excluded) \u2014 the data to compute a confidence-aware (Wilson) proportion or detect drift without fetching every run. Tri-state aware: `checked` (the n for the mention proportion) counts only snapshots where answerMentioned was recorded; `null` ("not checked") is excluded, never counted as not-mentioned. Returns per-query `total`/`checked`/`mentioned`/`cited` + derived `mentionRate` (mentioned/checked) and `citedRate` (cited/total), `firstObserved`/`lastObserved`, and pooled `totals`. Window with `since`/`until` (ISO), `lastRuns`, or `month=YYYY-MM` (mutually exclusive); with none set, EVERY completed/partial run is pooled (`window.runCount` says how many) \u2014 pass `lastRuns` for a recent sample. Set `groupBy=provider` for a per-provider breakdown whose counts sum to the pooled counts (`groupBy` is omitted from the response otherwise). Set `shareOfVoice=true` for brand-mention share with explicit basis, availability, and reason. Tracked competitors are exclusive when configured; otherwise 3 stored direct competitors must each have 3 answer mentions. Raw names and platforms are excluded. Advanced portfolios use frozen assignments across all markets; Simple sites use the same attributed snapshot set \u2014 scoped to NON-BRAND queries by default, because a branded query names the project (it is mentioned on nearly all of them and a competitor cannot be), so a pooled figure reports brand recall as category placement. Pass `queryClass="branded"` for the recall figure; the response echoes which class it served.',
1539
+ access: "read",
1540
+ tier: "monitoring",
1541
+ inputSchema: z3.object({
1542
+ project: projectNameSchema,
1543
+ since: z3.string().optional().describe("Inclusive lower bound on run createdAt (ISO 8601). A date-only value (YYYY-MM-DD) is the start of that UTC day. Mutually exclusive with lastRuns/month."),
1544
+ until: z3.string().optional().describe("Inclusive upper bound on run createdAt (ISO 8601). A date-only value (YYYY-MM-DD) covers the whole UTC day (through 23:59:59.999). Mutually exclusive with lastRuns/month."),
1545
+ lastRuns: z3.number().int().positive().optional().describe("Aggregate only the most recent N answer-visibility runs. Mutually exclusive with since/until/month."),
1546
+ month: z3.string().optional().describe("Aggregate one calendar month (YYYY-MM), expanded to that month's inclusive UTC bounds. Mutually exclusive with since/until/lastRuns."),
1547
+ groupBy: z3.enum(["provider"]).optional().describe('Set to "provider" for a per-provider breakdown.'),
1548
+ shareOfVoice: z3.boolean().optional().describe("Include brand-mention share with tracked/observed basis and availability across the same window (non-brand queries unless queryClass says otherwise)."),
1549
+ queryClass: z3.enum(["branded", "non-brand"]).optional().describe('Query class for shareOfVoice. Defaults to non-brand. There is no "all": branded and non-brand never share a denominator.')
1550
+ }),
1551
+ annotations: readAnnotations(),
1552
+ openApiOperations: ["GET /api/v1/projects/{name}/visibility-stats"],
1553
+ handler: (client, input) => client.getVisibilityStats(input.project, {
1554
+ since: input.since,
1555
+ until: input.until,
1556
+ lastRuns: input.lastRuns,
1557
+ groupBy: input.groupBy,
1558
+ month: input.month,
1559
+ shareOfVoice: input.shareOfVoice,
1560
+ queryClass: input.queryClass
1561
+ })
1562
+ }),
1563
+ defineTool({
1564
+ name: "canonry_visibility_compare",
1565
+ title: "Compare AEO visibility month over month",
1566
+ description: "Statistically honest month-over-month AEO comparison in ONE call \u2014 use this instead of hand-computing deltas from two visibility-stats calls. Share of voice (`mention-share-of-voice`, `driftRobust: true`) is less exposed to broad model-wide naming propensity than absolute rates, but it never overrides model continuity. The response restricts to common query/provider pairs, then includes only providers with exactly one known, identical configured model id in both months. `continuity` surfaces every provider, its model evidence, and whether it was excluded for a changed, mixed mid-month, or unknown model. When no provider remains, metrics return `model-discontinuous` or `model-unknown`, never a directional call. A silent upstream version bump under an unchanged configured id remains undetectable. `from` must be a month strictly before `to`.",
1567
+ access: "read",
1568
+ tier: "monitoring",
1569
+ inputSchema: z3.object({
1570
+ project: projectNameSchema,
1571
+ from: z3.string().describe('Earlier calendar month (YYYY-MM), the baseline. Must be strictly before "to".'),
1572
+ to: z3.string().describe('Later calendar month (YYYY-MM), compared against "from".')
1573
+ }),
1574
+ annotations: readAnnotations(),
1575
+ openApiOperations: ["GET /api/v1/projects/{name}/visibility-compare"],
1576
+ handler: (client, input) => client.getVisibilityCompare(input.project, input.from, input.to)
1577
+ }),
1578
+ defineTool({
1579
+ name: "canonry_content_targets",
1580
+ title: "Get content targets",
1581
+ description: 'Ranked, action-typed content opportunities. Each row is `{query, action \u2208 create|expand|refresh|add-schema, ourBestPage?, winningCompetitor?, score, scoreBreakdown, drivers[], demandSource, actionConfidence, winnabilityClass, winnability?}`. `winnabilityClass` is the winnability gate: "ownable" (worth a brief) vs "ceded" (aggregator/editorial head term to skip); ownable rows sort first. Filter with `winnabilityClass`/`ownable`. Use this to recommend which post the user should write or refresh next.',
1582
+ access: "read",
1583
+ tier: "monitoring",
1584
+ inputSchema: contentTargetsInputSchema,
1585
+ annotations: readAnnotations(),
1586
+ openApiOperations: ["GET /api/v1/projects/{name}/content/targets"],
1587
+ handler: (client, input) => client.getContentTargets(input.project, {
1588
+ limit: input.limit,
1589
+ includeInProgress: input.includeInProgress,
1590
+ winnabilityClass: input.winnabilityClass,
1591
+ ownable: input.ownable
1592
+ })
1593
+ }),
1594
+ defineTool({
1595
+ name: "canonry_content_brief",
1596
+ title: "Synthesize content brief",
1597
+ description: "Synthesize (or fetch cached) a STRUCTURED content brief for an ownable target: `{targetQuery, winnabilityClass, angle, whyWinnable, schemaHookup, controllableSurfaceRationale}`. Gated to ownable targets \u2014 a ceded head term is rejected. Costs one analyze-tier LLM call on a cache miss; repeat calls are free. Pass a targetRef from canonry_content_targets.",
1598
+ access: "write",
1599
+ tier: "monitoring",
1600
+ inputSchema: contentBriefInputSchema,
1601
+ annotations: writeAnnotations({ idempotentHint: true }),
1602
+ openApiOperations: ["POST /api/v1/projects/{name}/content/recommendations/{targetRef}/brief"],
1603
+ handler: (client, input) => client.synthesizeContentBrief(input.project, input.targetRef, {
1604
+ provider: input.provider,
1605
+ model: input.model,
1606
+ forceRefresh: input.forceRefresh
1607
+ })
1608
+ }),
1609
+ defineTool({
1610
+ name: "canonry_content_map",
1611
+ title: "Get domain classifications (winnability map)",
1612
+ description: 'The per-domain cited-surface classifications behind the winnabilityClass gate: `{domain, competitorType \u2208 direct-competitor|ota-aggregator|editorial-media|other|unknown, hits, updatedAt}`, ranked by recurrence. Aggregator/editorial domains are the "ceded" surfaces. Running discovery improves coverage.',
1613
+ access: "read",
1614
+ tier: "monitoring",
1615
+ inputSchema: contentMapInputSchema,
1616
+ annotations: readAnnotations(),
1617
+ openApiOperations: ["GET /api/v1/projects/{name}/content/domain-classifications"],
1618
+ handler: (client, input) => client.getDomainClassifications(input.project)
1619
+ }),
1620
+ defineTool({
1621
+ name: "canonry_content_sources",
1622
+ title: "Get grounding sources",
1623
+ description: "URL-level competitive grounding-source map. Per query, lists every URL the LLM cited (our domain vs competitors) with citation count and providers. Read this to understand which specific competitor URL is winning a query.",
1624
+ access: "read",
1625
+ tier: "monitoring",
1626
+ inputSchema: projectInputSchema,
1627
+ annotations: readAnnotations(),
1628
+ openApiOperations: ["GET /api/v1/projects/{name}/content/sources"],
1629
+ handler: (client, input) => client.getContentSources(input.project)
1630
+ }),
1631
+ defineTool({
1632
+ name: "canonry_content_gaps",
1633
+ title: "Get content gaps",
1634
+ description: 'Queries where competitors are cited but our domain is not, ranked by miss rate. The blunt-instrument view of "what competitors are winning that we are not." Use canonry_content_targets for action-typed recommendations on the same data.',
1635
+ access: "read",
1636
+ tier: "monitoring",
1637
+ inputSchema: projectInputSchema,
1638
+ annotations: readAnnotations(),
1639
+ openApiOperations: ["GET /api/v1/projects/{name}/content/gaps"],
1640
+ handler: (client, input) => client.getContentGaps(input.project)
1641
+ }),
1642
+ defineTool({
1643
+ name: "canonry_queries_list",
1644
+ title: "List queries",
1645
+ description: "List tracked queries for a Canonry project.",
1646
+ access: "read",
1647
+ tier: "setup",
1648
+ inputSchema: projectInputSchema,
1649
+ annotations: readAnnotations(),
1650
+ openApiOperations: ["GET /api/v1/projects/{name}/queries"],
1651
+ handler: (client, input) => client.listQueries(input.project)
1652
+ }),
1653
+ defineTool({
1654
+ name: "canonry_keywords_list",
1655
+ title: "List keywords (legacy alias)",
1656
+ description: "Legacy alias for canonry_queries_list. Returns tracked queries using the pre-queries keyword response shape.",
1657
+ access: "read",
1658
+ tier: "setup",
1659
+ inputSchema: projectInputSchema,
1660
+ annotations: readAnnotations(),
1661
+ openApiOperations: ["GET /api/v1/projects/{name}/keywords"],
1662
+ handler: (client, input) => client.listKeywords(input.project)
1663
+ }),
1664
+ defineTool({
1665
+ name: "canonry_competitors_list",
1666
+ title: "List competitors",
1667
+ description: "List tracked competitors for a Canonry project.",
1668
+ access: "read",
1669
+ tier: "setup",
1670
+ inputSchema: projectInputSchema,
1671
+ annotations: readAnnotations(),
1672
+ openApiOperations: ["GET /api/v1/projects/{name}/competitors"],
1673
+ handler: (client, input) => client.listCompetitors(input.project)
1674
+ }),
1675
+ defineTool({
1676
+ name: "canonry_schedule_get",
1677
+ title: "Get schedule",
1678
+ description: 'Get the scheduled run configuration for a Canonry project. Pass `kind` to read a non-default schedule (e.g. "traffic-sync"); defaults to "answer-visibility".',
1679
+ access: "read",
1680
+ tier: "setup",
1681
+ inputSchema: scheduleReadInputSchema,
1682
+ annotations: readAnnotations(),
1683
+ openApiOperations: ["GET /api/v1/projects/{name}/schedule"],
1684
+ handler: (client, input) => client.getSchedule(input.project, input.kind)
1685
+ }),
1686
+ defineTool({
1687
+ name: "canonry_schedules_list",
1688
+ title: "List project schedules",
1689
+ description: "List every stored schedule for a Canonry project, including each schedulable run kind. This reads configuration only and never starts work.",
1690
+ access: "read",
1691
+ tier: "setup",
1692
+ inputSchema: projectInputSchema,
1693
+ outputSchema: scheduleListOutputSchema,
1694
+ annotations: readAnnotations(),
1695
+ openApiOperations: ["GET /api/v1/projects/{name}/schedules"],
1696
+ handler: (client, input) => client.listSchedules(input.project)
1697
+ }),
1698
+ defineTool({
1699
+ name: "canonry_notification_events",
1700
+ title: "List notification event types",
1701
+ description: "List the supported stored notification event names. This is a static capability read; it does not send, test, or change notifications.",
1702
+ access: "read",
1703
+ tier: "setup",
1704
+ inputSchema: emptyInputSchema,
1705
+ outputSchema: notificationEventsOutputSchema,
1706
+ annotations: readAnnotations(),
1707
+ openApiOperations: ["GET /api/v1/notifications/events"],
1708
+ handler: (client) => client.listNotificationEvents()
1709
+ }),
1710
+ defineTool({
1711
+ name: "canonry_backlinks_latest_release",
1712
+ title: "Discover latest Common Crawl release",
1713
+ description: "Probes Common Crawl to find the latest published hyperlinkgraph release. Returns the release id and file URLs/sizes ready to feed into a backlinks sync (or null if no candidate slug responded).",
1714
+ access: "read",
1715
+ tier: "setup",
1716
+ inputSchema: emptyInputSchema,
1717
+ annotations: readAnnotations(true),
1718
+ openApiOperations: ["GET /api/v1/backlinks/latest-release"],
1719
+ handler: (client) => client.backlinksLatestRelease()
1720
+ }),
1721
+ defineTool({
1722
+ name: "canonry_backlinks_domains",
1723
+ title: "List backlink domains",
1724
+ description: "Source-aware backlink summary + top linking domains for a project. `source=commoncrawl` (default) reads the most recent ready Common Crawl release; `source=bing-webmaster` is retained only for historical rows created by older Canonry versions. Off-site authority signal that correlates with citation likelihood. Returns null summary when the source has no data yet.",
1725
+ access: "read",
1726
+ tier: "setup",
1727
+ inputSchema: backlinksDomainsInputSchema,
1728
+ annotations: readAnnotations(),
1729
+ openApiOperations: ["GET /api/v1/projects/{name}/backlinks/domains"],
1730
+ handler: (client, input) => client.backlinksDomains(input.project, {
1731
+ limit: input.limit ?? 50,
1732
+ release: input.release,
1733
+ source: input.source
1734
+ })
1735
+ }),
1736
+ defineTool({
1737
+ name: "canonry_backlinks_sources",
1738
+ title: "Backlink source availability",
1739
+ description: "Reports active Common Crawl readiness plus any retained historical backlink rows. `bing-webmaster` is never reported as connected and is not refreshed.",
1740
+ access: "read",
1741
+ tier: "setup",
1742
+ inputSchema: backlinksSourcesInputSchema,
1743
+ annotations: readAnnotations(),
1744
+ openApiOperations: ["GET /api/v1/projects/{name}/backlinks/sources"],
1745
+ handler: (client, input) => client.backlinksSources(input.project)
1746
+ }),
1747
+ defineTool({
1748
+ name: "canonry_settings_get",
1749
+ title: "Get settings",
1750
+ description: "Get Canonry API settings and configured provider status.",
1751
+ access: "read",
1752
+ tier: "core",
1753
+ inputSchema: emptyInputSchema,
1754
+ outputSchema: settingsDtoSchema,
1755
+ annotations: readAnnotations(),
1756
+ openApiOperations: ["GET /api/v1/settings"],
1757
+ handler: (client) => client.getSettings()
1758
+ }),
1759
+ defineTool({
1760
+ name: "canonry_key_self",
1761
+ title: "Get current API key identity",
1762
+ description: "Read safe metadata for the credential backing this MCP connection, including scopes, project reach, read-only status, and host-derived operator authority. It never exposes the bearer token or stored hash and cannot grant authority.",
1763
+ access: "read",
1764
+ tier: "core",
1765
+ inputSchema: emptyInputSchema,
1766
+ outputSchema: apiKeyDtoSchema,
1767
+ annotations: readAnnotations(),
1768
+ openApiOperations: ["GET /api/v1/keys/self"],
1769
+ handler: (client) => client.getApiKeySelf()
1770
+ }),
1771
+ defineTool({
1772
+ name: "canonry_logs_list",
1773
+ requiresOperator: true,
1774
+ title: "Read runtime logs",
1775
+ description: "Read bounded, redacted runtime logs, including application and HTTP failures. Filter by actor, request, run, project, level, module, or time. Inspect retention, retentionPolicy, dropped, and captureErrors for coverage; file-backed hosts retain logs across restarts. Separate from audit history. Requires a host-approved operator bearer and instance-wide logs.read (or wildcard). Customer admins, browser/OAuth/delegated sessions, and project-scoped keys cannot grant access. Cursors expire on retention eviction; reuse with the same filters. No provider calls.",
1776
+ access: "read",
1777
+ tier: "setup",
1778
+ inputSchema: logQuerySchema,
1779
+ outputSchema: operationalLogListDtoSchema,
1780
+ annotations: readAnnotations(false),
1781
+ openApiOperations: ["GET /api/v1/operations/logs"],
1782
+ handler: async (client, input) => client.listOperationalLogs(input)
1783
+ }),
1784
+ defineTool({
1785
+ name: "canonry_telemetry_update",
1786
+ requiresOperator: true,
1787
+ title: "Update server telemetry preference",
1788
+ description: "Set the connected server telemetry preference with explicit approval, host-approved operator authority, and settings.write. Customer roles and scopes alone cannot grant operator access. Returns effective state including environment overrides. Does not change the agent machine configuration.",
1789
+ access: "write",
1790
+ requiredScope: "settings.write",
1791
+ tier: "setup",
1792
+ inputSchema: z3.object({ enabled: z3.boolean() }).strict(),
1793
+ outputSchema: telemetryStatusDtoSchema,
1794
+ annotations: writeAnnotations({ idempotentHint: true }),
1795
+ openApiOperations: ["PUT /api/v1/telemetry"],
1796
+ handler: async (client, input) => client.updateTelemetry(input.enabled)
1797
+ }),
1798
+ defineTool({
1799
+ name: "canonry_provider_settings_update",
1800
+ title: "Update non-secret provider settings",
1801
+ description: "Change model or quota for an already-configured provider, with explicit approval and settings.write. Preserves existing credentials. Does not accept API keys, base URLs, or credential rotation; use the secure operator setup flow for those.",
1802
+ access: "write",
1803
+ requiredScope: "settings.write",
1804
+ tier: "setup",
1805
+ inputSchema: z3.object({ provider: z3.string().min(1), model: z3.string().trim().min(1).optional(), quota: providerQuotaPolicySchema.partial().strict().optional() }).strict().refine((input) => input.model !== void 0 || input.quota !== void 0 && Object.keys(input.quota).length > 0, "Provide a model or at least one quota field"),
1806
+ outputSchema: providerSummaryEntryDtoSchema,
1807
+ annotations: writeAnnotations({ idempotentHint: true }),
1808
+ openApiOperations: ["PUT /api/v1/settings/providers/{name}"],
1809
+ handler: async (client, { provider, ...request }) => client.updateProvider(provider, request)
1810
+ }),
1811
+ defineTool({
1812
+ name: "canonry_telemetry_get",
1813
+ requiresOperator: true,
1814
+ title: "Get telemetry status",
1815
+ description: "Read internal server telemetry status and effective reason. Requires a host-approved operator bearer, not a customer admin role or scopes alone. Does not change telemetry collection.",
1816
+ access: "read",
1817
+ tier: "setup",
1818
+ inputSchema: emptyInputSchema,
1819
+ outputSchema: telemetryStatusDtoSchema,
1820
+ annotations: readAnnotations(),
1821
+ openApiOperations: ["GET /api/v1/telemetry"],
1822
+ handler: (client) => client.getTelemetry()
1823
+ }),
1824
+ defineTool({
1825
+ name: "canonry_google_connections_list",
1826
+ title: "List Google connections",
1827
+ description: "List configured Google connections for a Canonry project.",
1828
+ access: "read",
1829
+ tier: "gsc",
1830
+ inputSchema: projectInputSchema,
1831
+ annotations: readAnnotations(),
1832
+ openApiOperations: ["GET /api/v1/projects/{name}/google/connections"],
1833
+ handler: (client, input) => client.googleConnections(input.project)
1834
+ }),
1835
+ defineTool({
1836
+ name: "canonry_gsc_performance",
1837
+ title: "Get GSC performance",
1838
+ description: "Get stored Google Search Console performance rows for a Canonry project. Rows are ordered by clicks descending unless orderBy says otherwise, and the response reports totalMatching / truncated / latestAvailableDate. Never sum these rows for a property total, use canonry_gsc_performance_daily.",
1839
+ access: "read",
1840
+ tier: "gsc",
1841
+ inputSchema: gscPerformanceInputSchema,
1842
+ annotations: readAnnotations(),
1843
+ openApiOperations: ["GET /api/v1/projects/{name}/google/gsc/performance"],
1844
+ handler: (client, input) => client.gscPerformance(input.project, compactStringParams(input, ["startDate", "endDate", "query", "page", "limit", "offset", "orderBy", "window"]))
1845
+ }),
1846
+ defineTool({
1847
+ name: "canonry_gsc_performance_daily",
1848
+ title: "Get GSC daily performance summary",
1849
+ description: "Get GSC search performance aggregated by date with window totals (clicks, impressions, CTR). Use this for charts and headline metrics \u2014 never recompute by summing the paged canonry_gsc_performance rows, which only cover one page.",
1850
+ access: "read",
1851
+ tier: "gsc",
1852
+ inputSchema: gscPerformanceDailyInputSchema,
1853
+ annotations: readAnnotations(),
1854
+ openApiOperations: ["GET /api/v1/projects/{name}/google/gsc/performance/daily"],
1855
+ handler: (client, input) => client.gscPerformanceDaily(input.project, compactStringParams(input, ["startDate", "endDate", "window"]))
1856
+ }),
1857
+ defineTool({
1858
+ name: "canonry_gsc_top_pages",
1859
+ title: "Get top GSC pages",
1860
+ description: 'Get the project\'s pages ranked by summed GSC clicks, aggregated in SQL. The rows are a RANKING and their clicks/impressions do NOT add up to the site total: Google withholds rare queries and repeats one impression per ranking page. Read the window total from the returned `totals` block (labelled totalsSource "property-daily"), which is null when no property-level figure covers the window. Never sum the rows.',
1861
+ access: "read",
1862
+ tier: "gsc",
1863
+ inputSchema: gscTopPagesInputSchema,
1864
+ annotations: readAnnotations(),
1865
+ openApiOperations: ["GET /api/v1/projects/{name}/google/gsc/top-pages"],
1866
+ handler: (client, input) => client.gscTopPages(input.project, compactStringParams(input, ["startDate", "endDate", "limit", "window"]))
1867
+ }),
1868
+ defineTool({
1869
+ name: "canonry_gsc_inspections",
1870
+ title: "List GSC inspections",
1871
+ description: "List stored URL inspection rows for a Canonry project.",
1872
+ access: "read",
1873
+ tier: "gsc",
1874
+ inputSchema: gscInspectionsInputSchema,
1875
+ annotations: readAnnotations(),
1876
+ openApiOperations: ["GET /api/v1/projects/{name}/google/gsc/inspections"],
1877
+ handler: (client, input) => client.gscInspections(input.project, compactStringParams(input, ["url", "limit"]))
1878
+ }),
1879
+ defineTool({
1880
+ name: "canonry_gsc_deindexed",
1881
+ title: "List deindexed GSC URLs",
1882
+ description: "List URLs that appear to have become deindexed in Google Search Console data.",
1883
+ access: "read",
1884
+ tier: "gsc",
1885
+ inputSchema: projectInputSchema,
1886
+ annotations: readAnnotations(),
1887
+ openApiOperations: ["GET /api/v1/projects/{name}/google/gsc/deindexed"],
1888
+ handler: (client, input) => client.gscDeindexed(input.project)
1889
+ }),
1890
+ defineTool({
1891
+ name: "canonry_gsc_coverage",
1892
+ title: "Get GSC coverage",
1893
+ description: "Get Google Search Console coverage summary for a Canonry project.",
1894
+ access: "read",
1895
+ tier: "gsc",
1896
+ inputSchema: projectInputSchema,
1897
+ annotations: readAnnotations(),
1898
+ openApiOperations: ["GET /api/v1/projects/{name}/google/gsc/coverage"],
1899
+ handler: (client, input) => client.gscCoverage(input.project)
1900
+ }),
1901
+ defineTool({
1902
+ name: "canonry_gsc_coverage_history",
1903
+ title: "Get GSC coverage history",
1904
+ description: "Get Google Search Console coverage history snapshots for a Canonry project.",
1905
+ access: "read",
1906
+ tier: "gsc",
1907
+ inputSchema: gscCoverageHistoryInputSchema,
1908
+ annotations: readAnnotations(),
1909
+ openApiOperations: ["GET /api/v1/projects/{name}/google/gsc/coverage/history"],
1910
+ handler: (client, input) => client.gscCoverageHistory(input.project, { limit: input.limit })
1911
+ }),
1912
+ defineTool({
1913
+ name: "canonry_gsc_sitemaps",
1914
+ title: "Get GSC sitemaps",
1915
+ description: "Get sitemap data from Google Search Console for a Canonry project.",
1916
+ access: "read",
1917
+ tier: "gsc",
1918
+ inputSchema: gscSitemapsInputSchema,
1919
+ annotations: readAnnotations(true),
1920
+ openApiOperations: ["GET /api/v1/projects/{name}/google/gsc/sitemaps"],
1921
+ handler: (client, input) => client.gscSitemaps(input.project, { sitemapIndex: input.sitemapIndex })
1922
+ }),
1923
+ defineTool({
1924
+ name: "canonry_gsc_sitemaps_submit",
1925
+ title: "Submit GSC sitemaps",
1926
+ description: "Submit up to 50 explicit sitemap URLs, preferred sitemap indexes, or every standalone top-level file plus index child to Google Search Console for refetching. Parent indexes are not redundantly submitted in all-files mode. Google acceptance does not guarantee indexing.",
1927
+ access: "write",
1928
+ tier: "gsc",
1929
+ inputSchema: gscSitemapsSubmitInputSchema,
1930
+ annotations: writeAnnotations({ idempotentHint: false, openWorldHint: true }),
1931
+ openApiOperations: ["POST /api/v1/projects/{name}/google/gsc/sitemaps/submit"],
1932
+ handler: submitGscSitemapsFromMcp
1933
+ }),
1934
+ defineTool({
1935
+ name: "canonry_ga_status",
1936
+ title: "Get GA status",
1937
+ description: "Get Google Analytics connection status for a Canonry project.",
1938
+ access: "read",
1939
+ tier: "ga",
1940
+ inputSchema: projectInputSchema,
1941
+ annotations: readAnnotations(),
1942
+ openApiOperations: ["GET /api/v1/projects/{name}/ga/status"],
1943
+ handler: (client, input) => client.gaStatus(input.project)
1944
+ }),
1945
+ defineTool({
1946
+ name: "canonry_ga_properties",
1947
+ title: "List GA4 properties",
1948
+ description: `List the GA4 properties the project's connected Google account can read, with their numeric property ids. Use this to find the id that "canonry ga connect --property-id" needs; it cannot be derived from the domain or the OAuth grant.`,
1949
+ access: "read",
1950
+ tier: "ga",
1951
+ inputSchema: projectInputSchema,
1952
+ annotations: readAnnotations(),
1953
+ openApiOperations: ["GET /api/v1/projects/{name}/ga/properties"],
1954
+ handler: (client, input) => client.gaProperties(input.project)
1955
+ }),
1956
+ defineTool({
1957
+ name: "canonry_ga_measurement_analysis",
1958
+ title: "Analyze GA acquisition and search demand",
1959
+ description: "Compare native GA4 channels and lead events with branded/non-brand Search Console demand over fixed 30-day cohorts.",
1960
+ access: "read",
1961
+ tier: "ga",
1962
+ inputSchema: gaMeasurementAnalysisInputSchema,
1963
+ annotations: readAnnotations(),
1964
+ openApiOperations: ["GET /api/v1/projects/{name}/ga/measurement-analysis"],
1965
+ handler: (client, input) => client.gaMeasurementAnalysis(
1966
+ input.project,
1967
+ compactStringParams(input, ["window", "hostScope", "pathPrefix", "limit"])
1968
+ )
1969
+ }),
1970
+ defineTool({
1971
+ name: "canonry_ga_traffic",
1972
+ title: "Get GA traffic",
1973
+ description: "Get Google Analytics traffic summary for a Canonry project.",
1974
+ access: "read",
1975
+ tier: "ga",
1976
+ inputSchema: gaTrafficInputSchema,
1977
+ annotations: readAnnotations(),
1978
+ openApiOperations: ["GET /api/v1/projects/{name}/ga/traffic"],
1979
+ handler: (client, input) => client.gaTraffic(input.project, compactStringParams(input, ["limit", ...GA_RANGE_PARAMS]))
1980
+ }),
1981
+ defineTool({
1982
+ name: "canonry_ga_coverage",
1983
+ title: "Get GA coverage",
1984
+ description: "Get Google Analytics page coverage for a Canonry project.",
1985
+ access: "read",
1986
+ tier: "ga",
1987
+ inputSchema: projectInputSchema,
1988
+ annotations: readAnnotations(),
1989
+ openApiOperations: ["GET /api/v1/projects/{name}/ga/coverage"],
1990
+ handler: (client, input) => client.gaCoverage(input.project)
1991
+ }),
1992
+ defineTool({
1993
+ name: "canonry_ga_ai_referral_history",
1994
+ title: "Get GA AI referral detail rows",
1995
+ description: "Get raw AI referral rows per day, landing page, and attribution dimension. Detail, not totals: use canonry_ga_ai_referral_daily for session counts.",
1996
+ access: "read",
1997
+ tier: "ga",
1998
+ inputSchema: gaWindowInputSchema,
1999
+ annotations: readAnnotations(),
2000
+ openApiOperations: ["GET /api/v1/projects/{name}/ga/ai-referral-history"],
2001
+ handler: (client, input) => client.gaAiReferralHistory(input.project, compactStringParams(input, GA_RANGE_PARAMS))
2002
+ }),
2003
+ defineTool({
2004
+ name: "canonry_ga_ai_referral_daily",
2005
+ title: "Get GA AI referral sessions per day",
2006
+ description: "Get AI referral sessions per day and per source, deduplicated across attribution dimensions. Totals match the AI sessions reported by canonry_ga_traffic.",
2007
+ access: "read",
2008
+ tier: "ga",
2009
+ inputSchema: gaWindowInputSchema,
2010
+ annotations: readAnnotations(),
2011
+ openApiOperations: ["GET /api/v1/projects/{name}/ga/ai-referral-daily"],
2012
+ handler: (client, input) => client.gaAiReferralDaily(input.project, compactStringParams(input, GA_RANGE_PARAMS))
2013
+ }),
2014
+ defineTool({
2015
+ name: "canonry_ga_social_referral_history",
2016
+ title: "Get GA social referral history",
2017
+ description: "Get social referral sessions per day grouped by source.",
2018
+ access: "read",
2019
+ tier: "ga",
2020
+ inputSchema: gaWindowInputSchema,
2021
+ annotations: readAnnotations(),
2022
+ openApiOperations: ["GET /api/v1/projects/{name}/ga/social-referral-history"],
2023
+ handler: (client, input) => client.gaSocialReferralHistory(input.project, compactStringParams(input, GA_RANGE_PARAMS))
2024
+ }),
2025
+ defineTool({
2026
+ name: "canonry_ga_social_referral_trend",
2027
+ title: "Get GA social referral trend",
2028
+ description: "Get social referral trend with biggest mover for a Canonry project.",
2029
+ access: "read",
2030
+ tier: "ga",
2031
+ inputSchema: projectInputSchema,
2032
+ annotations: readAnnotations(),
2033
+ openApiOperations: ["GET /api/v1/projects/{name}/ga/social-referral-trend"],
2034
+ handler: (client, input) => client.gaSocialReferralTrend(input.project)
2035
+ }),
2036
+ defineTool({
2037
+ name: "canonry_ga_attribution_trend",
2038
+ title: "Get GA attribution trend",
2039
+ description: "Get per-channel attribution trends for organic, AI, social, direct, and total sessions.",
2040
+ access: "read",
2041
+ tier: "ga",
2042
+ inputSchema: projectInputSchema,
2043
+ annotations: readAnnotations(),
2044
+ openApiOperations: ["GET /api/v1/projects/{name}/ga/attribution-trend"],
2045
+ handler: (client, input) => client.gaAttributionTrend(input.project)
2046
+ }),
2047
+ defineTool({
2048
+ name: "canonry_ga_session_history",
2049
+ title: "Get GA session history",
2050
+ description: "Get total sessions per day for a Canonry project.",
2051
+ access: "read",
2052
+ tier: "ga",
2053
+ inputSchema: gaWindowInputSchema,
2054
+ annotations: readAnnotations(),
2055
+ openApiOperations: ["GET /api/v1/projects/{name}/ga/session-history"],
2056
+ handler: (client, input) => client.gaSessionHistory(input.project, compactStringParams(input, GA_RANGE_PARAMS))
2057
+ }),
2058
+ // ----- Google Business Profile (Phase 1: auth + discovery) -----
2059
+ defineTool({
2060
+ name: "canonry_gbp_accounts",
2061
+ title: "List Google Business Profile accounts",
2062
+ description: "List the Google Business Profile accounts the connected OAuth user can access. Use this to pick which account a project should track via canonry_gbp_locations_discover (accountName).",
2063
+ access: "read",
2064
+ tier: "gbp",
2065
+ inputSchema: gbpAccountsInputSchema,
2066
+ annotations: readAnnotations(),
2067
+ openApiOperations: ["GET /api/v1/projects/{name}/gbp/accounts"],
2068
+ handler: (client, input) => client.listGbpAccounts(input.project)
2069
+ }),
2070
+ defineTool({
2071
+ name: "canonry_gbp_locations",
2072
+ title: "List Google Business Profile locations",
2073
+ description: "List discovered Google Business Profile locations for a Canonry project, including their selection state.",
2074
+ access: "read",
2075
+ tier: "gbp",
2076
+ inputSchema: gbpListLocationsInputSchema,
2077
+ annotations: readAnnotations(),
2078
+ openApiOperations: ["GET /api/v1/projects/{name}/gbp/locations"],
2079
+ handler: (client, input) => client.listGbpLocations(input.project, input.selected === void 0 ? void 0 : { selected: input.selected })
2080
+ }),
2081
+ defineTool({
2082
+ name: "canonry_gbp_locations_discover",
2083
+ title: "Discover Google Business Profile locations",
2084
+ description: 'Re-discover Google Business Profile locations from Google and upsert them. New locations get the default selection state from `selectAllNew`; existing locations keep their selection. Pass `accountName` ("accounts/{n}", from canonry_gbp_accounts) to target a specific account; switching a project to a DIFFERENT account is destructive and requires `switchAccount: true`.',
2085
+ access: "write",
2086
+ tier: "gbp",
2087
+ inputSchema: gbpDiscoverInputSchema,
2088
+ annotations: writeAnnotations({ idempotentHint: true }),
2089
+ openApiOperations: ["POST /api/v1/projects/{name}/gbp/locations/discover"],
2090
+ handler: (client, input) => client.discoverGbpLocations(input.project, {
2091
+ selectAllNew: input.selectAllNew,
2092
+ accountName: input.accountName,
2093
+ switchAccount: input.switchAccount
2094
+ })
2095
+ }),
2096
+ defineTool({
2097
+ name: "canonry_gbp_location_select",
2098
+ title: "Toggle GBP location selection",
2099
+ description: "Mark a Google Business Profile location as selected or deselected for sync.",
2100
+ access: "write",
2101
+ tier: "gbp",
2102
+ inputSchema: gbpLocationSelectionInputSchema,
2103
+ annotations: writeAnnotations({ idempotentHint: true }),
2104
+ openApiOperations: ["PUT /api/v1/projects/{name}/gbp/locations/{locationName}/selection"],
2105
+ handler: (client, input) => client.setGbpLocationSelection(input.project, input.locationName, input.selected)
2106
+ }),
2107
+ defineTool({
2108
+ name: "canonry_gbp_disconnect",
2109
+ title: "Disconnect Google Business Profile",
2110
+ description: "Remove the Google Business Profile OAuth connection and all discovered locations for a project.",
2111
+ access: "write",
2112
+ tier: "gbp",
2113
+ inputSchema: projectInputSchema,
2114
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
2115
+ openApiOperations: ["DELETE /api/v1/projects/{name}/gbp/connection"],
2116
+ handler: (client, input) => client.disconnectGbp(input.project)
2117
+ }),
2118
+ // ----- Google Business Profile (Phase 2: performance sync) -----
2119
+ defineTool({
2120
+ name: "canonry_gbp_sync",
2121
+ title: "Sync Google Business Profile performance",
2122
+ description: "Trigger a GBP performance sync (daily metrics + monthly keyword impressions) for the project's selected locations. Returns the run id; poll canonry_run_get for status.",
2123
+ access: "write",
2124
+ tier: "gbp",
2125
+ inputSchema: gbpSyncInputSchema,
2126
+ annotations: writeAnnotations({ idempotentHint: true }),
2127
+ openApiOperations: ["POST /api/v1/projects/{name}/gbp/sync"],
2128
+ handler: (client, input) => client.triggerGbpSync(input.project, {
2129
+ locationNames: input.locationNames,
2130
+ daysOfMetrics: input.daysOfMetrics,
2131
+ monthsOfKeywords: input.monthsOfKeywords
2132
+ })
2133
+ }),
2134
+ defineTool({
2135
+ name: "canonry_gbp_metrics",
2136
+ title: "Get GBP daily metrics",
2137
+ description: "List stored Google Business Profile daily performance metrics (impressions, direction requests, website/call clicks) for a project.",
2138
+ access: "read",
2139
+ tier: "gbp",
2140
+ inputSchema: gbpMetricsInputSchema,
2141
+ annotations: readAnnotations(),
2142
+ openApiOperations: ["GET /api/v1/projects/{name}/gbp/metrics"],
2143
+ handler: (client, input) => client.listGbpMetrics(input.project, compactStringParams(input, ["locationName", "metric"]))
2144
+ }),
2145
+ defineTool({
2146
+ name: "canonry_gbp_keywords",
2147
+ title: "Get GBP keyword impressions",
2148
+ description: "List stored Google Business Profile monthly search-keyword impressions for a project. Includes a thresholdedPct (share privacy-redacted by Google).",
2149
+ access: "read",
2150
+ tier: "gbp",
2151
+ inputSchema: gbpLocationScopedInputSchema,
2152
+ annotations: readAnnotations(),
2153
+ openApiOperations: ["GET /api/v1/projects/{name}/gbp/keywords"],
2154
+ handler: (client, input) => client.listGbpKeywords(input.project, compactStringParams(input, ["locationName"]))
2155
+ }),
2156
+ defineTool({
2157
+ name: "canonry_gbp_place_actions",
2158
+ title: "Get GBP place actions",
2159
+ description: "List stored Google Business Profile place action links (booking / reservation / order CTAs) for a project.",
2160
+ access: "read",
2161
+ tier: "gbp",
2162
+ inputSchema: gbpLocationScopedInputSchema,
2163
+ annotations: readAnnotations(),
2164
+ openApiOperations: ["GET /api/v1/projects/{name}/gbp/place-actions"],
2165
+ handler: (client, input) => client.listGbpPlaceActions(input.project, compactStringParams(input, ["locationName"]))
2166
+ }),
2167
+ defineTool({
2168
+ name: "canonry_gbp_lodging",
2169
+ title: "Get GBP lodging attributes",
2170
+ description: 'List the latest Google Business Profile lodging snapshot per location (hotel structured attributes). populatedGroupCount=0 means the Lodging API returned no readable structured groups, which can happen even for complete hotels whose owner-facing "Hotel details" panel has amenities set. Treat it as a "verify the Hotel details panel", not a confirmed gap.',
2171
+ access: "read",
2172
+ tier: "gbp",
2173
+ inputSchema: gbpLocationScopedInputSchema,
2174
+ annotations: readAnnotations(),
2175
+ openApiOperations: ["GET /api/v1/projects/{name}/gbp/lodging"],
2176
+ handler: (client, input) => client.listGbpLodging(input.project, compactStringParams(input, ["locationName"]))
2177
+ }),
2178
+ defineTool({
2179
+ name: "canonry_gbp_attributes",
2180
+ title: "Get GBP owner-set attributes",
2181
+ description: "List the latest Google Business Profile owner-set attribute snapshot per location: the generic amenity / service / accessibility / identity / social-URL tags the owner has set (e.g. has_onsite_services, offers_online_estimates, is_owned_by_women, wheelchair accessibility, url_instagram). Works for every business category, unlike canonry_gbp_lodging (hotels only). Returns only attributes the owner has set, so attributeCount is a real, owner-readable completeness signal (not a verify-nudge).",
2182
+ access: "read",
2183
+ tier: "gbp",
2184
+ inputSchema: gbpLocationScopedInputSchema,
2185
+ annotations: readAnnotations(),
2186
+ openApiOperations: ["GET /api/v1/projects/{name}/gbp/attributes"],
2187
+ handler: (client, input) => client.listGbpAttributes(input.project, compactStringParams(input, ["locationName"]))
2188
+ }),
2189
+ defineTool({
2190
+ name: "canonry_gbp_places",
2191
+ title: "Get GBP Places rendered-listing data",
2192
+ description: "List the latest Google Places (New) snapshot per location \u2014 the amenities Google's public listing advertises (server-derived `amenities`), cross-referenced against the GBP profile to surface listing discrepancies. Empty until a Places API key is configured and a gbp sync runs.",
2193
+ access: "read",
2194
+ tier: "gbp",
2195
+ inputSchema: gbpLocationScopedInputSchema,
2196
+ annotations: readAnnotations(),
2197
+ openApiOperations: ["GET /api/v1/projects/{name}/gbp/places"],
2198
+ handler: (client, input) => client.listGbpPlaces(input.project, compactStringParams(input, ["locationName"]))
2199
+ }),
2200
+ defineTool({
2201
+ name: "canonry_gbp_summary",
2202
+ title: "Get GBP local-AEO summary",
2203
+ description: "Composite Google Business Profile summary for a project: performance totals + 7d deltas, keyword thresholded %, place-action CTA presence, Lodging API readable-group counts, and owner-content profile completeness (how many selected locations populate secondary categories / description / service area / hours / phone, plus closed-status counts). All derived numbers computed server-side.",
2204
+ access: "read",
2205
+ tier: "gbp",
2206
+ inputSchema: gbpLocationScopedInputSchema,
2207
+ annotations: readAnnotations(),
2208
+ openApiOperations: ["GET /api/v1/projects/{name}/gbp/summary"],
2209
+ handler: (client, input) => client.getGbpSummary(input.project, compactStringParams(input, ["locationName"]))
2210
+ }),
2211
+ defineTool({
2212
+ name: "canonry_traffic_sources_list",
2213
+ title: "List traffic sources",
2214
+ description: "List server-side traffic sources for a Canonry project (Cloud Run, etc.). Returns non-archived sources with status, last sync timestamp, last error, and the stored config (gcpProjectId, serviceName, location, authMode). Pair with canonry_traffic_source_get for last-24h totals on a single source.",
2215
+ access: "read",
2216
+ tier: "traffic",
2217
+ inputSchema: projectInputSchema,
2218
+ annotations: readAnnotations(),
2219
+ openApiOperations: ["GET /api/v1/projects/{name}/traffic/sources"],
2220
+ handler: (client, input) => client.trafficListSources(input.project)
2221
+ }),
2222
+ defineTool({
2223
+ name: "canonry_traffic_source_get",
2224
+ title: "Get traffic source detail",
2225
+ description: "Get one traffic source plus 24h totals (crawler hits, AI-referral sessions, raw event sample count) and the latest traffic-sync run summary. Use to confirm a source is healthy and observing traffic before drilling into events.",
2226
+ access: "read",
2227
+ tier: "traffic",
2228
+ inputSchema: trafficSourceIdInputSchema,
2229
+ annotations: readAnnotations(),
2230
+ openApiOperations: ["GET /api/v1/projects/{name}/traffic/sources/{id}"],
2231
+ handler: (client, input) => client.trafficGetSource(input.project, input.sourceId)
2232
+ }),
2233
+ defineTool({
2234
+ name: "canonry_traffic_status",
2235
+ title: "Traffic status (all sources)",
2236
+ description: "Single-call composite returning every non-archived traffic source plus its last-24h totals (crawler hits, AI-referral sessions, sample count) and latest source-scoped traffic-sync run. Same per-entry shape as canonry_traffic_source_get, but one call covers all sources \u2014 prefer this over a list+per-source fan-out.",
2237
+ access: "read",
2238
+ tier: "traffic",
2239
+ inputSchema: projectInputSchema,
2240
+ annotations: readAnnotations(),
2241
+ openApiOperations: ["GET /api/v1/projects/{name}/traffic/status"],
2242
+ handler: (client, input) => client.trafficStatus(input.project)
2243
+ }),
2244
+ defineTool({
2245
+ name: "canonry_traffic_events",
2246
+ title: "List traffic events",
2247
+ description: "Read crawler, AI user-fetch, and AI-referral rollups from server-side traffic sources. Returns complete full-window chart series and totals plus a capped discriminated detail list with explicit truncation metadata. Window defaults to last 24h.",
2248
+ access: "read",
2249
+ tier: "traffic",
2250
+ inputSchema: trafficEventsInputSchema,
2251
+ annotations: readAnnotations(),
2252
+ openApiOperations: ["GET /api/v1/projects/{name}/traffic/events"],
2253
+ handler: (client, input) => {
2254
+ const params = {};
2255
+ if (input.since) params.since = input.since;
2256
+ if (input.until) params.until = input.until;
2257
+ if (input.kind) params.kind = input.kind;
2258
+ if (input.sourceId) params.sourceId = input.sourceId;
2259
+ if (input.limit !== void 0) params.limit = input.limit;
2260
+ if (input.granularity) params.granularity = input.granularity;
2261
+ return client.trafficListEvents(input.project, params);
2262
+ }
2263
+ }),
2264
+ defineTool({
2265
+ name: "canonry_traffic_connect_cloud_run",
2266
+ title: "Connect Cloud Run traffic source",
2267
+ description: "Connect a Google Cloud Run service as a server-side traffic source. v1 requires service-account JSON content (paste the file contents into `keyJson`); OAuth-mode is not yet supported. Reconnecting an existing source updates the credential and config in place. The private key is stored in ~/.canonry/config.yaml (not the DB) and never echoed back.",
2268
+ access: "write",
2269
+ tier: "traffic",
2270
+ inputSchema: trafficConnectCloudRunInputSchema,
2271
+ annotations: writeAnnotations({ idempotentHint: true, openWorldHint: true }),
2272
+ openApiOperations: ["POST /api/v1/projects/{name}/traffic/connect/cloud-run"],
2273
+ handler: (client, input) => client.trafficConnectCloudRun(input.project, input.request)
2274
+ }),
2275
+ defineTool({
2276
+ name: "canonry_traffic_connect_wordpress",
2277
+ title: "Connect WordPress traffic-logger source",
2278
+ description: "Connect a WordPress site (running the canonry traffic-logger plugin) as a server-side traffic source. Probes the plugin endpoint with the supplied Application Password before persisting; a bad credential or unreachable host surfaces as a 502 error. Reconnecting the same endpoint updates the active source in place. Changing baseUrl archives the old lineage and creates a fresh source so rollups cannot mix across endpoints. The Application Password is stored in ~/.canonry/config.yaml (not the DB) and never echoed back.",
2279
+ access: "write",
2280
+ tier: "traffic",
2281
+ inputSchema: trafficConnectWordpressInputSchema,
2282
+ annotations: writeAnnotations({ idempotentHint: true, openWorldHint: true }),
2283
+ openApiOperations: ["POST /api/v1/projects/{name}/traffic/connect/wordpress"],
2284
+ handler: (client, input) => client.trafficConnectWordpress(input.project, input.request)
2285
+ }),
2286
+ defineTool({
2287
+ name: "canonry_traffic_connect_vercel",
2288
+ title: "Connect Vercel traffic source",
2289
+ description: "Connect a Vercel project as a server-side traffic source. Pulls request logs from Vercel's internal request-logs endpoint \u2014 no in-app instrumentation needed. Probes the endpoint with the supplied API token before persisting \u2014 a bad token or wrong project / team id surfaces as a 502 error. Reconnecting updates the existing active Vercel source in place. The API token is stored in ~/.canonry/config.yaml (not the DB) and never echoed back.",
2290
+ access: "write",
2291
+ tier: "traffic",
2292
+ inputSchema: trafficConnectVercelInputSchema,
2293
+ annotations: writeAnnotations({ idempotentHint: true, openWorldHint: true }),
2294
+ openApiOperations: ["POST /api/v1/projects/{name}/traffic/connect/vercel"],
2295
+ handler: (client, input) => client.trafficConnectVercel(input.project, input.request)
2296
+ }),
2297
+ defineTool({
2298
+ name: "canonry_traffic_sync",
2299
+ title: "Sync traffic source",
2300
+ description: "Pull a Cloud Run, WordPress, Vercel, or Cloudflare Queue traffic source, classify crawler / AI-referral / unknown traffic, and upsert hourly rollups plus raw samples. Time-window pulls clamp forward to lastSyncedAt. WordPress uses a fixed bounded window, defaults a new or idle source to 365d, validates the returned events remain inside that window, and serializes syncs with a source lease.",
2301
+ access: "write",
2302
+ tier: "traffic",
2303
+ inputSchema: trafficSyncInputSchema,
2304
+ annotations: writeAnnotations({ idempotentHint: false, openWorldHint: true }),
2305
+ openApiOperations: ["POST /api/v1/projects/{name}/traffic/sources/{id}/sync"],
2306
+ handler: (client, input) => client.trafficSync(input.project, input.sourceId, input.sinceMinutes !== void 0 ? { sinceMinutes: input.sinceMinutes } : void 0)
2307
+ }),
2308
+ defineTool({
2309
+ name: "canonry_traffic_backfill",
2310
+ title: "Backfill traffic source",
2311
+ description: 'Async one-shot reclassification for Cloud Run or Vercel traffic. It replaces the selected window\u2019s rollups with a fresh bounded pull and current classifier output. WordPress generic replace backfill is unavailable because the endpoint cannot prove retained coverage; use a retention-aware repair that explicitly declares any unrecoverable span. Returns `{ runId, status: "running" }`; poll canonry_run_get for completion.',
2312
+ access: "write",
2313
+ tier: "traffic",
2314
+ inputSchema: trafficBackfillInputSchema,
2315
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true, openWorldHint: true }),
2316
+ openApiOperations: ["POST /api/v1/projects/{name}/traffic/sources/{id}/backfill"],
2317
+ handler: (client, input) => client.trafficBackfill(input.project, input.sourceId, input.days !== void 0 ? { days: input.days } : void 0)
2318
+ }),
2319
+ defineTool({
2320
+ name: "canonry_traffic_reset",
2321
+ title: "Advance traffic source lastSyncedAt to NOW",
2322
+ description: "Operator recovery for a stuck traffic source. Advances `lastSyncedAt` to NOW, sets `status` back to `connected`, and clears `last_error`. A WordPress reset also clears its continuation state and records an unrecovered span so its next bounded drain cannot combine an old cursor with a new window. That stops replay but is not historical repair. Generic WordPress replace-mode backfill is unavailable because retained coverage is unproven; use a retention-aware repair that explicitly declares any unrecoverable span. Archived sources are rejected; reconnect them with the appropriate canonry_traffic_connect_* tool.",
2323
+ access: "write",
2324
+ tier: "traffic",
2325
+ inputSchema: trafficResetInputSchema,
2326
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
2327
+ openApiOperations: ["POST /api/v1/projects/{name}/traffic/sources/{id}/reset"],
2328
+ handler: (client, input) => client.trafficReset(input.project, input.sourceId)
2329
+ }),
2330
+ defineTool({
2331
+ name: "canonry_snapshot",
2332
+ title: "Generate a prospect snapshot",
2333
+ description: "Generate a one-shot AI perception report for a company and domain without creating a project. Spends provider quota and fetches the site. Optional providers and providerMode constrain both answers and analysis; omitted selection uses all configured providers. Browser-only selection requires manual queries.",
2334
+ access: "write",
2335
+ tier: "discovery",
2336
+ inputSchema: snapshotRequestInputSchema,
2337
+ annotations: writeAnnotations({ idempotentHint: false, openWorldHint: true }),
2338
+ openApiOperations: ["POST /api/v1/snapshot"],
2339
+ handler: (client, input) => client.createSnapshot(input)
2340
+ }),
2341
+ defineTool({
2342
+ name: "canonry_project_upsert",
2343
+ title: "Create or replace project",
2344
+ description: "Create or replace a Canonry project. PUT semantics \u2014 fields not in the request are reset to their defaults. Provide the full intended project shape.",
2345
+ access: "write",
2346
+ tier: "setup",
2347
+ inputSchema: projectUpsertInputSchema,
2348
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
2349
+ openApiOperations: ["PUT /api/v1/projects/{name}"],
2350
+ handler: (client, input) => client.putProject(input.project, input.request)
2351
+ }),
2352
+ defineTool({
2353
+ name: "canonry_apply_config",
2354
+ title: "Apply project config",
2355
+ description: "Apply one Canonry config-as-code project document. Replaces the project to match the config \u2014 fields omitted from the spec are reset to defaults, with one exception: a spec with neither queries nor keywords leaves the tracked-query basket unchanged. To clear the basket, pass an explicit empty queries list. For multi-document YAML, call this tool once per project document.",
2356
+ access: "write",
2357
+ tier: "core",
2358
+ inputSchema: applyConfigInputSchema,
2359
+ // Declarative apply is safe to repeat, but it replaces configured child state.
2360
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
2361
+ openApiOperations: ["POST /api/v1/apply"],
2362
+ handler: (client, input) => client.apply(input.config)
2363
+ }),
2364
+ defineTool({
2365
+ name: "canonry_queries_generate",
2366
+ title: "Generate query suggestions",
2367
+ description: "Generate candidate queries using a configured provider. Returns suggestions only; use canonry_queries_add to persist them.",
2368
+ access: "write",
2369
+ tier: "setup",
2370
+ inputSchema: queryGenerateInputSchema,
2371
+ annotations: writeAnnotations({ idempotentHint: false, openWorldHint: true }),
2372
+ openApiOperations: ["POST /api/v1/projects/{name}/queries/generate"],
2373
+ handler: (client, input) => client.generateQueries(input.project, input.request.provider, input.request.count)
2374
+ }),
2375
+ defineTool({
2376
+ name: "canonry_keywords_generate",
2377
+ title: "Generate keyword suggestions (legacy alias)",
2378
+ description: "Legacy alias for canonry_queries_generate. Returns suggestions using the pre-queries keyword response shape.",
2379
+ access: "write",
2380
+ tier: "setup",
2381
+ inputSchema: keywordGenerateInputSchema,
2382
+ annotations: writeAnnotations({ idempotentHint: false, openWorldHint: true }),
2383
+ openApiOperations: ["POST /api/v1/projects/{name}/keywords/generate"],
2384
+ handler: (client, input) => client.generateKeywords(input.project, input.request.provider, input.request.count)
2385
+ }),
2386
+ defineTool({
2387
+ name: "canonry_queries_replace",
2388
+ title: "Replace queries",
2389
+ description: "Replace the tracked query set for a Canonry project.",
2390
+ access: "write",
2391
+ tier: "setup",
2392
+ inputSchema: queriesInputSchema,
2393
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
2394
+ openApiOperations: ["PUT /api/v1/projects/{name}/queries"],
2395
+ handler: async (client, input) => {
2396
+ await client.putQueries(input.project, uniqueStrings(input.request.queries));
2397
+ }
2398
+ }),
2399
+ defineTool({
2400
+ name: "canonry_queries_replace_preview",
2401
+ title: "Preview query replace",
2402
+ description: "Preview the impact of replacing a project's tracked query set: current vs proposed, added/removed/unchanged diff, and the count of snapshots that would detach (queryId \u2192 NULL; queryText preserved). Read-only.",
2403
+ access: "read",
2404
+ tier: "setup",
2405
+ inputSchema: queriesInputSchema,
2406
+ annotations: readAnnotations(),
2407
+ openApiOperations: ["POST /api/v1/projects/{name}/queries/replace-preview"],
2408
+ handler: (client, input) => client.previewReplaceQueries(input.project, uniqueStrings(input.request.queries))
2409
+ }),
2410
+ defineTool({
2411
+ name: "canonry_keywords_replace",
2412
+ title: "Replace keywords (legacy alias)",
2413
+ description: "Legacy alias for canonry_queries_replace. Replaces the same canonical tracked query set.",
2414
+ access: "write",
2415
+ tier: "setup",
2416
+ inputSchema: keywordsInputSchema,
2417
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
2418
+ openApiOperations: ["PUT /api/v1/projects/{name}/keywords"],
2419
+ handler: async (client, input) => {
2420
+ await client.putKeywords(input.project, uniqueStrings(input.request.keywords));
2421
+ }
2422
+ }),
2423
+ defineTool({
2424
+ name: "canonry_measurement_discovery",
2425
+ title: "Discover measurement Targets",
2426
+ description: "Fetch a public sitemap under bounded network policy and deterministically classify project-owned URLs into Target proposals, aliases, shared paths, unmatched paths, and exclusions. Does not publish a plan.",
2427
+ access: "write",
2428
+ tier: "setup",
2429
+ inputSchema: measurementDiscoveryInputSchema,
2430
+ annotations: writeAnnotations({ idempotentHint: false, openWorldHint: true }),
2431
+ openApiOperations: ["POST /api/v1/projects/{name}/measurement-discovery"],
2432
+ handler: (client, input) => client.discoverMeasurementTargets(input.project, {
2433
+ sitemapUrl: input.sitemapUrl,
2434
+ rule: input.rule,
2435
+ ...input.maxUrls === void 0 ? {} : { maxUrls: input.maxUrls }
2436
+ })
2437
+ }),
2438
+ defineTool({
2439
+ name: "canonry_measurement_plan_get",
2440
+ title: "Get measurement plan",
2441
+ description: "Get the active measurement plan for a project.",
2442
+ access: "read",
2443
+ tier: "setup",
2444
+ inputSchema: projectInputSchema,
2445
+ annotations: readAnnotations(),
2446
+ openApiOperations: ["GET /api/v1/projects/{name}/measurement-plan"],
2447
+ handler: (client, input) => client.getMeasurementPlan(input.project)
2448
+ }),
2449
+ defineTool({
2450
+ name: "canonry_measurement_plan_versions",
2451
+ title: "List measurement plan versions",
2452
+ description: "List immutable measurement-plan revisions.",
2453
+ access: "read",
2454
+ tier: "setup",
2455
+ inputSchema: projectInputSchema,
2456
+ annotations: readAnnotations(),
2457
+ openApiOperations: ["GET /api/v1/projects/{name}/measurement-plan/versions"],
2458
+ handler: (client, input) => client.listMeasurementPlanVersions(input.project)
2459
+ }),
2460
+ defineTool({
2461
+ name: "canonry_measurement_plan_version_get",
2462
+ title: "Get measurement plan revision",
2463
+ description: "Get one immutable measurement-plan revision.",
2464
+ access: "read",
2465
+ tier: "setup",
2466
+ inputSchema: measurementPlanVersionInputSchema,
2467
+ annotations: readAnnotations(),
2468
+ openApiOperations: ["GET /api/v1/projects/{name}/measurement-plan/versions/{revision}"],
2469
+ handler: (client, input) => client.getMeasurementPlanVersion(input.project, input.revision)
2470
+ }),
2471
+ defineTool({
2472
+ name: "canonry_measurement_plan_compile_preview",
2473
+ title: "Compile measurement plan",
2474
+ description: "Validate and compile a Target/group plan without publishing it.",
2475
+ access: "write",
2476
+ tier: "setup",
2477
+ inputSchema: measurementPlanPreviewInputSchema,
2478
+ annotations: writeAnnotations({ idempotentHint: true }),
2479
+ openApiOperations: ["POST /api/v1/projects/{name}/measurement-plan/compile-preview"],
2480
+ handler: (client, input) => client.compileMeasurementPlanPreview(input.project, input.plan)
2481
+ }),
2482
+ defineTool({
2483
+ name: "canonry_measurement_plan_diff_preview",
2484
+ title: "Preview measurement plan changes",
2485
+ description: "Compare a compiled Target/group plan with the active immutable revision without publishing it.",
2486
+ access: "write",
2487
+ tier: "setup",
2488
+ inputSchema: measurementPlanPreviewInputSchema,
2489
+ annotations: writeAnnotations({ idempotentHint: true }),
2490
+ openApiOperations: ["POST /api/v1/projects/{name}/measurement-plan/diff-preview"],
2491
+ handler: (client, input) => client.diffMeasurementPlanPreview(input.project, input.plan)
2492
+ }),
2493
+ defineTool({
2494
+ name: "canonry_measurement_plan_publish",
2495
+ title: "Publish measurement plan (legacy v1)",
2496
+ description: "Legacy schema-v1 publish. For Advanced Measurement use canonry_measurement_draft_action instead: this tool compiles only schema v1 and refuses when the active revision is schema v2 rather than downgrade it. Publish only when the active revision still matches the revision you reviewed. Use null only when you reviewed a planless project.",
2497
+ access: "write",
2498
+ tier: "setup",
2499
+ inputSchema: measurementPlanPublishInputSchema,
2500
+ annotations: writeAnnotations({ idempotentHint: true }),
2501
+ openApiOperations: ["PUT /api/v1/projects/{name}/measurement-plan"],
2502
+ handler: (client, input) => client.publishMeasurementPlan(input.project, {
2503
+ expectedActiveRevision: input.expectedActiveRevision,
2504
+ plan: input.plan
2505
+ })
2506
+ }),
2507
+ defineTool({
2508
+ name: "canonry_measurement_plan_segment_retire",
2509
+ title: "Retire measurement segment",
2510
+ description: "Permanently retire an inactive Target or group stable key. Publish a revision without it first; there is no unretire.",
2511
+ access: "write",
2512
+ tier: "setup",
2513
+ inputSchema: measurementPlanRetireInputSchema,
2514
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
2515
+ openApiOperations: ["POST /api/v1/projects/{name}/measurement-plan/segments/{stableKey}/retire"],
2516
+ handler: (client, input) => client.retireMeasurementPlanSegment(input.project, input.stableKey)
2517
+ }),
2518
+ defineTool({
2519
+ name: "canonry_measurement_setup",
2520
+ title: "Get Advanced Measurement setup state",
2521
+ description: "Return the project\u2019s stored Advanced Measurement setup state, mode, active revision, draft freshness, and next action. It never starts provider work or incurs provider cost, and refuses an unknown project.",
2522
+ access: "read",
2523
+ tier: "setup",
2524
+ inputSchema: projectInputSchema,
2525
+ annotations: readAnnotations(),
2526
+ openApiOperations: ["GET /api/v1/projects/{name}/measurement-setup"],
2527
+ handler: (client, input) => client.getMeasurementSetup(input.project)
2528
+ }),
2529
+ defineTool({
2530
+ name: "canonry_measurement_overview",
2531
+ title: "Get Advanced Measurement overview",
2532
+ description: "For best/worst Property mention rankings, prefer canonry_measurement_portfolio_summary and its mentionRanking. An unavailable aggregate does not invalidate available Property metrics. Return stored, revision-pinned Advanced Measurement metrics and a bounded page of Property rows for all Properties, one reporting group, or one Property. Filter by query class, provider, location, date window, run, or Property search; search filters rows without changing metric denominators. It ranks one run snapshot only and never infers a trend or compares across revisions. Choose label-asc (default), label-desc, citationCoverage-asc/desc, or mentionCoverage-asc/desc. For a coverage sort, unavailable rows form the first bucket in either direction; available rows then follow the requested numeric direction. The cursor is sort-aware, pins pagination to the active revision, displayed run, evidence snapshot, and filters even if a newer run completes, and must be reused unchanged with the same sort and filters. Legacy label cursors work only when sort is omitted, while any explicit sort needs a new sort-bound cursor. It never starts provider work or incurs provider cost; page size is at most 100, and it refuses invalid scope keys, cursor combinations, appended evidence, or a run pinned to another revision.",
2533
+ access: "read",
2534
+ tier: "setup",
2535
+ inputSchema: measurementOverviewInputSchema,
2536
+ annotations: readAnnotations(),
2537
+ openApiOperations: ["GET /api/v1/projects/{name}/measurement-overview"],
2538
+ handler: (client, input) => {
2539
+ const { project, ...query } = input;
2540
+ return client.getMeasurementOverview(project, query);
2541
+ }
2542
+ }),
2543
+ defineTool({
2544
+ name: "canonry_measurement_property_evidence",
2545
+ title: "Page one Property's measurement evidence",
2546
+ description: "Return the stored evidence rows for exactly one Property out of one revision-pinned run, optionally narrowed to a question class, provider, or location. shape chooses what a row is: sources (the default) returns one row per cited URL under evidence; answers returns one row per measured answer under answers, with the cited URLs nested inside and both signals on the row (mentioned, cited). Use shape=answers to explain a GAP \u2014 an answer that mentioned the Property without linking it, or that named nobody at all, has no URL to hang a source row on and does not appear in the default shape, so counting source rows understates what was measured. mentioned is null, never false, when the answer text was never captured: that is a missing signal, not a measured no. Exactly one of evidence or answers is returned and the other is absent rather than empty. A cursor is bound to the shape that issued it and is refused on the other. Prefer this over canonry_measurement_report when you want one Property: the report reconstructs every group and every Target for a revision and does not paginate. Run selection matches canonry_measurement_overview \u2014 the most recent completed run pinned to the active revision unless runId names another, and a run pinned to a different revision is refused rather than joined. The cursor also pins the active revision, displayed run, evidence snapshot, and filters; reuse it unchanged. Not available for a schema v1 revision, which records no question class to scope by. It never starts provider work; page size is at most 100. An empty page under measurement.state = not_measured means the Property has not been measured at all, which is NOT a measured result of zero.",
2547
+ access: "read",
2548
+ tier: "setup",
2549
+ inputSchema: measurementPropertyEvidenceInputSchema,
2550
+ annotations: readAnnotations(),
2551
+ openApiOperations: ["GET /api/v1/projects/{name}/measurement-property-evidence"],
2552
+ handler: (client, input) => {
2553
+ const { project, ...query } = input;
2554
+ return client.getMeasurementPropertyEvidence(project, query);
2555
+ }
2556
+ }),
2557
+ defineTool({
2558
+ name: "canonry_measurement_portfolio_summary",
2559
+ title: "Summarize measured Properties",
2560
+ description: "Start here for best/worst Property mention performance. Defaults to non-brand questions; state the returned queryClass and keep branded comparisons separate unless all was requested. mentionRanking.strongest and .weakest rank every Property with an available mention rate before applying limit, independently of citation availability or an unavailable portfolio aggregate. mentionRanking.excluded names every excluded Property and its reason; flag those separately, never call all mention rankings unavailable because one identity is ambiguous. Report numerator/denominator with each rate, and any unattributed count beside it: answers left out of both because their Property identity was unresolved, never not-mentioned answers. Rates may tie (stable label/key order); this is descriptive coverage, not a confidence-adjusted or unique-winner claim. weakestProperties retains combined mention/citation weaknesses with stored replacement names; markets compares every named market worst-first. Each market is scoped to the displayed run; markets may share Properties, so they never sum to portfolio totals. Reads stored data only; it never starts provider work.",
2561
+ access: "read",
2562
+ tier: "monitoring",
2563
+ inputSchema: measurementPortfolioSummaryInputSchema,
2564
+ annotations: readAnnotations(),
2565
+ openApiOperations: ["GET /api/v1/projects/{name}/measurement-portfolio-summary"],
2566
+ handler: (client, input) => {
2567
+ const { project, ...query } = input;
2568
+ return client.getMeasurementPortfolioSummary(project, query);
2569
+ }
2570
+ }),
2571
+ defineTool({
2572
+ name: "canonry_measurement_property_questions",
2573
+ title: "List a Property\u2019s stored questions",
2574
+ description: "After identifying a Property, list its stored question outcomes and use a returned resultId with canonry_measurement_question_result. Reads stored data only; it never starts provider work.",
2575
+ access: "read",
2576
+ tier: "monitoring",
2577
+ inputSchema: measurementPropertyQuestionsInputSchema,
2578
+ annotations: readAnnotations(),
2579
+ openApiOperations: ["GET /api/v1/projects/{name}/measurement-property-questions"],
2580
+ handler: (client, input) => {
2581
+ const { project, ...query } = input;
2582
+ return client.getMeasurementPropertyQuestions(project, query);
2583
+ }
2584
+ }),
2585
+ defineTool({
2586
+ name: "canonry_measurement_question_result",
2587
+ title: "Get a stored question result",
2588
+ description: "After listing a Property\u2019s questions, expand one returned resultId into its stored answer and attribution sources. Reads stored data only; it never starts provider work.",
2589
+ access: "read",
2590
+ tier: "monitoring",
2591
+ inputSchema: measurementQuestionResultInputSchema,
2592
+ annotations: readAnnotations(),
2593
+ openApiOperations: ["GET /api/v1/projects/{name}/measurement-question-result"],
2594
+ handler: (client, input) => {
2595
+ const { project, ...query } = input;
2596
+ return client.getMeasurementQuestionResult(project, query);
2597
+ }
2598
+ }),
2599
+ defineTool({
2600
+ name: "canonry_measurement_property_competitors",
2601
+ title: "List a Property\u2019s stored replacements",
2602
+ description: "After reviewing a Property\u2019s question outcomes, find repeated stored replacement names for its misses. Reads stored data only; it never starts provider work.",
2603
+ access: "read",
2604
+ tier: "monitoring",
2605
+ inputSchema: measurementPropertyCompetitorsInputSchema,
2606
+ annotations: readAnnotations(),
2607
+ openApiOperations: ["GET /api/v1/projects/{name}/measurement-property-competitors"],
2608
+ handler: (client, input) => {
2609
+ const { project, ...query } = input;
2610
+ return client.getMeasurementPropertyCompetitors(project, query);
2611
+ }
2612
+ }),
2613
+ defineTool({
2614
+ name: "canonry_measurement_changes",
2615
+ title: "Compare stored measurement changes",
2616
+ description: "After reviewing the current summary, compare stored runs only when their execution identity is compatible. Reads stored data only; it never starts provider work.",
2617
+ access: "read",
2618
+ tier: "monitoring",
2619
+ inputSchema: measurementChangesInputSchema,
2620
+ annotations: readAnnotations(),
2621
+ openApiOperations: ["GET /api/v1/projects/{name}/measurement-changes"],
2622
+ handler: (client, input) => {
2623
+ const { project, ...query } = input;
2624
+ return client.getMeasurementChanges(project, query);
2625
+ }
2626
+ }),
2627
+ defineTool({
2628
+ name: "canonry_measurement_data_quality",
2629
+ title: "Inspect stored measurement data quality",
2630
+ description: "Before acting on a measurement, inspect its stored completeness, capture, retrieval, and comparability context. Reads stored data only; it never starts provider work.",
2631
+ access: "read",
2632
+ tier: "monitoring",
2633
+ inputSchema: measurementDataQualityInputSchema,
2634
+ annotations: readAnnotations(),
2635
+ openApiOperations: ["GET /api/v1/projects/{name}/measurement-data-quality"],
2636
+ handler: (client, input) => {
2637
+ const { project, ...query } = input;
2638
+ return client.getMeasurementDataQuality(project, query);
2639
+ }
2640
+ }),
2641
+ defineTool({
2642
+ name: "canonry_measurement_draft_get",
2643
+ title: "Get Advanced Measurement draft",
2644
+ description: "Return the stored v2 draft and its current ETag, or draft:null when none exists. It never starts provider work or incurs provider cost, and refuses an unknown project.",
2645
+ access: "read",
2646
+ tier: "setup",
2647
+ inputSchema: projectInputSchema,
2648
+ annotations: readAnnotations(),
2649
+ openApiOperations: ["GET /api/v1/projects/{name}/measurement-plan/draft"],
2650
+ handler: (client, input) => client.getMeasurementPlanDraft(input.project)
2651
+ }),
2652
+ defineTool({
2653
+ name: "canonry_measurement_draft_targets",
2654
+ title: "List Advanced Measurement draft Targets",
2655
+ description: "Return one stored, cursor-paged set of draft Targets. It never starts provider work or incurs provider cost; pages are capped at 100 rows and it refuses a missing draft or unknown project.",
2656
+ access: "read",
2657
+ tier: "setup",
2658
+ inputSchema: measurementDraftCollectionInputSchema,
2659
+ annotations: readAnnotations(),
2660
+ openApiOperations: ["GET /api/v1/projects/{name}/measurement-plan/draft/targets"],
2661
+ handler: (client, input) => {
2662
+ const { project, ...query } = input;
2663
+ return client.getMeasurementDraftTargets(project, query);
2664
+ }
2665
+ }),
2666
+ defineTool({
2667
+ name: "canonry_measurement_draft_assignments",
2668
+ title: "List Advanced Measurement draft assignments",
2669
+ description: "Return one stored, cursor-paged set of draft Target-to-query assignments. It never starts provider work or incurs provider cost; pages are capped at 100 rows and it refuses a missing draft or unknown project.",
2670
+ access: "read",
2671
+ tier: "setup",
2672
+ inputSchema: measurementDraftCollectionInputSchema,
2673
+ annotations: readAnnotations(),
2674
+ openApiOperations: ["GET /api/v1/projects/{name}/measurement-plan/draft/assignments"],
2675
+ handler: (client, input) => {
2676
+ const { project, ...query } = input;
2677
+ return client.getMeasurementDraftAssignments(project, query);
2678
+ }
2679
+ }),
2680
+ defineTool({
2681
+ name: "canonry_measurement_draft_groups",
2682
+ title: "List Advanced Measurement draft groups",
2683
+ description: "Return one stored, cursor-paged set of draft reporting groups and competitors. It never starts provider work or incurs provider cost; pages are capped at 100 rows and it refuses a missing draft or unknown project.",
2684
+ access: "read",
2685
+ tier: "setup",
2686
+ inputSchema: measurementDraftCollectionInputSchema,
2687
+ annotations: readAnnotations(),
2688
+ openApiOperations: ["GET /api/v1/projects/{name}/measurement-plan/draft/groups"],
2689
+ handler: (client, input) => {
2690
+ const { project, ...query } = input;
2691
+ return client.getMeasurementDraftGroups(project, query);
2692
+ }
2693
+ }),
2694
+ defineTool({
2695
+ name: "canonry_measurement_query_sets",
2696
+ title: "List Advanced Measurement query sets",
2697
+ description: "Return stored query-set metadata for one project. It never starts provider work or incurs provider cost, and refuses an unknown project.",
2698
+ access: "read",
2699
+ tier: "setup",
2700
+ inputSchema: projectInputSchema,
2701
+ annotations: readAnnotations(),
2702
+ openApiOperations: ["GET /api/v1/projects/{name}/measurement-query-sets"],
2703
+ handler: (client, input) => client.listMeasurementQuerySets(input.project)
2704
+ }),
2705
+ defineTool({
2706
+ name: "canonry_measurement_query_set_get",
2707
+ title: "Get Advanced Measurement query set",
2708
+ description: "Return one stored query set with its ordered query members. It never starts provider work or incurs provider cost, and refuses an unknown project or query-set ID.",
2709
+ access: "read",
2710
+ tier: "setup",
2711
+ inputSchema: measurementQuerySetInputSchema,
2712
+ annotations: readAnnotations(),
2713
+ openApiOperations: ["GET /api/v1/projects/{name}/measurement-query-sets/{setId}"],
2714
+ handler: (client, input) => client.getMeasurementQuerySet(input.project, input.setId)
2715
+ }),
2716
+ defineTool({
2717
+ name: "canonry_measurement_query_templates",
2718
+ title: "List Advanced Measurement query templates",
2719
+ description: "Return stored query-template metadata for one project. It never starts provider work or incurs provider cost, and refuses an unknown project.",
2720
+ access: "read",
2721
+ tier: "setup",
2722
+ inputSchema: projectInputSchema,
2723
+ annotations: readAnnotations(),
2724
+ openApiOperations: ["GET /api/v1/projects/{name}/measurement-query-templates"],
2725
+ handler: (client, input) => client.listMeasurementQueryTemplates(input.project)
2726
+ }),
2727
+ defineTool({
2728
+ name: "canonry_measurement_draft_action",
2729
+ title: "Act on an Advanced Measurement draft",
2730
+ description: "Create, edit, inspect, publish, or discard one v2 draft. Pass project plus exactly one typed operation branch; each operation correlates its action with the required request, ETag, and idempotency key so the live schema is self-describing. Requires measurement-plan write authority. Every mutating action needs idempotencyKey: reuse it only for an identical retry; a changed request with the same key is refused. Create has no ETag. Draft edits, publish, and discard should pass the latest ETag from canonry_measurement_draft_get; a missing ETag reaches the API as actionable 428, while a stale ETag returns 412. Compile-preview and diff-preview never mutate or start provider work and require neither header. Import-sitemap performs a bounded public sitemap fetch but does not publish or run measurement; publish replaces the active plan after validation, and discard permanently removes the draft.",
2731
+ access: "write",
2732
+ tier: "setup",
2733
+ inputSchema: measurementDraftActionInputSchema,
2734
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true, openWorldHint: true }),
2735
+ openApiOperations: measurementDraftActionOpenApiOperations,
2736
+ handler: (client, input) => runMeasurementDraftAction(client, input.project, input.operation)
2737
+ }),
2738
+ defineTool({
2739
+ name: "canonry_measurement_plan_deactivate",
2740
+ title: "Deactivate Advanced Measurement plan",
2741
+ description: "Remove the active Advanced Measurement plan pointer while retaining immutable revisions. Requires measurement-plan write authority, the active revision you reviewed, and idempotencyKey; reuse that key only to retry the identical request. It does not use a draft ETag and refuses a moved or missing active plan.",
2742
+ access: "write",
2743
+ tier: "setup",
2744
+ inputSchema: measurementPlanDeactivateInputSchema,
2745
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
2746
+ openApiOperations: ["POST /api/v1/projects/{name}/measurement-plan/actions/deactivate"],
2747
+ handler: (client, input) => client.deactivateMeasurementPlan(input.project, {
2748
+ expectedActiveRevision: input.expectedActiveRevision
2749
+ }, input.idempotencyKey)
2750
+ }),
2751
+ defineTool({
2752
+ name: "canonry_measurement_query_set_upsert",
2753
+ title: "Create or replace Advanced Measurement query set",
2754
+ description: "Create or replace one query set and its ordered query references. Requires measurement-plan write authority. This is an idempotent PUT with no mutation headers; safely retry the same request. It replaces the named query-set contents but never deletes project queries or published plans.",
2755
+ access: "write",
2756
+ tier: "setup",
2757
+ inputSchema: measurementQuerySetUpsertInputSchema,
2758
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
2759
+ openApiOperations: ["PUT /api/v1/projects/{name}/measurement-query-sets/{setId}"],
2760
+ handler: (client, input) => client.upsertMeasurementQuerySet(input.project, input.setId, input.request)
2761
+ }),
2762
+ defineTool({
2763
+ name: "canonry_measurement_query_set_delete",
2764
+ title: "Delete Advanced Measurement query set",
2765
+ description: "Delete one query set without deleting its project queries or published plans. Requires measurement-plan write authority. DELETE is idempotent and uses no mutation headers; retrying the same request is safe, while an unknown query-set ID is refused.",
2766
+ access: "write",
2767
+ tier: "setup",
2768
+ inputSchema: measurementQuerySetInputSchema,
2769
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
2770
+ openApiOperations: ["DELETE /api/v1/projects/{name}/measurement-query-sets/{setId}"],
2771
+ handler: (client, input) => client.deleteMeasurementQuerySet(input.project, input.setId)
2772
+ }),
2773
+ defineTool({
2774
+ name: "canonry_measurement_query_template_upsert",
2775
+ title: "Create or replace Advanced Measurement query template",
2776
+ description: "Create or replace one query template. Requires measurement-plan write authority. This is an idempotent PUT with no mutation headers; safely retry the same request. It replaces only the named template and does not create queries until an explicit apply.",
2777
+ access: "write",
2778
+ tier: "setup",
2779
+ inputSchema: measurementQueryTemplateUpsertInputSchema,
2780
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
2781
+ openApiOperations: ["PUT /api/v1/projects/{name}/measurement-query-templates/{templateId}"],
2782
+ handler: (client, input) => client.upsertMeasurementQueryTemplate(input.project, input.templateId, input.request)
2783
+ }),
2784
+ defineTool({
2785
+ name: "canonry_measurement_query_template_delete",
2786
+ title: "Delete Advanced Measurement query template",
2787
+ description: "Delete one query template without deleting previously expanded project queries. Requires measurement-plan write authority. DELETE is idempotent and uses no mutation headers; retrying the same request is safe, while an unknown template ID is refused.",
2788
+ access: "write",
2789
+ tier: "setup",
2790
+ inputSchema: measurementQueryTemplateInputSchema,
2791
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
2792
+ openApiOperations: ["DELETE /api/v1/projects/{name}/measurement-query-templates/{templateId}"],
2793
+ handler: (client, input) => client.deleteMeasurementQueryTemplate(input.project, input.templateId)
2794
+ }),
2795
+ defineTool({
2796
+ name: "canonry_measurement_query_template_apply",
2797
+ title: "Apply Advanced Measurement query template",
2798
+ description: "Expand one query template into project queries and optionally add them to a query set. Requires measurement-plan write authority and idempotencyKey; reuse that key only to retry the identical apply. This additive action returns created and already-existing queries, does not use a draft ETag, and never removes prior queries.",
2799
+ access: "write",
2800
+ tier: "setup",
2801
+ inputSchema: measurementQueryTemplateApplyInputSchema,
2802
+ annotations: writeAnnotations({ idempotentHint: true }),
2803
+ openApiOperations: ["POST /api/v1/projects/{name}/measurement-query-templates/{templateId}/apply"],
2804
+ handler: (client, input) => client.applyMeasurementQueryTemplate(
2805
+ input.project,
2806
+ input.templateId,
2807
+ input.request,
2808
+ input.idempotencyKey
2809
+ )
2810
+ }),
2811
+ defineTool({
2812
+ name: "canonry_measurement_report",
2813
+ title: "Get measurement report",
2814
+ description: "Get a revision-pinned Target and group measurement report from stored runs. Optionally pin the evidence to an exact eligible full run. Never starts live provider work.",
2815
+ access: "read",
2816
+ tier: "setup",
2817
+ inputSchema: measurementReportInputSchema,
2818
+ annotations: readAnnotations(),
2819
+ openApiOperations: ["GET /api/v1/projects/{name}/measurement-report"],
2820
+ handler: (client, input) => client.getMeasurementReport(input.project, input.revision, input.runId)
2821
+ }),
2822
+ defineTool({
2823
+ name: "canonry_run_trigger",
2824
+ title: "Trigger run",
2825
+ description: "Trigger an answer-visibility run for a Canonry project. Omit both scoping fields for a full sweep. Pass request.queries[] to measure only some of the project's tracked questions. On a project with a published measurement plan, pass request.measurementScope with at least one group or target to measure one slice of that plan \u2014 a group stands for its member Targets, and only the questions those Targets selected are measured. A scope naming nothing is rejected rather than treated as a full sweep, so omit the field entirely when you want everything. Either kind of slice is recorded as a spot check: it never replaces a full sweep in the dashboard, analytics or report. For verification scenarios (testing whether a provider migration worked, reproducing a regression, sanity-checking after a code change), set request.trigger='probe' \u2014 probe runs write a snapshot you can inspect via canonry_runs_get but are EXCLUDED from dashboard, analytics, intelligence, report, and notifications. Use 'probe' whenever you are testing on your own initiative rather than producing data the user/dashboard will consume.",
2826
+ access: "write",
2827
+ tier: "core",
2828
+ inputSchema: runTriggerInputSchema,
2829
+ annotations: writeAnnotations({ idempotentHint: false, openWorldHint: true }),
2830
+ openApiOperations: ["POST /api/v1/projects/{name}/runs"],
2831
+ handler: (client, input) => client.triggerRun(input.project, input.request)
2832
+ }),
2833
+ defineTool({
2834
+ name: "canonry_run_cancel",
2835
+ title: "Cancel run",
2836
+ description: "Cancel a queued or running Canonry run.",
2837
+ access: "write",
2838
+ tier: "core",
2839
+ inputSchema: runGetInputSchema,
2840
+ annotations: writeAnnotations({ idempotentHint: false, destructiveHint: true }),
2841
+ openApiOperations: ["POST /api/v1/runs/{id}/cancel"],
2842
+ handler: (client, input) => client.cancelRun(input.runId)
2843
+ }),
2844
+ defineTool({
2845
+ name: "canonry_queries_add",
2846
+ title: "Add queries",
2847
+ description: "Append tracked queries to a Canonry project; existing queries are skipped by the API.",
2848
+ access: "write",
2849
+ tier: "setup",
2850
+ inputSchema: queriesInputSchema,
2851
+ annotations: writeAnnotations({ idempotentHint: true }),
2852
+ openApiOperations: ["POST /api/v1/projects/{name}/queries"],
2853
+ handler: async (client, input) => {
2854
+ await client.appendQueries(input.project, uniqueStrings(input.request.queries));
2855
+ }
2856
+ }),
2857
+ defineTool({
2858
+ name: "canonry_keywords_add",
2859
+ title: "Add keywords (legacy alias)",
2860
+ description: "Legacy alias for canonry_queries_add. Appends to the same canonical tracked query set.",
2861
+ access: "write",
2862
+ tier: "setup",
2863
+ inputSchema: keywordsInputSchema,
2864
+ annotations: writeAnnotations({ idempotentHint: true }),
2865
+ openApiOperations: ["POST /api/v1/projects/{name}/keywords"],
2866
+ handler: async (client, input) => {
2867
+ await client.appendKeywords(input.project, uniqueStrings(input.request.keywords));
2868
+ }
2869
+ }),
2870
+ defineTool({
2871
+ name: "canonry_queries_remove",
2872
+ title: "Remove queries",
2873
+ description: "Remove tracked queries from a Canonry project.",
2874
+ access: "write",
2875
+ tier: "setup",
2876
+ inputSchema: queriesInputSchema,
2877
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
2878
+ openApiOperations: ["DELETE /api/v1/projects/{name}/queries"],
2879
+ handler: async (client, input) => {
2880
+ await client.deleteQueries(input.project, uniqueStrings(input.request.queries));
2881
+ }
2882
+ }),
2883
+ defineTool({
2884
+ name: "canonry_keywords_remove",
2885
+ title: "Remove keywords (legacy alias)",
2886
+ description: "Legacy alias for canonry_queries_remove. Removes from the same canonical tracked query set.",
2887
+ access: "write",
2888
+ tier: "setup",
2889
+ inputSchema: keywordsInputSchema,
2890
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
2891
+ openApiOperations: ["DELETE /api/v1/projects/{name}/keywords"],
2892
+ handler: async (client, input) => {
2893
+ await client.deleteKeywords(input.project, uniqueStrings(input.request.keywords));
2894
+ }
2895
+ }),
2896
+ defineTool({
2897
+ name: "canonry_competitors_add",
2898
+ title: "Add competitors",
2899
+ description: "Add tracked competitor domains to a Canonry project.",
2900
+ access: "write",
2901
+ tier: "setup",
2902
+ inputSchema: competitorsInputSchema,
2903
+ annotations: writeAnnotations({ idempotentHint: true }),
2904
+ openApiOperations: ["POST /api/v1/projects/{name}/competitors"],
2905
+ handler: async (client, input) => {
2906
+ await client.appendCompetitors(input.project, uniqueStrings(input.request.competitors));
2907
+ }
2908
+ }),
2909
+ defineTool({
2910
+ name: "canonry_competitors_remove",
2911
+ title: "Remove competitors",
2912
+ description: "Remove tracked competitor domains from a Canonry project.",
2913
+ access: "write",
2914
+ tier: "setup",
2915
+ inputSchema: competitorsInputSchema,
2916
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
2917
+ openApiOperations: ["DELETE /api/v1/projects/{name}/competitors"],
2918
+ handler: async (client, input) => {
2919
+ await client.deleteCompetitors(input.project, uniqueStrings(input.request.competitors));
2920
+ }
2921
+ }),
2922
+ defineTool({
2923
+ name: "canonry_schedule_set",
2924
+ title: "Set schedule",
2925
+ description: "Create or replace the scheduled run configuration for a Canonry project.",
2926
+ access: "write",
2927
+ tier: "setup",
2928
+ inputSchema: scheduleSetInputSchema,
2929
+ annotations: writeAnnotations({ idempotentHint: true }),
2930
+ openApiOperations: ["PUT /api/v1/projects/{name}/schedule"],
2931
+ handler: (client, input) => client.putSchedule(input.project, input.schedule)
2932
+ }),
2933
+ defineTool({
2934
+ name: "canonry_schedule_delete",
2935
+ title: "Delete schedule",
2936
+ description: 'Delete the scheduled run configuration for a Canonry project. Pass `kind` to delete a non-default schedule (e.g. "traffic-sync"); defaults to "answer-visibility".',
2937
+ access: "write",
2938
+ tier: "setup",
2939
+ inputSchema: scheduleReadInputSchema,
2940
+ annotations: writeAnnotations({ idempotentHint: false, destructiveHint: true }),
2941
+ openApiOperations: ["DELETE /api/v1/projects/{name}/schedule"],
2942
+ handler: async (client, input) => {
2943
+ await client.deleteSchedule(input.project, input.kind);
2944
+ }
2945
+ }),
2946
+ defineTool({
2947
+ name: "canonry_insight_dismiss",
2948
+ title: "Dismiss insight",
2949
+ description: "Dismiss an intelligence insight for a Canonry project.",
2950
+ access: "write",
2951
+ tier: "setup",
2952
+ inputSchema: insightInputSchema,
2953
+ annotations: writeAnnotations({ idempotentHint: true }),
2954
+ openApiOperations: ["POST /api/v1/projects/{name}/insights/{id}/dismiss"],
2955
+ handler: (client, input) => client.dismissInsight(input.project, input.insightId)
2956
+ }),
2957
+ defineTool({
2958
+ name: "canonry_memory_list",
2959
+ title: "List agent memory",
2960
+ description: "Read project-scoped durable notes Aero has stored via canonry_memory_set (plus compaction summaries). Returns entries newest-first. The N most-recent entries are also injected into the system prompt at session start, so you usually do not need to call this \u2014 reach for it when you need older context or the full note value.",
2961
+ access: "read",
2962
+ tier: "agent",
2963
+ inputSchema: projectInputSchema,
2964
+ annotations: readAnnotations(),
2965
+ openApiOperations: ["GET /api/v1/projects/{name}/agent/memory"],
2966
+ handler: (client, input) => client.listAgentMemory(input.project)
2967
+ }),
2968
+ defineTool({
2969
+ name: "canonry_memory_set",
2970
+ title: "Upsert agent memory",
2971
+ description: 'Persist a project-scoped durable note visible to every future Aero session for this project. Upsert \u2014 writing the same key replaces the prior value. Capped at 2 KB per note. Reserved key prefix "compaction:" is rejected.',
2972
+ access: "write",
2973
+ tier: "agent",
2974
+ inputSchema: memoryUpsertInputSchema,
2975
+ annotations: writeAnnotations({ idempotentHint: true }),
2976
+ openApiOperations: ["PUT /api/v1/projects/{name}/agent/memory"],
2977
+ handler: (client, input) => client.setAgentMemory(input.project, { key: input.key, value: input.value })
2978
+ }),
2979
+ defineTool({
2980
+ name: "canonry_memory_forget",
2981
+ title: "Delete agent memory",
2982
+ description: 'Delete a durable note by key. Returns status="missing" (non-error) when the key did not exist. Reserved key prefix "compaction:" cannot be forgotten directly \u2014 those notes are pruned automatically.',
2983
+ access: "write",
2984
+ tier: "agent",
2985
+ inputSchema: memoryForgetInputSchema,
2986
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
2987
+ openApiOperations: ["DELETE /api/v1/projects/{name}/agent/memory"],
2988
+ handler: (client, input) => client.forgetAgentMemory(input.project, input.key)
2989
+ }),
2990
+ defineTool({
2991
+ name: "canonry_agent_conversations_list",
2992
+ title: "List Aero conversations",
2993
+ description: "List Aero conversations. Instance administrators only. New and resume preserve the previous conversation; shared project notes are retained. Creation requires a UUID that must be reused on retries.",
2994
+ access: "read",
2995
+ tier: "agent",
2996
+ inputSchema: projectInputSchema.extend({ offset: z3.number().int().min(0).optional(), limit: z3.number().int().min(1).max(100).optional() }),
2997
+ annotations: readAnnotations(),
2998
+ openApiOperations: ["GET /api/v1/projects/{name}/agent/conversations"],
2999
+ handler: (client, input) => client.listAgentConversations(input.project, { offset: input.offset, limit: input.limit })
3000
+ }),
3001
+ defineTool({
3002
+ name: "canonry_agent_conversations_get",
3003
+ title: "Read an Aero conversation",
3004
+ description: "Read an Aero conversation. Instance administrators only. New and resume preserve the previous conversation; shared project notes are retained. Creation requires a UUID that must be reused on retries.",
3005
+ access: "read",
3006
+ tier: "agent",
3007
+ inputSchema: projectInputSchema.extend({ id: z3.string().min(1) }),
3008
+ annotations: readAnnotations(),
3009
+ openApiOperations: ["GET /api/v1/projects/{name}/agent/conversations/{id}"],
3010
+ handler: (client, input) => client.getAgentConversation(input.project, input.id)
3011
+ }),
3012
+ defineTool({
3013
+ name: "canonry_agent_conversations_new",
3014
+ title: "Start a new Aero conversation",
3015
+ description: "Start a new Aero conversation. Instance administrators only. New and resume preserve the previous conversation; shared project notes are retained. Creation requires a UUID that must be reused on retries.",
3016
+ access: "write",
3017
+ tier: "agent",
3018
+ inputSchema: projectInputSchema.extend(agentConversationCreateSchema.shape),
3019
+ annotations: writeAnnotations({ idempotentHint: true }),
3020
+ openApiOperations: ["POST /api/v1/projects/{name}/agent/conversations"],
3021
+ handler: (client, input) => client.createAgentConversation(input.project, input.id)
3022
+ }),
3023
+ defineTool({
3024
+ name: "canonry_agent_conversations_resume",
3025
+ title: "Resume an Aero conversation",
3026
+ description: "Resume an Aero conversation. Instance administrators only. New and resume preserve the previous conversation; shared project notes are retained. Creation requires a UUID that must be reused on retries.",
3027
+ access: "write",
3028
+ tier: "agent",
3029
+ inputSchema: projectInputSchema.extend({ id: z3.string().min(1) }),
3030
+ annotations: writeAnnotations({ idempotentHint: true }),
3031
+ openApiOperations: ["POST /api/v1/projects/{name}/agent/conversations/{id}/resume"],
3032
+ handler: (client, input) => client.resumeAgentConversation(input.project, input.id)
3033
+ }),
3034
+ defineTool({
3035
+ name: "canonry_agent_conversations_delete",
3036
+ title: "Delete an Aero conversation",
3037
+ description: "Delete an Aero conversation. Instance administrators only. New and resume preserve the previous conversation; shared project notes are retained. Creation requires a UUID that must be reused on retries.",
3038
+ access: "write",
3039
+ tier: "agent",
3040
+ inputSchema: projectInputSchema.extend({ id: z3.string().min(1) }),
3041
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
3042
+ openApiOperations: ["DELETE /api/v1/projects/{name}/agent/conversations/{id}"],
3043
+ handler: (client, input) => client.deleteAgentConversation(input.project, input.id)
3044
+ }),
3045
+ defineTool({
3046
+ name: "canonry_agent_clear",
3047
+ title: "Clear agent transcript",
3048
+ description: "Clear the rolling Aero conversation for a project \u2014 wipes the transcript, the in-memory pending follow-up buffer, and the persisted follow-up queue. Memory entries (canonry_memory_*) are preserved. Destructive legacy operation. Prefer canonry_agent_conversations_new to start fresh while preserving history.",
3049
+ access: "write",
3050
+ tier: "agent",
3051
+ inputSchema: projectInputSchema,
3052
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
3053
+ openApiOperations: ["DELETE /api/v1/projects/{name}/agent/transcript"],
3054
+ handler: async (client, input) => {
3055
+ await client.resetAgentTranscript(input.project);
3056
+ return { status: "cleared", project: input.project };
3057
+ }
3058
+ }),
3059
+ defineTool({
3060
+ name: "canonry_agent_webhook_attach",
3061
+ title: "Attach agent webhook",
3062
+ description: "Attach an external agent webhook to project run and insight events.",
3063
+ access: "write",
3064
+ tier: "core",
3065
+ inputSchema: agentWebhookAttachInputSchema,
3066
+ annotations: writeAnnotations({ idempotentHint: true }),
3067
+ openApiOperations: ["GET /api/v1/projects/{name}/notifications", "POST /api/v1/projects/{name}/notifications"],
3068
+ handler: async (client, input) => {
3069
+ const existing = await client.listNotifications(input.project);
3070
+ const agentNotification = existing.find((notification2) => notification2.source === "agent");
3071
+ if (agentNotification) {
3072
+ return { status: "already-attached", project: input.project, notificationId: agentNotification.id };
3073
+ }
3074
+ const request = notificationCreateRequestSchema.parse({
3075
+ channel: "webhook",
3076
+ url: input.url,
3077
+ events: AGENT_WEBHOOK_EVENTS,
3078
+ source: "agent"
3079
+ });
3080
+ const notification = await client.createNotification(input.project, request);
3081
+ return { status: "attached", project: input.project, notificationId: notification.id };
3082
+ }
3083
+ }),
3084
+ defineTool({
3085
+ name: "canonry_agent_webhook_detach",
3086
+ title: "Detach agent webhook",
3087
+ description: "Detach the external agent webhook for a Canonry project.",
3088
+ access: "write",
3089
+ tier: "agent",
3090
+ inputSchema: projectInputSchema,
3091
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
3092
+ openApiOperations: ["GET /api/v1/projects/{name}/notifications", "DELETE /api/v1/projects/{name}/notifications/{id}"],
3093
+ handler: async (client, input) => {
3094
+ const existing = await client.listNotifications(input.project);
3095
+ const agentNotification = existing.find((notification) => notification.source === "agent");
3096
+ if (!agentNotification) {
3097
+ return { status: "not-attached", project: input.project };
3098
+ }
3099
+ await client.deleteNotification(input.project, agentNotification.id);
3100
+ return { status: "detached", project: input.project };
3101
+ }
3102
+ }),
3103
+ defineTool({
3104
+ name: "canonry_research_run_start",
3105
+ requiredScope: RESEARCH_RUN_SCOPE,
3106
+ title: "Start research query run",
3107
+ description: "Run final free-form queries once each against one API provider, with an optional exact model, location, or one configured market or Property destination. Scope never changes query text or fans out a group. Optional template provenance records the source template ID and version; callers must submit the fully expanded, editable final query text. Results retain that destination and provenance for later inspection. This does not add any query to the tracked basket or affect overview tracking.",
3108
+ access: "write",
3109
+ tier: "discovery",
3110
+ inputSchema: researchRunStartInputSchema,
3111
+ annotations: writeAnnotations({ idempotentHint: false, openWorldHint: true }),
3112
+ openApiOperations: ["POST /api/v1/projects/{name}/research/runs"],
3113
+ handler: (client, input) => client.startResearchRun(input.project, input.request)
3114
+ }),
3115
+ defineTool({
3116
+ name: "canonry_research_batch_start",
3117
+ requiredScope: RESEARCH_RUN_SCOPE,
3118
+ title: "Start reviewed research across destinations",
3119
+ description: "Accept a reviewed research batch across explicit markets, Properties, or configured locations. Submit fully expanded, editable final queries with explicit provider/model/location for each destination; the API does not expand patterns or groups. All runs are saved together or none are saved, then processed independently. The required idempotencyKey prevents duplicate work on retries; changed input with the same key conflicts. Each run retains its scope, location and optional pattern provenance. Uses paid answer engines, never adds tracked queries or affects visibility measurements.",
3120
+ access: "write",
3121
+ tier: "discovery",
3122
+ inputSchema: researchBatchStartInputSchema,
3123
+ annotations: writeAnnotations({ idempotentHint: true, openWorldHint: true }),
3124
+ openApiOperations: ["POST /api/v1/projects/{name}/research/batches"],
3125
+ handler: (client, input) => client.startResearchBatch(input.project, input.request)
3126
+ }),
3127
+ defineTool({
3128
+ name: "canonry_research_runs_list",
3129
+ title: "List research query runs",
3130
+ description: "List saved research query runs for a project, newest first. Research and ICP discovery are distinct workflows: these are direct free-form query experiments and never modify the tracked basket.",
3131
+ access: "read",
3132
+ tier: "discovery",
3133
+ inputSchema: researchRunsListInputSchema,
3134
+ annotations: readAnnotations(),
3135
+ openApiOperations: ["GET /api/v1/projects/{name}/research/runs"],
3136
+ handler: (client, input) => client.listResearchRuns(input.project, input.limit === void 0 && input.cursor === void 0 ? void 0 : { limit: input.limit, cursor: input.cursor })
3137
+ }),
3138
+ defineTool({
3139
+ name: "canonry_research_run_get",
3140
+ title: "Get research query run",
3141
+ description: "Get saved per-query answers, source links, cited domains, answer-text named competitors, cited competitor domains, and independent mention/citation results for one research run. It is read-only and does not promote or track any query.",
3142
+ access: "read",
3143
+ tier: "discovery",
3144
+ inputSchema: researchRunIdInputSchema,
3145
+ annotations: readAnnotations(),
3146
+ openApiOperations: ["GET /api/v1/projects/{name}/research/runs/{runId}"],
3147
+ handler: (client, input) => client.getResearchRun(input.project, input.runId)
3148
+ }),
3149
+ defineTool({
3150
+ name: "canonry_discover_run_start",
3151
+ title: "Start discovery run",
3152
+ description: 'Kick off a discovery session for a project: ICP \u2192 seed (Gemini grounded prompt) \u2192 embed + cluster + pick representative \u2192 probe each canonical \u2192 classify into cited / aspirational / wasted-surface \u2192 aggregate competitor map. Returns {runId, sessionId, status:"running"} immediately; the work runs in the background. Poll canonry_discover_session_get with the returned sessionId until status is "completed" or "failed". Costs roughly $1 / session at default budget; budget capped at 500 probes / session.',
3153
+ access: "write",
3154
+ tier: "discovery",
3155
+ inputSchema: discoveryRunInputSchema,
3156
+ annotations: writeAnnotations({ idempotentHint: false, openWorldHint: true }),
3157
+ openApiOperations: ["POST /api/v1/projects/{name}/discover/run"],
3158
+ handler: (client, input) => client.triggerDiscoveryRun(input.project, input.request)
3159
+ }),
3160
+ defineTool({
3161
+ name: "canonry_discover_sessions_list",
3162
+ title: "List discovery sessions",
3163
+ description: "List recent discovery sessions for a project, newest first. Returns the session-level summary (status, seed counts, bucket counts, competitor map). Use canonry_discover_session_get to drill into per-query probe rows.",
3164
+ access: "read",
3165
+ tier: "discovery",
3166
+ inputSchema: discoverySessionsListInputSchema,
3167
+ annotations: readAnnotations(),
3168
+ openApiOperations: ["GET /api/v1/projects/{name}/discover/sessions"],
3169
+ handler: (client, input) => client.listDiscoverySessions(input.project, input.limit !== void 0 ? { limit: input.limit } : void 0)
3170
+ }),
3171
+ defineTool({
3172
+ name: "canonry_discover_session_get",
3173
+ title: "Get discovery session",
3174
+ description: 'Get one discovery session with the full probe list (per-query bucket + cited domains). Use after canonry_discover_run_start to inspect what the discovery pipeline produced; this is the canonical read for "what did discovery find" before calling canonry_discover_promote.',
3175
+ access: "read",
3176
+ tier: "discovery",
3177
+ inputSchema: discoverySessionIdInputSchema,
3178
+ annotations: readAnnotations(),
3179
+ openApiOperations: ["GET /api/v1/projects/{name}/discover/sessions/{id}"],
3180
+ handler: (client, input) => client.getDiscoverySession(input.project, input.sessionId)
3181
+ }),
3182
+ defineTool({
3183
+ name: "canonry_discover_harvest",
3184
+ title: "Harvest discovery search queries",
3185
+ description: `Read the search queries the answer engine actually issued (Gemini's grounding fan-out) back out of a session's stored probes, gate them for buyer-intent + novelty, and return the survivors as candidate seeds ranked by how many distinct probes issued each one. These are a THIRD signal \u2014 issued retrieval queries \u2014 distinct from mention (answer text) and cited (source list); they carry no demand of their own. Read-only and derived: nothing is probed, tracked, or promoted. Use it to surface "queries the model searched for that you aren't tracking yet"; the operator/agent then decides what to add via canonry_query_add. minProbeHits raises the recurrence floor; anchor=false disables the subject filter. stats carries the raw count and per-reason rejection tally.`,
3186
+ access: "read",
3187
+ tier: "discovery",
3188
+ inputSchema: discoveryHarvestInputSchema,
3189
+ annotations: readAnnotations(),
3190
+ openApiOperations: ["GET /api/v1/projects/{name}/discover/sessions/{id}/harvest"],
3191
+ handler: (client, input) => client.getDiscoveryHarvest(input.project, input.sessionId, {
3192
+ minProbeHits: input.minProbeHits,
3193
+ anchor: input.anchor
3194
+ })
3195
+ }),
3196
+ defineTool({
3197
+ name: "canonry_discover_promote_preview",
3198
+ title: "Preview discovery promotion",
3199
+ description: "Read-only preview of available promotion candidates for a session: bucketed query lists and recurring suggested competitor domains not already in the project's tracked competitor list. Use it to confirm a basket before calling canonry_discover_promote.",
3200
+ access: "read",
3201
+ tier: "discovery",
3202
+ inputSchema: discoverySessionIdInputSchema,
3203
+ annotations: readAnnotations(),
3204
+ openApiOperations: ["GET /api/v1/projects/{name}/discover/sessions/{id}/promote"],
3205
+ handler: (client, input) => client.previewDiscoveryPromote(input.project, input.sessionId)
3206
+ }),
3207
+ defineTool({
3208
+ name: "canonry_discover_promote",
3209
+ title: "Promote discovery session",
3210
+ description: 'Adopt a completed discovery session\'s bucketed queries into the project\'s tracked basket, tagged with provenance "discovery:<sessionId>". By default, only cited + aspirational queries are promoted; include wasted-surface explicitly when off-ICP competitor gaps should also be tracked. Recurring discovered competitor domains classified as direct-competitor are also merged by default \u2014 pass request.competitorTypes to adopt editorial-media channels or recover legacy unknown entries. Add-only and idempotent: queries/domains already tracked are returned under `skipped`, never inserted twice. Only sessions with status "completed" can be promoted. Call canonry_discover_promote_preview first to inspect candidates.',
3211
+ access: "write",
3212
+ tier: "discovery",
3213
+ inputSchema: discoveryPromoteInputSchema,
3214
+ annotations: writeAnnotations({ idempotentHint: true }),
3215
+ openApiOperations: ["POST /api/v1/projects/{name}/discover/sessions/{id}/promote"],
3216
+ handler: (client, input) => client.promoteDiscovery(input.project, input.sessionId, input.request)
3217
+ }),
3218
+ defineTool({
3219
+ name: "canonry_site_health_overview",
3220
+ title: "Get Site Health overview",
3221
+ description: "Start a Site Health investigation with the latest or selected scan summary: root URL, completeness, crawl and link counts, budgets, versions, termination, and dead-link check state. Follow with a focused Site Health subgraph, shortest path, or scan changes; never request the interactive graph projection through MCP.",
3222
+ access: "read",
3223
+ tier: "monitoring",
3224
+ inputSchema: technicalAeoCrawlInputSchema,
3225
+ annotations: readAnnotations(),
3226
+ openApiOperations: ["GET /api/v1/projects/{name}/technical-aeo/crawl"],
3227
+ handler: (client, input) => client.getTechnicalAeoCrawl(input.project, { runId: input.runId })
3228
+ }),
3229
+ defineTool({
3230
+ name: "canonry_site_health_page_audit",
3231
+ title: "Get Site Health page audit",
3232
+ description: "Connect one graph page's audit score to its exact persisted evidence: factor scores, stable finding codes and messages, recommendations, and critical defects. The response includes crawl provenance and explicit no-crawl, details-unavailable, not-found, not-audited, ready/scores-only states. Use nodeKey from a Site Health page or subgraph read when possible. Link score remains an importance signal, not an audit finding.",
3233
+ access: "read",
3234
+ tier: "monitoring",
3235
+ inputSchema: siteHealthPageAuditInputSchema,
3236
+ annotations: readAnnotations(),
3237
+ openApiOperations: ["GET /api/v1/projects/{name}/technical-aeo/crawl/pages/audit"],
3238
+ handler: (client, input) => client.getTechnicalAeoPageAudit(input.project, {
3239
+ runId: input.runId,
3240
+ nodeKey: input.nodeKey,
3241
+ url: input.url
3242
+ })
3243
+ }),
3244
+ defineTool({
3245
+ name: "canonry_site_health_subgraph",
3246
+ title: "Inspect focused Site Health subgraph",
3247
+ description: "Read a compact canonical neighborhood around one page or the crawl root. Defaults to 25 nodes and 50 edges; expand hops or refocus only when needed. `countAccuracy=lower-bound` means traversal reached a cap, and `complete`/`termination` qualify partial-crawl results. This intentionally omits visualization coordinates and never materializes the full site graph.",
3248
+ access: "read",
3249
+ tier: "monitoring",
3250
+ inputSchema: siteHealthSubgraphInputSchema,
3251
+ annotations: readAnnotations(),
3252
+ openApiOperations: ["GET /api/v1/projects/{name}/technical-aeo/subgraph"],
3253
+ handler: (client, input) => client.getSiteHealthSubgraph(input.project, {
3254
+ runId: input.runId,
3255
+ nodeKey: input.nodeKey,
3256
+ url: input.url,
3257
+ hops: input.hops,
3258
+ maxNodes: input.maxNodes,
3259
+ maxEdges: input.maxEdges
3260
+ })
3261
+ }),
3262
+ defineTool({
3263
+ name: "canonry_site_health_path",
3264
+ title: "Find shortest Site Health path",
3265
+ description: "Find the shortest directed path of followable internal links from the crawl root or a selected page to one required destination. Use `complete` and `termination` to qualify unreachable or truncated outcomes from a partial crawl; it returns one path, not a graph traversal.",
3266
+ access: "read",
3267
+ tier: "monitoring",
3268
+ inputSchema: siteHealthPathInputSchema,
3269
+ annotations: readAnnotations(),
3270
+ openApiOperations: ["GET /api/v1/projects/{name}/technical-aeo/path"],
3271
+ handler: (client, input) => client.getSiteHealthPath(input.project, {
3272
+ runId: input.runId,
3273
+ fromNodeKey: input.fromNodeKey,
3274
+ fromUrl: input.fromUrl,
3275
+ toNodeKey: input.toNodeKey,
3276
+ toUrl: input.toUrl,
3277
+ maxDepth: input.maxDepth
3278
+ })
3279
+ }),
3280
+ defineTool({
3281
+ name: "canonry_site_health_changes",
3282
+ title: "Compare Site Health scans",
3283
+ description: "Compare two immutable complete scans (or the latest pair) for added, removed, and changed canonical pages or links. Results echo resolved filters and scan IDs; the first page has an exact summary, while continuations omit summary/total. MCP pages cap at 25 because every change carries before/after DTOs; filter before paging.",
3284
+ access: "read",
3285
+ tier: "monitoring",
3286
+ inputSchema: siteHealthChangesInputSchema,
3287
+ annotations: readAnnotations(),
3288
+ openApiOperations: ["GET /api/v1/projects/{name}/technical-aeo/changes"],
3289
+ handler: (client, input) => client.getSiteHealthChanges(input.project, {
3290
+ fromRunId: input.fromRunId,
3291
+ toRunId: input.toRunId,
3292
+ scope: input.scope,
3293
+ change: input.change,
3294
+ cursor: input.cursor,
3295
+ limit: input.limit
3296
+ })
3297
+ }),
3298
+ defineTool({
3299
+ name: "canonry_technical_aeo_score",
3300
+ title: "Get Technical AEO score",
3301
+ description: "Get the Technical AEO scorecard for a project: the latest site-audit aggregate 0\u2013100 score, per-factor site-level averages (with pass/partial/fail distribution), cross-cutting issues, prioritized fixes, and the delta vs the previous audit. When `hasData` is false the project has never been audited \u2014 call canonry_technical_aeo_run first.",
3302
+ access: "read",
3303
+ tier: "monitoring",
3304
+ inputSchema: technicalAeoScoreInputSchema,
3305
+ annotations: readAnnotations(),
3306
+ openApiOperations: ["GET /api/v1/projects/{name}/technical-aeo"],
3307
+ handler: (client, input) => client.getTechnicalAeoScore(input.project, { runId: input.runId })
3308
+ }),
3309
+ defineTool({
3310
+ name: "canonry_technical_aeo_pages",
3311
+ title: "List Technical AEO pages",
3312
+ description: "List the per-page breakdown of the latest site-audit run (paginated). Filter status=error to surface unreachable pages, or sort score-asc (default) to surface the worst-scoring pages first. Use after canonry_technical_aeo_score to drill into which pages drag the site score down.",
3313
+ access: "read",
3314
+ tier: "monitoring",
3315
+ inputSchema: technicalAeoPagesInputSchema,
3316
+ annotations: readAnnotations(),
3317
+ openApiOperations: ["GET /api/v1/projects/{name}/technical-aeo/pages"],
3318
+ handler: (client, input) => client.getTechnicalAeoPages(input.project, {
3319
+ runId: input.runId,
3320
+ status: input.status,
3321
+ sort: input.sort,
3322
+ limit: input.limit,
3323
+ offset: input.offset
3324
+ })
3325
+ }),
3326
+ defineTool({
3327
+ name: "canonry_technical_aeo_trend",
3328
+ title: "Get Technical AEO trend",
3329
+ description: 'Get the aggregate Technical AEO score over time (oldest-first) across past site-audit runs. Use to answer "is our technical AEO improving?".',
3330
+ access: "read",
3331
+ tier: "monitoring",
3332
+ inputSchema: technicalAeoTrendInputSchema,
3333
+ annotations: readAnnotations(),
3334
+ openApiOperations: ["GET /api/v1/projects/{name}/technical-aeo/trend"],
3335
+ handler: (client, input) => client.getTechnicalAeoTrend(input.project, input.limit !== void 0 ? { limit: input.limit } : void 0)
3336
+ }),
3337
+ defineTool({
3338
+ name: "canonry_technical_aeo_crawl",
3339
+ title: "Get persisted Technical AEO crawl",
3340
+ description: "Read persisted full-crawl metadata for the latest or selected Technical AEO site-audit run: root URL, crawl/indexability/link-score versions, effective budgets, completeness, counts, and explicit dead-link check state. This does not fabricate a graph from older scorecard-only audits.",
3341
+ access: "read",
3342
+ tier: "monitoring",
3343
+ inputSchema: technicalAeoCrawlInputSchema,
3344
+ annotations: readAnnotations(),
3345
+ openApiOperations: ["GET /api/v1/projects/{name}/technical-aeo/crawl"],
3346
+ handler: (client, input) => client.getTechnicalAeoCrawl(input.project, { runId: input.runId })
3347
+ }),
3348
+ defineTool({
3349
+ name: "canonry_technical_aeo_crawl_pages",
3350
+ title: "List Technical AEO crawl pages",
3351
+ description: "Read one bounded, cursor-paged list of canonical crawl nodes. Filter crawler-derived indexability and audit state, then follow nextCursor; this is technical inventory eligibility, not a claim about Google index coverage.",
3352
+ access: "read",
3353
+ tier: "monitoring",
3354
+ inputSchema: technicalAeoCrawlPagesInputSchema,
3355
+ annotations: readAnnotations(),
3356
+ openApiOperations: ["GET /api/v1/projects/{name}/technical-aeo/crawl/pages"],
3357
+ handler: (client, input) => client.getTechnicalAeoCrawlPages(input.project, {
3358
+ runId: input.runId,
3359
+ inventoryEligible: input.inventoryEligible,
3360
+ fetchState: input.fetchState,
3361
+ indexabilityState: input.indexabilityState,
3362
+ auditState: input.auditState,
3363
+ sort: input.sort,
3364
+ cursor: input.cursor,
3365
+ limit: input.limit
3366
+ })
3367
+ }),
3368
+ defineTool({
3369
+ name: "canonry_technical_aeo_structure",
3370
+ title: "List Technical AEO site structure",
3371
+ description: "Read one bounded level of the persisted site hierarchy below parentPath. Follow nextCursor for more siblings; request a child path separately rather than attempting to materialize the entire website tree.",
3372
+ access: "read",
3373
+ tier: "monitoring",
3374
+ inputSchema: technicalAeoStructureInputSchema,
3375
+ annotations: readAnnotations(),
3376
+ openApiOperations: ["GET /api/v1/projects/{name}/technical-aeo/structure"],
3377
+ handler: (client, input) => client.getTechnicalAeoStructure(input.project, {
3378
+ runId: input.runId,
3379
+ parentPath: input.parentPath,
3380
+ cursor: input.cursor,
3381
+ limit: input.limit
3382
+ })
3383
+ }),
3384
+ defineTool({
3385
+ name: "canonry_technical_aeo_internal_links",
3386
+ title: "List Technical AEO internal links",
3387
+ description: "Read a bounded, cursor-paged list of persisted internal crawl edges. Filter by source URL, target URL, followability, or link kind. Nav, header, and footer links are marked isTemplate; templateSource says which rule decided each one (placement, ubiquity, or unmeasured when neither had evidence) and templateDetection reports the same for the scan, so an empty content-only list is not evidence of no content links and two scans classified by different rules are never compared as if they were the same measurement. Use the neighbors tool for one page rather than loading a graph.",
3388
+ access: "read",
3389
+ tier: "monitoring",
3390
+ inputSchema: technicalAeoInternalLinksInputSchema,
3391
+ annotations: readAnnotations(),
3392
+ openApiOperations: ["GET /api/v1/projects/{name}/technical-aeo/internal-links"],
3393
+ handler: (client, input) => client.getTechnicalAeoInternalLinks(input.project, {
3394
+ runId: input.runId,
3395
+ sourceUrl: input.sourceUrl,
3396
+ targetUrl: input.targetUrl,
3397
+ followable: input.followable,
3398
+ linkKind: input.linkKind,
3399
+ cursor: input.cursor,
3400
+ limit: input.limit
3401
+ })
3402
+ }),
3403
+ defineTool({
3404
+ name: "canonry_technical_aeo_link_neighbors",
3405
+ title: "Get Technical AEO page link neighbors",
3406
+ description: "Read bounded inbound and outbound internal links for exactly one crawl node, selected by nodeKey or URL. Filter by link kind to separate editorial links from nav, header, and footer links. It returns independent truncation flags for inbound and outbound edges, not a transitive traversal.",
3407
+ access: "read",
3408
+ tier: "monitoring",
3409
+ inputSchema: technicalAeoLinkNeighborsInputSchema,
3410
+ annotations: readAnnotations(),
3411
+ openApiOperations: ["GET /api/v1/projects/{name}/technical-aeo/internal-links/neighbors"],
3412
+ handler: (client, input) => client.getTechnicalAeoInternalLinkNeighbors(input.project, {
3413
+ runId: input.runId,
3414
+ nodeKey: input.nodeKey,
3415
+ url: input.url,
3416
+ linkKind: input.linkKind,
3417
+ limit: input.limit
3418
+ })
3419
+ }),
3420
+ defineTool({
3421
+ name: "canonry_technical_aeo_dead_links",
3422
+ title: "List Technical AEO dead links",
3423
+ description: "Read the persisted dead-link check state and a bounded, cursor-paged list of findings. A disabled result means the crawl was run without opt-in checks; it never means zero dead links.",
3424
+ access: "read",
3425
+ tier: "monitoring",
3426
+ inputSchema: technicalAeoDeadLinksInputSchema,
3427
+ annotations: readAnnotations(),
3428
+ openApiOperations: ["GET /api/v1/projects/{name}/technical-aeo/dead-links"],
3429
+ handler: (client, input) => client.getTechnicalAeoDeadLinks(input.project, {
3430
+ runId: input.runId,
3431
+ cursor: input.cursor,
3432
+ limit: input.limit
3433
+ })
3434
+ }),
3435
+ defineTool({
3436
+ name: "canonry_technical_aeo_run",
3437
+ title: "Run Technical AEO site audit",
3438
+ description: "Start a site-audit run. The run discovers root, sitemap, and linked pages. It defaults to 1,000 pages, and the engine derives the link-observation budget from that page count with a 100,000 floor, so raising maxPages raises it too. An explicit maxEdges is a ceiling that replaces the derivation and can sit below it. It returns {runId, status} and continues in the background. If an active run has identical effective options, this tool returns it; different options are refused. Poll canonry_run_get, then read the crawl and score tools.",
3439
+ access: "write",
3440
+ tier: "monitoring",
3441
+ inputSchema: technicalAeoRunInputSchema,
3442
+ annotations: writeAnnotations({ idempotentHint: false, openWorldHint: true }),
3443
+ openApiOperations: ["POST /api/v1/projects/{name}/technical-aeo/runs"],
3444
+ handler: (client, input) => client.triggerSiteAudit(input.project, {
3445
+ sitemapUrl: input.sitemapUrl,
3446
+ limit: input.limit,
3447
+ maxPages: input.maxPages,
3448
+ maxEdges: input.maxEdges,
3449
+ maxDepth: input.maxDepth,
3450
+ checkDeadLinks: input.checkDeadLinks
3451
+ })
3452
+ }),
3453
+ // ----- Google Marketing: Google Ads + Google Tag Manager -----
3454
+ // OAuth, selection, disconnect, and contract writes deliberately stay on
3455
+ // the operator CLI/dashboard. The agent surface can only inspect bounded
3456
+ // evidence, trigger bounded read-only syncs, and assess stored integrity.
3457
+ defineTool({
3458
+ name: "canonry_google_ads_status",
3459
+ title: "Get Google Ads connection status",
3460
+ description: "Read the project-local Google Ads connection, selected customer, and stored-evidence freshness. This never exposes credentials or changes a Google Ads account.",
3461
+ access: "read",
3462
+ tier: "google-ads",
3463
+ inputSchema: projectInputSchema,
3464
+ annotations: readAnnotations(),
3465
+ openApiOperations: ["GET /api/v1/projects/{name}/google-ads/status"],
3466
+ handler: (client, input) => client.getGoogleAdsStatus(input.project)
3467
+ }),
3468
+ defineTool({
3469
+ name: "canonry_google_ads_customers",
3470
+ title: "List live Google Ads customers",
3471
+ description: "Read the bounded set of Google Ads customers available to the project OAuth connection. This makes live provider GET requests and requires google-marketing.read-live; customer selection remains an explicit operator action.",
3472
+ access: "read",
3473
+ tier: "google-ads",
3474
+ inputSchema: projectInputSchema,
3475
+ annotations: readAnnotations(true),
3476
+ openApiOperations: ["GET /api/v1/projects/{name}/google-ads/customers"],
3477
+ handler: (client, input) => client.listGoogleAdsCustomers(input.project)
3478
+ }),
3479
+ defineTool({
3480
+ name: "canonry_conversion_tracking_options",
3481
+ title: "List selectable conversion actions and GTM tags",
3482
+ description: "List the Google Ads conversion actions and Tag Manager tags a conversion contract can point at, read from the newest stored snapshots. Stored read: quota-free, never calls a provider, never spends the advertiser budget. An empty list means the provider has not synced, not that no options exist.",
3483
+ access: "read",
3484
+ tier: "setup",
3485
+ inputSchema: projectInputSchema,
3486
+ annotations: readAnnotations(),
3487
+ openApiOperations: ["GET /api/v1/projects/{name}/conversion-tracking/options"],
3488
+ handler: (client, input) => client.getConversionTrackingOptions(input.project)
3489
+ }),
3490
+ defineTool({
3491
+ name: "canonry_google_ads_performance",
3492
+ title: "Read stored Google Ads performance",
3493
+ description: "Read impressions, clicks, cost (integer micros), conversions, the densified daily series, per-campaign totals, and the prior-equal-period comparison from the newest stored campaign-metrics snapshot. Stored read: it is quota-free, never calls Google, and never spends the advertiser budget. The capture day is partial and is excluded, so every window ends on the newest closed day.",
3494
+ access: "read",
3495
+ tier: "google-ads",
3496
+ inputSchema: googleAdsPerformanceInputSchema,
3497
+ annotations: readAnnotations(),
3498
+ openApiOperations: ["GET /api/v1/projects/{name}/google-ads/performance"],
3499
+ handler: (client, input) => client.getGoogleAdsPerformance(
3500
+ input.project,
3501
+ input.window ? { window: input.window } : {}
3502
+ )
3503
+ }),
3504
+ defineTool({
3505
+ name: "canonry_google_ads_snapshots",
3506
+ title: "List stored Google Ads evidence",
3507
+ description: "List a bounded page of redacted, append-only Google Ads snapshots. Stored reads are quota-free and do not call Google.",
3508
+ access: "read",
3509
+ tier: "google-ads",
3510
+ inputSchema: googleMarketingSnapshotPageInputSchema,
3511
+ annotations: readAnnotations(),
3512
+ openApiOperations: ["GET /api/v1/projects/{name}/google-ads/snapshots"],
3513
+ handler: (client, input) => client.listGoogleAdsSnapshots(input.project, {
3514
+ limit: input.limit,
3515
+ cursor: input.cursor
3516
+ })
3517
+ }),
3518
+ defineTool({
3519
+ name: "canonry_google_ads_snapshot_get",
3520
+ title: "Get stored Google Ads evidence",
3521
+ description: "Read one redacted Google Ads evidence snapshot by ID. It can show conversion actions and effective campaign goals, but cannot prove a browser conversion fired.",
3522
+ access: "read",
3523
+ tier: "google-ads",
3524
+ inputSchema: googleMarketingSnapshotInputSchema,
3525
+ annotations: readAnnotations(),
3526
+ openApiOperations: ["GET /api/v1/projects/{name}/google-ads/snapshots/{snapshotId}"],
3527
+ handler: (client, input) => client.getGoogleAdsSnapshot(input.project, input.snapshotId)
3528
+ }),
3529
+ defineTool({
3530
+ name: "canonry_google_ads_sync",
3531
+ title: "Sync Google Ads conversion evidence",
3532
+ description: "Queue a bounded read-only Google Ads evidence sync using GETs and SearchStream POSTs. It creates a local run and sanitized snapshots, but never changes campaigns, conversion actions, goals, bids, or budgets. Requires google-marketing.read-live and write authority.",
3533
+ access: "write",
3534
+ tier: "google-ads",
3535
+ inputSchema: projectInputSchema,
3536
+ annotations: writeAnnotations({ idempotentHint: false, openWorldHint: true }),
3537
+ openApiOperations: ["POST /api/v1/projects/{name}/google-ads/sync"],
3538
+ handler: (client, input) => client.triggerGoogleAdsSync(input.project)
3539
+ }),
3540
+ defineTool({
3541
+ name: "canonry_gtm_status",
3542
+ title: "Get Google Tag Manager connection status",
3543
+ description: "Read the project-local GTM connection, selected account/container/workspace, and stored-evidence freshness. This never exposes credentials or edits GTM.",
3544
+ access: "read",
3545
+ tier: "gtm",
3546
+ inputSchema: projectInputSchema,
3547
+ annotations: readAnnotations(),
3548
+ openApiOperations: ["GET /api/v1/projects/{name}/gtm/status"],
3549
+ handler: (client, input) => client.getGtmStatus(input.project)
3550
+ }),
3551
+ defineTool({
3552
+ name: "canonry_gtm_accounts",
3553
+ title: "List live GTM accounts",
3554
+ description: "Read the bounded set of GTM accounts available to the project OAuth connection. This makes live provider GET requests and requires google-marketing.read-live; resource selection remains operator-only.",
3555
+ access: "read",
3556
+ tier: "gtm",
3557
+ inputSchema: projectInputSchema,
3558
+ annotations: readAnnotations(true),
3559
+ openApiOperations: ["GET /api/v1/projects/{name}/gtm/accounts"],
3560
+ handler: (client, input) => client.listGtmAccounts(input.project)
3561
+ }),
3562
+ defineTool({
3563
+ name: "canonry_gtm_containers",
3564
+ title: "List live GTM containers",
3565
+ description: "Read the bounded set of containers in one GTM account. This is a live GET-only provider read; it cannot edit or publish a container.",
3566
+ access: "read",
3567
+ tier: "gtm",
3568
+ inputSchema: gtmAccountInputSchema,
3569
+ annotations: readAnnotations(true),
3570
+ openApiOperations: ["GET /api/v1/projects/{name}/gtm/accounts/{accountId}/containers"],
3571
+ handler: (client, input) => client.listGtmContainers(
3572
+ input.project,
3573
+ canonicalGtmMcpAccountId(input.accountId)
3574
+ )
3575
+ }),
3576
+ defineTool({
3577
+ name: "canonry_gtm_workspaces",
3578
+ title: "List live GTM workspaces",
3579
+ description: "Read the bounded set of workspaces in one GTM container. This is a live GET-only provider read; it cannot edit a workspace or publish a version.",
3580
+ access: "read",
3581
+ tier: "gtm",
3582
+ inputSchema: gtmContainerInputSchema,
3583
+ annotations: readAnnotations(true),
3584
+ openApiOperations: ["GET /api/v1/projects/{name}/gtm/accounts/{accountId}/containers/{containerId}/workspaces"],
3585
+ handler: (client, input) => {
3586
+ const selection = canonicalGtmMcpSelection(input.accountId, input.containerId);
3587
+ return client.listGtmWorkspaces(input.project, selection.accountId, selection.containerId);
3588
+ }
3589
+ }),
3590
+ defineTool({
3591
+ name: "canonry_gtm_snapshots",
3592
+ title: "List stored GTM evidence",
3593
+ description: "List a bounded page of redacted, append-only GTM live/draft graph snapshots. Stored reads are quota-free and do not call Google.",
3594
+ access: "read",
3595
+ tier: "gtm",
3596
+ inputSchema: googleMarketingSnapshotPageInputSchema,
3597
+ annotations: readAnnotations(),
3598
+ openApiOperations: ["GET /api/v1/projects/{name}/gtm/snapshots"],
3599
+ handler: (client, input) => client.listGtmSnapshots(input.project, {
3600
+ limit: input.limit,
3601
+ cursor: input.cursor
3602
+ })
3603
+ }),
3604
+ defineTool({
3605
+ name: "canonry_gtm_snapshot_get",
3606
+ title: "Get stored GTM evidence",
3607
+ description: "Read one redacted GTM live/draft graph snapshot by ID. It can prove static tag and trigger configuration only; it cannot prove a browser event fired.",
3608
+ access: "read",
3609
+ tier: "gtm",
3610
+ inputSchema: googleMarketingSnapshotInputSchema,
3611
+ annotations: readAnnotations(),
3612
+ openApiOperations: ["GET /api/v1/projects/{name}/gtm/snapshots/{snapshotId}"],
3613
+ handler: (client, input) => client.getGtmSnapshot(input.project, input.snapshotId)
3614
+ }),
3615
+ defineTool({
3616
+ name: "canonry_gtm_sync",
3617
+ title: "Sync GTM conversion evidence",
3618
+ description: "Queue a bounded GTM GET-only evidence sync. It creates a local run and sanitized live/draft snapshots, but never edits a workspace or publishes a container version. Requires google-marketing.read-live and write authority.",
3619
+ access: "write",
3620
+ tier: "gtm",
3621
+ inputSchema: projectInputSchema,
3622
+ annotations: writeAnnotations({ idempotentHint: false, openWorldHint: true }),
3623
+ openApiOperations: ["POST /api/v1/projects/{name}/gtm/sync"],
3624
+ handler: (client, input) => client.triggerGtmSync(input.project)
3625
+ }),
3626
+ defineTool({
3627
+ name: "canonry_conversion_tracking_contracts",
3628
+ title: "List conversion-tracking contracts",
3629
+ description: "List the project\u2019s declared business contracts linking an application event to Google Ads and GTM identifiers. Contract creation and changes remain operator-only.",
3630
+ access: "read",
3631
+ tier: "conversion-tracking",
3632
+ inputSchema: projectInputSchema,
3633
+ annotations: readAnnotations(),
3634
+ openApiOperations: ["GET /api/v1/projects/{name}/conversion-tracking/contracts"],
3635
+ handler: (client, input) => client.listConversionTrackingContracts(input.project)
3636
+ }),
3637
+ defineTool({
3638
+ name: "canonry_conversion_tracking_contract_get",
3639
+ title: "Get conversion-tracking contract",
3640
+ description: "Read one declared conversion-tracking contract. It declares intended semantics; use integrity assessment to evaluate stored evidence.",
3641
+ access: "read",
3642
+ tier: "conversion-tracking",
3643
+ inputSchema: conversionTrackingContractInputSchema,
3644
+ annotations: readAnnotations(),
3645
+ openApiOperations: ["GET /api/v1/projects/{name}/conversion-tracking/contracts/{contractId}"],
3646
+ handler: (client, input) => client.getConversionTrackingContract(input.project, input.contractId)
3647
+ }),
3648
+ defineTool({
3649
+ name: "canonry_conversion_tracking_integrity",
3650
+ title: "Assess stored conversion integrity",
3651
+ description: "Evaluate a declared contract against stored redacted Google Ads and GTM evidence. This does not call providers and cannot treat static configuration as proof of a browser event or observed Google Ads conversion.",
3652
+ access: "read",
3653
+ tier: "conversion-tracking",
3654
+ inputSchema: conversionTrackingContractInputSchema,
3655
+ annotations: readAnnotations(),
3656
+ openApiOperations: ["GET /api/v1/projects/{name}/conversion-tracking/contracts/{contractId}/integrity"],
3657
+ handler: (client, input) => client.getConversionTrackingIntegrity(input.project, input.contractId)
3658
+ }),
3659
+ // ----- OpenAI ads (ChatGPT ads) -----
3660
+ defineTool({
3661
+ name: "canonry_ads_status",
3662
+ title: "OpenAI ads connection status",
3663
+ description: "Connection status and last sync time for the project's OpenAI ad account (ChatGPT ads).",
3664
+ access: "read",
3665
+ tier: "ads",
3666
+ inputSchema: projectInputSchema,
3667
+ annotations: readAnnotations(),
3668
+ openApiOperations: ["GET /api/v1/projects/{name}/ads/status"],
3669
+ handler: (client, input) => client.getAdsStatus(input.project)
3670
+ }),
3671
+ defineTool({
3672
+ name: "canonry_ads_account",
3673
+ title: "Read the live OpenAI ads account",
3674
+ description: "Read live OpenAI Ads account metadata, currency, timezone, status, and account-integrity review state. Use before planning or launch to confirm the connected advertiser account is the intended one and is eligible to serve.",
3675
+ access: "read",
3676
+ tier: "ads",
3677
+ inputSchema: projectInputSchema,
3678
+ annotations: readAnnotations(true),
3679
+ openApiOperations: ["GET /api/v1/projects/{name}/ads/account"],
3680
+ handler: (client, input) => client.getAdsAccount(input.project)
3681
+ }),
3682
+ defineTool({
3683
+ name: "canonry_ads_geo_search",
3684
+ title: "Search OpenAI ads locations",
3685
+ description: "Search the live OpenAI Ads geo catalog by place name. Returns provider location IDs and canonical labels; use those IDs in campaign locationIds instead of inventing or guessing targeting identifiers.",
3686
+ access: "read",
3687
+ tier: "ads",
3688
+ inputSchema: adsGeoSearchInputSchema,
3689
+ annotations: readAnnotations(true),
3690
+ openApiOperations: ["GET /api/v1/projects/{name}/ads/geo/search"],
3691
+ handler: (client, input) => client.searchAdsGeo(input.project, { q: input.q, limit: input.limit })
3692
+ }),
3693
+ defineTool({
3694
+ name: "canonry_ads_conversion_pixels",
3695
+ title: "List OpenAI ads conversion pixels",
3696
+ description: "List conversion pixels from the live OpenAI ad account. Use with conversion event settings to verify that measurable conversion infrastructure exists before recommending activation or budget changes.",
3697
+ access: "read",
3698
+ tier: "ads",
3699
+ inputSchema: projectInputSchema,
3700
+ annotations: readAnnotations(true),
3701
+ openApiOperations: ["GET /api/v1/projects/{name}/ads/conversions/pixels"],
3702
+ handler: (client, input) => client.getAdsConversionPixels(input.project)
3703
+ }),
3704
+ defineTool({
3705
+ name: "canonry_ads_conversion_event_settings",
3706
+ title: "List OpenAI ads conversion event settings",
3707
+ description: "List live OpenAI Ads conversion event settings, attribution windows, and attached pixel or CAPI sources. Use to select and verify the conversion goal before launch.",
3708
+ access: "read",
3709
+ tier: "ads",
3710
+ inputSchema: projectInputSchema,
3711
+ annotations: readAnnotations(true),
3712
+ openApiOperations: ["GET /api/v1/projects/{name}/ads/conversions/event-settings"],
3713
+ handler: (client, input) => client.getAdsConversionEventSettings(input.project)
3714
+ }),
3715
+ defineTool({
3716
+ name: "canonry_ads_campaigns",
3717
+ title: "List synced ad campaigns",
3718
+ description: "Synced campaign snapshots with nested ad groups (context hints \u2014 the targeting primitive) and ads. Paid-surface structure; never conflate with organic cited/mentioned signals.",
3719
+ access: "read",
3720
+ tier: "ads",
3721
+ inputSchema: projectInputSchema,
3722
+ annotations: readAnnotations(),
3723
+ openApiOperations: ["GET /api/v1/projects/{name}/ads/campaigns"],
3724
+ handler: (client, input) => client.getAdsCampaigns(input.project)
3725
+ }),
3726
+ defineTool({
3727
+ name: "canonry_ads_insights",
3728
+ title: "Daily paid-performance rollups",
3729
+ description: "Daily paid-performance rollups per level (campaign/ad_group). Spend is integer micros; ctr/cpcMicros are derived server-side (null on zero denominators).",
3730
+ access: "read",
3731
+ tier: "ads",
3732
+ inputSchema: adsInsightsInputSchema,
3733
+ annotations: readAnnotations(),
3734
+ openApiOperations: ["GET /api/v1/projects/{name}/ads/insights"],
3735
+ handler: (client, input) => client.getAdsInsights(input.project, compactStringParams(input, ["level", "entityId", "from", "to"]))
3736
+ }),
3737
+ defineTool({
3738
+ name: "canonry_ads_summary",
3739
+ title: "Paid-performance summary",
3740
+ description: "Composite paid summary: campaign/ad-group/ad counts, campaign-level totals (impressions, clicks, spend micros, derived ctr/cpc) and the covered date window.",
3741
+ access: "read",
3742
+ tier: "ads",
3743
+ inputSchema: projectInputSchema,
3744
+ annotations: readAnnotations(),
3745
+ openApiOperations: ["GET /api/v1/projects/{name}/ads/summary"],
3746
+ handler: (client, input) => client.getAdsSummary(input.project)
3747
+ }),
3748
+ defineTool({
3749
+ name: "canonry_ads_delivery_diagnostics",
3750
+ title: "Stored ads delivery diagnostics",
3751
+ description: "Read stored ads snapshot provenance, account/campaign/ad-group/ad configuration facts, and historical campaign activity in one call. This is not a live OpenAI eligibility or serving verdict.",
3752
+ access: "read",
3753
+ tier: "ads",
3754
+ inputSchema: projectInputSchema,
3755
+ annotations: readAnnotations(),
3756
+ openApiOperations: ["GET /api/v1/projects/{name}/ads/delivery-diagnostics"],
3757
+ handler: (client, input) => client.getAdsDeliveryDiagnostics(input.project)
3758
+ }),
3759
+ defineTool({
3760
+ name: "canonry_ads_live_delivery",
3761
+ title: "Live ads read with stored-snapshot delta",
3762
+ description: "Call the OpenAI Ads API right now and return the provider's current status and metrics per campaign / ad group / ad, unaggregated, alongside the stored snapshot values and an explicit per-entity delta. Use this when stored data is suspected of being stale or contradicted by the advertiser UI. Read-only: it mutates nothing and does not wait for a sync. The walk is bounded and one project may issue at most one live read per minute, so prefer canonry_ads_delivery_diagnostics for routine checks.",
3763
+ access: "read",
3764
+ tier: "ads",
3765
+ inputSchema: adsLiveDeliveryInputSchema,
3766
+ annotations: readAnnotations(),
3767
+ openApiOperations: ["GET /api/v1/projects/{name}/ads/live-delivery"],
3768
+ handler: (client, input) => client.getAdsLiveDelivery(input.project, {
3769
+ campaignId: input.campaignId,
3770
+ lookbackDays: input.lookbackDays
3771
+ })
3772
+ }),
3773
+ defineTool({
3774
+ name: "canonry_ads_operations_unresolved",
3775
+ title: "List unresolved ads mutation receipts",
3776
+ description: "List pending, unknown, or actively reconciling OpenAI Ads mutation receipts that need recovery. Pass nextCursor back as cursor to advance past permanent rows. Use this before new lifecycle work so an ambiguous earlier outcome is settled instead of retried under another key. Route campaign_tree_activate receipts to canonry_ads_operation_resume_activation; use generic reconciliation only for other supported receipt kinds.",
3777
+ access: "read",
3778
+ tier: "ads",
3779
+ inputSchema: adsUnresolvedOperationsInputSchema,
3780
+ annotations: readAnnotations(),
3781
+ openApiOperations: ["GET /api/v1/projects/{name}/ads/operations"],
3782
+ handler: (client, input) => client.getUnresolvedAdsOperations(input.project, {
3783
+ state: input.state,
3784
+ limit: input.limit,
3785
+ cursor: input.cursor
3786
+ })
3787
+ }),
3788
+ defineTool({
3789
+ name: "canonry_ads_operation_get",
3790
+ title: "Get an ads mutation receipt",
3791
+ description: "Read the durable receipt for an OpenAI Ads mutation by its caller-supplied operation key. Never retry a pending or unknown receipt with a new key because the upstream request may already have succeeded. Resume campaign_tree_activate receipts through canonry_ads_operation_resume_activation; send other supported receipt kinds to generic reconciliation.",
3792
+ access: "read",
3793
+ tier: "ads",
3794
+ inputSchema: adsOperationInputSchema,
3795
+ annotations: readAnnotations(),
3796
+ openApiOperations: ["GET /api/v1/projects/{name}/ads/operations/{operationKey}"],
3797
+ handler: (client, input) => client.getAdsOperation(input.project, input.operationKey)
3798
+ }),
3799
+ defineTool({
3800
+ name: "canonry_ads_operation_reconcile",
3801
+ title: "Reconcile an ads mutation receipt",
3802
+ description: "Verify a checkpointed provider entity against the receipt-bound OpenAI ad account without retrying the original mutation. Uncheckpointed creates remain unresolved because mutable-field matching cannot prove provenance. This generic tool rejects campaign_tree_activate receipts; use canonry_ads_operation_resume_activation for those.",
3803
+ access: "write",
3804
+ tier: "ads",
3805
+ inputSchema: adsOperationReconcileInputSchema,
3806
+ annotations: writeAnnotations({ idempotentHint: true, openWorldHint: true }),
3807
+ openApiOperations: ["POST /api/v1/projects/{name}/ads/operations/{operationKey}/reconcile"],
3808
+ handler: (client, input) => client.reconcileAdsOperation(input.project, input.operationKey)
3809
+ }),
3810
+ defineTool({
3811
+ name: "canonry_ads_operation_resume_activation",
3812
+ title: "Resume an ads activation receipt",
3813
+ description: "Resume recovery for an existing campaign_tree_activate receipt using its durable approval grant and ordered step ledger. The request is bodyless, requires ads.activate on the exact executor key already bound to the grant, and cannot replace the operation, grant, manifest, campaign, or account. Canonry inspects provider state and never blindly resends an ambiguous activation mutation.",
3814
+ access: "write",
3815
+ tier: "ads",
3816
+ inputSchema: adsOperationResumeActivationInputSchema,
3817
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true, openWorldHint: true }),
3818
+ openApiOperations: ["POST /api/v1/projects/{name}/ads/operations/{operationKey}/resume-activation"],
3819
+ handler: (client, input) => client.resumeAdsActivation(input.project, input.operationKey)
3820
+ }),
3821
+ defineTool({
3822
+ name: "canonry_ads_image_upload",
3823
+ title: "Upload an ads image from URL",
3824
+ description: "Upload a public HTTPS image URL to the connected OpenAI ad account. The operation key makes a repeated identical request replay its receipt without another upstream upload. Save the returned file entityId for chat-card creation.",
3825
+ access: "write",
3826
+ tier: "ads",
3827
+ inputSchema: adsImageUploadInputSchema,
3828
+ annotations: writeAnnotations({ idempotentHint: true, openWorldHint: true }),
3829
+ openApiOperations: ["POST /api/v1/projects/{name}/ads/files"],
3830
+ handler: (client, input) => client.uploadAdsImage(input.project, input.request)
3831
+ }),
3832
+ defineTool({
3833
+ name: "canonry_ads_campaign_create",
3834
+ title: "Create a paused ads campaign",
3835
+ description: "Create an OpenAI Ads campaign PAUSED with an explicit lifetime spend limit and location allowlist. biddingType is what the account is BILLED for (clicks or the legacy impressions default) and is immutable after creation; conversionEventSettingIds are what delivery OPTIMIZES toward and are independent, so a clicks campaign may omit them; adding provider-issued IDs makes it conversion-optimized. The server ignores any status concept and always sends paused. Inspect the receipt, then create matching paused ad groups and ads. A human must separately approve the exact tree before this operator can activate it.",
3836
+ access: "write",
3837
+ tier: "ads",
3838
+ inputSchema: adsCampaignCreateInputSchema,
3839
+ annotations: writeAnnotations({ idempotentHint: true, openWorldHint: true }),
3840
+ openApiOperations: ["POST /api/v1/projects/{name}/ads/campaigns"],
3841
+ handler: (client, input) => client.createAdsCampaign(input.project, input.request)
3842
+ }),
3843
+ defineTool({
3844
+ name: "canonry_ads_campaign_update",
3845
+ title: "Update an ads campaign",
3846
+ description: "Update a PAUSED campaign copy, dates, lifetime spend limit, or locations without changing status. Active campaigns fail closed: pause first, sync, and use the refreshed upstreamUpdatedAt. A human must approve the exact updated tree before this operator can reactivate it. Uses a durable operation key.",
3847
+ access: "write",
3848
+ tier: "ads",
3849
+ inputSchema: adsCampaignUpdateInputSchema,
3850
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true, openWorldHint: true }),
3851
+ openApiOperations: ["POST /api/v1/projects/{name}/ads/campaigns/{id}"],
3852
+ handler: (client, input) => client.updateAdsCampaign(input.project, input.campaignId, input.request)
3853
+ }),
3854
+ defineTool({
3855
+ name: "canonry_ads_campaign_activate_tree",
3856
+ title: "Activate an approved ads campaign tree",
3857
+ description: "Execute one short-lived human approval grant for the exact paused campaign, ad groups, and reviewed ads named by its manifest. The grant is bound to this executor key and manifest hash. Canonry checkpoints every step, activates ads before parents, verifies active state, and rolls back parent-first on failure. This tool cannot create or widen an approval.",
3858
+ access: "write",
3859
+ tier: "ads",
3860
+ inputSchema: adsCampaignActivateTreeInputSchema,
3861
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true, openWorldHint: true }),
3862
+ openApiOperations: ["POST /api/v1/projects/{name}/ads/campaigns/{id}/activate-tree"],
3863
+ handler: (client, input) => client.activateAdsCampaignTree(input.project, input.campaignId, input.request)
3864
+ }),
3865
+ defineTool({
3866
+ name: "canonry_ads_campaign_pause",
3867
+ title: "Pause an ads campaign",
3868
+ description: "Pause a campaign immediately. Use this first when spend, conversion tracking, landing-page behavior, or policy status is unsafe. The operation is idempotent through its durable operation key.",
3869
+ access: "write",
3870
+ tier: "ads",
3871
+ inputSchema: adsCampaignPauseInputSchema,
3872
+ annotations: writeAnnotations({ idempotentHint: true, openWorldHint: true }),
3873
+ openApiOperations: ["POST /api/v1/projects/{name}/ads/campaigns/{id}/pause"],
3874
+ handler: (client, input) => client.pauseAdsCampaign(input.project, input.campaignId, input.request)
3875
+ }),
3876
+ defineTool({
3877
+ name: "canonry_ads_ad_group_create",
3878
+ title: "Create a paused ads ad group",
3879
+ description: "Create a PAUSED ad group under a reviewed campaign. Context hints describe when the audit offer is useful. Set billingEventType=click under a clicks campaign; omit it for the legacy impression mode. Canonry reads the live parent and rejects a billing/bidding mismatch before mutation.",
3880
+ access: "write",
3881
+ tier: "ads",
3882
+ inputSchema: adsAdGroupCreateInputSchema,
3883
+ annotations: writeAnnotations({ idempotentHint: true, openWorldHint: true }),
3884
+ openApiOperations: ["POST /api/v1/projects/{name}/ads/ad-groups"],
3885
+ handler: (client, input) => client.createAdsAdGroup(input.project, input.request)
3886
+ }),
3887
+ defineTool({
3888
+ name: "canonry_ads_ad_group_update",
3889
+ title: "Update an ads ad group",
3890
+ description: "Update a PAUSED ad group name, description, context hints, or max bid without changing its billing event or status. Active ad groups fail closed: pause first, sync, and use the refreshed upstreamUpdatedAt. A human must reactivate after review. Uses a durable operation key.",
3891
+ access: "write",
3892
+ tier: "ads",
3893
+ inputSchema: adsAdGroupUpdateInputSchema,
3894
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true, openWorldHint: true }),
3895
+ openApiOperations: ["POST /api/v1/projects/{name}/ads/ad-groups/{id}"],
3896
+ handler: (client, input) => client.updateAdsAdGroup(input.project, input.adGroupId, input.request)
3897
+ }),
3898
+ defineTool({
3899
+ name: "canonry_ads_ad_group_pause",
3900
+ title: "Pause an ads ad group",
3901
+ description: "Pause an ad group through a durable idempotent operation receipt.",
3902
+ access: "write",
3903
+ tier: "ads",
3904
+ inputSchema: adsAdGroupPauseInputSchema,
3905
+ annotations: writeAnnotations({ idempotentHint: true, openWorldHint: true }),
3906
+ openApiOperations: ["POST /api/v1/projects/{name}/ads/ad-groups/{id}/pause"],
3907
+ handler: (client, input) => client.pauseAdsAdGroup(input.project, input.adGroupId, input.request)
3908
+ }),
3909
+ defineTool({
3910
+ name: "canonry_ads_ad_create",
3911
+ title: "Create a paused chat-card ad",
3912
+ description: "Create a PAUSED ChatGPT chat-card ad using a previously uploaded file entityId and an HTTPS destination. Title is 3-50 characters and body is at most 100. Activation is deliberately human-only for the beta.",
3913
+ access: "write",
3914
+ tier: "ads",
3915
+ inputSchema: adsAdCreateInputSchema,
3916
+ annotations: writeAnnotations({ idempotentHint: true, openWorldHint: true }),
3917
+ openApiOperations: ["POST /api/v1/projects/{name}/ads/ads"],
3918
+ handler: (client, input) => client.createAdsAd(input.project, input.request)
3919
+ }),
3920
+ defineTool({
3921
+ name: "canonry_ads_ad_update",
3922
+ title: "Update a chat-card ad",
3923
+ description: "Update a PAUSED ad name or full chat-card creative without changing status. Active ads fail closed: pause first, sync, and use the refreshed upstreamUpdatedAt. A human must reactivate after review. Uses a durable operation key.",
3924
+ access: "write",
3925
+ tier: "ads",
3926
+ inputSchema: adsAdUpdateInputSchema,
3927
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true, openWorldHint: true }),
3928
+ openApiOperations: ["POST /api/v1/projects/{name}/ads/ads/{id}"],
3929
+ handler: (client, input) => client.updateAdsAd(input.project, input.adId, input.request)
3930
+ }),
3931
+ defineTool({
3932
+ name: "canonry_ads_ad_pause",
3933
+ title: "Pause a chat-card ad",
3934
+ description: "Pause an individual ad through a durable idempotent operation receipt.",
3935
+ access: "write",
3936
+ tier: "ads",
3937
+ inputSchema: adsAdPauseInputSchema,
3938
+ annotations: writeAnnotations({ idempotentHint: true, openWorldHint: true }),
3939
+ openApiOperations: ["POST /api/v1/projects/{name}/ads/ads/{id}/pause"],
3940
+ handler: (client, input) => client.pauseAdsAd(input.project, input.adId, input.request)
3941
+ }),
3942
+ defineTool({
3943
+ name: "canonry_ads_sync",
3944
+ title: "Trigger ads sync",
3945
+ description: "Trigger an ads-sync run (entity snapshots + daily paid-performance rollups) for the connected OpenAI ad account. Returns the run id; poll canonry_run_get for status.",
3946
+ access: "write",
3947
+ tier: "ads",
3948
+ inputSchema: projectInputSchema,
3949
+ annotations: writeAnnotations({ idempotentHint: true }),
3950
+ openApiOperations: ["POST /api/v1/projects/{name}/ads/sync"],
3951
+ handler: (client, input) => client.triggerAdsSync(input.project)
3952
+ })
3953
+ ];
3954
+ var CANONRY_MCP_TOOL_COUNT = canonryMcpTools.length;
3955
+ var CANONRY_MCP_READ_TOOL_COUNT = canonryMcpTools.filter((tool) => tool.access === "read").length;
3956
+ var CANONRY_MCP_CORE_TOOL_COUNT = canonryMcpTools.filter((tool) => tool.tier === "core").length;
3957
+ var CanonryMcpToolNames = Object.freeze(
3958
+ Object.fromEntries(canonryMcpTools.map((tool) => [tool.name, tool.name]))
3959
+ );
3960
+
3961
+ export {
3962
+ isMachineFormat,
3963
+ EXIT_USER_ERROR,
3964
+ EXIT_SYSTEM_ERROR,
3965
+ CliError,
3966
+ usageError,
3967
+ isEndpointMissing,
3968
+ systemError,
3969
+ printCliError,
3970
+ PACKAGE_VERSION,
3971
+ measurementDraftOperationSchema,
3972
+ runMeasurementDraftAction,
3973
+ canonryMcpTools,
3974
+ CanonryMcpToolNames
3975
+ };