@ainyc/canonry 2.10.3 → 2.12.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.
@@ -1540,15 +1540,55 @@ var ccCachedReleaseSchema = z14.object({
1540
1540
  lastUsedAt: z14.string().nullable()
1541
1541
  });
1542
1542
 
1543
- // ../contracts/src/content.ts
1543
+ // ../contracts/src/composites.ts
1544
1544
  import { z as z15 } from "zod";
1545
- var contentActionSchema = z15.enum(["create", "expand", "refresh", "add-schema"]);
1545
+ var searchHitKindSchema = z15.enum(["snapshot", "insight"]);
1546
+ var projectSearchSnapshotHitSchema = z15.object({
1547
+ kind: z15.literal("snapshot"),
1548
+ id: z15.string(),
1549
+ runId: z15.string(),
1550
+ keyword: z15.string(),
1551
+ provider: z15.string(),
1552
+ model: z15.string().nullable(),
1553
+ citationState: citationStateSchema,
1554
+ matchedField: z15.enum(["answerText", "citedDomains", "searchQueries", "keyword"]),
1555
+ snippet: z15.string(),
1556
+ createdAt: z15.string()
1557
+ });
1558
+ var projectSearchInsightHitSchema = z15.object({
1559
+ kind: z15.literal("insight"),
1560
+ id: z15.string(),
1561
+ runId: z15.string().nullable(),
1562
+ type: z15.enum(["regression", "gain", "opportunity"]),
1563
+ severity: z15.enum(["critical", "high", "medium", "low"]),
1564
+ title: z15.string(),
1565
+ keyword: z15.string(),
1566
+ provider: z15.string(),
1567
+ matchedField: z15.enum(["title", "keyword", "recommendation", "cause"]),
1568
+ snippet: z15.string(),
1569
+ dismissed: z15.boolean(),
1570
+ createdAt: z15.string()
1571
+ });
1572
+ var projectSearchHitSchema = z15.discriminatedUnion("kind", [
1573
+ projectSearchSnapshotHitSchema,
1574
+ projectSearchInsightHitSchema
1575
+ ]);
1576
+ var projectSearchResponseSchema = z15.object({
1577
+ query: z15.string(),
1578
+ totalHits: z15.number().int().nonnegative(),
1579
+ truncated: z15.boolean(),
1580
+ hits: z15.array(projectSearchHitSchema)
1581
+ });
1582
+
1583
+ // ../contracts/src/content.ts
1584
+ import { z as z16 } from "zod";
1585
+ var contentActionSchema = z16.enum(["create", "expand", "refresh", "add-schema"]);
1546
1586
  var ContentActions = contentActionSchema.enum;
1547
- var demandSourceSchema = z15.enum(["gsc", "competitor-evidence", "both"]);
1587
+ var demandSourceSchema = z16.enum(["gsc", "competitor-evidence", "both"]);
1548
1588
  var DemandSources = demandSourceSchema.enum;
1549
- var actionConfidenceSchema = z15.enum(["high", "medium", "low"]);
1589
+ var actionConfidenceSchema = z16.enum(["high", "medium", "low"]);
1550
1590
  var ActionConfidences = actionConfidenceSchema.enum;
1551
- var pageTypeSchema = z15.enum([
1591
+ var pageTypeSchema = z16.enum([
1552
1592
  "blog-post",
1553
1593
  "comparison",
1554
1594
  "listicle",
@@ -1557,7 +1597,7 @@ var pageTypeSchema = z15.enum([
1557
1597
  "glossary"
1558
1598
  ]);
1559
1599
  var PageTypes = pageTypeSchema.enum;
1560
- var contentActionStateSchema = z15.enum([
1600
+ var contentActionStateSchema = z16.enum([
1561
1601
  "proposed",
1562
1602
  "briefed",
1563
1603
  "payload-generated",
@@ -1567,79 +1607,79 @@ var contentActionStateSchema = z15.enum([
1567
1607
  "dismissed"
1568
1608
  ]);
1569
1609
  var ContentActionStates = contentActionStateSchema.enum;
1570
- var ourBestPageSchema = z15.object({
1571
- url: z15.string(),
1572
- gscImpressions: z15.number().nonnegative(),
1573
- gscClicks: z15.number().nonnegative(),
1610
+ var ourBestPageSchema = z16.object({
1611
+ url: z16.string(),
1612
+ gscImpressions: z16.number().nonnegative(),
1613
+ gscClicks: z16.number().nonnegative(),
1574
1614
  // Null when the page came from the inventory fallback (no GSC ranking data).
1575
- gscAvgPosition: z15.number().nonnegative().nullable(),
1576
- organicSessions: z15.number().nonnegative()
1577
- });
1578
- var winningCompetitorSchema = z15.object({
1579
- domain: z15.string(),
1580
- url: z15.string(),
1581
- title: z15.string(),
1582
- citationCount: z15.number().int().nonnegative()
1583
- });
1584
- var scoreBreakdownSchema = z15.object({
1585
- demand: z15.number(),
1586
- competitor: z15.number(),
1587
- absence: z15.number(),
1588
- gapSeverity: z15.number()
1589
- });
1590
- var existingActionRefSchema = z15.object({
1591
- actionId: z15.string(),
1615
+ gscAvgPosition: z16.number().nonnegative().nullable(),
1616
+ organicSessions: z16.number().nonnegative()
1617
+ });
1618
+ var winningCompetitorSchema = z16.object({
1619
+ domain: z16.string(),
1620
+ url: z16.string(),
1621
+ title: z16.string(),
1622
+ citationCount: z16.number().int().nonnegative()
1623
+ });
1624
+ var scoreBreakdownSchema = z16.object({
1625
+ demand: z16.number(),
1626
+ competitor: z16.number(),
1627
+ absence: z16.number(),
1628
+ gapSeverity: z16.number()
1629
+ });
1630
+ var existingActionRefSchema = z16.object({
1631
+ actionId: z16.string(),
1592
1632
  state: contentActionStateSchema,
1593
- lastUpdated: z15.string()
1633
+ lastUpdated: z16.string()
1594
1634
  });
1595
- var contentTargetRowDtoSchema = z15.object({
1596
- targetRef: z15.string(),
1597
- query: z15.string(),
1635
+ var contentTargetRowDtoSchema = z16.object({
1636
+ targetRef: z16.string(),
1637
+ query: z16.string(),
1598
1638
  action: contentActionSchema,
1599
1639
  ourBestPage: ourBestPageSchema.nullable(),
1600
1640
  winningCompetitor: winningCompetitorSchema.nullable(),
1601
- score: z15.number(),
1641
+ score: z16.number(),
1602
1642
  scoreBreakdown: scoreBreakdownSchema,
1603
- drivers: z15.array(z15.string()),
1643
+ drivers: z16.array(z16.string()),
1604
1644
  demandSource: demandSourceSchema,
1605
1645
  actionConfidence: actionConfidenceSchema,
1606
1646
  existingAction: existingActionRefSchema.nullable()
1607
1647
  });
1608
- var contentTargetsResponseDtoSchema = z15.object({
1609
- targets: z15.array(contentTargetRowDtoSchema),
1610
- contextMetrics: z15.object({
1611
- totalAiReferralSessions: z15.number().int().nonnegative(),
1612
- latestRunId: z15.string(),
1613
- runTimestamp: z15.string()
1648
+ var contentTargetsResponseDtoSchema = z16.object({
1649
+ targets: z16.array(contentTargetRowDtoSchema),
1650
+ contextMetrics: z16.object({
1651
+ totalAiReferralSessions: z16.number().int().nonnegative(),
1652
+ latestRunId: z16.string(),
1653
+ runTimestamp: z16.string()
1614
1654
  })
1615
1655
  });
1616
- var contentGroundingSourceSchema = z15.object({
1617
- uri: z15.string(),
1618
- title: z15.string(),
1619
- domain: z15.string(),
1620
- isOurDomain: z15.boolean(),
1621
- isCompetitor: z15.boolean(),
1622
- citationCount: z15.number().int().nonnegative(),
1623
- providers: z15.array(providerNameSchema)
1624
- });
1625
- var contentSourceRowDtoSchema = z15.object({
1626
- query: z15.string(),
1627
- groundingSources: z15.array(contentGroundingSourceSchema)
1628
- });
1629
- var contentSourcesResponseDtoSchema = z15.object({
1630
- sources: z15.array(contentSourceRowDtoSchema),
1631
- latestRunId: z15.string()
1632
- });
1633
- var contentGapRowDtoSchema = z15.object({
1634
- query: z15.string(),
1635
- competitorDomains: z15.array(z15.string()),
1636
- competitorCount: z15.number().int().nonnegative(),
1637
- missRate: z15.number().min(0).max(1),
1638
- lastSeenInRunId: z15.string()
1639
- });
1640
- var contentGapsResponseDtoSchema = z15.object({
1641
- gaps: z15.array(contentGapRowDtoSchema),
1642
- latestRunId: z15.string()
1656
+ var contentGroundingSourceSchema = z16.object({
1657
+ uri: z16.string(),
1658
+ title: z16.string(),
1659
+ domain: z16.string(),
1660
+ isOurDomain: z16.boolean(),
1661
+ isCompetitor: z16.boolean(),
1662
+ citationCount: z16.number().int().nonnegative(),
1663
+ providers: z16.array(providerNameSchema)
1664
+ });
1665
+ var contentSourceRowDtoSchema = z16.object({
1666
+ query: z16.string(),
1667
+ groundingSources: z16.array(contentGroundingSourceSchema)
1668
+ });
1669
+ var contentSourcesResponseDtoSchema = z16.object({
1670
+ sources: z16.array(contentSourceRowDtoSchema),
1671
+ latestRunId: z16.string()
1672
+ });
1673
+ var contentGapRowDtoSchema = z16.object({
1674
+ query: z16.string(),
1675
+ competitorDomains: z16.array(z16.string()),
1676
+ competitorCount: z16.number().int().nonnegative(),
1677
+ missRate: z16.number().min(0).max(1),
1678
+ lastSeenInRunId: z16.string()
1679
+ });
1680
+ var contentGapsResponseDtoSchema = z16.object({
1681
+ gaps: z16.array(contentGapRowDtoSchema),
1682
+ latestRunId: z16.string()
1643
1683
  });
1644
1684
 
1645
1685
  // src/client.ts
@@ -2233,6 +2273,17 @@ var ApiClient = class {
2233
2273
  async getHealth(project) {
2234
2274
  return this.request("GET", `/projects/${encodeURIComponent(project)}/health/latest`);
2235
2275
  }
2276
+ async getProjectOverview(project) {
2277
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/overview`);
2278
+ }
2279
+ async searchProject(project, opts) {
2280
+ const params = new URLSearchParams({ q: opts.q });
2281
+ if (opts.limit !== void 0) params.set("limit", String(opts.limit));
2282
+ return this.request(
2283
+ "GET",
2284
+ `/projects/${encodeURIComponent(project)}/search?${params.toString()}`
2285
+ );
2286
+ }
2236
2287
  async getHealthHistory(project, limit) {
2237
2288
  const qs = limit ? `?limit=${limit}` : "";
2238
2289
  return this.request("GET", `/projects/${encodeURIComponent(project)}/health/history${qs}`);
package/dist/cli.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  setGoogleAuthConfig,
18
18
  showFirstRunNotice,
19
19
  trackEvent
20
- } from "./chunk-FAP76VXF.js";
20
+ } from "./chunk-FCYNFM4B.js";
21
21
  import {
22
22
  CcReleaseSyncStatuses,
23
23
  CliError,
@@ -41,7 +41,7 @@ import {
41
41
  saveConfig,
42
42
  saveConfigPatch,
43
43
  usageError
44
- } from "./chunk-Z3BWDCBJ.js";
44
+ } from "./chunk-PLI7EOPM.js";
45
45
  import {
46
46
  apiKeys,
47
47
  competitors,
@@ -1847,7 +1847,7 @@ async function gaSocialReferralSummary(project, opts) {
1847
1847
  console.log(` Sessions: ${traffic.socialSessions} (${traffic.socialSharePct}% of ${traffic.totalSessions} total)`);
1848
1848
  console.log(` Users: ${traffic.socialUsers}`);
1849
1849
  console.log();
1850
- const fmtTrend = (pct) => pct === null ? "n/a" : `${pct >= 0 ? "+" : ""}${pct}%`;
1850
+ const fmtTrend = (pct2) => pct2 === null ? "n/a" : `${pct2 >= 0 ? "+" : ""}${pct2}%`;
1851
1851
  console.log(` 7d trend: ${fmtTrend(trend.trend7dPct)} (${trend.socialSessions7d} vs ${trend.socialSessionsPrev7d})`);
1852
1852
  console.log(` 30d trend: ${fmtTrend(trend.trend30dPct)} (${trend.socialSessions30d} vs ${trend.socialSessionsPrev30d})`);
1853
1853
  if (trend.biggestMover) {
@@ -1890,7 +1890,7 @@ async function gaSocialReferralSummary(project, opts) {
1890
1890
  async function gaAttribution(project, opts) {
1891
1891
  const client = getClient4();
1892
1892
  const traffic = await client.gaTraffic(project);
1893
- const fmtTrend = (pct) => pct === null ? "n/a" : `${pct >= 0 ? "+" : ""}${pct}%`;
1893
+ const fmtTrend = (pct2) => pct2 === null ? "n/a" : `${pct2 >= 0 ? "+" : ""}${pct2}%`;
1894
1894
  if (opts?.trend) {
1895
1895
  const trend = await client.gaAttributionTrend(project);
1896
1896
  if (opts.format === "json") {
@@ -4047,14 +4047,14 @@ function printMetrics(data) {
4047
4047
  console.log(`
4048
4048
  Citation Rate Trends (${data.window})`);
4049
4049
  console.log("\u2500".repeat(50));
4050
- const pct = (n) => `${(n * 100).toFixed(1)}%`;
4051
- console.log(` Overall: ${pct(data.overall.citationRate)} (${data.overall.cited}/${data.overall.total})`);
4050
+ const pct2 = (n) => `${(n * 100).toFixed(1)}%`;
4051
+ console.log(` Overall: ${pct2(data.overall.citationRate)} (${data.overall.cited}/${data.overall.total})`);
4052
4052
  console.log(` Trend: ${data.trend}`);
4053
4053
  if (Object.keys(data.byProvider).length > 0) {
4054
4054
  console.log(`
4055
4055
  By Provider:`);
4056
4056
  for (const [provider, metric] of Object.entries(data.byProvider)) {
4057
- console.log(` ${provider.padEnd(10)} ${pct(metric.citationRate).padStart(6)} (${metric.cited}/${metric.total})`);
4057
+ console.log(` ${provider.padEnd(10)} ${pct2(metric.citationRate).padStart(6)} (${metric.cited}/${metric.total})`);
4058
4058
  }
4059
4059
  }
4060
4060
  if (data.buckets.length > 0) {
@@ -4063,7 +4063,7 @@ Citation Rate Trends (${data.window})`);
4063
4063
  for (const bucket of data.buckets) {
4064
4064
  const start = bucket.startDate.slice(0, 10);
4065
4065
  const bar = bucket.total > 0 ? "\u2588".repeat(Math.round(bucket.citationRate * 20)) : "";
4066
- console.log(` ${start} ${pct(bucket.citationRate).padStart(6)} ${bar}`);
4066
+ console.log(` ${start} ${pct2(bucket.citationRate).padStart(6)} ${bar}`);
4067
4067
  }
4068
4068
  }
4069
4069
  }
@@ -4100,9 +4100,9 @@ Source Origin Breakdown`);
4100
4100
  return;
4101
4101
  }
4102
4102
  for (const cat of data.overall) {
4103
- const pct = `${(cat.percentage * 100).toFixed(1)}%`;
4103
+ const pct2 = `${(cat.percentage * 100).toFixed(1)}%`;
4104
4104
  const domains = cat.topDomains.slice(0, 3).map((d) => d.domain).join(", ");
4105
- console.log(` ${cat.label.padEnd(20)} ${pct.padStart(6)} (${cat.count}) ${domains}`);
4105
+ console.log(` ${cat.label.padEnd(20)} ${pct2.padStart(6)} (${cat.count}) ${domains}`);
4106
4106
  }
4107
4107
  }
4108
4108
 
@@ -6026,6 +6026,84 @@ async function showHealth(project, opts) {
6026
6026
  }
6027
6027
  }
6028
6028
 
6029
+ // src/commands/overview.ts
6030
+ async function showOverview(project, opts) {
6031
+ const client = createApiClient();
6032
+ const overview = await client.getProjectOverview(project);
6033
+ if (opts.format === "json") {
6034
+ console.log(JSON.stringify(overview, null, 2));
6035
+ return;
6036
+ }
6037
+ const { project: meta, latestRun, health, topInsights, keywordCounts, providers, transitions } = overview;
6038
+ console.log(`Overview: ${meta.displayName ?? meta.name} (${meta.name})
6039
+ `);
6040
+ console.log(` Domain: ${meta.canonicalDomain}`);
6041
+ console.log(` Country: ${meta.country}`);
6042
+ console.log(` Language: ${meta.language}`);
6043
+ if (latestRun.run) {
6044
+ const finished = latestRun.run.finishedAt ?? "\u2014";
6045
+ console.log(`
6046
+ Latest run: ${latestRun.run.id} (${latestRun.run.status}, ${finished})`);
6047
+ console.log(` Total runs: ${latestRun.totalRuns}`);
6048
+ } else {
6049
+ console.log("\n No runs yet.");
6050
+ }
6051
+ console.log(`
6052
+ Keywords cited: ${keywordCounts.citedKeywords}/${keywordCounts.totalKeywords} (${pct(keywordCounts.citedRate)})`);
6053
+ if (providers.length > 0) {
6054
+ console.log(" Providers:");
6055
+ for (const p of providers) {
6056
+ console.log(` ${p.provider.padEnd(10)} ${p.cited}/${p.total} (${pct(p.citedRate)})`);
6057
+ }
6058
+ }
6059
+ if (transitions.since) {
6060
+ console.log(`
6061
+ vs run at ${transitions.since}: +${transitions.gained} gained, -${transitions.lost} lost, ${transitions.emerging} emerging`);
6062
+ }
6063
+ if (health) {
6064
+ console.log(`
6065
+ Health: ${pct(health.overallCitedRate)} cited (${health.citedPairs}/${health.totalPairs} pairs)`);
6066
+ }
6067
+ if (topInsights.length > 0) {
6068
+ console.log("\n Top insights:");
6069
+ for (const insight of topInsights) {
6070
+ console.log(` [${insight.severity.toUpperCase()}] ${insight.type} \u2014 ${insight.title}`);
6071
+ }
6072
+ }
6073
+ }
6074
+ function pct(value) {
6075
+ return `${(value * 100).toFixed(1)}%`;
6076
+ }
6077
+
6078
+ // src/commands/search.ts
6079
+ async function searchProject(project, opts) {
6080
+ const client = createApiClient();
6081
+ const result = await client.searchProject(project, { q: opts.query, limit: opts.limit });
6082
+ if (opts.format === "json") {
6083
+ console.log(JSON.stringify(result, null, 2));
6084
+ return;
6085
+ }
6086
+ console.log(`Search: "${result.query}" \u2014 ${result.totalHits} hit${result.totalHits === 1 ? "" : "s"}${result.truncated ? " (truncated)" : ""}
6087
+ `);
6088
+ if (result.hits.length === 0) {
6089
+ console.log(" No matches.");
6090
+ return;
6091
+ }
6092
+ for (const hit of result.hits) {
6093
+ if (hit.kind === "snapshot") {
6094
+ console.log(` [snapshot] ${hit.keyword} (${hit.provider}, ${hit.citationState}) \u2014 ${hit.matchedField}`);
6095
+ console.log(` ${hit.snippet}`);
6096
+ console.log(` run=${hit.runId} at ${hit.createdAt}`);
6097
+ } else {
6098
+ const dismissed = hit.dismissed ? " [dismissed]" : "";
6099
+ console.log(` [insight ${hit.severity.toUpperCase()}] ${hit.type} \u2014 ${hit.title}${dismissed}`);
6100
+ console.log(` ${hit.snippet}`);
6101
+ console.log(` keyword=${hit.keyword} at ${hit.createdAt}`);
6102
+ }
6103
+ console.log("");
6104
+ }
6105
+ }
6106
+
6029
6107
  // src/cli-commands/intelligence.ts
6030
6108
  var INTELLIGENCE_CLI_COMMANDS = [
6031
6109
  {
@@ -6072,6 +6150,34 @@ var INTELLIGENCE_CLI_COMMANDS = [
6072
6150
  });
6073
6151
  await showHealth(project, { history, limit, format: input.format });
6074
6152
  }
6153
+ },
6154
+ {
6155
+ path: ["overview"],
6156
+ usage: "canonry overview <project> [--format json]",
6157
+ options: {},
6158
+ run: async (input) => {
6159
+ const usage = "canonry overview <project> [--format json]";
6160
+ const project = requireProject(input, "overview", usage);
6161
+ await showOverview(project, { format: input.format });
6162
+ }
6163
+ },
6164
+ {
6165
+ path: ["search"],
6166
+ usage: "canonry search <project> <query> [--limit <n>] [--format json]",
6167
+ options: {
6168
+ limit: { type: "string" }
6169
+ },
6170
+ run: async (input) => {
6171
+ const usage = "canonry search <project> <query> [--limit <n>] [--format json]";
6172
+ const project = requireProject(input, "search", usage);
6173
+ const query = requirePositional(input, 1, { command: "search", usage, message: "query is required" });
6174
+ const limit = parseIntegerOption(input, "limit", {
6175
+ command: "search",
6176
+ usage,
6177
+ message: "--limit must be an integer"
6178
+ });
6179
+ await searchProject(project, { query, limit, format: input.format });
6180
+ }
6075
6181
  }
6076
6182
  ];
6077
6183
 
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  createServer
3
- } from "./chunk-FAP76VXF.js";
3
+ } from "./chunk-FCYNFM4B.js";
4
4
  import {
5
5
  loadConfig
6
- } from "./chunk-Z3BWDCBJ.js";
6
+ } from "./chunk-PLI7EOPM.js";
7
7
  import "./chunk-UM6RDSRJ.js";
8
8
  import "./chunk-MLKGABMK.js";
9
9
  export {
package/dist/mcp.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  projectUpsertRequestSchema,
11
11
  runTriggerRequestSchema,
12
12
  scheduleUpsertRequestSchema
13
- } from "./chunk-Z3BWDCBJ.js";
13
+ } from "./chunk-PLI7EOPM.js";
14
14
  import "./chunk-MLKGABMK.js";
15
15
 
16
16
  // src/mcp/cli.ts
@@ -202,6 +202,32 @@ var canonryMcpTools = [
202
202
  openApiOperations: ["GET /api/v1/projects/{name}"],
203
203
  handler: (client, input) => client.getProject(input.project)
204
204
  }),
205
+ defineTool({
206
+ name: "canonry_project_overview",
207
+ title: "Get project overview (composite)",
208
+ description: 'One-call summary for "how is project X doing?" \u2014 bundles project info, latest run, top undismissed insights, latest health snapshot, keyword cited rate, per-provider breakdown, and gained/lost/emerging vs the previous run. Prefer this over fanning out to separate tools.',
209
+ access: "read",
210
+ tier: "core",
211
+ inputSchema: projectInputSchema,
212
+ annotations: readAnnotations(),
213
+ openApiOperations: ["GET /api/v1/projects/{name}/overview"],
214
+ handler: (client, input) => client.getProjectOverview(input.project)
215
+ }),
216
+ defineTool({
217
+ name: "canonry_search",
218
+ title: "Search project (composite)",
219
+ description: "Search query snapshots and intelligence insights for the given text. Looks at snapshot answer text, cited domains, raw provider responses, and insight title/keyword/recommendation/cause. Returns ranked hits with snippets \u2014 use it instead of paginating snapshots when you need to find a competitor mention or term.",
220
+ access: "read",
221
+ tier: "core",
222
+ inputSchema: z2.object({
223
+ project: projectNameSchema,
224
+ q: z2.string().min(2).describe("Search term, at least 2 characters."),
225
+ limit: z2.number().int().positive().max(50).optional().describe("Max combined hits (1-50, default 25).")
226
+ }),
227
+ annotations: readAnnotations(),
228
+ openApiOperations: ["GET /api/v1/projects/{name}/search"],
229
+ handler: (client, input) => client.searchProject(input.project, { q: input.q, limit: input.limit })
230
+ }),
205
231
  defineTool({
206
232
  name: "canonry_project_export",
207
233
  title: "Export project config",
@@ -885,7 +911,9 @@ var DynamicToolCatalog = class {
885
911
  loaded = /* @__PURE__ */ new Set();
886
912
  eager;
887
913
  scope;
888
- constructor(entries, scope, options = {}) {
914
+ server;
915
+ constructor(server, entries, scope, options = {}) {
916
+ this.server = server;
889
917
  this.entries = entries;
890
918
  this.scope = scope;
891
919
  this.eager = Boolean(options.eager);
@@ -899,9 +927,11 @@ var DynamicToolCatalog = class {
899
927
  }
900
928
  applyInitialEnablement() {
901
929
  if (this.eager) return;
902
- for (const entry of this.entries) {
903
- if (entry.tool.tier !== "core") entry.registered.disable();
904
- }
930
+ this.batchListChanged(() => {
931
+ for (const entry of this.entries) {
932
+ if (entry.tool.tier !== "core") entry.registered.disable();
933
+ }
934
+ });
905
935
  }
906
936
  loadToolkit(rawName) {
907
937
  if (!isCanonryMcpToolkitName(rawName)) {
@@ -916,9 +946,11 @@ var DynamicToolCatalog = class {
916
946
  if (this.loaded.has(name)) {
917
947
  return { status: "already-loaded", name, tools: matches.map((entry) => entry.tool.name) };
918
948
  }
919
- for (const entry of matches) {
920
- entry.registered.enable();
921
- }
949
+ this.batchListChanged(() => {
950
+ for (const entry of matches) {
951
+ entry.registered.enable();
952
+ }
953
+ });
922
954
  this.loaded.add(name);
923
955
  return { status: "loaded", name, tools: matches.map((entry) => entry.tool.name) };
924
956
  }
@@ -929,7 +961,7 @@ var DynamicToolCatalog = class {
929
961
  loadedToolkits: [...this.loaded].sort(),
930
962
  coreTools: this.entries.filter((entry) => entry.tool.tier === "core").map((entry) => entry.tool.name),
931
963
  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."
964
+ 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. Wait for its response before calling any newly enabled tool."
933
965
  };
934
966
  }
935
967
  toolkitEntry(toolkit) {
@@ -947,6 +979,25 @@ var DynamicToolCatalog = class {
947
979
  toolsForToolkit(name) {
948
980
  return this.entries.filter((entry) => entry.tool.tier === name).map((entry) => entry.tool.name);
949
981
  }
982
+ // RegisteredTool.enable/disable each call sendToolListChanged on the McpServer
983
+ // we registered with. Loading an 11-tool toolkit emits 11 notifications under
984
+ // that contract, which a spec-compliant client will treat as 11 catalog
985
+ // refetches. Coalesce them into one notification per batch by intercepting
986
+ // the SDK's sender for the duration of the batch.
987
+ batchListChanged(fn) {
988
+ const host = this.server;
989
+ const original = host.sendToolListChanged;
990
+ let suppressed = false;
991
+ host.sendToolListChanged = () => {
992
+ suppressed = true;
993
+ };
994
+ try {
995
+ fn();
996
+ } finally {
997
+ host.sendToolListChanged = original;
998
+ }
999
+ if (suppressed) original.call(host);
1000
+ }
950
1001
  };
951
1002
 
952
1003
  // src/mcp/server.ts
@@ -981,7 +1032,7 @@ function createCanonryMcpServerWithCatalog(options = {}) {
981
1032
  );
982
1033
  entries.push({ tool, registered });
983
1034
  }
984
- const catalog = new DynamicToolCatalog(entries, scope, { eager: options.eager });
1035
+ const catalog = new DynamicToolCatalog(server, entries, scope, { eager: options.eager });
985
1036
  catalog.applyInitialEnablement();
986
1037
  registerMetaTools(server, catalog);
987
1038
  return { server, catalog };
@@ -1004,7 +1055,7 @@ function registerMetaTools(server, catalog) {
1004
1055
  "canonry_load_toolkit",
1005
1056
  {
1006
1057
  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.",
1058
+ description: `Register a toolkit's tools for this session and emit one notifications/tools/list_changed. Idempotent. Loaded toolkits remain loaded for the rest of the session. Wait for this call to return before calling any newly enabled tool \u2014 pipelining the call with a tools/call on the same connection can race the registration and fail with "MCP error -32602: Tool ... disabled".`,
1008
1059
  inputSchema: loadToolkitInputSchema.shape,
1009
1060
  annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false }
1010
1061
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainyc/canonry",
3
- "version": "2.10.3",
3
+ "version": "2.12.1",
4
4
  "type": "module",
5
5
  "description": "Agent-first open-source AEO operating platform - track how answer engines cite your domain",
6
6
  "license": "FSL-1.1-ALv2",
@@ -59,20 +59,20 @@
59
59
  "@types/node-cron": "^3.0.11",
60
60
  "tsup": "^8.5.1",
61
61
  "tsx": "^4.19.0",
62
- "@ainyc/canonry-api-routes": "0.0.0",
63
- "@ainyc/canonry-config": "0.0.0",
62
+ "@ainyc/canonry-contracts": "0.0.0",
64
63
  "@ainyc/canonry-db": "0.0.0",
65
64
  "@ainyc/canonry-intelligence": "0.0.0",
66
- "@ainyc/canonry-contracts": "0.0.0",
65
+ "@ainyc/canonry-config": "0.0.0",
67
66
  "@ainyc/canonry-integration-bing": "0.0.0",
67
+ "@ainyc/canonry-integration-commoncrawl": "0.0.0",
68
+ "@ainyc/canonry-api-routes": "0.0.0",
68
69
  "@ainyc/canonry-integration-google": "0.0.0",
69
- "@ainyc/canonry-provider-cdp": "0.0.0",
70
70
  "@ainyc/canonry-integration-wordpress": "0.0.0",
71
71
  "@ainyc/canonry-provider-claude": "0.0.0",
72
+ "@ainyc/canonry-provider-cdp": "0.0.0",
72
73
  "@ainyc/canonry-provider-gemini": "0.0.0",
73
- "@ainyc/canonry-provider-openai": "0.0.0",
74
- "@ainyc/canonry-integration-commoncrawl": "0.0.0",
75
74
  "@ainyc/canonry-provider-local": "0.0.0",
75
+ "@ainyc/canonry-provider-openai": "0.0.0",
76
76
  "@ainyc/canonry-provider-perplexity": "0.0.0"
77
77
  },
78
78
  "scripts": {