iso27001-mcp 0.8.4 → 0.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +302 -16
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Turn Claude into an ISO 27001 compliance assistant** — controls, risk register, policies, evidence tracking, SoA generation, and full audit workflows in one local encrypted MCP server.
4
4
 
5
- [![Socket Badge](https://badge.socket.dev/npm/package/iso27001-mcp/0.8.4)](https://socket.dev/npm/package/iso27001-mcp/overview/0.8.4)
5
+ [![Socket Badge](https://badge.socket.dev/npm/package/iso27001-mcp/0.8.5)](https://socket.dev/npm/package/iso27001-mcp/overview/0.8.5)
6
6
  [![npm version](https://img.shields.io/npm/v/iso27001-mcp.svg)](https://npmjs.com/package/iso27001-mcp)
7
7
  [![npm downloads](https://img.shields.io/npm/dt/iso27001-mcp.svg)](https://npmjs.com/package/iso27001-mcp)
8
8
  [![CI](https://github.com/Sushegaad/MCP-Server-for-ISO27001/actions/workflows/ci.yml/badge.svg)](https://github.com/Sushegaad/MCP-Server-for-ISO27001/actions/workflows/ci.yml)
package/dist/index.js CHANGED
@@ -24797,7 +24797,7 @@ var require_package = __commonJS({
24797
24797
  "package.json"(exports2, module2) {
24798
24798
  module2.exports = {
24799
24799
  name: "iso27001-mcp",
24800
- version: "0.8.4",
24800
+ version: "0.8.5",
24801
24801
  mcpName: "io.github.Sushegaad/iso27001-mcp",
24802
24802
  description: "ISO 27001 compliance workspace for Claude \u2014 controls, risks, policies, evidence, audits, and SoA in one local encrypted MCP server",
24803
24803
  license: "MIT",
@@ -26260,13 +26260,24 @@ var MIGRATION_0006 = `-- =======================================================
26260
26260
  -- without a default; prev_hash is nullable (NULL = first in chain).
26261
26261
  ALTER TABLE audit_log ADD COLUMN prev_hash TEXT;
26262
26262
  `;
26263
+ var MIGRATION_0007 = `-- ============================================================
26264
+ -- iso27001-mcp Migration 0007 \u2014 Organisation Profile Branding
26265
+ -- Adds optional branding and document personalisation fields.
26266
+ -- !! Never edit this file after it has been applied !!
26267
+ -- ============================================================
26268
+ ALTER TABLE organization_profile ADD COLUMN logo_url TEXT;
26269
+ ALTER TABLE organization_profile ADD COLUMN primary_color TEXT;
26270
+ ALTER TABLE organization_profile ADD COLUMN document_footer TEXT;
26271
+ ALTER TABLE organization_profile ADD COLUMN certification_body TEXT;
26272
+ `;
26263
26273
  var MIGRATIONS = [
26264
26274
  { filename: "0001_initial.sql", sql: MIGRATION_0001 },
26265
26275
  { filename: "0002_fts_index.sql", sql: MIGRATION_0002 },
26266
26276
  { filename: "0003_org_profile_procedures.sql", sql: MIGRATION_0003 },
26267
26277
  { filename: "0004_management_review_improvement.sql", sql: MIGRATION_0004 },
26268
26278
  { filename: "0005_evidence_documents.sql", sql: MIGRATION_0005 },
26269
- { filename: "0006_audit_log_hmac.sql", sql: MIGRATION_0006 }
26279
+ { filename: "0006_audit_log_hmac.sql", sql: MIGRATION_0006 },
26280
+ { filename: "0007_org_profile_branding.sql", sql: MIGRATION_0007 }
26270
26281
  ];
26271
26282
 
26272
26283
  // src/security/secrets.ts
@@ -33675,8 +33686,6 @@ var paginationLimit = import_zod.z.number().int().min(1).max(100).optional().def
33675
33686
  var paginationOffset = import_zod.z.number().int().min(0).optional().default(0);
33676
33687
  var versionEnum = import_zod.z.enum(["2022", "2013"]);
33677
33688
  var formatMarkdownCsvJson = import_zod.z.enum(["markdown", "csv", "json"]);
33678
- var formatMarkdownCsv = import_zod.z.enum(["markdown", "csv"]);
33679
- var formatMarkdownJson = import_zod.z.enum(["markdown", "json"]);
33680
33689
  var riskLevelEnum = import_zod.z.enum(["Low", "Medium", "High", "Critical"]);
33681
33690
  var likelihood1to5 = import_zod.z.number().int().min(1).max(5);
33682
33691
  var roleEnum = import_zod.z.enum(["viewer", "analyst", "admin"]);
@@ -33935,7 +33944,7 @@ var UpdateSoaEntrySchema = import_zod.z.object({
33935
33944
  });
33936
33945
  var ExportSoaSchema = import_zod.z.object({
33937
33946
  soa_id: uuid,
33938
- format: formatMarkdownCsv
33947
+ format: import_zod.z.enum(["markdown", "csv", "html"])
33939
33948
  });
33940
33949
  var CreateAuditSchema = import_zod.z.object({
33941
33950
  name: shortText(200),
@@ -33972,7 +33981,7 @@ var UpdateCorrectiveActionSchema = import_zod.z.object({
33972
33981
  });
33973
33982
  var GenerateAuditReportSchema = import_zod.z.object({
33974
33983
  audit_id: uuid,
33975
- format: formatMarkdownJson
33984
+ format: import_zod.z.enum(["markdown", "json", "html"])
33976
33985
  });
33977
33986
  var RegisterEvidenceSchema = import_zod.z.object({
33978
33987
  control_id: import_zod.z.string().min(1).max(20),
@@ -34037,7 +34046,11 @@ var SetOrganizationProfileSchema = import_zod.z.object({
34037
34046
  isms_manager: shortText(200).optional(),
34038
34047
  internal_auditor: shortText(200).optional()
34039
34048
  }).optional(),
34040
- review_cadence_months: import_zod.z.number().int().min(1).max(36).optional().default(12)
34049
+ review_cadence_months: import_zod.z.number().int().min(1).max(36).optional().default(12),
34050
+ logo_url: import_zod.z.string().url().max(2e3).optional(),
34051
+ primary_color: import_zod.z.string().regex(/^#[0-9a-fA-F]{6}$/, "must be 6-digit hex e.g. #1e3a5f").optional(),
34052
+ document_footer: import_zod.z.string().max(500).optional(),
34053
+ certification_body: import_zod.z.string().max(200).optional()
34041
34054
  });
34042
34055
  var GetOrganizationProfileSchema = import_zod.z.object({});
34043
34056
  var procedureTypeEnum = import_zod.z.enum([
@@ -34091,7 +34104,7 @@ var ListProceduresSchema = import_zod.z.object({
34091
34104
  });
34092
34105
  var ExportProcedureSchema = import_zod.z.object({
34093
34106
  procedure_id: uuid,
34094
- format: formatMarkdownJson
34107
+ format: import_zod.z.enum(["markdown", "json", "html"])
34095
34108
  });
34096
34109
  var reviewStatusEnum = import_zod.z.enum(["planned", "in_progress", "completed"]);
34097
34110
  var reviewInputCategoryEnum = import_zod.z.enum([
@@ -35416,6 +35429,171 @@ function stripFrontmatter(raw) {
35416
35429
  }
35417
35430
  return { template, clauseMappings, controlMappings };
35418
35431
  }
35432
+ function markdownToHtml(md) {
35433
+ const lines = md.split("\n");
35434
+ const out = [];
35435
+ let inTable = false;
35436
+ let inList = false;
35437
+ let tableHeaderDone = false;
35438
+ const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
35439
+ const inline = (s) => s.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>").replace(/`([^`]+)`/g, "<code>$1</code>");
35440
+ for (const line of lines) {
35441
+ const h1 = line.match(/^# (.+)/);
35442
+ const h2 = line.match(/^## (.+)/);
35443
+ const h3 = line.match(/^### (.+)/);
35444
+ if (h1 || h2 || h3) {
35445
+ if (inList) {
35446
+ out.push("</ul>");
35447
+ inList = false;
35448
+ }
35449
+ if (inTable) {
35450
+ out.push("</tbody></table>");
35451
+ inTable = false;
35452
+ tableHeaderDone = false;
35453
+ }
35454
+ if (h1) {
35455
+ out.push(`<h1>${inline(esc(h1[1]))}</h1>`);
35456
+ continue;
35457
+ }
35458
+ if (h2) {
35459
+ out.push(`<h2>${inline(esc(h2[1]))}</h2>`);
35460
+ continue;
35461
+ }
35462
+ if (h3) {
35463
+ out.push(`<h3>${inline(esc(h3[1]))}</h3>`);
35464
+ continue;
35465
+ }
35466
+ }
35467
+ if (/^---+$/.test(line.trim())) {
35468
+ if (inList) {
35469
+ out.push("</ul>");
35470
+ inList = false;
35471
+ }
35472
+ if (inTable) {
35473
+ out.push("</tbody></table>");
35474
+ inTable = false;
35475
+ tableHeaderDone = false;
35476
+ }
35477
+ out.push("<hr>");
35478
+ continue;
35479
+ }
35480
+ if (line.trim().startsWith("|")) {
35481
+ if (inList) {
35482
+ out.push("</ul>");
35483
+ inList = false;
35484
+ }
35485
+ if (/^\|[-| :]+\|$/.test(line.trim())) {
35486
+ if (!tableHeaderDone) {
35487
+ out.push("<tbody>");
35488
+ tableHeaderDone = true;
35489
+ }
35490
+ continue;
35491
+ }
35492
+ const cells = line.split("|").slice(1, -1).map((c) => c.trim());
35493
+ if (!inTable) {
35494
+ out.push("<table><thead><tr>");
35495
+ cells.forEach((c) => out.push(`<th>${inline(esc(c))}</th>`));
35496
+ out.push("</tr></thead>");
35497
+ inTable = true;
35498
+ tableHeaderDone = false;
35499
+ continue;
35500
+ }
35501
+ out.push("<tr>");
35502
+ cells.forEach((c) => out.push(`<td>${inline(esc(c))}</td>`));
35503
+ out.push("</tr>");
35504
+ continue;
35505
+ }
35506
+ if (inTable) {
35507
+ out.push("</tbody></table>");
35508
+ inTable = false;
35509
+ tableHeaderDone = false;
35510
+ }
35511
+ if (/^[-*] /.test(line)) {
35512
+ if (!inList) {
35513
+ out.push("<ul>");
35514
+ inList = true;
35515
+ }
35516
+ out.push(`<li>${inline(esc(line.replace(/^[-*] /, "")))}</li>`);
35517
+ continue;
35518
+ }
35519
+ if (inList && line.trim() === "") {
35520
+ out.push("</ul>");
35521
+ inList = false;
35522
+ }
35523
+ if (line.trim() === "") {
35524
+ out.push("");
35525
+ continue;
35526
+ }
35527
+ if (!inList) out.push(`<p>${inline(esc(line))}</p>`);
35528
+ }
35529
+ if (inList) out.push("</ul>");
35530
+ if (inTable) out.push("</tbody></table>");
35531
+ return out.join("\n");
35532
+ }
35533
+ function renderHtmlDocument(bodyHtml, meta) {
35534
+ const color = meta.primary_color ?? "#1e3a5f";
35535
+ const footer = meta.document_footer ?? meta.organisation_name;
35536
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
35537
+ return `<!DOCTYPE html>
35538
+ <html lang="en">
35539
+ <head>
35540
+ <meta charset="UTF-8">
35541
+ <meta name="viewport" content="width=device-width,initial-scale=1.0">
35542
+ <title>${meta.title} \u2014 ${meta.organisation_name}</title>
35543
+ <style>
35544
+ *,*::before,*::after{box-sizing:border-box}
35545
+ body{font-family:'Segoe UI',Arial,sans-serif;font-size:11pt;line-height:1.6;color:#1a1a1a;margin:0;padding:0;background:#fff}
35546
+ .doc-header{background:${color};color:#fff;padding:24px 40px 18px;display:flex;align-items:center;gap:20px}
35547
+ .doc-header-logo{max-height:48px;max-width:160px;object-fit:contain}
35548
+ .doc-header-text{flex:1}
35549
+ .doc-header h1{margin:0 0 4px;font-size:18pt;font-weight:700;color:#fff}
35550
+ .doc-header-sub{font-size:10pt;opacity:0.85}
35551
+ .doc-meta-bar{background:#f5f7fa;border-bottom:2px solid ${color};padding:10px 40px;display:flex;gap:32px;font-size:9pt;color:#444}
35552
+ .doc-meta-item strong{color:#111}
35553
+ .doc-body{padding:28px 40px;max-width:900px}
35554
+ h1{font-size:16pt;color:${color};margin-top:24px;border-bottom:2px solid ${color};padding-bottom:4px}
35555
+ h2{font-size:13pt;color:${color};margin-top:20px}
35556
+ h3{font-size:11pt;color:#333;margin-top:16px}
35557
+ table{width:100%;border-collapse:collapse;margin:14px 0;font-size:10pt}
35558
+ th{background:${color};color:#fff;padding:7px 10px;text-align:left;font-weight:600}
35559
+ td{padding:6px 10px;border-bottom:1px solid #e0e4ea;vertical-align:top}
35560
+ tr:nth-child(even) td{background:#f8f9fb}
35561
+ ul{padding-left:20px;margin:8px 0}
35562
+ li{margin:3px 0}
35563
+ hr{border:none;border-top:1px solid #dce1ea;margin:20px 0}
35564
+ p{margin:8px 0}
35565
+ code{background:#f0f2f5;padding:1px 5px;border-radius:3px;font-family:monospace;font-size:10pt}
35566
+ .doc-footer{margin-top:40px;padding:14px 40px;background:#f5f7fa;border-top:2px solid ${color};font-size:9pt;color:#666;display:flex;justify-content:space-between}
35567
+ @media print{
35568
+ body{font-size:10pt}
35569
+ .doc-header,.doc-meta-bar,th,.doc-footer{-webkit-print-color-adjust:exact;print-color-adjust:exact}
35570
+ h1,h2{page-break-after:avoid}
35571
+ table{page-break-inside:avoid}
35572
+ }
35573
+ </style>
35574
+ </head>
35575
+ <body>
35576
+ <div class="doc-header">
35577
+ ${meta.logo_url ? `<img class="doc-header-logo" src="${meta.logo_url}" alt="${meta.organisation_name} logo">` : ""}
35578
+ <div class="doc-header-text">
35579
+ <h1>${meta.title}</h1>
35580
+ <div class="doc-header-sub">${meta.organisation_name}${meta.doc_type ? ` \xB7 ${meta.doc_type}` : ""}</div>
35581
+ </div>
35582
+ </div>
35583
+ <div class="doc-meta-bar">
35584
+ ${meta.version ? `<span><strong>Version:</strong> ${meta.version}</span>` : ""}
35585
+ ${meta.effective_date ? `<span><strong>Effective:</strong> ${meta.effective_date}</span>` : ""}
35586
+ ${meta.owner ? `<span><strong>Owner:</strong> ${meta.owner}</span>` : ""}
35587
+ <span><strong>Generated:</strong> ${today}</span>
35588
+ </div>
35589
+ <div class="doc-body">${bodyHtml}</div>
35590
+ <div class="doc-footer">
35591
+ <span>${footer}</span>
35592
+ <span>INTERNAL \u2014 ISMS Controlled Document \xB7 Generated ${today}</span>
35593
+ </div>
35594
+ </body>
35595
+ </html>`;
35596
+ }
35419
35597
 
35420
35598
  // src/tools/org-profile.ts
35421
35599
  var ORG_PROFILE_ID = "00000000-0000-4000-8000-000000000001";
@@ -35423,11 +35601,15 @@ function ok4(data) {
35423
35601
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], isError: false };
35424
35602
  }
35425
35603
  function loadOrgProfileDefaults(db) {
35426
- const row = db.prepare("SELECT legal_entity_name, isms_scope_statement FROM organization_profile WHERE id = ?").get(ORG_PROFILE_ID);
35604
+ const row = db.prepare("SELECT legal_entity_name, isms_scope_statement, logo_url, primary_color, document_footer, certification_body FROM organization_profile WHERE id = ?").get(ORG_PROFILE_ID);
35427
35605
  if (!row) return null;
35428
35606
  return {
35429
35607
  organisation_name: row.legal_entity_name,
35430
- scope: row.isms_scope_statement
35608
+ scope: row.isms_scope_statement,
35609
+ logo_url: row.logo_url,
35610
+ primary_color: row.primary_color,
35611
+ document_footer: row.document_footer,
35612
+ certification_body: row.certification_body
35431
35613
  };
35432
35614
  }
35433
35615
  function handleSetOrganizationProfile(args2) {
@@ -35439,17 +35621,22 @@ function handleSetOrganizationProfile(args2) {
35439
35621
  isms_scope_statement,
35440
35622
  declared_exclusions,
35441
35623
  raci_roles,
35442
- review_cadence_months = 12
35624
+ review_cadence_months = 12,
35625
+ logo_url,
35626
+ primary_color,
35627
+ document_footer,
35628
+ certification_body
35443
35629
  } = args2;
35444
35630
  const ts = now();
35445
35631
  getDb().prepare(`
35446
35632
  INSERT OR REPLACE INTO organization_profile
35447
35633
  (id, legal_entity_name, registered_jurisdiction, regulatory_licences,
35448
35634
  in_scope_activities, isms_scope_statement, declared_exclusions,
35449
- raci_roles, review_cadence_months, created_at, updated_at)
35635
+ raci_roles, review_cadence_months, created_at, updated_at,
35636
+ logo_url, primary_color, document_footer, certification_body)
35450
35637
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE(
35451
35638
  (SELECT created_at FROM organization_profile WHERE id = ?), ?
35452
- ), ?)
35639
+ ), ?, ?, ?, ?, ?)
35453
35640
  `).run(
35454
35641
  ORG_PROFILE_ID,
35455
35642
  legal_entity_name,
@@ -35462,7 +35649,11 @@ function handleSetOrganizationProfile(args2) {
35462
35649
  review_cadence_months,
35463
35650
  ORG_PROFILE_ID,
35464
35651
  ts,
35465
- ts
35652
+ ts,
35653
+ logo_url ?? null,
35654
+ primary_color ?? null,
35655
+ document_footer ?? null,
35656
+ certification_body ?? null
35466
35657
  );
35467
35658
  return ok4({
35468
35659
  id: ORG_PROFILE_ID,
@@ -35830,6 +36021,44 @@ function handleExportSoa(args2) {
35830
36021
  `).all(soa.isms_version, soa_id);
35831
36022
  const includedCount = entries.filter((e) => e.included === 1).length;
35832
36023
  const excludedCount = entries.filter((e) => e.included === 0).length;
36024
+ const ORG_PROFILE_ID_SOA = "00000000-0000-4000-8000-000000000001";
36025
+ if (format === "html") {
36026
+ const profileRow = db.prepare(
36027
+ "SELECT legal_entity_name, logo_url, primary_color, document_footer FROM organization_profile WHERE id = ?"
36028
+ ).get(ORG_PROFILE_ID_SOA);
36029
+ const tableRows = entries.map((e) => {
36030
+ const inc = e.included === 1;
36031
+ return `<tr>
36032
+ <td><strong>${e.control_id}</strong></td>
36033
+ <td>${e.control_name ?? "\u2014"}</td>
36034
+ <td>${e.theme ?? "\u2014"}</td>
36035
+ <td style="color:${inc ? "#065f46" : "#991b1b"};font-weight:600">${inc ? "\u2713 Yes" : "\u2717 No"}</td>
36036
+ <td>${e.justification}</td>
36037
+ <td>${e.status ?? "\u2014"}</td>
36038
+ <td>${e.responsible_party ?? "\u2014"}</td>
36039
+ </tr>`;
36040
+ }).join("\n");
36041
+ const bodyHtml = `
36042
+ <p><strong>ISMS Version:</strong> ISO 27001:${soa.isms_version} &nbsp;\xB7&nbsp;
36043
+ <strong>Total:</strong> ${entries.length} &nbsp;\xB7&nbsp;
36044
+ <strong>Included:</strong> ${includedCount} &nbsp;\xB7&nbsp;
36045
+ <strong>Excluded:</strong> ${excludedCount}</p>
36046
+ <table>
36047
+ <thead>
36048
+ <tr><th>Control ID</th><th>Name</th><th>Theme</th><th>Included</th><th>Justification</th><th>Status</th><th>Responsible Party</th></tr>
36049
+ </thead>
36050
+ <tbody>${tableRows}</tbody>
36051
+ </table>`;
36052
+ const html = renderHtmlDocument(bodyHtml, {
36053
+ title: "Statement of Applicability",
36054
+ organisation_name: profileRow?.legal_entity_name ?? "Organisation",
36055
+ logo_url: profileRow?.logo_url,
36056
+ primary_color: profileRow?.primary_color,
36057
+ document_footer: profileRow?.document_footer,
36058
+ doc_type: `ISO 27001:${soa.isms_version}`
36059
+ });
36060
+ return ok6({ format: "html", content: html });
36061
+ }
35833
36062
  if (format === "csv") {
35834
36063
  const header = "control_id,name,theme,included,justification,status,responsible_party";
35835
36064
  const rows = entries.map(
@@ -36063,6 +36292,46 @@ function handleGenerateAuditReport(args2) {
36063
36292
  carsByFinding.set(car.finding_id, existing);
36064
36293
  }
36065
36294
  const countByType = (type) => findings.filter((f) => f.type === type).length;
36295
+ if (args2.format === "html") {
36296
+ const mdLines = [
36297
+ `# ${audit.name}`,
36298
+ ``,
36299
+ `**Auditor:** ${audit.auditor} `,
36300
+ `**Planned Date:** ${audit.planned_date} `,
36301
+ `**Status:** ${audit.status} `,
36302
+ `**Scope:** ${audit.scope}`,
36303
+ ``,
36304
+ `## Findings (${findings.length})`,
36305
+ ``,
36306
+ `| ID | Type | Severity | Clause/Control | Description |`,
36307
+ `|---|---|---|---|---|`,
36308
+ ...findings.map(
36309
+ (f) => `| ${f.id.slice(-8)} | ${f.type.toUpperCase()} | ${f.severity ?? "\u2014"} | ${f.clause_or_control} | ${f.description.slice(0, 80)} |`
36310
+ ),
36311
+ ``,
36312
+ `## Corrective Actions (${cars.length})`,
36313
+ ``,
36314
+ `| CAR ID | Finding | Owner | Due Date | Status | Verified |`,
36315
+ `|---|---|---|---|---|---|`,
36316
+ ...cars.map(
36317
+ (c) => `| ${c.id.slice(-8)} | ${c.finding_id.slice(-8)} | ${c.owner} | ${c.due_date} | ${c.status} | ${c.effectiveness_verified ? "\u2713 Yes" : "No"} |`
36318
+ )
36319
+ ];
36320
+ const db2 = getDb();
36321
+ const ORG_PROFILE_ID_AUDIT = "00000000-0000-4000-8000-000000000001";
36322
+ const profileRow = db2.prepare(
36323
+ "SELECT legal_entity_name, logo_url, primary_color, document_footer FROM organization_profile WHERE id = ?"
36324
+ ).get(ORG_PROFILE_ID_AUDIT);
36325
+ const html = renderHtmlDocument(markdownToHtml(mdLines.join("\n")), {
36326
+ title: audit.name,
36327
+ organisation_name: profileRow?.legal_entity_name ?? "Organisation",
36328
+ logo_url: profileRow?.logo_url,
36329
+ primary_color: profileRow?.primary_color,
36330
+ document_footer: profileRow?.document_footer,
36331
+ doc_type: "Internal Audit Report"
36332
+ });
36333
+ return ok7({ format: "html", content: html });
36334
+ }
36066
36335
  if (format === "json") {
36067
36336
  return ok7({
36068
36337
  audit: shapeAudit(audit),
@@ -36658,6 +36927,23 @@ function handleExportProcedure(args2) {
36658
36927
  const db = getDb();
36659
36928
  const row = db.prepare("SELECT * FROM procedures WHERE id = ?").get(procedure_id);
36660
36929
  if (!row) throw notFound("procedure", procedure_id);
36930
+ if (format === "html") {
36931
+ const db2 = getDb();
36932
+ const defaults = loadOrgProfileDefaults(db2);
36933
+ const bodyHtml = markdownToHtml(row.content);
36934
+ const html = renderHtmlDocument(bodyHtml, {
36935
+ title: row.procedure_type.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()) + " Procedure",
36936
+ organisation_name: row.organisation_name,
36937
+ logo_url: defaults?.logo_url,
36938
+ primary_color: defaults?.primary_color,
36939
+ document_footer: defaults?.document_footer,
36940
+ version: String(row.version),
36941
+ effective_date: row.effective_date,
36942
+ owner: row.owner,
36943
+ doc_type: "Procedure"
36944
+ });
36945
+ return ok9({ format: "html", content: html });
36946
+ }
36661
36947
  if (format === "markdown") {
36662
36948
  const relatedControls = fromJsonArray(row.related_controls);
36663
36949
  const controlsSection = relatedControls.length > 0 ? `
@@ -38627,12 +38913,12 @@ ${divider}
38627
38913
  } else {
38628
38914
  try {
38629
38915
  const rows = openDb(dbPath2).prepare("SELECT filename FROM _migrations ORDER BY id").all();
38630
- const passed2 = rows.length >= 6;
38916
+ const passed2 = rows.length >= 7;
38631
38917
  check(
38632
38918
  "Migrations",
38633
38919
  passed2,
38634
38920
  false,
38635
- `${rows.length}/6 applied${passed2 ? "" : " \u2014 DB may need re-initialisation"}`
38921
+ `${rows.length}/7 applied${passed2 ? "" : " \u2014 DB may need re-initialisation"}`
38636
38922
  );
38637
38923
  record(passed2);
38638
38924
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iso27001-mcp",
3
- "version": "0.8.4",
3
+ "version": "0.8.5",
4
4
  "mcpName": "io.github.Sushegaad/iso27001-mcp",
5
5
  "description": "ISO 27001 compliance workspace for Claude — controls, risks, policies, evidence, audits, and SoA in one local encrypted MCP server",
6
6
  "license": "MIT",