@ainyc/canonry 2.5.1 → 2.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp.js ADDED
@@ -0,0 +1,848 @@
1
+ import {
2
+ CliError,
3
+ competitorBatchRequestSchema,
4
+ createApiClient,
5
+ keywordBatchRequestSchema,
6
+ keywordGenerateRequestSchema,
7
+ notificationCreateRequestSchema,
8
+ notificationEventSchema,
9
+ projectConfigSchema,
10
+ projectUpsertRequestSchema,
11
+ runTriggerRequestSchema,
12
+ scheduleUpsertRequestSchema
13
+ } from "./chunk-FPZUQADO.js";
14
+ import "./chunk-MLKGABMK.js";
15
+
16
+ // src/mcp/cli.ts
17
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
18
+
19
+ // src/mcp/server.ts
20
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
21
+
22
+ // src/package-version.ts
23
+ import { createRequire } from "module";
24
+ var _require = createRequire(import.meta.url);
25
+ var PACKAGE_VERSION = _require("../package.json").version;
26
+
27
+ // src/mcp/tool-registry.ts
28
+ import { z as z2 } from "zod";
29
+
30
+ // src/mcp/schema.ts
31
+ import { z } from "zod";
32
+ var projectNameSchema = z.string().min(1).describe("Canonry project name.");
33
+ var runIdSchema = z.string().min(1).describe("Canonry run ID.");
34
+ var insightIdSchema = z.string().min(1).describe("Canonry insight ID.");
35
+ var analyticsWindowSchema = z.enum(["7d", "30d", "90d", "all"]).describe("Analytics time window.");
36
+ var emptyInputSchema = z.object({});
37
+ var projectInputSchema = z.object({
38
+ project: projectNameSchema
39
+ });
40
+ function toJsonSchema(schema, name) {
41
+ return {
42
+ ...z.toJSONSchema(schema, { target: "draft-7" }),
43
+ title: name
44
+ };
45
+ }
46
+ function compactStringParams(values, keys) {
47
+ const params = {};
48
+ for (const key of keys) {
49
+ const value = values[key];
50
+ if (value === void 0 || value === null || value === "") continue;
51
+ params[key] = String(value);
52
+ }
53
+ return Object.keys(params).length ? params : void 0;
54
+ }
55
+ function uniqueStrings(values) {
56
+ const seen = /* @__PURE__ */ new Set();
57
+ const result = [];
58
+ for (const value of values) {
59
+ const trimmed = value.trim();
60
+ if (!trimmed || seen.has(trimmed)) continue;
61
+ seen.add(trimmed);
62
+ result.push(trimmed);
63
+ }
64
+ return result;
65
+ }
66
+
67
+ // src/mcp/tool-registry.ts
68
+ var readAnnotations = (openWorldHint) => ({
69
+ readOnlyHint: true,
70
+ ...openWorldHint ? { openWorldHint } : {}
71
+ });
72
+ var writeAnnotations = (opts) => ({
73
+ readOnlyHint: false,
74
+ idempotentHint: opts.idempotentHint,
75
+ destructiveHint: Boolean(opts.destructiveHint),
76
+ ...opts.openWorldHint ? { openWorldHint: opts.openWorldHint } : {}
77
+ });
78
+ function defineTool(tool) {
79
+ return {
80
+ ...tool,
81
+ inputJsonSchema: toJsonSchema(tool.inputSchema, tool.name)
82
+ };
83
+ }
84
+ var runTriggerInputSchema = z2.object({
85
+ project: projectNameSchema,
86
+ request: runTriggerRequestSchema.optional()
87
+ });
88
+ var runsListInputSchema = z2.object({
89
+ project: projectNameSchema,
90
+ limit: z2.number().int().positive().max(500).optional()
91
+ });
92
+ var runGetInputSchema = z2.object({
93
+ runId: runIdSchema
94
+ });
95
+ var timelineInputSchema = z2.object({
96
+ project: projectNameSchema,
97
+ location: z2.string().optional().describe("Location label. Use an empty string for locationless results.")
98
+ });
99
+ var snapshotsListInputSchema = z2.object({
100
+ project: projectNameSchema,
101
+ limit: z2.number().int().positive().max(500).optional(),
102
+ offset: z2.number().int().nonnegative().optional(),
103
+ location: z2.string().optional().describe("Location label. Use an empty string for locationless results.")
104
+ });
105
+ var snapshotsDiffInputSchema = z2.object({
106
+ project: projectNameSchema,
107
+ run1: runIdSchema,
108
+ run2: runIdSchema
109
+ });
110
+ var insightsListInputSchema = z2.object({
111
+ project: projectNameSchema,
112
+ dismissed: z2.boolean().optional(),
113
+ runId: runIdSchema.optional()
114
+ });
115
+ var insightInputSchema = z2.object({
116
+ project: projectNameSchema,
117
+ insightId: insightIdSchema
118
+ });
119
+ var healthHistoryInputSchema = z2.object({
120
+ project: projectNameSchema,
121
+ limit: z2.number().int().positive().max(100).optional()
122
+ });
123
+ var gscPerformanceInputSchema = z2.object({
124
+ project: projectNameSchema,
125
+ startDate: z2.string().optional(),
126
+ endDate: z2.string().optional(),
127
+ query: z2.string().optional(),
128
+ page: z2.string().optional(),
129
+ limit: z2.number().int().positive().max(500).optional(),
130
+ window: analyticsWindowSchema.optional()
131
+ });
132
+ var gscInspectionsInputSchema = z2.object({
133
+ project: projectNameSchema,
134
+ url: z2.string().optional(),
135
+ limit: z2.number().int().positive().max(500).optional()
136
+ });
137
+ var gscCoverageHistoryInputSchema = z2.object({
138
+ project: projectNameSchema,
139
+ limit: z2.number().int().positive().max(500).optional()
140
+ });
141
+ var gaWindowInputSchema = z2.object({
142
+ project: projectNameSchema,
143
+ window: analyticsWindowSchema.optional()
144
+ });
145
+ var gaTrafficInputSchema = gaWindowInputSchema.extend({
146
+ limit: z2.number().int().positive().max(500).optional()
147
+ });
148
+ var keywordsInputSchema = z2.object({
149
+ project: projectNameSchema,
150
+ request: keywordBatchRequestSchema
151
+ });
152
+ var keywordGenerateInputSchema = z2.object({
153
+ project: projectNameSchema,
154
+ request: keywordGenerateRequestSchema
155
+ });
156
+ var competitorsInputSchema = z2.object({
157
+ project: projectNameSchema,
158
+ request: competitorBatchRequestSchema
159
+ });
160
+ var projectUpsertInputSchema = z2.object({
161
+ project: projectNameSchema,
162
+ request: projectUpsertRequestSchema
163
+ });
164
+ var applyConfigInputSchema = z2.object({
165
+ config: projectConfigSchema
166
+ });
167
+ var scheduleSetInputSchema = z2.object({
168
+ project: projectNameSchema,
169
+ schedule: scheduleUpsertRequestSchema
170
+ });
171
+ var agentWebhookAttachInputSchema = z2.object({
172
+ project: projectNameSchema,
173
+ url: z2.string().url()
174
+ });
175
+ var AGENT_WEBHOOK_EVENTS = [
176
+ notificationEventSchema.enum["run.completed"],
177
+ notificationEventSchema.enum["insight.critical"],
178
+ notificationEventSchema.enum["insight.high"],
179
+ notificationEventSchema.enum["citation.gained"]
180
+ ];
181
+ var canonryMcpTools = [
182
+ defineTool({
183
+ name: "canonry_projects_list",
184
+ title: "List Canonry projects",
185
+ description: "List all Canonry projects available through the configured API.",
186
+ access: "read",
187
+ inputSchema: emptyInputSchema,
188
+ annotations: readAnnotations(),
189
+ openApiOperations: ["GET /api/v1/projects"],
190
+ handler: (client) => client.listProjects()
191
+ }),
192
+ defineTool({
193
+ name: "canonry_project_get",
194
+ title: "Get project",
195
+ description: "Get a Canonry project by name.",
196
+ access: "read",
197
+ inputSchema: projectInputSchema,
198
+ annotations: readAnnotations(),
199
+ openApiOperations: ["GET /api/v1/projects/{name}"],
200
+ handler: (client, input) => client.getProject(input.project)
201
+ }),
202
+ defineTool({
203
+ name: "canonry_project_export",
204
+ title: "Export project config",
205
+ description: "Export a Canonry project in config-as-code format.",
206
+ access: "read",
207
+ inputSchema: projectInputSchema,
208
+ annotations: readAnnotations(),
209
+ openApiOperations: ["GET /api/v1/projects/{name}/export"],
210
+ handler: (client, input) => client.getExport(input.project)
211
+ }),
212
+ defineTool({
213
+ name: "canonry_project_history",
214
+ title: "Get project history",
215
+ description: "Get audit history for a Canonry project.",
216
+ access: "read",
217
+ inputSchema: projectInputSchema,
218
+ annotations: readAnnotations(),
219
+ openApiOperations: ["GET /api/v1/projects/{name}/history"],
220
+ handler: (client, input) => client.getHistory(input.project)
221
+ }),
222
+ defineTool({
223
+ name: "canonry_runs_list",
224
+ title: "List project runs",
225
+ description: "List runs for a Canonry project.",
226
+ access: "read",
227
+ inputSchema: runsListInputSchema,
228
+ annotations: readAnnotations(),
229
+ openApiOperations: ["GET /api/v1/projects/{name}/runs"],
230
+ handler: (client, input) => client.listRuns(input.project, input.limit)
231
+ }),
232
+ defineTool({
233
+ name: "canonry_runs_latest",
234
+ title: "Get latest project run",
235
+ description: "Get the latest run and total run count for a Canonry project.",
236
+ access: "read",
237
+ inputSchema: projectInputSchema,
238
+ annotations: readAnnotations(),
239
+ openApiOperations: ["GET /api/v1/projects/{name}/runs/latest"],
240
+ handler: (client, input) => client.getLatestRun(input.project)
241
+ }),
242
+ defineTool({
243
+ name: "canonry_run_get",
244
+ title: "Get run",
245
+ description: "Get a Canonry run with its snapshots.",
246
+ access: "read",
247
+ inputSchema: runGetInputSchema,
248
+ annotations: readAnnotations(),
249
+ openApiOperations: ["GET /api/v1/runs/{id}"],
250
+ handler: (client, input) => client.getRun(input.runId)
251
+ }),
252
+ defineTool({
253
+ name: "canonry_timeline_get",
254
+ title: "Get project timeline",
255
+ description: "Get per-keyword citation history for a Canonry project.",
256
+ access: "read",
257
+ inputSchema: timelineInputSchema,
258
+ annotations: readAnnotations(),
259
+ openApiOperations: ["GET /api/v1/projects/{name}/timeline"],
260
+ handler: (client, input) => client.getTimeline(input.project, input.location)
261
+ }),
262
+ defineTool({
263
+ name: "canonry_snapshots_list",
264
+ title: "List query snapshots",
265
+ description: "List paginated query snapshots for a Canonry project.",
266
+ access: "read",
267
+ inputSchema: snapshotsListInputSchema,
268
+ annotations: readAnnotations(),
269
+ openApiOperations: ["GET /api/v1/projects/{name}/snapshots"],
270
+ handler: (client, input) => client.getSnapshots(input.project, {
271
+ limit: input.limit,
272
+ offset: input.offset,
273
+ location: input.location
274
+ })
275
+ }),
276
+ defineTool({
277
+ name: "canonry_snapshots_diff",
278
+ title: "Diff snapshots",
279
+ description: "Compare query snapshot states between two Canonry runs.",
280
+ access: "read",
281
+ inputSchema: snapshotsDiffInputSchema,
282
+ annotations: readAnnotations(),
283
+ openApiOperations: ["GET /api/v1/projects/{name}/snapshots/diff"],
284
+ handler: (client, input) => client.getSnapshotDiff(input.project, input.run1, input.run2)
285
+ }),
286
+ defineTool({
287
+ name: "canonry_insights_list",
288
+ title: "List insights",
289
+ description: "List intelligence insights for a Canonry project.",
290
+ access: "read",
291
+ inputSchema: insightsListInputSchema,
292
+ annotations: readAnnotations(),
293
+ openApiOperations: ["GET /api/v1/projects/{name}/insights"],
294
+ handler: (client, input) => client.getInsights(input.project, { dismissed: input.dismissed, runId: input.runId })
295
+ }),
296
+ defineTool({
297
+ name: "canonry_insight_get",
298
+ title: "Get insight",
299
+ description: "Get one intelligence insight for a Canonry project.",
300
+ access: "read",
301
+ inputSchema: insightInputSchema,
302
+ annotations: readAnnotations(),
303
+ openApiOperations: ["GET /api/v1/projects/{name}/insights/{id}"],
304
+ handler: (client, input) => client.getInsight(input.project, input.insightId)
305
+ }),
306
+ defineTool({
307
+ name: "canonry_health_latest",
308
+ title: "Get latest health",
309
+ description: "Get the latest health snapshot for a Canonry project.",
310
+ access: "read",
311
+ inputSchema: projectInputSchema,
312
+ annotations: readAnnotations(),
313
+ openApiOperations: ["GET /api/v1/projects/{name}/health/latest"],
314
+ handler: (client, input) => client.getHealth(input.project)
315
+ }),
316
+ defineTool({
317
+ name: "canonry_health_history",
318
+ title: "Get health history",
319
+ description: "Get health snapshot history for a Canonry project.",
320
+ access: "read",
321
+ inputSchema: healthHistoryInputSchema,
322
+ annotations: readAnnotations(),
323
+ openApiOperations: ["GET /api/v1/projects/{name}/health/history"],
324
+ handler: (client, input) => client.getHealthHistory(input.project, input.limit)
325
+ }),
326
+ defineTool({
327
+ name: "canonry_keywords_list",
328
+ title: "List keywords",
329
+ description: "List tracked keywords for a Canonry project.",
330
+ access: "read",
331
+ inputSchema: projectInputSchema,
332
+ annotations: readAnnotations(),
333
+ openApiOperations: ["GET /api/v1/projects/{name}/keywords"],
334
+ handler: (client, input) => client.listKeywords(input.project)
335
+ }),
336
+ defineTool({
337
+ name: "canonry_competitors_list",
338
+ title: "List competitors",
339
+ description: "List tracked competitors for a Canonry project.",
340
+ access: "read",
341
+ inputSchema: projectInputSchema,
342
+ annotations: readAnnotations(),
343
+ openApiOperations: ["GET /api/v1/projects/{name}/competitors"],
344
+ handler: (client, input) => client.listCompetitors(input.project)
345
+ }),
346
+ defineTool({
347
+ name: "canonry_schedule_get",
348
+ title: "Get schedule",
349
+ description: "Get the scheduled run configuration for a Canonry project.",
350
+ access: "read",
351
+ inputSchema: projectInputSchema,
352
+ annotations: readAnnotations(),
353
+ openApiOperations: ["GET /api/v1/projects/{name}/schedule"],
354
+ handler: (client, input) => client.getSchedule(input.project)
355
+ }),
356
+ defineTool({
357
+ name: "canonry_settings_get",
358
+ title: "Get settings",
359
+ description: "Get Canonry API settings and configured provider status.",
360
+ access: "read",
361
+ inputSchema: emptyInputSchema,
362
+ annotations: readAnnotations(),
363
+ openApiOperations: ["GET /api/v1/settings"],
364
+ handler: (client) => client.getSettings()
365
+ }),
366
+ defineTool({
367
+ name: "canonry_google_connections_list",
368
+ title: "List Google connections",
369
+ description: "List configured Google connections for a Canonry project.",
370
+ access: "read",
371
+ inputSchema: projectInputSchema,
372
+ annotations: readAnnotations(),
373
+ openApiOperations: ["GET /api/v1/projects/{name}/google/connections"],
374
+ handler: (client, input) => client.googleConnections(input.project)
375
+ }),
376
+ defineTool({
377
+ name: "canonry_gsc_performance",
378
+ title: "Get GSC performance",
379
+ description: "Get stored Google Search Console performance rows for a Canonry project.",
380
+ access: "read",
381
+ inputSchema: gscPerformanceInputSchema,
382
+ annotations: readAnnotations(),
383
+ openApiOperations: ["GET /api/v1/projects/{name}/google/gsc/performance"],
384
+ handler: (client, input) => client.gscPerformance(input.project, compactStringParams(input, ["startDate", "endDate", "query", "page", "limit", "window"]))
385
+ }),
386
+ defineTool({
387
+ name: "canonry_gsc_inspections",
388
+ title: "List GSC inspections",
389
+ description: "List stored URL inspection rows for a Canonry project.",
390
+ access: "read",
391
+ inputSchema: gscInspectionsInputSchema,
392
+ annotations: readAnnotations(),
393
+ openApiOperations: ["GET /api/v1/projects/{name}/google/gsc/inspections"],
394
+ handler: (client, input) => client.gscInspections(input.project, compactStringParams(input, ["url", "limit"]))
395
+ }),
396
+ defineTool({
397
+ name: "canonry_gsc_deindexed",
398
+ title: "List deindexed GSC URLs",
399
+ description: "List URLs that appear to have become deindexed in Google Search Console data.",
400
+ access: "read",
401
+ inputSchema: projectInputSchema,
402
+ annotations: readAnnotations(),
403
+ openApiOperations: ["GET /api/v1/projects/{name}/google/gsc/deindexed"],
404
+ handler: (client, input) => client.gscDeindexed(input.project)
405
+ }),
406
+ defineTool({
407
+ name: "canonry_gsc_coverage",
408
+ title: "Get GSC coverage",
409
+ description: "Get Google Search Console coverage summary for a Canonry project.",
410
+ access: "read",
411
+ inputSchema: projectInputSchema,
412
+ annotations: readAnnotations(),
413
+ openApiOperations: ["GET /api/v1/projects/{name}/google/gsc/coverage"],
414
+ handler: (client, input) => client.gscCoverage(input.project)
415
+ }),
416
+ defineTool({
417
+ name: "canonry_gsc_coverage_history",
418
+ title: "Get GSC coverage history",
419
+ description: "Get Google Search Console coverage history snapshots for a Canonry project.",
420
+ access: "read",
421
+ inputSchema: gscCoverageHistoryInputSchema,
422
+ annotations: readAnnotations(),
423
+ openApiOperations: ["GET /api/v1/projects/{name}/google/gsc/coverage/history"],
424
+ handler: (client, input) => client.gscCoverageHistory(input.project, { limit: input.limit })
425
+ }),
426
+ defineTool({
427
+ name: "canonry_gsc_sitemaps",
428
+ title: "Get GSC sitemaps",
429
+ description: "Get sitemap data from Google Search Console for a Canonry project.",
430
+ access: "read",
431
+ inputSchema: projectInputSchema,
432
+ annotations: readAnnotations(true),
433
+ openApiOperations: ["GET /api/v1/projects/{name}/google/gsc/sitemaps"],
434
+ handler: (client, input) => client.gscSitemaps(input.project)
435
+ }),
436
+ defineTool({
437
+ name: "canonry_ga_status",
438
+ title: "Get GA status",
439
+ description: "Get Google Analytics connection status for a Canonry project.",
440
+ access: "read",
441
+ inputSchema: projectInputSchema,
442
+ annotations: readAnnotations(),
443
+ openApiOperations: ["GET /api/v1/projects/{name}/ga/status"],
444
+ handler: (client, input) => client.gaStatus(input.project)
445
+ }),
446
+ defineTool({
447
+ name: "canonry_ga_traffic",
448
+ title: "Get GA traffic",
449
+ description: "Get Google Analytics traffic summary for a Canonry project.",
450
+ access: "read",
451
+ inputSchema: gaTrafficInputSchema,
452
+ annotations: readAnnotations(),
453
+ openApiOperations: ["GET /api/v1/projects/{name}/ga/traffic"],
454
+ handler: (client, input) => client.gaTraffic(input.project, compactStringParams(input, ["limit", "window"]))
455
+ }),
456
+ defineTool({
457
+ name: "canonry_ga_coverage",
458
+ title: "Get GA coverage",
459
+ description: "Get Google Analytics page coverage for a Canonry project.",
460
+ access: "read",
461
+ inputSchema: projectInputSchema,
462
+ annotations: readAnnotations(),
463
+ openApiOperations: ["GET /api/v1/projects/{name}/ga/coverage"],
464
+ handler: (client, input) => client.gaCoverage(input.project)
465
+ }),
466
+ defineTool({
467
+ name: "canonry_ga_ai_referral_history",
468
+ title: "Get GA AI referral history",
469
+ description: "Get AI referral sessions per day grouped by source.",
470
+ access: "read",
471
+ inputSchema: gaWindowInputSchema,
472
+ annotations: readAnnotations(),
473
+ openApiOperations: ["GET /api/v1/projects/{name}/ga/ai-referral-history"],
474
+ handler: (client, input) => client.gaAiReferralHistory(input.project, compactStringParams(input, ["window"]))
475
+ }),
476
+ defineTool({
477
+ name: "canonry_ga_social_referral_history",
478
+ title: "Get GA social referral history",
479
+ description: "Get social referral sessions per day grouped by source.",
480
+ access: "read",
481
+ inputSchema: gaWindowInputSchema,
482
+ annotations: readAnnotations(),
483
+ openApiOperations: ["GET /api/v1/projects/{name}/ga/social-referral-history"],
484
+ handler: (client, input) => client.gaSocialReferralHistory(input.project, compactStringParams(input, ["window"]))
485
+ }),
486
+ defineTool({
487
+ name: "canonry_ga_social_referral_trend",
488
+ title: "Get GA social referral trend",
489
+ description: "Get social referral trend with biggest mover for a Canonry project.",
490
+ access: "read",
491
+ inputSchema: projectInputSchema,
492
+ annotations: readAnnotations(),
493
+ openApiOperations: ["GET /api/v1/projects/{name}/ga/social-referral-trend"],
494
+ handler: (client, input) => client.gaSocialReferralTrend(input.project)
495
+ }),
496
+ defineTool({
497
+ name: "canonry_ga_attribution_trend",
498
+ title: "Get GA attribution trend",
499
+ description: "Get per-channel attribution trends for organic, AI, social, and total sessions.",
500
+ access: "read",
501
+ inputSchema: projectInputSchema,
502
+ annotations: readAnnotations(),
503
+ openApiOperations: ["GET /api/v1/projects/{name}/ga/attribution-trend"],
504
+ handler: (client, input) => client.gaAttributionTrend(input.project)
505
+ }),
506
+ defineTool({
507
+ name: "canonry_ga_session_history",
508
+ title: "Get GA session history",
509
+ description: "Get total sessions per day for a Canonry project.",
510
+ access: "read",
511
+ inputSchema: gaWindowInputSchema,
512
+ annotations: readAnnotations(),
513
+ openApiOperations: ["GET /api/v1/projects/{name}/ga/session-history"],
514
+ handler: (client, input) => client.gaSessionHistory(input.project, compactStringParams(input, ["window"]))
515
+ }),
516
+ defineTool({
517
+ name: "canonry_project_upsert",
518
+ title: "Create or replace project",
519
+ 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.",
520
+ access: "write",
521
+ inputSchema: projectUpsertInputSchema,
522
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
523
+ openApiOperations: ["PUT /api/v1/projects/{name}"],
524
+ handler: (client, input) => client.putProject(input.project, input.request)
525
+ }),
526
+ defineTool({
527
+ name: "canonry_apply_config",
528
+ title: "Apply project config",
529
+ 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. For multi-document YAML, call this tool once per project document.",
530
+ access: "write",
531
+ inputSchema: applyConfigInputSchema,
532
+ // Declarative apply is safe to repeat, but it replaces configured child state.
533
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
534
+ openApiOperations: ["POST /api/v1/apply"],
535
+ handler: (client, input) => client.apply(input.config)
536
+ }),
537
+ defineTool({
538
+ name: "canonry_keywords_generate",
539
+ title: "Generate keyword suggestions",
540
+ description: "Generate candidate key phrases using a configured provider. Returns suggestions only; use canonry_keywords_add to persist them.",
541
+ access: "write",
542
+ inputSchema: keywordGenerateInputSchema,
543
+ annotations: writeAnnotations({ idempotentHint: false, openWorldHint: true }),
544
+ openApiOperations: ["POST /api/v1/projects/{name}/keywords/generate"],
545
+ handler: (client, input) => client.generateKeywords(input.project, input.request.provider, input.request.count)
546
+ }),
547
+ defineTool({
548
+ name: "canonry_keywords_replace",
549
+ title: "Replace keywords",
550
+ description: "Replace the tracked keyword set for a Canonry project.",
551
+ access: "write",
552
+ inputSchema: keywordsInputSchema,
553
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
554
+ openApiOperations: ["PUT /api/v1/projects/{name}/keywords"],
555
+ handler: async (client, input) => {
556
+ await client.putKeywords(input.project, uniqueStrings(input.request.keywords));
557
+ }
558
+ }),
559
+ defineTool({
560
+ name: "canonry_run_trigger",
561
+ title: "Trigger run",
562
+ description: "Trigger an answer-visibility run for a Canonry project.",
563
+ access: "write",
564
+ inputSchema: runTriggerInputSchema,
565
+ annotations: writeAnnotations({ idempotentHint: false, openWorldHint: true }),
566
+ openApiOperations: ["POST /api/v1/projects/{name}/runs"],
567
+ handler: (client, input) => client.triggerRun(input.project, input.request)
568
+ }),
569
+ defineTool({
570
+ name: "canonry_run_cancel",
571
+ title: "Cancel run",
572
+ description: "Cancel a queued or running Canonry run.",
573
+ access: "write",
574
+ inputSchema: runGetInputSchema,
575
+ annotations: writeAnnotations({ idempotentHint: false, destructiveHint: true }),
576
+ openApiOperations: ["POST /api/v1/runs/{id}/cancel"],
577
+ handler: (client, input) => client.cancelRun(input.runId)
578
+ }),
579
+ defineTool({
580
+ name: "canonry_keywords_add",
581
+ title: "Add keywords",
582
+ description: "Append tracked keywords to a Canonry project; existing keywords are skipped by the API.",
583
+ access: "write",
584
+ inputSchema: keywordsInputSchema,
585
+ annotations: writeAnnotations({ idempotentHint: true }),
586
+ openApiOperations: ["POST /api/v1/projects/{name}/keywords"],
587
+ handler: async (client, input) => {
588
+ await client.appendKeywords(input.project, uniqueStrings(input.request.keywords));
589
+ }
590
+ }),
591
+ defineTool({
592
+ name: "canonry_keywords_remove",
593
+ title: "Remove keywords",
594
+ description: "Remove tracked keywords from a Canonry project.",
595
+ access: "write",
596
+ inputSchema: keywordsInputSchema,
597
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
598
+ openApiOperations: ["DELETE /api/v1/projects/{name}/keywords"],
599
+ handler: async (client, input) => {
600
+ await client.deleteKeywords(input.project, uniqueStrings(input.request.keywords));
601
+ }
602
+ }),
603
+ defineTool({
604
+ name: "canonry_competitors_add",
605
+ title: "Add competitors",
606
+ description: "Add tracked competitor domains to a Canonry project.",
607
+ access: "write",
608
+ inputSchema: competitorsInputSchema,
609
+ annotations: writeAnnotations({ idempotentHint: true }),
610
+ openApiOperations: ["POST /api/v1/projects/{name}/competitors"],
611
+ handler: async (client, input) => {
612
+ await client.appendCompetitors(input.project, uniqueStrings(input.request.competitors));
613
+ }
614
+ }),
615
+ defineTool({
616
+ name: "canonry_competitors_remove",
617
+ title: "Remove competitors",
618
+ description: "Remove tracked competitor domains from a Canonry project.",
619
+ access: "write",
620
+ inputSchema: competitorsInputSchema,
621
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
622
+ openApiOperations: ["DELETE /api/v1/projects/{name}/competitors"],
623
+ handler: async (client, input) => {
624
+ await client.deleteCompetitors(input.project, uniqueStrings(input.request.competitors));
625
+ }
626
+ }),
627
+ defineTool({
628
+ name: "canonry_schedule_set",
629
+ title: "Set schedule",
630
+ description: "Create or replace the scheduled run configuration for a Canonry project.",
631
+ access: "write",
632
+ inputSchema: scheduleSetInputSchema,
633
+ annotations: writeAnnotations({ idempotentHint: true }),
634
+ openApiOperations: ["PUT /api/v1/projects/{name}/schedule"],
635
+ handler: (client, input) => client.putSchedule(input.project, input.schedule)
636
+ }),
637
+ defineTool({
638
+ name: "canonry_schedule_delete",
639
+ title: "Delete schedule",
640
+ description: "Delete the scheduled run configuration for a Canonry project.",
641
+ access: "write",
642
+ inputSchema: projectInputSchema,
643
+ annotations: writeAnnotations({ idempotentHint: false, destructiveHint: true }),
644
+ openApiOperations: ["DELETE /api/v1/projects/{name}/schedule"],
645
+ handler: async (client, input) => {
646
+ await client.deleteSchedule(input.project);
647
+ }
648
+ }),
649
+ defineTool({
650
+ name: "canonry_insight_dismiss",
651
+ title: "Dismiss insight",
652
+ description: "Dismiss an intelligence insight for a Canonry project.",
653
+ access: "write",
654
+ inputSchema: insightInputSchema,
655
+ annotations: writeAnnotations({ idempotentHint: true }),
656
+ openApiOperations: ["POST /api/v1/projects/{name}/insights/{id}/dismiss"],
657
+ handler: (client, input) => client.dismissInsight(input.project, input.insightId)
658
+ }),
659
+ defineTool({
660
+ name: "canonry_agent_webhook_attach",
661
+ title: "Attach agent webhook",
662
+ description: "Attach an external agent webhook to project run and insight events.",
663
+ access: "write",
664
+ inputSchema: agentWebhookAttachInputSchema,
665
+ annotations: writeAnnotations({ idempotentHint: true }),
666
+ openApiOperations: ["GET /api/v1/projects/{name}/notifications", "POST /api/v1/projects/{name}/notifications"],
667
+ handler: async (client, input) => {
668
+ const existing = await client.listNotifications(input.project);
669
+ const agentNotification = existing.find((notification2) => notification2.source === "agent");
670
+ if (agentNotification) {
671
+ return { status: "already-attached", project: input.project, notificationId: agentNotification.id };
672
+ }
673
+ const request = notificationCreateRequestSchema.parse({
674
+ channel: "webhook",
675
+ url: input.url,
676
+ events: AGENT_WEBHOOK_EVENTS,
677
+ source: "agent"
678
+ });
679
+ const notification = await client.createNotification(input.project, request);
680
+ return { status: "attached", project: input.project, notificationId: notification.id };
681
+ }
682
+ }),
683
+ defineTool({
684
+ name: "canonry_agent_webhook_detach",
685
+ title: "Detach agent webhook",
686
+ description: "Detach the external agent webhook for a Canonry project.",
687
+ access: "write",
688
+ inputSchema: projectInputSchema,
689
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
690
+ openApiOperations: ["GET /api/v1/projects/{name}/notifications", "DELETE /api/v1/projects/{name}/notifications/{id}"],
691
+ handler: async (client, input) => {
692
+ const existing = await client.listNotifications(input.project);
693
+ const agentNotification = existing.find((notification) => notification.source === "agent");
694
+ if (!agentNotification) {
695
+ return { status: "not-attached", project: input.project };
696
+ }
697
+ await client.deleteNotification(input.project, agentNotification.id);
698
+ return { status: "detached", project: input.project };
699
+ }
700
+ })
701
+ ];
702
+ var CANONRY_MCP_TOOL_COUNT = canonryMcpTools.length;
703
+ var CANONRY_MCP_READ_TOOL_COUNT = canonryMcpTools.filter((tool) => tool.access === "read").length;
704
+
705
+ // src/mcp/results.ts
706
+ function jsonToolResult(value) {
707
+ const result = value === void 0 ? { ok: true } : value;
708
+ return {
709
+ content: [
710
+ {
711
+ type: "text",
712
+ text: JSON.stringify(result, null, 2)
713
+ }
714
+ ]
715
+ };
716
+ }
717
+ function errorToolResult(error) {
718
+ return {
719
+ isError: true,
720
+ content: [
721
+ {
722
+ type: "text",
723
+ text: JSON.stringify(toCanonryErrorEnvelope(error), null, 2)
724
+ }
725
+ ]
726
+ };
727
+ }
728
+ async function withToolErrors(handler) {
729
+ try {
730
+ return jsonToolResult(await handler());
731
+ } catch (error) {
732
+ return errorToolResult(error);
733
+ }
734
+ }
735
+ function toCanonryErrorEnvelope(error) {
736
+ if (error instanceof CliError) {
737
+ return {
738
+ error: {
739
+ code: error.code,
740
+ message: error.message,
741
+ ...error.details ? { details: error.details } : {}
742
+ }
743
+ };
744
+ }
745
+ if (hasErrorEnvelope(error)) {
746
+ return {
747
+ error: {
748
+ code: String(error.error.code ?? "API_ERROR"),
749
+ message: String(error.error.message ?? "Canonry API error"),
750
+ ...error.error.details !== void 0 ? { details: error.error.details } : {}
751
+ }
752
+ };
753
+ }
754
+ if (error instanceof Error) {
755
+ return {
756
+ error: {
757
+ code: "MCP_TOOL_ERROR",
758
+ message: error.message
759
+ }
760
+ };
761
+ }
762
+ return {
763
+ error: {
764
+ code: "MCP_TOOL_ERROR",
765
+ message: "Unknown MCP tool error"
766
+ }
767
+ };
768
+ }
769
+ function hasErrorEnvelope(value) {
770
+ if (!value || typeof value !== "object" || !("error" in value)) return false;
771
+ const error = value.error;
772
+ return Boolean(error && typeof error === "object");
773
+ }
774
+
775
+ // src/mcp/server.ts
776
+ function createCanonryMcpServer(options = {}) {
777
+ const clientFactory = options.clientFactory ?? createApiClient;
778
+ const client = clientFactory();
779
+ const scope = options.scope ?? "all";
780
+ const server = new McpServer({
781
+ name: "canonry",
782
+ version: PACKAGE_VERSION
783
+ });
784
+ for (const registryTool of getCanonryMcpTools(scope)) {
785
+ const tool = registryTool;
786
+ const handler = tool.handler;
787
+ server.registerTool(
788
+ tool.name,
789
+ {
790
+ title: tool.title,
791
+ description: tool.description,
792
+ inputSchema: tool.inputSchema,
793
+ annotations: tool.annotations
794
+ },
795
+ async (input) => withToolErrors(() => handler(client, input))
796
+ );
797
+ }
798
+ return server;
799
+ }
800
+ function getCanonryMcpTools(scope = "all") {
801
+ return scope === "read-only" ? canonryMcpTools.filter((tool) => tool.access === "read") : [...canonryMcpTools];
802
+ }
803
+
804
+ // src/mcp/cli.ts
805
+ async function main(argv = process.argv.slice(2)) {
806
+ const server = createCanonryMcpServer({ scope: parseScope(argv) });
807
+ await server.connect(new StdioServerTransport());
808
+ }
809
+ function parseScope(argv, envScope = process.env.CANONRY_MCP_SCOPE) {
810
+ let scope = normalizeScope(envScope);
811
+ for (let i = 0; i < argv.length; i += 1) {
812
+ const arg = argv[i];
813
+ if (arg === "--read-only") {
814
+ scope = "read-only";
815
+ continue;
816
+ }
817
+ if (arg === "--scope") {
818
+ const next = argv[i + 1];
819
+ if (!next) throw new Error("Missing value for --scope");
820
+ scope = normalizeScope(next);
821
+ i += 1;
822
+ continue;
823
+ }
824
+ if (arg?.startsWith("--scope=")) {
825
+ scope = normalizeScope(arg.slice("--scope=".length));
826
+ continue;
827
+ }
828
+ throw new Error(`Unknown canonry-mcp argument: ${arg}`);
829
+ }
830
+ return scope;
831
+ }
832
+ function normalizeScope(value) {
833
+ if (!value || value === "all") return "all";
834
+ if (value === "read-only") return "read-only";
835
+ throw new Error(`Invalid MCP scope "${value}". Expected "all" or "read-only".`);
836
+ }
837
+ if (import.meta.url === `file://${process.argv[1]}`) {
838
+ main().catch((error) => {
839
+ const message = error instanceof Error ? error.message : "canonry-mcp failed";
840
+ process.stderr.write(`${message}
841
+ `);
842
+ process.exitCode = 1;
843
+ });
844
+ }
845
+ export {
846
+ main,
847
+ parseScope
848
+ };