@ainyc/canonry 2.8.2 → 2.10.1

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 CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  projectUpsertRequestSchema,
11
11
  runTriggerRequestSchema,
12
12
  scheduleUpsertRequestSchema
13
- } from "./chunk-FPZUQADO.js";
13
+ } from "./chunk-KWQCQMPY.js";
14
14
  import "./chunk-MLKGABMK.js";
15
15
 
16
16
  // src/mcp/cli.ts
@@ -18,6 +18,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
18
18
 
19
19
  // src/mcp/server.ts
20
20
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
21
+ import { z as z3 } from "zod";
21
22
 
22
23
  // src/package-version.ts
23
24
  import { createRequire } from "module";
@@ -184,6 +185,7 @@ var canonryMcpTools = [
184
185
  title: "List Canonry projects",
185
186
  description: "List all Canonry projects available through the configured API.",
186
187
  access: "read",
188
+ tier: "core",
187
189
  inputSchema: emptyInputSchema,
188
190
  annotations: readAnnotations(),
189
191
  openApiOperations: ["GET /api/v1/projects"],
@@ -194,6 +196,7 @@ var canonryMcpTools = [
194
196
  title: "Get project",
195
197
  description: "Get a Canonry project by name.",
196
198
  access: "read",
199
+ tier: "core",
197
200
  inputSchema: projectInputSchema,
198
201
  annotations: readAnnotations(),
199
202
  openApiOperations: ["GET /api/v1/projects/{name}"],
@@ -204,6 +207,7 @@ var canonryMcpTools = [
204
207
  title: "Export project config",
205
208
  description: "Export a Canonry project in config-as-code format.",
206
209
  access: "read",
210
+ tier: "setup",
207
211
  inputSchema: projectInputSchema,
208
212
  annotations: readAnnotations(),
209
213
  openApiOperations: ["GET /api/v1/projects/{name}/export"],
@@ -214,6 +218,7 @@ var canonryMcpTools = [
214
218
  title: "Get project history",
215
219
  description: "Get audit history for a Canonry project.",
216
220
  access: "read",
221
+ tier: "monitoring",
217
222
  inputSchema: projectInputSchema,
218
223
  annotations: readAnnotations(),
219
224
  openApiOperations: ["GET /api/v1/projects/{name}/history"],
@@ -224,6 +229,7 @@ var canonryMcpTools = [
224
229
  title: "List project runs",
225
230
  description: "List runs for a Canonry project.",
226
231
  access: "read",
232
+ tier: "monitoring",
227
233
  inputSchema: runsListInputSchema,
228
234
  annotations: readAnnotations(),
229
235
  openApiOperations: ["GET /api/v1/projects/{name}/runs"],
@@ -234,6 +240,7 @@ var canonryMcpTools = [
234
240
  title: "Get latest project run",
235
241
  description: "Get the latest run and total run count for a Canonry project.",
236
242
  access: "read",
243
+ tier: "monitoring",
237
244
  inputSchema: projectInputSchema,
238
245
  annotations: readAnnotations(),
239
246
  openApiOperations: ["GET /api/v1/projects/{name}/runs/latest"],
@@ -244,6 +251,7 @@ var canonryMcpTools = [
244
251
  title: "Get run",
245
252
  description: "Get a Canonry run with its snapshots.",
246
253
  access: "read",
254
+ tier: "monitoring",
247
255
  inputSchema: runGetInputSchema,
248
256
  annotations: readAnnotations(),
249
257
  openApiOperations: ["GET /api/v1/runs/{id}"],
@@ -254,6 +262,7 @@ var canonryMcpTools = [
254
262
  title: "Get project timeline",
255
263
  description: "Get per-keyword citation history for a Canonry project.",
256
264
  access: "read",
265
+ tier: "monitoring",
257
266
  inputSchema: timelineInputSchema,
258
267
  annotations: readAnnotations(),
259
268
  openApiOperations: ["GET /api/v1/projects/{name}/timeline"],
@@ -264,6 +273,7 @@ var canonryMcpTools = [
264
273
  title: "List query snapshots",
265
274
  description: "List paginated query snapshots for a Canonry project.",
266
275
  access: "read",
276
+ tier: "monitoring",
267
277
  inputSchema: snapshotsListInputSchema,
268
278
  annotations: readAnnotations(),
269
279
  openApiOperations: ["GET /api/v1/projects/{name}/snapshots"],
@@ -278,6 +288,7 @@ var canonryMcpTools = [
278
288
  title: "Diff snapshots",
279
289
  description: "Compare query snapshot states between two Canonry runs.",
280
290
  access: "read",
291
+ tier: "monitoring",
281
292
  inputSchema: snapshotsDiffInputSchema,
282
293
  annotations: readAnnotations(),
283
294
  openApiOperations: ["GET /api/v1/projects/{name}/snapshots/diff"],
@@ -288,6 +299,7 @@ var canonryMcpTools = [
288
299
  title: "List insights",
289
300
  description: "List intelligence insights for a Canonry project.",
290
301
  access: "read",
302
+ tier: "monitoring",
291
303
  inputSchema: insightsListInputSchema,
292
304
  annotations: readAnnotations(),
293
305
  openApiOperations: ["GET /api/v1/projects/{name}/insights"],
@@ -298,6 +310,7 @@ var canonryMcpTools = [
298
310
  title: "Get insight",
299
311
  description: "Get one intelligence insight for a Canonry project.",
300
312
  access: "read",
313
+ tier: "monitoring",
301
314
  inputSchema: insightInputSchema,
302
315
  annotations: readAnnotations(),
303
316
  openApiOperations: ["GET /api/v1/projects/{name}/insights/{id}"],
@@ -306,8 +319,9 @@ var canonryMcpTools = [
306
319
  defineTool({
307
320
  name: "canonry_health_latest",
308
321
  title: "Get latest health",
309
- description: "Get the latest health snapshot for a Canonry project.",
322
+ 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.',
310
323
  access: "read",
324
+ tier: "monitoring",
311
325
  inputSchema: projectInputSchema,
312
326
  annotations: readAnnotations(),
313
327
  openApiOperations: ["GET /api/v1/projects/{name}/health/latest"],
@@ -318,6 +332,7 @@ var canonryMcpTools = [
318
332
  title: "Get health history",
319
333
  description: "Get health snapshot history for a Canonry project.",
320
334
  access: "read",
335
+ tier: "monitoring",
321
336
  inputSchema: healthHistoryInputSchema,
322
337
  annotations: readAnnotations(),
323
338
  openApiOperations: ["GET /api/v1/projects/{name}/health/history"],
@@ -328,6 +343,7 @@ var canonryMcpTools = [
328
343
  title: "List keywords",
329
344
  description: "List tracked keywords for a Canonry project.",
330
345
  access: "read",
346
+ tier: "setup",
331
347
  inputSchema: projectInputSchema,
332
348
  annotations: readAnnotations(),
333
349
  openApiOperations: ["GET /api/v1/projects/{name}/keywords"],
@@ -338,6 +354,7 @@ var canonryMcpTools = [
338
354
  title: "List competitors",
339
355
  description: "List tracked competitors for a Canonry project.",
340
356
  access: "read",
357
+ tier: "setup",
341
358
  inputSchema: projectInputSchema,
342
359
  annotations: readAnnotations(),
343
360
  openApiOperations: ["GET /api/v1/projects/{name}/competitors"],
@@ -348,6 +365,7 @@ var canonryMcpTools = [
348
365
  title: "Get schedule",
349
366
  description: "Get the scheduled run configuration for a Canonry project.",
350
367
  access: "read",
368
+ tier: "setup",
351
369
  inputSchema: projectInputSchema,
352
370
  annotations: readAnnotations(),
353
371
  openApiOperations: ["GET /api/v1/projects/{name}/schedule"],
@@ -358,6 +376,7 @@ var canonryMcpTools = [
358
376
  title: "Get settings",
359
377
  description: "Get Canonry API settings and configured provider status.",
360
378
  access: "read",
379
+ tier: "core",
361
380
  inputSchema: emptyInputSchema,
362
381
  annotations: readAnnotations(),
363
382
  openApiOperations: ["GET /api/v1/settings"],
@@ -368,6 +387,7 @@ var canonryMcpTools = [
368
387
  title: "List Google connections",
369
388
  description: "List configured Google connections for a Canonry project.",
370
389
  access: "read",
390
+ tier: "gsc",
371
391
  inputSchema: projectInputSchema,
372
392
  annotations: readAnnotations(),
373
393
  openApiOperations: ["GET /api/v1/projects/{name}/google/connections"],
@@ -378,6 +398,7 @@ var canonryMcpTools = [
378
398
  title: "Get GSC performance",
379
399
  description: "Get stored Google Search Console performance rows for a Canonry project.",
380
400
  access: "read",
401
+ tier: "gsc",
381
402
  inputSchema: gscPerformanceInputSchema,
382
403
  annotations: readAnnotations(),
383
404
  openApiOperations: ["GET /api/v1/projects/{name}/google/gsc/performance"],
@@ -388,6 +409,7 @@ var canonryMcpTools = [
388
409
  title: "List GSC inspections",
389
410
  description: "List stored URL inspection rows for a Canonry project.",
390
411
  access: "read",
412
+ tier: "gsc",
391
413
  inputSchema: gscInspectionsInputSchema,
392
414
  annotations: readAnnotations(),
393
415
  openApiOperations: ["GET /api/v1/projects/{name}/google/gsc/inspections"],
@@ -398,6 +420,7 @@ var canonryMcpTools = [
398
420
  title: "List deindexed GSC URLs",
399
421
  description: "List URLs that appear to have become deindexed in Google Search Console data.",
400
422
  access: "read",
423
+ tier: "gsc",
401
424
  inputSchema: projectInputSchema,
402
425
  annotations: readAnnotations(),
403
426
  openApiOperations: ["GET /api/v1/projects/{name}/google/gsc/deindexed"],
@@ -408,6 +431,7 @@ var canonryMcpTools = [
408
431
  title: "Get GSC coverage",
409
432
  description: "Get Google Search Console coverage summary for a Canonry project.",
410
433
  access: "read",
434
+ tier: "gsc",
411
435
  inputSchema: projectInputSchema,
412
436
  annotations: readAnnotations(),
413
437
  openApiOperations: ["GET /api/v1/projects/{name}/google/gsc/coverage"],
@@ -418,6 +442,7 @@ var canonryMcpTools = [
418
442
  title: "Get GSC coverage history",
419
443
  description: "Get Google Search Console coverage history snapshots for a Canonry project.",
420
444
  access: "read",
445
+ tier: "gsc",
421
446
  inputSchema: gscCoverageHistoryInputSchema,
422
447
  annotations: readAnnotations(),
423
448
  openApiOperations: ["GET /api/v1/projects/{name}/google/gsc/coverage/history"],
@@ -428,6 +453,7 @@ var canonryMcpTools = [
428
453
  title: "Get GSC sitemaps",
429
454
  description: "Get sitemap data from Google Search Console for a Canonry project.",
430
455
  access: "read",
456
+ tier: "gsc",
431
457
  inputSchema: projectInputSchema,
432
458
  annotations: readAnnotations(true),
433
459
  openApiOperations: ["GET /api/v1/projects/{name}/google/gsc/sitemaps"],
@@ -438,6 +464,7 @@ var canonryMcpTools = [
438
464
  title: "Get GA status",
439
465
  description: "Get Google Analytics connection status for a Canonry project.",
440
466
  access: "read",
467
+ tier: "ga",
441
468
  inputSchema: projectInputSchema,
442
469
  annotations: readAnnotations(),
443
470
  openApiOperations: ["GET /api/v1/projects/{name}/ga/status"],
@@ -448,6 +475,7 @@ var canonryMcpTools = [
448
475
  title: "Get GA traffic",
449
476
  description: "Get Google Analytics traffic summary for a Canonry project.",
450
477
  access: "read",
478
+ tier: "ga",
451
479
  inputSchema: gaTrafficInputSchema,
452
480
  annotations: readAnnotations(),
453
481
  openApiOperations: ["GET /api/v1/projects/{name}/ga/traffic"],
@@ -458,6 +486,7 @@ var canonryMcpTools = [
458
486
  title: "Get GA coverage",
459
487
  description: "Get Google Analytics page coverage for a Canonry project.",
460
488
  access: "read",
489
+ tier: "ga",
461
490
  inputSchema: projectInputSchema,
462
491
  annotations: readAnnotations(),
463
492
  openApiOperations: ["GET /api/v1/projects/{name}/ga/coverage"],
@@ -468,6 +497,7 @@ var canonryMcpTools = [
468
497
  title: "Get GA AI referral history",
469
498
  description: "Get AI referral sessions per day grouped by source.",
470
499
  access: "read",
500
+ tier: "ga",
471
501
  inputSchema: gaWindowInputSchema,
472
502
  annotations: readAnnotations(),
473
503
  openApiOperations: ["GET /api/v1/projects/{name}/ga/ai-referral-history"],
@@ -478,6 +508,7 @@ var canonryMcpTools = [
478
508
  title: "Get GA social referral history",
479
509
  description: "Get social referral sessions per day grouped by source.",
480
510
  access: "read",
511
+ tier: "ga",
481
512
  inputSchema: gaWindowInputSchema,
482
513
  annotations: readAnnotations(),
483
514
  openApiOperations: ["GET /api/v1/projects/{name}/ga/social-referral-history"],
@@ -488,6 +519,7 @@ var canonryMcpTools = [
488
519
  title: "Get GA social referral trend",
489
520
  description: "Get social referral trend with biggest mover for a Canonry project.",
490
521
  access: "read",
522
+ tier: "ga",
491
523
  inputSchema: projectInputSchema,
492
524
  annotations: readAnnotations(),
493
525
  openApiOperations: ["GET /api/v1/projects/{name}/ga/social-referral-trend"],
@@ -498,6 +530,7 @@ var canonryMcpTools = [
498
530
  title: "Get GA attribution trend",
499
531
  description: "Get per-channel attribution trends for organic, AI, social, and total sessions.",
500
532
  access: "read",
533
+ tier: "ga",
501
534
  inputSchema: projectInputSchema,
502
535
  annotations: readAnnotations(),
503
536
  openApiOperations: ["GET /api/v1/projects/{name}/ga/attribution-trend"],
@@ -508,6 +541,7 @@ var canonryMcpTools = [
508
541
  title: "Get GA session history",
509
542
  description: "Get total sessions per day for a Canonry project.",
510
543
  access: "read",
544
+ tier: "ga",
511
545
  inputSchema: gaWindowInputSchema,
512
546
  annotations: readAnnotations(),
513
547
  openApiOperations: ["GET /api/v1/projects/{name}/ga/session-history"],
@@ -518,6 +552,7 @@ var canonryMcpTools = [
518
552
  title: "Create or replace project",
519
553
  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
554
  access: "write",
555
+ tier: "setup",
521
556
  inputSchema: projectUpsertInputSchema,
522
557
  annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
523
558
  openApiOperations: ["PUT /api/v1/projects/{name}"],
@@ -528,6 +563,7 @@ var canonryMcpTools = [
528
563
  title: "Apply project config",
529
564
  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
565
  access: "write",
566
+ tier: "core",
531
567
  inputSchema: applyConfigInputSchema,
532
568
  // Declarative apply is safe to repeat, but it replaces configured child state.
533
569
  annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
@@ -539,6 +575,7 @@ var canonryMcpTools = [
539
575
  title: "Generate keyword suggestions",
540
576
  description: "Generate candidate key phrases using a configured provider. Returns suggestions only; use canonry_keywords_add to persist them.",
541
577
  access: "write",
578
+ tier: "setup",
542
579
  inputSchema: keywordGenerateInputSchema,
543
580
  annotations: writeAnnotations({ idempotentHint: false, openWorldHint: true }),
544
581
  openApiOperations: ["POST /api/v1/projects/{name}/keywords/generate"],
@@ -549,6 +586,7 @@ var canonryMcpTools = [
549
586
  title: "Replace keywords",
550
587
  description: "Replace the tracked keyword set for a Canonry project.",
551
588
  access: "write",
589
+ tier: "setup",
552
590
  inputSchema: keywordsInputSchema,
553
591
  annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
554
592
  openApiOperations: ["PUT /api/v1/projects/{name}/keywords"],
@@ -561,6 +599,7 @@ var canonryMcpTools = [
561
599
  title: "Trigger run",
562
600
  description: "Trigger an answer-visibility run for a Canonry project.",
563
601
  access: "write",
602
+ tier: "core",
564
603
  inputSchema: runTriggerInputSchema,
565
604
  annotations: writeAnnotations({ idempotentHint: false, openWorldHint: true }),
566
605
  openApiOperations: ["POST /api/v1/projects/{name}/runs"],
@@ -571,6 +610,7 @@ var canonryMcpTools = [
571
610
  title: "Cancel run",
572
611
  description: "Cancel a queued or running Canonry run.",
573
612
  access: "write",
613
+ tier: "core",
574
614
  inputSchema: runGetInputSchema,
575
615
  annotations: writeAnnotations({ idempotentHint: false, destructiveHint: true }),
576
616
  openApiOperations: ["POST /api/v1/runs/{id}/cancel"],
@@ -581,6 +621,7 @@ var canonryMcpTools = [
581
621
  title: "Add keywords",
582
622
  description: "Append tracked keywords to a Canonry project; existing keywords are skipped by the API.",
583
623
  access: "write",
624
+ tier: "setup",
584
625
  inputSchema: keywordsInputSchema,
585
626
  annotations: writeAnnotations({ idempotentHint: true }),
586
627
  openApiOperations: ["POST /api/v1/projects/{name}/keywords"],
@@ -593,6 +634,7 @@ var canonryMcpTools = [
593
634
  title: "Remove keywords",
594
635
  description: "Remove tracked keywords from a Canonry project.",
595
636
  access: "write",
637
+ tier: "setup",
596
638
  inputSchema: keywordsInputSchema,
597
639
  annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
598
640
  openApiOperations: ["DELETE /api/v1/projects/{name}/keywords"],
@@ -605,6 +647,7 @@ var canonryMcpTools = [
605
647
  title: "Add competitors",
606
648
  description: "Add tracked competitor domains to a Canonry project.",
607
649
  access: "write",
650
+ tier: "setup",
608
651
  inputSchema: competitorsInputSchema,
609
652
  annotations: writeAnnotations({ idempotentHint: true }),
610
653
  openApiOperations: ["POST /api/v1/projects/{name}/competitors"],
@@ -617,6 +660,7 @@ var canonryMcpTools = [
617
660
  title: "Remove competitors",
618
661
  description: "Remove tracked competitor domains from a Canonry project.",
619
662
  access: "write",
663
+ tier: "setup",
620
664
  inputSchema: competitorsInputSchema,
621
665
  annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
622
666
  openApiOperations: ["DELETE /api/v1/projects/{name}/competitors"],
@@ -629,6 +673,7 @@ var canonryMcpTools = [
629
673
  title: "Set schedule",
630
674
  description: "Create or replace the scheduled run configuration for a Canonry project.",
631
675
  access: "write",
676
+ tier: "setup",
632
677
  inputSchema: scheduleSetInputSchema,
633
678
  annotations: writeAnnotations({ idempotentHint: true }),
634
679
  openApiOperations: ["PUT /api/v1/projects/{name}/schedule"],
@@ -639,6 +684,7 @@ var canonryMcpTools = [
639
684
  title: "Delete schedule",
640
685
  description: "Delete the scheduled run configuration for a Canonry project.",
641
686
  access: "write",
687
+ tier: "setup",
642
688
  inputSchema: projectInputSchema,
643
689
  annotations: writeAnnotations({ idempotentHint: false, destructiveHint: true }),
644
690
  openApiOperations: ["DELETE /api/v1/projects/{name}/schedule"],
@@ -651,6 +697,7 @@ var canonryMcpTools = [
651
697
  title: "Dismiss insight",
652
698
  description: "Dismiss an intelligence insight for a Canonry project.",
653
699
  access: "write",
700
+ tier: "setup",
654
701
  inputSchema: insightInputSchema,
655
702
  annotations: writeAnnotations({ idempotentHint: true }),
656
703
  openApiOperations: ["POST /api/v1/projects/{name}/insights/{id}/dismiss"],
@@ -661,6 +708,7 @@ var canonryMcpTools = [
661
708
  title: "Attach agent webhook",
662
709
  description: "Attach an external agent webhook to project run and insight events.",
663
710
  access: "write",
711
+ tier: "core",
664
712
  inputSchema: agentWebhookAttachInputSchema,
665
713
  annotations: writeAnnotations({ idempotentHint: true }),
666
714
  openApiOperations: ["GET /api/v1/projects/{name}/notifications", "POST /api/v1/projects/{name}/notifications"],
@@ -685,6 +733,7 @@ var canonryMcpTools = [
685
733
  title: "Detach agent webhook",
686
734
  description: "Detach the external agent webhook for a Canonry project.",
687
735
  access: "write",
736
+ tier: "agent",
688
737
  inputSchema: projectInputSchema,
689
738
  annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
690
739
  openApiOperations: ["GET /api/v1/projects/{name}/notifications", "DELETE /api/v1/projects/{name}/notifications/{id}"],
@@ -701,8 +750,10 @@ var canonryMcpTools = [
701
750
  ];
702
751
  var CANONRY_MCP_TOOL_COUNT = canonryMcpTools.length;
703
752
  var CANONRY_MCP_READ_TOOL_COUNT = canonryMcpTools.filter((tool) => tool.access === "read").length;
753
+ var CANONRY_MCP_CORE_TOOL_COUNT = canonryMcpTools.filter((tool) => tool.tier === "core").length;
704
754
 
705
755
  // src/mcp/results.ts
756
+ import { ZodError } from "zod";
706
757
  function jsonToolResult(value) {
707
758
  const result = value === void 0 ? { ok: true } : value;
708
759
  return {
@@ -733,6 +784,15 @@ async function withToolErrors(handler) {
733
784
  }
734
785
  }
735
786
  function toCanonryErrorEnvelope(error) {
787
+ if (error instanceof ZodError) {
788
+ return {
789
+ error: {
790
+ code: "VALIDATION_ERROR",
791
+ message: zodErrorMessage(error),
792
+ details: { issues: error.issues.map(formatZodIssue) }
793
+ }
794
+ };
795
+ }
736
796
  if (error instanceof CliError) {
737
797
  return {
738
798
  error: {
@@ -771,9 +831,129 @@ function hasErrorEnvelope(value) {
771
831
  const error = value.error;
772
832
  return Boolean(error && typeof error === "object");
773
833
  }
834
+ function formatZodIssue(issue) {
835
+ return { path: issue.path.map(String).join("."), message: issue.message };
836
+ }
837
+ function zodErrorMessage(error) {
838
+ const first = error.issues[0];
839
+ if (!first) return "Input validation failed";
840
+ const path = first.path.map(String).join(".");
841
+ return path ? `${path}: ${first.message}` : first.message;
842
+ }
843
+
844
+ // src/mcp/toolkits.ts
845
+ var CANONRY_MCP_TOOLKIT_NAMES = ["monitoring", "setup", "gsc", "ga", "agent"];
846
+ var CANONRY_MCP_TOOLKITS = [
847
+ {
848
+ name: "monitoring",
849
+ title: "Runs, snapshots, insights, health",
850
+ description: "Inspect run history, query snapshots, intelligence insights, and health timelines.",
851
+ whenToLoad: "Load when investigating regressions, comparing runs, or reviewing insights and health history."
852
+ },
853
+ {
854
+ name: "setup",
855
+ title: "Project configuration",
856
+ description: "Manage keywords, competitors, schedules, project upsert, and config-as-code roundtrips.",
857
+ whenToLoad: "Load when onboarding a new project or editing tracked keywords, competitors, or schedules."
858
+ },
859
+ {
860
+ name: "gsc",
861
+ title: "Google Search Console",
862
+ description: "Read GSC performance, inspections, coverage, sitemaps, and deindexed URLs.",
863
+ whenToLoad: "Load when you need indexing, coverage, or sitemap data from Google Search Console."
864
+ },
865
+ {
866
+ name: "ga",
867
+ title: "Google Analytics 4",
868
+ description: "Read GA traffic, AI/social referral history, attribution trend, and session history.",
869
+ whenToLoad: "Load when you need traffic, referral, or attribution data from Google Analytics 4."
870
+ },
871
+ {
872
+ name: "agent",
873
+ title: "Agent webhook lifecycle",
874
+ description: "Detach the configured external-agent webhook from a project.",
875
+ whenToLoad: "Load when removing an agent webhook subscription. (Attach lives in the core tier.)"
876
+ }
877
+ ];
878
+ function isCanonryMcpToolkitName(value) {
879
+ return CANONRY_MCP_TOOLKIT_NAMES.includes(value);
880
+ }
881
+
882
+ // src/mcp/dynamic-catalog.ts
883
+ var DynamicToolCatalog = class {
884
+ entries;
885
+ loaded = /* @__PURE__ */ new Set();
886
+ eager;
887
+ scope;
888
+ constructor(entries, scope, options = {}) {
889
+ this.entries = entries;
890
+ this.scope = scope;
891
+ this.eager = Boolean(options.eager);
892
+ if (this.eager) {
893
+ for (const toolkit of CANONRY_MCP_TOOLKITS) {
894
+ if (this.toolsForToolkit(toolkit.name).length > 0) {
895
+ this.loaded.add(toolkit.name);
896
+ }
897
+ }
898
+ }
899
+ }
900
+ applyInitialEnablement() {
901
+ if (this.eager) return;
902
+ for (const entry of this.entries) {
903
+ if (entry.tool.tier !== "core") entry.registered.disable();
904
+ }
905
+ }
906
+ loadToolkit(rawName) {
907
+ if (!isCanonryMcpToolkitName(rawName)) {
908
+ const valid = CANONRY_MCP_TOOLKITS.map((t) => t.name).join(", ");
909
+ throw new Error(`Unknown toolkit "${rawName}". Available: ${valid}.`);
910
+ }
911
+ const name = rawName;
912
+ const matches = this.entries.filter((entry) => entry.tool.tier === name);
913
+ if (matches.length === 0) {
914
+ return { status: "empty", name, tools: [] };
915
+ }
916
+ if (this.loaded.has(name)) {
917
+ return { status: "already-loaded", name, tools: matches.map((entry) => entry.tool.name) };
918
+ }
919
+ for (const entry of matches) {
920
+ entry.registered.enable();
921
+ }
922
+ this.loaded.add(name);
923
+ return { status: "loaded", name, tools: matches.map((entry) => entry.tool.name) };
924
+ }
925
+ helpResult() {
926
+ return {
927
+ scope: this.scope,
928
+ eager: this.eager,
929
+ loadedToolkits: [...this.loaded].sort(),
930
+ coreTools: this.entries.filter((entry) => entry.tool.tier === "core").map((entry) => entry.tool.name),
931
+ toolkits: CANONRY_MCP_TOOLKITS.map((toolkit) => this.toolkitEntry(toolkit)).filter((entry) => entry.toolCount > 0),
932
+ usage: "Call canonry_load_toolkit with one of the toolkit names listed in `toolkits[].name` to register its tools for the rest of this session."
933
+ };
934
+ }
935
+ toolkitEntry(toolkit) {
936
+ const tools = this.toolsForToolkit(toolkit.name);
937
+ return {
938
+ name: toolkit.name,
939
+ title: toolkit.title,
940
+ description: toolkit.description,
941
+ whenToLoad: toolkit.whenToLoad,
942
+ toolCount: tools.length,
943
+ tools,
944
+ loaded: this.loaded.has(toolkit.name)
945
+ };
946
+ }
947
+ toolsForToolkit(name) {
948
+ return this.entries.filter((entry) => entry.tool.tier === name).map((entry) => entry.tool.name);
949
+ }
950
+ };
774
951
 
775
952
  // src/mcp/server.ts
776
953
  function createCanonryMcpServer(options = {}) {
954
+ return createCanonryMcpServerWithCatalog(options).server;
955
+ }
956
+ function createCanonryMcpServerWithCatalog(options = {}) {
777
957
  const clientFactory = options.clientFactory ?? createApiClient;
778
958
  const client = clientFactory();
779
959
  const scope = options.scope ?? "all";
@@ -781,10 +961,12 @@ function createCanonryMcpServer(options = {}) {
781
961
  name: "canonry",
782
962
  version: PACKAGE_VERSION
783
963
  });
964
+ server.validateToolInput = async (_tool, args) => args;
965
+ const entries = [];
784
966
  for (const registryTool of getCanonryMcpTools(scope)) {
785
967
  const tool = registryTool;
786
968
  const handler = tool.handler;
787
- server.registerTool(
969
+ const registered = server.registerTool(
788
970
  tool.name,
789
971
  {
790
972
  title: tool.title,
@@ -792,28 +974,103 @@ function createCanonryMcpServer(options = {}) {
792
974
  inputSchema: tool.inputSchema,
793
975
  annotations: tool.annotations
794
976
  },
795
- async (input) => withToolErrors(() => handler(client, input))
977
+ async (input) => withToolErrors(async () => {
978
+ const parsed = tool.inputSchema.parse(input ?? {});
979
+ return handler(client, parsed);
980
+ })
796
981
  );
982
+ entries.push({ tool, registered });
797
983
  }
798
- return server;
984
+ const catalog = new DynamicToolCatalog(entries, scope, { eager: options.eager });
985
+ catalog.applyInitialEnablement();
986
+ registerMetaTools(server, catalog);
987
+ return { server, catalog };
988
+ }
989
+ var loadToolkitInputSchema = z3.object({
990
+ name: z3.enum(CANONRY_MCP_TOOLKIT_NAMES).describe("Toolkit name. List options with canonry_help.")
991
+ });
992
+ function registerMetaTools(server, catalog) {
993
+ server.registerTool(
994
+ "canonry_help",
995
+ {
996
+ title: "List Canonry MCP toolkits",
997
+ description: "List available toolkits and which are loaded. Call before canonry_load_toolkit if unsure which to load.",
998
+ inputSchema: {},
999
+ annotations: { readOnlyHint: true }
1000
+ },
1001
+ async () => withToolErrors(async () => catalog.helpResult())
1002
+ );
1003
+ server.registerTool(
1004
+ "canonry_load_toolkit",
1005
+ {
1006
+ title: "Load a Canonry MCP toolkit",
1007
+ description: "Register a toolkit's tools for this session and emit notifications/tools/list_changed. Idempotent. Loaded toolkits remain loaded for the rest of the session.",
1008
+ inputSchema: loadToolkitInputSchema.shape,
1009
+ annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false }
1010
+ },
1011
+ async (input) => withToolErrors(async () => {
1012
+ const parsed = loadToolkitInputSchema.parse(input ?? {});
1013
+ return catalog.loadToolkit(parsed.name);
1014
+ })
1015
+ );
799
1016
  }
800
1017
  function getCanonryMcpTools(scope = "all") {
801
1018
  return scope === "read-only" ? canonryMcpTools.filter((tool) => tool.access === "read") : [...canonryMcpTools];
802
1019
  }
803
1020
 
804
1021
  // src/mcp/cli.ts
1022
+ var HELP_TEXT = `Usage: canonry-mcp [--read-only | --scope=<all|read-only>] [--eager]
1023
+
1024
+ Stdio MCP adapter over the Canonry public API. Inherits config from
1025
+ ~/.canonry/config.yaml (or $CANONRY_CONFIG_DIR/config.yaml).
1026
+
1027
+ Flags:
1028
+ --read-only Expose read tools only
1029
+ --scope=<all|read-only>
1030
+ Same as --read-only when "read-only"
1031
+ --eager Load all toolkits at start (skip progressive discovery)
1032
+ --help, -h Show this message
1033
+
1034
+ Environment variables:
1035
+ CANONRY_MCP_SCOPE "all" (default) or "read-only"
1036
+ CANONRY_MCP_EAGER "1" / "true" / "yes" to enable eager mode
1037
+ `;
1038
+ var HelpRequested = class extends Error {
1039
+ constructor() {
1040
+ super("canonry-mcp --help requested");
1041
+ this.name = "HelpRequested";
1042
+ }
1043
+ };
805
1044
  async function main(argv = process.argv.slice(2)) {
806
- const server = createCanonryMcpServer({ scope: parseScope(argv) });
1045
+ let options;
1046
+ try {
1047
+ options = parseCliOptions(argv);
1048
+ } catch (error) {
1049
+ if (error instanceof HelpRequested) {
1050
+ process.stderr.write(HELP_TEXT);
1051
+ return;
1052
+ }
1053
+ throw error;
1054
+ }
1055
+ const server = createCanonryMcpServer({ scope: options.scope, eager: options.eager });
807
1056
  await server.connect(new StdioServerTransport());
808
1057
  }
809
- function parseScope(argv, envScope = process.env.CANONRY_MCP_SCOPE) {
810
- let scope = normalizeScope(envScope);
1058
+ function parseCliOptions(argv, env = process.env) {
1059
+ if (argv.includes("--help") || argv.includes("-h")) {
1060
+ throw new HelpRequested();
1061
+ }
1062
+ let scope = normalizeScope(env.CANONRY_MCP_SCOPE);
1063
+ let eager = parseEagerEnv(env.CANONRY_MCP_EAGER);
811
1064
  for (let i = 0; i < argv.length; i += 1) {
812
1065
  const arg = argv[i];
813
1066
  if (arg === "--read-only") {
814
1067
  scope = "read-only";
815
1068
  continue;
816
1069
  }
1070
+ if (arg === "--eager") {
1071
+ eager = true;
1072
+ continue;
1073
+ }
817
1074
  if (arg === "--scope") {
818
1075
  const next = argv[i + 1];
819
1076
  if (!next) throw new Error("Missing value for --scope");
@@ -827,13 +1084,18 @@ function parseScope(argv, envScope = process.env.CANONRY_MCP_SCOPE) {
827
1084
  }
828
1085
  throw new Error(`Unknown canonry-mcp argument: ${arg}`);
829
1086
  }
830
- return scope;
1087
+ return { scope, eager };
831
1088
  }
832
1089
  function normalizeScope(value) {
833
1090
  if (!value || value === "all") return "all";
834
1091
  if (value === "read-only") return "read-only";
835
1092
  throw new Error(`Invalid MCP scope "${value}". Expected "all" or "read-only".`);
836
1093
  }
1094
+ function parseEagerEnv(value) {
1095
+ if (!value) return false;
1096
+ const normalized = value.trim().toLowerCase();
1097
+ return normalized === "1" || normalized === "true" || normalized === "yes";
1098
+ }
837
1099
  if (import.meta.url === `file://${process.argv[1]}`) {
838
1100
  main().catch((error) => {
839
1101
  const message = error instanceof Error ? error.message : "canonry-mcp failed";
@@ -843,6 +1105,8 @@ if (import.meta.url === `file://${process.argv[1]}`) {
843
1105
  });
844
1106
  }
845
1107
  export {
1108
+ HELP_TEXT,
1109
+ HelpRequested,
846
1110
  main,
847
- parseScope
1111
+ parseCliOptions
848
1112
  };