@mgsoftwarebv/mg-dashboard-mcp 7.1.0 → 7.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -956,9 +956,9 @@ async function fetchAndFormatRun(conn, proxy, sshExec2, instance, runId) {
956
956
  return { content: [{ type: "text", text: `Invalid API response:
957
957
  ${rawJson.substring(0, 500)}` }] };
958
958
  }
959
- let text7 = formatRunDetail(run);
960
- if (logs) text7 += "\n\n--- Logs ---\n" + logs;
961
- return { content: [{ type: "text", text: text7 }] };
959
+ let text8 = formatRunDetail(run);
960
+ if (logs) text8 += "\n\n--- Logs ---\n" + logs;
961
+ return { content: [{ type: "text", text: text8 }] };
962
962
  }
963
963
  async function waitForCompletion(conn, proxy, sshExec2, instance, runId, waitSeconds) {
964
964
  const pollInterval = 3e3;
@@ -980,10 +980,10 @@ async function waitForCompletion(conn, proxy, sshExec2, instance, runId, waitSec
980
980
  continue;
981
981
  }
982
982
  if (TERMINAL_STATUSES.has(run.status)) {
983
- let text7 = formatRunDetail(run);
983
+ let text8 = formatRunDetail(run);
984
984
  const logs = await fetchRunLogs(runId, conn, proxy, sshExec2);
985
- if (logs) text7 += "\n\n--- Logs ---\n" + logs;
986
- return { content: [{ type: "text", text: text7 }] };
985
+ if (logs) text8 += "\n\n--- Logs ---\n" + logs;
986
+ return { content: [{ type: "text", text: text8 }] };
987
987
  }
988
988
  }
989
989
  return {
@@ -3670,10 +3670,10 @@ var ZodObject = class _ZodObject extends ZodType {
3670
3670
  // }) as any;
3671
3671
  // return merged;
3672
3672
  // }
3673
- catchall(index5) {
3673
+ catchall(index6) {
3674
3674
  return new _ZodObject({
3675
3675
  ...this._def,
3676
- catchall: index5
3676
+ catchall: index6
3677
3677
  });
3678
3678
  }
3679
3679
  pick(mask) {
@@ -3991,9 +3991,9 @@ function mergeValues(a, b) {
3991
3991
  return { valid: false };
3992
3992
  }
3993
3993
  const newArray = [];
3994
- for (let index5 = 0; index5 < a.length; index5++) {
3995
- const itemA = a[index5];
3996
- const itemB = b[index5];
3994
+ for (let index6 = 0; index6 < a.length; index6++) {
3995
+ const itemA = a[index6];
3996
+ const itemB = b[index6];
3997
3997
  const sharedValue = mergeValues(itemA, itemB);
3998
3998
  if (!sharedValue.valid) {
3999
3999
  return { valid: false };
@@ -4199,10 +4199,10 @@ var ZodMap = class extends ZodType {
4199
4199
  }
4200
4200
  const keyType = this._def.keyType;
4201
4201
  const valueType = this._def.valueType;
4202
- const pairs = [...ctx.data.entries()].map(([key, value], index5) => {
4202
+ const pairs = [...ctx.data.entries()].map(([key, value], index6) => {
4203
4203
  return {
4204
- key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, [index5, "key"])),
4205
- value: valueType._parse(new ParseInputLazyPath(ctx, value, ctx.path, [index5, "value"]))
4204
+ key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, [index6, "key"])),
4205
+ value: valueType._parse(new ParseInputLazyPath(ctx, value, ctx.path, [index6, "value"]))
4206
4206
  };
4207
4207
  });
4208
4208
  if (ctx.common.async) {
@@ -5170,6 +5170,222 @@ pgTable("two_factor", {
5170
5170
  secret: text("secret").notNull(),
5171
5171
  backupCodes: text("backup_codes")
5172
5172
  });
5173
+ var contentSourceType = pgEnum("content_source_type", [
5174
+ "own_site",
5175
+ "client_site",
5176
+ "external_source",
5177
+ "competitor",
5178
+ "news",
5179
+ "government",
5180
+ "supplier",
5181
+ "unknown"
5182
+ ]);
5183
+ var contentSnapshotKind = pgEnum("content_snapshot_kind", [
5184
+ "corpus",
5185
+ "source"
5186
+ ]);
5187
+ var contentSources = pgTable(
5188
+ "content_sources",
5189
+ {
5190
+ id: uuid("id").primaryKey().defaultRandom(),
5191
+ url: text("url").notNull(),
5192
+ canonicalUrl: text("canonical_url"),
5193
+ domain: text("domain").notNull(),
5194
+ sourceType: contentSourceType("source_type").notNull().default("unknown"),
5195
+ title: text("title"),
5196
+ author: text("author"),
5197
+ publishedAt: timestamp("published_at", { withTimezone: true }),
5198
+ extractionMethod: text("extraction_method"),
5199
+ summary: text("summary"),
5200
+ facts: jsonb("facts").$type().notNull().default([]),
5201
+ limitations: jsonb("limitations").$type().notNull().default([]),
5202
+ fullText: text("full_text"),
5203
+ textHash: text("text_hash"),
5204
+ wordCount: integer("word_count"),
5205
+ language: text("language"),
5206
+ projectKey: text("project_key"),
5207
+ siteKey: text("site_key"),
5208
+ ticketNumber: text("ticket_number"),
5209
+ metadata: jsonb("metadata").$type().notNull().default({}),
5210
+ fetchedAt: timestamp("fetched_at", { withTimezone: true }).notNull().defaultNow(),
5211
+ lastSeenAt: timestamp("last_seen_at", { withTimezone: true }).notNull().defaultNow(),
5212
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5213
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5214
+ },
5215
+ (table) => [
5216
+ uniqueIndex("content_sources_canonical_url_uidx").on(table.canonicalUrl),
5217
+ index("content_sources_domain_idx").on(table.domain),
5218
+ index("content_sources_type_idx").on(table.sourceType),
5219
+ index("content_sources_text_hash_idx").on(table.textHash),
5220
+ index("content_sources_project_idx").on(table.projectKey),
5221
+ index("content_sources_ticket_idx").on(table.ticketNumber),
5222
+ index("content_sources_last_seen_idx").on(table.lastSeenAt.desc())
5223
+ ]
5224
+ );
5225
+ var contentEvidencePacks = pgTable(
5226
+ "content_evidence_packs",
5227
+ {
5228
+ id: uuid("id").primaryKey().defaultRandom(),
5229
+ topic: text("topic"),
5230
+ locale: text("locale"),
5231
+ projectKey: text("project_key"),
5232
+ siteKey: text("site_key"),
5233
+ ticketNumber: text("ticket_number"),
5234
+ generatedAt: timestamp("generated_at", { withTimezone: true }).notNull().defaultNow(),
5235
+ sources: jsonb("sources").$type().notNull().default([]),
5236
+ verifiedFacts: jsonb("verified_facts").$type().notNull().default([]),
5237
+ conflicts: jsonb("conflicts").$type().notNull().default([]),
5238
+ uniqueClaims: jsonb("unique_claims").$type().notNull().default([]),
5239
+ openQuestions: jsonb("open_questions").$type().notNull().default([]),
5240
+ verbatimWarnings: jsonb("verbatim_warnings").$type().notNull().default([]),
5241
+ limitations: jsonb("limitations").$type().notNull().default([]),
5242
+ metadata: jsonb("metadata").$type().notNull().default({}),
5243
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5244
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5245
+ },
5246
+ (table) => [
5247
+ index("content_evidence_packs_project_idx").on(table.projectKey),
5248
+ index("content_evidence_packs_ticket_idx").on(table.ticketNumber),
5249
+ index("content_evidence_packs_generated_idx").on(table.generatedAt.desc())
5250
+ ]
5251
+ );
5252
+ pgTable(
5253
+ "content_evidence_pack_source",
5254
+ {
5255
+ id: uuid("id").primaryKey().defaultRandom(),
5256
+ packId: uuid("pack_id").notNull().references(() => contentEvidencePacks.id, { onDelete: "cascade" }),
5257
+ contentSourceId: uuid("content_source_id").notNull().references(() => contentSources.id, { onDelete: "cascade" }),
5258
+ packSourceRef: text("pack_source_ref"),
5259
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
5260
+ },
5261
+ (table) => [
5262
+ uniqueIndex("content_evidence_pack_source_uidx").on(
5263
+ table.packId,
5264
+ table.contentSourceId
5265
+ ),
5266
+ index("content_evidence_pack_source_source_idx").on(table.contentSourceId)
5267
+ ]
5268
+ );
5269
+ var contentCorpusItems = pgTable(
5270
+ "content_corpus_items",
5271
+ {
5272
+ id: uuid("id").primaryKey().defaultRandom(),
5273
+ projectKey: text("project_key").notNull(),
5274
+ siteKey: text("site_key"),
5275
+ url: text("url").notNull(),
5276
+ canonicalUrl: text("canonical_url"),
5277
+ domain: text("domain").notNull(),
5278
+ pagePath: text("page_path"),
5279
+ sourceType: contentSourceType("source_type").notNull().default("own_site"),
5280
+ title: text("title"),
5281
+ h1: text("h1"),
5282
+ headings: jsonb("headings").$type().notNull().default([]),
5283
+ wordCount: integer("word_count"),
5284
+ fullText: text("full_text"),
5285
+ textHash: text("text_hash"),
5286
+ internalLinks: jsonb("internal_links").$type().notNull().default([]),
5287
+ externalLinks: jsonb("external_links").$type().notNull().default([]),
5288
+ language: text("language"),
5289
+ ticketNumber: text("ticket_number"),
5290
+ metadata: jsonb("metadata").$type().notNull().default({}),
5291
+ extractedAt: timestamp("extracted_at", { withTimezone: true }).notNull().defaultNow(),
5292
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5293
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5294
+ },
5295
+ (table) => [
5296
+ uniqueIndex("content_corpus_items_project_canonical_uidx").on(
5297
+ table.projectKey,
5298
+ table.canonicalUrl
5299
+ ),
5300
+ index("content_corpus_items_domain_idx").on(table.domain),
5301
+ index("content_corpus_items_project_idx").on(table.projectKey),
5302
+ index("content_corpus_items_type_idx").on(table.sourceType),
5303
+ index("content_corpus_items_text_hash_idx").on(table.textHash),
5304
+ index("content_corpus_items_extracted_idx").on(table.extractedAt.desc())
5305
+ ]
5306
+ );
5307
+ pgTable(
5308
+ "content_quality_runs",
5309
+ {
5310
+ id: uuid("id").primaryKey().defaultRandom(),
5311
+ corpusItemId: uuid("corpus_item_id").references(
5312
+ () => contentCorpusItems.id,
5313
+ { onDelete: "set null" }
5314
+ ),
5315
+ contentSourceId: uuid("content_source_id").references(
5316
+ () => contentSources.id,
5317
+ { onDelete: "set null" }
5318
+ ),
5319
+ projectKey: text("project_key"),
5320
+ siteKey: text("site_key"),
5321
+ url: text("url"),
5322
+ canonicalUrl: text("canonical_url"),
5323
+ tool: text("tool").notNull(),
5324
+ toolVersion: text("tool_version"),
5325
+ score: integer("score"),
5326
+ flags: jsonb("flags").$type().notNull().default([]),
5327
+ report: jsonb("report").$type().notNull().default({}),
5328
+ reportUrl: text("report_url"),
5329
+ ticketNumber: text("ticket_number"),
5330
+ runAt: timestamp("run_at", { withTimezone: true }).notNull().defaultNow(),
5331
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
5332
+ },
5333
+ (table) => [
5334
+ index("content_quality_runs_corpus_idx").on(table.corpusItemId),
5335
+ index("content_quality_runs_source_idx").on(table.contentSourceId),
5336
+ index("content_quality_runs_project_run_idx").on(
5337
+ table.projectKey,
5338
+ table.runAt.desc()
5339
+ ),
5340
+ index("content_quality_runs_url_idx").on(table.url)
5341
+ ]
5342
+ );
5343
+ pgTable(
5344
+ "content_snapshots",
5345
+ {
5346
+ id: uuid("id").primaryKey().defaultRandom(),
5347
+ corpusItemId: uuid("corpus_item_id").references(
5348
+ () => contentCorpusItems.id,
5349
+ { onDelete: "cascade" }
5350
+ ),
5351
+ contentSourceId: uuid("content_source_id").references(
5352
+ () => contentSources.id,
5353
+ { onDelete: "cascade" }
5354
+ ),
5355
+ kind: contentSnapshotKind("kind").notNull(),
5356
+ url: text("url"),
5357
+ canonicalUrl: text("canonical_url"),
5358
+ domain: text("domain"),
5359
+ title: text("title"),
5360
+ fullText: text("full_text"),
5361
+ textHash: text("text_hash"),
5362
+ wordCount: integer("word_count"),
5363
+ headings: jsonb("headings").$type().notNull().default([]),
5364
+ internalLinks: jsonb("internal_links").$type().notNull().default([]),
5365
+ metadata: jsonb("metadata").$type().notNull().default({}),
5366
+ capturedAt: timestamp("captured_at", { withTimezone: true }).notNull().defaultNow(),
5367
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
5368
+ },
5369
+ (table) => [
5370
+ index("content_snapshots_corpus_idx").on(
5371
+ table.corpusItemId,
5372
+ table.capturedAt.desc()
5373
+ ),
5374
+ index("content_snapshots_source_idx").on(
5375
+ table.contentSourceId,
5376
+ table.capturedAt.desc()
5377
+ ),
5378
+ index("content_snapshots_text_hash_idx").on(table.textHash),
5379
+ uniqueIndex("content_snapshots_corpus_hash_uidx").on(
5380
+ table.corpusItemId,
5381
+ table.textHash
5382
+ ),
5383
+ uniqueIndex("content_snapshots_source_hash_uidx").on(
5384
+ table.contentSourceId,
5385
+ table.textHash
5386
+ )
5387
+ ]
5388
+ );
5173
5389
  var managedServerOs = pgEnum("managed_server_os", [
5174
5390
  "linux",
5175
5391
  "windows",
@@ -7075,7 +7291,7 @@ function getEncryptionKey() {
7075
7291
  throw new Error("ENCRYPTION_KEY must be a 64-character hex string");
7076
7292
  return buf;
7077
7293
  }
7078
- function encrypt(text7) {
7294
+ function encrypt(text8) {
7079
7295
  const key = getEncryptionKey();
7080
7296
  const iv = randomBytes(ENC_IV_LENGTH);
7081
7297
  const cipher = createCipheriv(
@@ -7083,7 +7299,7 @@ function encrypt(text7) {
7083
7299
  new Uint8Array(key),
7084
7300
  new Uint8Array(iv)
7085
7301
  );
7086
- let encrypted = cipher.update(text7, "utf8", "hex");
7302
+ let encrypted = cipher.update(text8, "utf8", "hex");
7087
7303
  encrypted += cipher.final("hex");
7088
7304
  const authTag = cipher.getAuthTag();
7089
7305
  return Buffer.concat([
@@ -7803,10 +8019,10 @@ async function r2GetObjectRange(bucket, key, range) {
7803
8019
  const body = result.Body;
7804
8020
  if (!body?.transformToString)
7805
8021
  throw new Error("R2 returned no readable body");
7806
- const text7 = await body.transformToString();
8022
+ const text8 = await body.transformToString();
7807
8023
  const header = `# range: bytes ${range.offset}-${end} of ${size} (${effectiveLen} bytes)`;
7808
8024
  return `${header}
7809
- ${text7}`;
8025
+ ${text8}`;
7810
8026
  } catch (e) {
7811
8027
  throw r2WrapError(bucket, key, e);
7812
8028
  }
@@ -8169,15 +8385,15 @@ async function sftpRead(opts, filePath, proxy, options) {
8169
8385
  clearTimeout(timer);
8170
8386
  cleanup?.();
8171
8387
  cleanup = void 0;
8172
- const text7 = Buffer.concat(
8388
+ const text8 = Buffer.concat(
8173
8389
  chunks.map((ch) => new Uint8Array(ch))
8174
8390
  ).toString("utf-8");
8175
8391
  if (!isWholeFileRequest) {
8176
8392
  const header = `# range: bytes ${offset}-${offset + effectiveLen - 1} of ${total} (${effectiveLen} bytes)`;
8177
8393
  resolve(`${header}
8178
- ${text7}`);
8394
+ ${text8}`);
8179
8395
  } else {
8180
- resolve(text7);
8396
+ resolve(text8);
8181
8397
  }
8182
8398
  });
8183
8399
  rs.on("error", (e) => {
@@ -8211,7 +8427,21 @@ var NO_FOOTER_TOOLS = /* @__PURE__ */ new Set();
8211
8427
  var RAW_JSON_TOOLS = /* @__PURE__ */ new Set([
8212
8428
  "get_mg_dashboard_commits",
8213
8429
  "extract_article",
8214
- "research_topic"
8430
+ "research_topic",
8431
+ // Content registry / corpus (2026-DASMG-041) — compact JSON consumed verbatim.
8432
+ "save_content_source",
8433
+ "get_content_source_by_url",
8434
+ "get_content_source_by_id",
8435
+ "search_content_sources",
8436
+ "save_research_pack",
8437
+ "get_research_pack",
8438
+ "save_content_corpus_item",
8439
+ "list_project_content_corpus",
8440
+ "get_content_context_for_project",
8441
+ "save_content_quality_run",
8442
+ "list_content_snapshots",
8443
+ "link_content_pack_to_ticket",
8444
+ "prune_content_snapshots"
8215
8445
  ]);
8216
8446
  var TOOL_CACHE_TTL_MS = {
8217
8447
  "list-servers": 6e4,
@@ -8312,11 +8542,11 @@ function getKnownContainers(serverId, maxAgeMs = 5 * 6e4) {
8312
8542
  if (Date.now() - e.capturedAt > maxAgeMs) return void 0;
8313
8543
  return e.names;
8314
8544
  }
8315
- function truncateForLLM(text7, maxBytes) {
8316
- const totalBytes = Buffer.byteLength(text7, "utf8");
8545
+ function truncateForLLM(text8, maxBytes) {
8546
+ const totalBytes = Buffer.byteLength(text8, "utf8");
8317
8547
  if (totalBytes <= maxBytes)
8318
- return { text: text7, truncated: false, totalBytes, shownBytes: totalBytes };
8319
- const buf = Buffer.from(text7, "utf8");
8548
+ return { text: text8, truncated: false, totalBytes, shownBytes: totalBytes };
8549
+ const buf = Buffer.from(text8, "utf8");
8320
8550
  let cut = maxBytes;
8321
8551
  while (cut > 0 && (buf[cut] & 192) === 128) cut--;
8322
8552
  const head = buf.subarray(0, cut).toString("utf8");
@@ -8347,10 +8577,10 @@ function postprocessResult(result, meta) {
8347
8577
  if (!result.content?.length) return result;
8348
8578
  if (RAW_JSON_TOOLS.has(meta.toolName)) return result;
8349
8579
  const block = result.content[0];
8350
- let text7 = String(block.text ?? "");
8351
- const trunc = truncateForLLM(text7, RESPONSE_MAX_BYTES);
8580
+ let text8 = String(block.text ?? "");
8581
+ const trunc = truncateForLLM(text8, RESPONSE_MAX_BYTES);
8352
8582
  if (trunc.truncated) {
8353
- text7 = trunc.text + "\n\n... " + buildTruncationHint(
8583
+ text8 = trunc.text + "\n\n... " + buildTruncationHint(
8354
8584
  meta.toolName,
8355
8585
  meta.args,
8356
8586
  trunc.totalBytes,
@@ -8364,11 +8594,11 @@ function postprocessResult(result, meta) {
8364
8594
  const parts = [`took ${tookStr}`, sizeStr];
8365
8595
  if (meta.serverIdLabel) parts.push(`server: ${meta.serverIdLabel}`);
8366
8596
  if (meta.cached) parts.push("cached");
8367
- text7 = `${text7}
8597
+ text8 = `${text8}
8368
8598
 
8369
8599
  [${parts.join(", ")}]`;
8370
8600
  }
8371
- return { ...result, content: [{ ...block, text: text7 }] };
8601
+ return { ...result, content: [{ ...block, text: text8 }] };
8372
8602
  }
8373
8603
  function buildPipelineScript(commands, shell, marker, stopOnError) {
8374
8604
  if (shell === "powershell") {
@@ -10420,6 +10650,317 @@ var TOOLS = [
10420
10650
  }
10421
10651
  }
10422
10652
  },
10653
+ // ----- Content registry / corpus persistence (2026-DASMG-041) -----
10654
+ {
10655
+ name: "save_content_source",
10656
+ description: "Persist (dedupe + upsert) ONE fetched source \u2014 the durable companion to extract_article (2026-DASMG-041). Pass a `url` (re-extracts internally) and/or already-extracted fields (title/summary/facts/fullText). Dedupes on canonical URL then text hash, and snapshots the previous version into content_snapshots when the text changed. External sources keep their `source_type` so they stay labelled as evidence, never own content. Returns the stored id + a compact summary + whether it was created/updated/snapshotted.",
10657
+ inputSchema: {
10658
+ type: "object",
10659
+ properties: {
10660
+ url: { type: "string", description: "Source URL (required)." },
10661
+ canonicalUrl: { type: "string", description: "Canonical URL used for dedupe." },
10662
+ sourceType: {
10663
+ type: "string",
10664
+ enum: [
10665
+ "own_site",
10666
+ "client_site",
10667
+ "external_source",
10668
+ "competitor",
10669
+ "news",
10670
+ "government",
10671
+ "supplier",
10672
+ "unknown"
10673
+ ],
10674
+ description: "Provenance label. own_site/client_site = managed content; the rest are evidence only."
10675
+ },
10676
+ title: { type: "string", description: "Title." },
10677
+ author: { type: "string", description: "Author." },
10678
+ publishedAt: { type: "string", description: "Publish date (any parseable format)." },
10679
+ summary: { type: "string", description: "Short factual summary." },
10680
+ facts: { type: "array", items: { type: "string" }, description: "Key facts." },
10681
+ limitations: { type: "array", items: { type: "string" } },
10682
+ fullText: { type: "string", description: "Extracted body text." },
10683
+ language: { type: "string", description: "Language code." },
10684
+ projectKey: { type: "string", description: "Project/site identifier." },
10685
+ siteKey: { type: "string", description: "Site identifier." },
10686
+ ticketNumber: { type: "string", description: "Related ticket number." },
10687
+ metadata: { type: "object" },
10688
+ reExtract: {
10689
+ type: "boolean",
10690
+ description: "Force a re-fetch even when fields are supplied."
10691
+ }
10692
+ },
10693
+ required: ["url"]
10694
+ }
10695
+ },
10696
+ {
10697
+ name: "get_content_source_by_url",
10698
+ description: "Look up a stored source by URL or canonical URL (2026-DASMG-041). Compact by default (id + summary + counts); set `includeFullText` for the body. Use to answer 'was this link already scanned?' before re-extracting.",
10699
+ inputSchema: {
10700
+ type: "object",
10701
+ properties: {
10702
+ url: { type: "string", description: "URL or canonical URL." },
10703
+ includeFullText: { type: "boolean", description: "Return full_text." }
10704
+ },
10705
+ required: ["url"]
10706
+ }
10707
+ },
10708
+ {
10709
+ name: "get_content_source_by_id",
10710
+ description: "Fetch a stored source by id (2026-DASMG-041). Compact by default; set `includeFullText` for the body.",
10711
+ inputSchema: {
10712
+ type: "object",
10713
+ properties: {
10714
+ id: { type: "string", description: "content_sources.id (uuid)." },
10715
+ includeFullText: { type: "boolean", description: "Return full_text." }
10716
+ },
10717
+ required: ["id"]
10718
+ }
10719
+ },
10720
+ {
10721
+ name: "search_content_sources",
10722
+ description: "Full-text + filter search over stored sources (2026-DASMG-041): query (title/summary/body), domain, sourceType, projectKey, ticketNumber, dateFrom/dateTo. Compact, paginated results. Use to reuse evidence already in the registry.",
10723
+ inputSchema: {
10724
+ type: "object",
10725
+ properties: {
10726
+ query: { type: "string", description: "Free-text search (title/summary/body)." },
10727
+ domain: { type: "string", description: "Exact domain filter." },
10728
+ sourceType: {
10729
+ type: "string",
10730
+ enum: [
10731
+ "own_site",
10732
+ "client_site",
10733
+ "external_source",
10734
+ "competitor",
10735
+ "news",
10736
+ "government",
10737
+ "supplier",
10738
+ "unknown"
10739
+ ]
10740
+ },
10741
+ projectKey: { type: "string", description: "Project filter." },
10742
+ ticketNumber: { type: "string", description: "Ticket filter." },
10743
+ dateFrom: { type: "string", description: "Last-seen lower bound (ISO/date)." },
10744
+ dateTo: { type: "string", description: "Last-seen upper bound (ISO/date)." },
10745
+ limit: { type: "integer", description: "Max rows (default 20, max 100)." },
10746
+ offset: { type: "integer", description: "Pagination offset." },
10747
+ includeFullText: { type: "boolean", description: "Return full_text per row." }
10748
+ }
10749
+ }
10750
+ },
10751
+ {
10752
+ name: "save_research_pack",
10753
+ description: "Persist a research_topic evidence pack (2026-DASMG-041): cross-checked verified facts, conflicts, unique claims, open questions. Pass an already-built `pack`, or `urls`/`topic` to research then save. Each source is deduped into content_sources and linked via the join table, so packs can be reused without re-crawling. Returns the pack id + compact counts + linked source ids.",
10754
+ inputSchema: {
10755
+ type: "object",
10756
+ properties: {
10757
+ pack: { type: "object", description: "A research_topic result payload." },
10758
+ urls: {
10759
+ type: "array",
10760
+ items: { type: "string" },
10761
+ description: "URLs to research if no pack is given."
10762
+ },
10763
+ topic: { type: "string", description: "Topic to research if no pack is given." },
10764
+ locale: { type: "string", description: "Locale of the pack." },
10765
+ projectKey: { type: "string", description: "Project identifier." },
10766
+ siteKey: { type: "string", description: "Site identifier." },
10767
+ ticketNumber: { type: "string", description: "Related ticket number." },
10768
+ sourceType: {
10769
+ type: "string",
10770
+ enum: [
10771
+ "own_site",
10772
+ "client_site",
10773
+ "external_source",
10774
+ "competitor",
10775
+ "news",
10776
+ "government",
10777
+ "supplier",
10778
+ "unknown"
10779
+ ]
10780
+ },
10781
+ metadata: { type: "object" }
10782
+ }
10783
+ }
10784
+ },
10785
+ {
10786
+ name: "get_research_pack",
10787
+ description: "Fetch a stored evidence pack by id (2026-DASMG-041). Compact by default (counts + provenance); set `includeSources` / `includeClaims` for the full arrays.",
10788
+ inputSchema: {
10789
+ type: "object",
10790
+ properties: {
10791
+ id: { type: "string", description: "content_evidence_packs.id (uuid)." },
10792
+ includeSources: { type: "boolean", description: "Return the source list." },
10793
+ includeClaims: {
10794
+ type: "boolean",
10795
+ description: "Return verified facts / conflicts / unique claims."
10796
+ }
10797
+ },
10798
+ required: ["id"]
10799
+ }
10800
+ },
10801
+ {
10802
+ name: "save_content_corpus_item",
10803
+ description: "Upsert ONE own/managed page into the corpus (2026-DASMG-041), keyed by (projectKey, canonical url) \u2014 the current state of content you own. Pass a `url` (re-extracts) and/or title/h1/headings/fullText/internalLinks. Snapshots the previous version when the text changed. Use this for your/your client's pages, not external evidence.",
10804
+ inputSchema: {
10805
+ type: "object",
10806
+ properties: {
10807
+ projectKey: { type: "string", description: "Project identifier (required)." },
10808
+ url: { type: "string", description: "Page URL (required)." },
10809
+ siteKey: { type: "string", description: "Site identifier." },
10810
+ canonicalUrl: { type: "string", description: "Canonical URL." },
10811
+ sourceType: {
10812
+ type: "string",
10813
+ enum: [
10814
+ "own_site",
10815
+ "client_site",
10816
+ "external_source",
10817
+ "competitor",
10818
+ "news",
10819
+ "government",
10820
+ "supplier",
10821
+ "unknown"
10822
+ ],
10823
+ description: "Defaults to own_site."
10824
+ },
10825
+ pagePath: { type: "string", description: "Path within the site." },
10826
+ title: { type: "string", description: "Title." },
10827
+ h1: { type: "string", description: "Main heading." },
10828
+ headings: {
10829
+ type: "array",
10830
+ items: { type: "object" },
10831
+ description: "Headings ({level,text})."
10832
+ },
10833
+ wordCount: { type: "integer", description: "Word count." },
10834
+ fullText: { type: "string", description: "Full page text." },
10835
+ internalLinks: {
10836
+ type: "array",
10837
+ items: { type: "object" },
10838
+ description: "Internal links ({url,text})."
10839
+ },
10840
+ externalLinks: { type: "array", items: { type: "object" } },
10841
+ language: { type: "string", description: "Language code." },
10842
+ ticketNumber: { type: "string", description: "Related ticket number." },
10843
+ metadata: { type: "object" },
10844
+ reExtract: {
10845
+ type: "boolean",
10846
+ description: "Force a re-fetch even when fields are supplied."
10847
+ }
10848
+ },
10849
+ required: ["projectKey", "url"]
10850
+ }
10851
+ },
10852
+ {
10853
+ name: "list_project_content_corpus",
10854
+ description: "List corpus items for a project (2026-DASMG-041), filter by domain/sourceType/query. Compact, paginated. Use to see content coverage and reuse own pages.",
10855
+ inputSchema: {
10856
+ type: "object",
10857
+ properties: {
10858
+ projectKey: { type: "string", description: "Project identifier (required)." },
10859
+ domain: { type: "string", description: "Domain filter." },
10860
+ sourceType: {
10861
+ type: "string",
10862
+ enum: [
10863
+ "own_site",
10864
+ "client_site",
10865
+ "external_source",
10866
+ "competitor",
10867
+ "news",
10868
+ "government",
10869
+ "supplier",
10870
+ "unknown"
10871
+ ]
10872
+ },
10873
+ query: { type: "string", description: "Free-text search." },
10874
+ limit: { type: "integer", description: "Max rows (default 20)." },
10875
+ offset: { type: "integer", description: "Pagination offset." },
10876
+ includeFullText: { type: "boolean", description: "Return full_text per row." }
10877
+ },
10878
+ required: ["projectKey"]
10879
+ }
10880
+ },
10881
+ {
10882
+ name: "get_content_context_for_project",
10883
+ description: "Aggregate context for a content agent (2026-DASMG-041): recent corpus items, recent evidence packs and related sources for a project (optionally focused by topic or url), plus totals. One call to brief an agent on what already exists before it writes.",
10884
+ inputSchema: {
10885
+ type: "object",
10886
+ properties: {
10887
+ projectKey: { type: "string", description: "Project identifier (required)." },
10888
+ topic: { type: "string", description: "Optional topic focus." },
10889
+ url: { type: "string", description: "Optional url focus." },
10890
+ limit: { type: "integer", description: "Max items per section (default 10)." }
10891
+ },
10892
+ required: ["projectKey"]
10893
+ }
10894
+ },
10895
+ {
10896
+ name: "save_content_quality_run",
10897
+ description: "Store a reference to an MG SEO CLI / audit run (2026-DASMG-041): score, flags and report, attached to a corpus item, source or url. MG Dashboard stores the run; the CLI does the scoring (the CLI stays the QC layer).",
10898
+ inputSchema: {
10899
+ type: "object",
10900
+ properties: {
10901
+ tool: { type: "string", description: "Tool name, e.g. mg-seo (required)." },
10902
+ corpusItemId: { type: "string", description: "content_corpus_items.id." },
10903
+ contentSourceId: { type: "string", description: "content_sources.id." },
10904
+ projectKey: { type: "string", description: "Project identifier." },
10905
+ siteKey: { type: "string", description: "Site identifier." },
10906
+ url: { type: "string", description: "Audited URL." },
10907
+ canonicalUrl: { type: "string", description: "Canonical URL." },
10908
+ toolVersion: { type: "string", description: "Tool version." },
10909
+ score: { type: "integer", description: "Overall score 0-100." },
10910
+ flags: {
10911
+ type: "array",
10912
+ items: { type: "object" },
10913
+ description: "Flags ({code,severity,message})."
10914
+ },
10915
+ report: { type: "object", description: "Full report payload." },
10916
+ reportUrl: { type: "string", description: "Link to the full report." },
10917
+ ticketNumber: { type: "string", description: "Related ticket number." },
10918
+ runAt: { type: "string", description: "Run timestamp (ISO)." }
10919
+ },
10920
+ required: ["tool"]
10921
+ }
10922
+ },
10923
+ {
10924
+ name: "list_content_snapshots",
10925
+ description: "List historical snapshots for a corpus item or source (2026-DASMG-041) by id, url or canonical url, newest first. Compact; set `includeFullText` for the captured body. Use to see how content changed over time.",
10926
+ inputSchema: {
10927
+ type: "object",
10928
+ properties: {
10929
+ corpusItemId: { type: "string", description: "content_corpus_items.id." },
10930
+ contentSourceId: { type: "string", description: "content_sources.id." },
10931
+ url: { type: "string", description: "URL to match." },
10932
+ canonicalUrl: { type: "string", description: "Canonical URL to match." },
10933
+ limit: { type: "integer", description: "Max rows (default 20)." },
10934
+ includeFullText: { type: "boolean", description: "Return captured full_text." }
10935
+ }
10936
+ }
10937
+ },
10938
+ {
10939
+ name: "link_content_pack_to_ticket",
10940
+ description: "Attach a stored evidence pack to a ticket number (2026-DASMG-041).",
10941
+ inputSchema: {
10942
+ type: "object",
10943
+ properties: {
10944
+ packId: { type: "string", description: "content_evidence_packs.id (uuid)." },
10945
+ ticketNumber: { type: "string", description: "Ticket number." }
10946
+ },
10947
+ required: ["packId", "ticketNumber"]
10948
+ }
10949
+ },
10950
+ {
10951
+ name: "prune_content_snapshots",
10952
+ description: "Retention cleanup (2026-DASMG-041): keep the newest N snapshots per item and drop the rest older than the retention window. Defaults: keepPerItem 20, olderThanDays 365. Returns how many were pruned.",
10953
+ inputSchema: {
10954
+ type: "object",
10955
+ properties: {
10956
+ keepPerItem: { type: "integer", description: "Snapshots to keep per item (default 20)." },
10957
+ olderThanDays: {
10958
+ type: "integer",
10959
+ description: "Only prune snapshots older than this many days (default 365)."
10960
+ }
10961
+ }
10962
+ }
10963
+ },
10423
10964
  {
10424
10965
  name: "search-team-memory",
10425
10966
  description: "Search the team's ENTIRE past Cursor history (every developer, every project) for how something was handled before. Use this FIRST, before investigating from scratch, whenever you: hit a non-trivial bug or error, are about to build something that may have been done before, need a project-specific convention/gotcha, or the user asks 'have we done X', 'how did we fix Y', or 'did we solve this already'. Hybrid semantic + keyword search over mirrored conversations. Returns ranked past chats with repo, date, a solution snippet and a similarity score. Phrase the query as the problem in natural language (e.g. 'release pipeline PM2 deploy fails with module not found').",
@@ -10723,6 +11264,47 @@ async function executeToolCall(name, a, _serverId) {
10723
11264
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
10724
11265
  };
10725
11266
  }
11267
+ // ----- Content registry / corpus persistence (2026-DASMG-041) -----
11268
+ // All content-registry tools proxy to one dispatcher route; the tool name
11269
+ // is the dispatcher `action` and the args are forwarded verbatim as
11270
+ // `params` (the route validates them with the matching Zod schema).
11271
+ case "save_content_source":
11272
+ case "get_content_source_by_url":
11273
+ case "get_content_source_by_id":
11274
+ case "search_content_sources":
11275
+ case "save_research_pack":
11276
+ case "get_research_pack":
11277
+ case "save_content_corpus_item":
11278
+ case "list_project_content_corpus":
11279
+ case "get_content_context_for_project":
11280
+ case "save_content_quality_run":
11281
+ case "list_content_snapshots":
11282
+ case "link_content_pack_to_ticket":
11283
+ case "prune_content_snapshots": {
11284
+ const res = await fetch(`${dashboardBaseUrl}/api/tools/content-registry`, {
11285
+ method: "POST",
11286
+ headers: {
11287
+ "content-type": "application/json",
11288
+ authorization: `Bearer ${apiKey}`
11289
+ },
11290
+ body: JSON.stringify({ action: name, params: a })
11291
+ });
11292
+ if (!res.ok) {
11293
+ const detail = await res.text().catch(() => "");
11294
+ return {
11295
+ content: [
11296
+ {
11297
+ type: "text",
11298
+ text: `Error: ${name} failed (${res.status}). ${detail.slice(0, 300)}`
11299
+ }
11300
+ ]
11301
+ };
11302
+ }
11303
+ const data = await res.json();
11304
+ return {
11305
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
11306
+ };
11307
+ }
10726
11308
  // ----- Team memory -----
10727
11309
  case "search-team-memory": {
10728
11310
  const query = typeof a.query === "string" ? a.query.trim() : "";
@@ -10764,7 +11346,7 @@ async function executeToolCall(name, a, _serverId) {
10764
11346
  ]
10765
11347
  };
10766
11348
  }
10767
- const lines = data.hits.map((hit, index5) => {
11349
+ const lines = data.hits.map((hit, index6) => {
10768
11350
  const when = hit.lastMessageAt ? new Date(hit.lastMessageAt).toISOString().slice(0, 10) : "unknown date";
10769
11351
  const repo = hit.repo ?? "unknown repo";
10770
11352
  const sim = hit.similarity !== null ? ` \xB7 ${(hit.similarity * 100).toFixed(0)}% match` : "";
@@ -10774,7 +11356,7 @@ async function executeToolCall(name, a, _serverId) {
10774
11356
  ...Array.isArray(hit.tech) ? hit.tech.slice(0, 4) : []
10775
11357
  ].filter(Boolean);
10776
11358
  const tags = facets.length > 0 ? ` \xB7 ${facets.join(", ")}` : "";
10777
- return `${index5 + 1}. [${repo} \xB7 ${when}${sim}${tags}] ${hit.title}
11359
+ return `${index6 + 1}. [${repo} \xB7 ${when}${sim}${tags}] ${hit.title}
10778
11360
  id: ${hit.id}
10779
11361
  ${(hit.summary ?? hit.snippet).replace(/\s+/g, " ").slice(0, 500)}`;
10780
11362
  });
@@ -11959,8 +12541,8 @@ ${sample.join("\n")}${files.length > 5 ? `
11959
12541
  };
11960
12542
  const filtered = sortRows(applyFilter(only.rows));
11961
12543
  if (format === "json") {
11962
- const text8 = filtered.map((c) => JSON.stringify(c)).join("\n") || "(no containers)";
11963
- return { content: [{ type: "text", text: text8 }] };
12544
+ const text9 = filtered.map((c) => JSON.stringify(c)).join("\n") || "(no containers)";
12545
+ return { content: [{ type: "text", text: text9 }] };
11964
12546
  }
11965
12547
  if (groupByProject) {
11966
12548
  const groups = /* @__PURE__ */ new Map();
@@ -11987,8 +12569,8 @@ ${sample.join("\n")}${files.length > 5 ? `
11987
12569
  }
11988
12570
  const header = `${"NAMES".padEnd(36)} ${"IMAGE".padEnd(40)} ${"STATUS".padEnd(24)} ${"HEALTH".padEnd(10)} ${"PORTS".padEnd(40)} PROJECT`;
11989
12571
  const body = filtered.map((r) => `${fmtRow(r)} ${r.Project || ""}`);
11990
- const text7 = filtered.length === 0 ? "(no containers match)" : [header, ...body].join("\n");
11991
- return { content: [{ type: "text", text: text7 }] };
12572
+ const text8 = filtered.length === 0 ? "(no containers match)" : [header, ...body].join("\n");
12573
+ return { content: [{ type: "text", text: text8 }] };
11992
12574
  }
11993
12575
  if (format === "json") {
11994
12576
  const lines = [];