honeybook-mcp 1.3.1 → 1.4.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.
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "HoneyBook client-portal tools for Claude Code",
10
- "version": "1.3.1"
10
+ "version": "1.4.0"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -15,7 +15,7 @@
15
15
  "displayName": "HoneyBook",
16
16
  "source": "./",
17
17
  "description": "HoneyBook client-portal MCP for Claude — view wedding-vendor contracts and invoices via MCP",
18
- "version": "1.3.1",
18
+ "version": "1.4.0",
19
19
  "author": {
20
20
  "name": "Chris Chall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "honeybook",
3
3
  "displayName": "HoneyBook",
4
- "version": "1.3.1",
4
+ "version": "1.4.0",
5
5
  "description": "HoneyBook client-portal MCP for Claude — view wedding-vendor contracts and invoices via MCP",
6
6
  "author": {
7
7
  "name": "Chris Chall",
package/README.md CHANGED
@@ -140,8 +140,8 @@ Tools that touch a vendor accept an optional `origin` argument (e.g. `https://ac
140
140
  | `list_projects` | Your projects with a vendor + their workspace ids | Auto |
141
141
  | `get_project` | Project details: date, location, people, custom fields | Auto |
142
142
  | `list_messages` | Messages (or the activity log) in a workspace, newest first | Auto |
143
- | `get_message` | One message in full: body, attachments, delivery status | Auto |
144
- | `send_message` | Send or reply through the portal (after you confirm) | Confirm |
143
+ | `get_message` | One message in full: body (text, Markdown or HTML), attachments, delivery status | Auto |
144
+ | `send_message` | Send or reply through the portal, in text, Markdown or HTML (after you confirm) | Confirm |
145
145
  | `mark_messages_seen` | Mark feed items seen (reads never do this on their own) | Auto |
146
146
  | `list_meetings` | Scheduled meetings with join links, latest time wins | Auto |
147
147
  | `list_tasks` | Tasks the vendor assigned you, with counts | Auto |
package/dist/bundle.js CHANGED
@@ -233,7 +233,7 @@ var require_util = __commonJS({
233
233
  else
234
234
  target[key] = value;
235
235
  }
236
- function mirrorShape2(target, source, keys, wrap) {
236
+ function mirrorShape2(target, source, keys, wrap2) {
237
237
  const raw = sourceShape2(source);
238
238
  for (const key of keys) {
239
239
  const desc = Object.getOwnPropertyDescriptor(raw, key);
@@ -242,10 +242,10 @@ var require_util = __commonJS({
242
242
  if (desc.get) {
243
243
  deferProp2(target, key, () => {
244
244
  const value = source._zod.def.shape[key];
245
- return wrap ? wrap(value, key) : value;
245
+ return wrap2 ? wrap2(value, key) : value;
246
246
  });
247
247
  } else
248
- putProp2(target, key, wrap ? wrap(desc.value, key) : desc.value);
248
+ putProp2(target, key, wrap2 ? wrap2(desc.value, key) : desc.value);
249
249
  }
250
250
  }
251
251
  function mirrorProps2(target, source) {
@@ -777,9 +777,9 @@ var require_util = __commonJS({
777
777
  }
778
778
  };
779
779
  exports.Class = Class2;
780
- function members2(proto, table) {
781
- for (const key in table) {
782
- const desc = Object.getOwnPropertyDescriptor(table, key);
780
+ function members2(proto, table2) {
781
+ for (const key in table2) {
782
+ const desc = Object.getOwnPropertyDescriptor(table2, key);
783
783
  if (desc.get)
784
784
  Object.defineProperty(proto, key, { ...desc, enumerable: false });
785
785
  else
@@ -793,10 +793,10 @@ var require_util = __commonJS({
793
793
  function hide2(inst, key, value) {
794
794
  return own2(inst, key, value, false);
795
795
  }
796
- function derived2(computes, table) {
796
+ function derived2(computes, table2) {
797
797
  for (const key in computes) {
798
798
  const compute = computes[key];
799
- Object.defineProperty(table, key, {
799
+ Object.defineProperty(table2, key, {
800
800
  configurable: true,
801
801
  enumerable: true,
802
802
  get() {
@@ -807,7 +807,7 @@ var require_util = __commonJS({
807
807
  }
808
808
  });
809
809
  }
810
- return table;
810
+ return table2;
811
811
  }
812
812
  function defineBound2(proto, key, fn) {
813
813
  Object.defineProperty(proto, key, {
@@ -18823,8 +18823,8 @@ var require_json_schema_processors = __commonJS({
18823
18823
  function aggregateChecks2(schema) {
18824
18824
  const agg = {};
18825
18825
  const def = schema._zod.def;
18826
- const list = schema._zod.traits.has("$ZodCheck") ? [schema, ...def.checks ?? []] : def.checks ?? [];
18827
- for (const ch of list)
18826
+ const list2 = schema._zod.traits.has("$ZodCheck") ? [schema, ...def.checks ?? []] : def.checks ?? [];
18827
+ for (const ch of list2)
18828
18828
  contributors2[ch._zod.def.check]?.(agg, ch._zod.def);
18829
18829
  const bag = schema._zod.bag;
18830
18830
  if (bag.minimum !== void 0)
@@ -23386,13 +23386,13 @@ var require_buffer_util = __commonJS({
23386
23386
  "use strict";
23387
23387
  var { EMPTY_BUFFER } = require_constants();
23388
23388
  var FastBuffer = Buffer[Symbol.species];
23389
- function concat(list, totalLength) {
23390
- if (list.length === 0) return EMPTY_BUFFER;
23391
- if (list.length === 1) return list[0];
23389
+ function concat(list2, totalLength) {
23390
+ if (list2.length === 0) return EMPTY_BUFFER;
23391
+ if (list2.length === 1) return list2[0];
23392
23392
  const target = Buffer.allocUnsafe(totalLength);
23393
23393
  let offset = 0;
23394
- for (let i = 0; i < list.length; i++) {
23395
- const buf = list[i];
23394
+ for (let i = 0; i < list2.length; i++) {
23395
+ const buf = list2[i];
23396
23396
  target.set(buf, offset);
23397
23397
  offset += buf.length;
23398
23398
  }
@@ -25178,14 +25178,14 @@ var require_sender = __commonJS({
25178
25178
  * @param {Function} [cb] Callback
25179
25179
  * @private
25180
25180
  */
25181
- sendFrame(list, cb) {
25182
- if (list.length === 2) {
25181
+ sendFrame(list2, cb) {
25182
+ if (list2.length === 2) {
25183
25183
  this._socket.cork();
25184
- this._socket.write(list[0]);
25185
- this._socket.write(list[1], cb);
25184
+ this._socket.write(list2[0]);
25185
+ this._socket.write(list2[1], cb);
25186
25186
  this._socket.uncork();
25187
25187
  } else {
25188
- this._socket.write(list[0], cb);
25188
+ this._socket.write(list2[0], cb);
25189
25189
  }
25190
25190
  }
25191
25191
  };
@@ -37708,7 +37708,7 @@ var require_contains = /* @__PURE__ */ __commonJSMin(((exports) => {
37708
37708
  const count = gen.let("count", 0);
37709
37709
  validateItems(schValid, () => gen.if(schValid, () => checkLimits(count)));
37710
37710
  }
37711
- function validateItems(_valid, block) {
37711
+ function validateItems(_valid, block2) {
37712
37712
  gen.forRange("i", 0, len, (i) => {
37713
37713
  cxt.subschema({
37714
37714
  keyword: "contains",
@@ -37716,7 +37716,7 @@ var require_contains = /* @__PURE__ */ __commonJSMin(((exports) => {
37716
37716
  dataPropType: util_1.Type.Num,
37717
37717
  compositeRule: true
37718
37718
  }, _valid);
37719
- block();
37719
+ block2();
37720
37720
  });
37721
37721
  }
37722
37722
  function checkLimits(count) {
@@ -40086,11 +40086,11 @@ var require_dist = /* @__PURE__ */ __commonJSMin(((exports, module) => {
40086
40086
  if (!f) throw new Error(`Unknown format "${name}"`);
40087
40087
  return f;
40088
40088
  };
40089
- function addFormats2(ajv, list, fs, exportName) {
40089
+ function addFormats2(ajv, list2, fs, exportName) {
40090
40090
  var _a3;
40091
40091
  var _b;
40092
40092
  (_a3 = (_b = ajv.opts.code).formats) !== null && _a3 !== void 0 || (_b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`);
40093
- for (const f of list) ajv.addFormat(f, fs[f]);
40093
+ for (const f of list2) ajv.addFormat(f, fs[f]);
40094
40094
  }
40095
40095
  module.exports = exports = formatsPlugin;
40096
40096
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -43506,7 +43506,7 @@ function putProp(target, key, value) {
43506
43506
  else
43507
43507
  target[key] = value;
43508
43508
  }
43509
- function mirrorShape(target, source, keys, wrap) {
43509
+ function mirrorShape(target, source, keys, wrap2) {
43510
43510
  const raw = sourceShape(source);
43511
43511
  for (const key of keys) {
43512
43512
  const desc = Object.getOwnPropertyDescriptor(raw, key);
@@ -43515,10 +43515,10 @@ function mirrorShape(target, source, keys, wrap) {
43515
43515
  if (desc.get) {
43516
43516
  deferProp(target, key, () => {
43517
43517
  const value = source._zod.def.shape[key];
43518
- return wrap ? wrap(value, key) : value;
43518
+ return wrap2 ? wrap2(value, key) : value;
43519
43519
  });
43520
43520
  } else
43521
- putProp(target, key, wrap ? wrap(desc.value, key) : desc.value);
43521
+ putProp(target, key, wrap2 ? wrap2(desc.value, key) : desc.value);
43522
43522
  }
43523
43523
  }
43524
43524
  function mirrorProps(target, source) {
@@ -44048,9 +44048,9 @@ var Class = class {
44048
44048
  constructor(..._args) {
44049
44049
  }
44050
44050
  };
44051
- function members(proto, table) {
44052
- for (const key in table) {
44053
- const desc = Object.getOwnPropertyDescriptor(table, key);
44051
+ function members(proto, table2) {
44052
+ for (const key in table2) {
44053
+ const desc = Object.getOwnPropertyDescriptor(table2, key);
44054
44054
  if (desc.get)
44055
44055
  Object.defineProperty(proto, key, { ...desc, enumerable: false });
44056
44056
  else
@@ -44065,10 +44065,10 @@ function hide(inst, key, value) {
44065
44065
  return own(inst, key, value, false);
44066
44066
  }
44067
44067
  // @__NO_SIDE_EFFECTS__
44068
- function derived(computes, table) {
44068
+ function derived(computes, table2) {
44069
44069
  for (const key in computes) {
44070
44070
  const compute = computes[key];
44071
- Object.defineProperty(table, key, {
44071
+ Object.defineProperty(table2, key, {
44072
44072
  configurable: true,
44073
44073
  enumerable: true,
44074
44074
  get() {
@@ -44079,7 +44079,7 @@ function derived(computes, table) {
44079
44079
  }
44080
44080
  });
44081
44081
  }
44082
- return table;
44082
+ return table2;
44083
44083
  }
44084
44084
  function defineBound(proto, key, fn) {
44085
44085
  Object.defineProperty(proto, key, {
@@ -58931,8 +58931,8 @@ var contributors = {
58931
58931
  function aggregateChecks(schema) {
58932
58932
  const agg = {};
58933
58933
  const def = schema._zod.def;
58934
- const list = schema._zod.traits.has("$ZodCheck") ? [schema, ...def.checks ?? []] : def.checks ?? [];
58935
- for (const ch of list)
58934
+ const list2 = schema._zod.traits.has("$ZodCheck") ? [schema, ...def.checks ?? []] : def.checks ?? [];
58935
+ for (const ch of list2)
58936
58936
  contributors[ch._zod.def.check]?.(agg, ch._zod.def);
58937
58937
  const bag = schema._zod.bag;
58938
58938
  if (bag.minimum !== void 0)
@@ -64467,7 +64467,7 @@ var pageSchema = {
64467
64467
 
64468
64468
  // src/version.ts
64469
64469
  var SERVER_NAME = "honeybook-mcp";
64470
- var VERSION = "1.3.1";
64470
+ var VERSION = "1.4.0";
64471
64471
 
64472
64472
  // src/sessions.ts
64473
64473
  import { homedir } from "node:os";
@@ -66264,14 +66264,14 @@ function assertAcceptsList(accepts) {
66264
66264
  }
66265
66265
  }
66266
66266
  }
66267
- function assertUnavailableCapabilities(list) {
66267
+ function assertUnavailableCapabilities(list2) {
66268
66268
  const label = "hello.unavailableCapabilities";
66269
- if (!Array.isArray(list))
66269
+ if (!Array.isArray(list2))
66270
66270
  throw new ProtocolError3(`${label}: expected array`);
66271
- if (list.length > MAX_UNAVAILABLE_CAPABILITIES) {
66272
- throw new ProtocolError3(`${label}: at most ${MAX_UNAVAILABLE_CAPABILITIES} entries, got ${list.length}`);
66271
+ if (list2.length > MAX_UNAVAILABLE_CAPABILITIES) {
66272
+ throw new ProtocolError3(`${label}: at most ${MAX_UNAVAILABLE_CAPABILITIES} entries, got ${list2.length}`);
66273
66273
  }
66274
- for (const c of list) {
66274
+ for (const c of list2) {
66275
66275
  if (typeof c !== "string") {
66276
66276
  throw new ProtocolError3(`${label}: entry must be string, got ${typeof c}`);
66277
66277
  }
@@ -70414,8 +70414,8 @@ var FetchproxyServer = class {
70414
70414
  throw new Error("FetchproxyServer.captureRequestHeader: no captureHeaders declared on this server \u2014 declare at least one entry in FetchproxyServerOpts.captureHeaders, or pass {host, headerName} explicitly");
70415
70415
  }
70416
70416
  if (decls.length > 1) {
70417
- const list = decls.map((d) => `${JSON.stringify(d.host)}${JSON.stringify(normPath(d.path))}/${JSON.stringify(d.headerName)}`).join(", ");
70418
- throw new Error(`FetchproxyServer.captureRequestHeader: multiple captureHeaders declared (${decls.length}: ${list}); pass {host, headerName} to disambiguate`);
70417
+ const list2 = decls.map((d) => `${JSON.stringify(d.host)}${JSON.stringify(normPath(d.path))}/${JSON.stringify(d.headerName)}`).join(", ");
70418
+ throw new Error(`FetchproxyServer.captureRequestHeader: multiple captureHeaders declared (${decls.length}: ${list2}); pass {host, headerName} to disambiguate`);
70419
70419
  }
70420
70420
  resolved = decls[0];
70421
70421
  } else {
@@ -72860,6 +72860,524 @@ function registerProjectTools(server) {
72860
72860
  );
72861
72861
  }
72862
72862
 
72863
+ // node_modules/@chrischall/mcp-utils/dist/rich/html.js
72864
+ var NAMED_ENTITIES = {
72865
+ amp: "&",
72866
+ lt: "<",
72867
+ gt: ">",
72868
+ quot: '"',
72869
+ apos: "'",
72870
+ nbsp: " "
72871
+ };
72872
+ function decodeEntities(s) {
72873
+ return s.replace(/&(?:#(\d{1,8})|#x([0-9a-f]{1,8})|([a-z]{1,10}));|&(amp|lt|gt|quot|nbsp)(?![a-z0-9=])/gi, (whole, dec2, hex3, name, legacy) => {
72874
+ if (legacy !== void 0)
72875
+ return NAMED_ENTITIES[legacy.toLowerCase()];
72876
+ if (name !== void 0)
72877
+ return NAMED_ENTITIES[name.toLowerCase()] ?? whole;
72878
+ const code2 = dec2 !== void 0 ? Number(dec2) : parseInt(hex3, 16);
72879
+ if (!Number.isFinite(code2) || code2 > 1114111)
72880
+ return whole;
72881
+ if (code2 < 32 && code2 !== 9 && code2 !== 10)
72882
+ return "";
72883
+ return String.fromCodePoint(code2);
72884
+ });
72885
+ }
72886
+ var HTML_ESCAPES = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
72887
+ function escapeHtml(s) {
72888
+ return s.replace(/[&<>"']/g, (c) => HTML_ESCAPES[c]);
72889
+ }
72890
+ function attr(attrs, name) {
72891
+ const m = new RegExp(`(?:^|\\s)${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s"'>]+))`, "i").exec(attrs);
72892
+ return m ? m[1] ?? m[2] ?? m[3] : void 0;
72893
+ }
72894
+ var INVISIBLE = /[\u200b\u200c\u2060\ufeff]/g;
72895
+ var visible = (s) => s.replace(INVISIBLE, "");
72896
+ var MAX_DEPTH = 200;
72897
+ var VOID = /* @__PURE__ */ new Set(["br", "img", "hr", "wbr", "input", "meta", "link", "col", "source", "area", "base", "embed", "param", "track"]);
72898
+ var DROPPED_WITH_CONTENT = "script|style|head|title|template|noscript|iframe|object|svg|math|xml";
72899
+ var BLOCK = /* @__PURE__ */ new Set(["p", "div", "ul", "ol", "pre", "blockquote", "table", "h1", "h2", "h3", "h4", "h5", "h6"]);
72900
+ var TAG = /<(\/?)([a-z][a-z0-9]*)\b((?:[^<>"']|"[^"<]*"|'[^'<]*')*)>/gi;
72901
+ var HIDDEN_STYLE = /(?:^|[;\s])(?:display\s*:\s*none|visibility\s*:\s*hidden|mso-hide\s*:\s*all)\b/i;
72902
+ function isHidden(attrs) {
72903
+ const names = attrs.replace(/"[^"]*"|'[^']*'/g, '""');
72904
+ if (/(?:^|\s)hidden(?:\s|=|\/|$)/i.test(names))
72905
+ return true;
72906
+ const style = attr(attrs, "style");
72907
+ return style !== void 0 && HIDDEN_STYLE.test(decodeEntities(style));
72908
+ }
72909
+ function parseHtml(html, opts = {}) {
72910
+ const root = { tag: "#root", attrs: "", children: [] };
72911
+ const stack = [root];
72912
+ const top = () => stack[stack.length - 1];
72913
+ const src = html.replace(/[\u0001-\u0005]/g, "").replace(/<!--[\s\S]*?(?:-->|$)/g, "").replace(/<!\[CDATA\[[\s\S]*?(?:\]\]>|$)/g, "").replace(/<![^>]*>/g, "").replace(new RegExp(`<(${DROPPED_WITH_CONTENT})\\b[\\s\\S]*?(?:<\\/\\1\\s*>|$)`, "gi"), "");
72914
+ let at = 0;
72915
+ for (const m of src.matchAll(TAG)) {
72916
+ if (m.index > at)
72917
+ top().children.push(src.slice(at, m.index));
72918
+ at = m.index + m[0].length;
72919
+ const tag = m[2].toLowerCase();
72920
+ if (m[1] === "/") {
72921
+ let i = stack.length - 1;
72922
+ while (i > 0 && stack[i].tag !== tag)
72923
+ i--;
72924
+ if (i > 0)
72925
+ stack.length = i;
72926
+ continue;
72927
+ }
72928
+ if (tag === "li" && top().tag === "li" || BLOCK.has(tag) && top().tag === "p")
72929
+ stack.pop();
72930
+ if ((tag === "td" || tag === "th" || tag === "tr") && (top().tag === "td" || top().tag === "th"))
72931
+ stack.pop();
72932
+ if (tag === "tr" && top().tag === "tr")
72933
+ stack.pop();
72934
+ const attrs = m[3];
72935
+ const el = { tag, attrs, children: [] };
72936
+ const hidden = opts.keepHidden !== true && isHidden(attrs);
72937
+ if (!hidden)
72938
+ top().children.push(el);
72939
+ const opens = !VOID.has(tag) && !attrs.trimEnd().endsWith("/");
72940
+ if (opens && (hidden || stack.length <= MAX_DEPTH))
72941
+ stack.push(el);
72942
+ }
72943
+ if (at < src.length)
72944
+ top().children.push(src.slice(at));
72945
+ return root;
72946
+ }
72947
+ function safeHref(attrs) {
72948
+ const raw = attr(attrs, "href");
72949
+ if (raw === void 0)
72950
+ return void 0;
72951
+ const href = decodeEntities(raw).trim();
72952
+ return /^(https?:\/\/|mailto:)/i.test(href) ? href : void 0;
72953
+ }
72954
+ function defaultImageText(el) {
72955
+ const alt = decodeEntities(attr(el.attrs, "alt") ?? "").replace(/\s+/g, " ").trim();
72956
+ return alt === "" ? "" : `[${alt}]`;
72957
+ }
72958
+ var QUOTE_OPEN = "";
72959
+ var QUOTE_CLOSE = "";
72960
+ var PARA = "";
72961
+ var BR = "";
72962
+ var MARKS = /[\u0004\u0005]/g;
72963
+ var paraBlock = (inner) => `
72964
+ ${PARA}
72965
+ ${inner}
72966
+ ${PARA}
72967
+ `;
72968
+ var TEXT_BLOCK = /* @__PURE__ */ new Set(["p", "div", "h1", "h2", "h3", "h4", "h5", "h6", "table", "hr"]);
72969
+ var LOOKS_LIKE_ADDRESS = /^(https?:\/\/|www\.)/i;
72970
+ function linkText(el, label) {
72971
+ const url4 = safeHref(el.attrs);
72972
+ if (url4 === void 0)
72973
+ return label;
72974
+ const shown = label.trim();
72975
+ if (shown === "" || shown === url4 || LOOKS_LIKE_ADDRESS.test(shown))
72976
+ return url4;
72977
+ return `${label} (${url4})`;
72978
+ }
72979
+ function rowText(tr, o) {
72980
+ const cells = tr.children.filter((n) => typeof n !== "string" && (n.tag === "td" || n.tag === "th"));
72981
+ if (cells.length === 0)
72982
+ return toText(tr.children, o);
72983
+ const raw = cells.map((c) => toText(c.children, o));
72984
+ const filled = raw.filter((t) => t.replace(/[\s\u0001-\u0005]+/g, "") !== "");
72985
+ if (filled.length === 1)
72986
+ return filled[0];
72987
+ const texts = raw.map((t) => t.replace(/[\n\u0001-\u0005]+/g, " ").trim());
72988
+ while (texts.length > 0 && texts[texts.length - 1] === "")
72989
+ texts.pop();
72990
+ return texts.join(" | ");
72991
+ }
72992
+ function toText(nodes, o) {
72993
+ const memo3 = o.textMemo ??= /* @__PURE__ */ new WeakMap();
72994
+ const hit = memo3.get(nodes);
72995
+ if (hit !== void 0)
72996
+ return hit;
72997
+ let out = "";
72998
+ for (const n of nodes) {
72999
+ if (typeof n === "string") {
73000
+ out += visible(decodeEntities(n));
73001
+ continue;
73002
+ }
73003
+ if (n.tag === "br") {
73004
+ out += `
73005
+ ${BR}`;
73006
+ continue;
73007
+ }
73008
+ const code2 = n.tag === "code" || n.tag === "pre";
73009
+ const inner = n.tag === "img" ? "" : code2 ? codeText(n.children) : toText(n.children, o);
73010
+ const hooked = o.element?.(n, "text", inner);
73011
+ if (hooked !== void 0)
73012
+ out += hooked;
73013
+ else if (n.tag === "img")
73014
+ out += defaultImageText(n);
73015
+ else if (n.tag === "a")
73016
+ out += linkText(n, inner);
73017
+ else if (n.tag === "tr")
73018
+ out += `
73019
+ ${rowText(n, o)}
73020
+ `;
73021
+ else if (n.tag === "blockquote")
73022
+ out += paraBlock(`${QUOTE_OPEN}
73023
+ ${inner}
73024
+ ${QUOTE_CLOSE}`);
73025
+ else if (n.tag === "ol" || n.tag === "ul") {
73026
+ const start = Number(attr(n.attrs, "start"));
73027
+ let i = n.tag === "ol" && Number.isInteger(start) && start >= 0 ? start : 1;
73028
+ let items = "";
73029
+ for (const c of n.children) {
73030
+ if (typeof c !== "string" && c.tag === "li") {
73031
+ const text2 = toText(c.children, o).replace(/^[\s\u0004\u0005]+|[\s\u0004\u0005]+$/g, "");
73032
+ items += `
73033
+ ${n.tag === "ol" ? `${i++}. ` : "- "}${text2}
73034
+ `;
73035
+ } else
73036
+ items += toText([c], o);
73037
+ }
73038
+ out += paraBlock(items);
73039
+ } else if (n.tag === "pre" || TEXT_BLOCK.has(n.tag))
73040
+ out += paraBlock(inner);
73041
+ else if (n.tag === "li")
73042
+ out += `
73043
+ ${inner}
73044
+ `;
73045
+ else
73046
+ out += inner;
73047
+ }
73048
+ memo3.set(nodes, out);
73049
+ return out;
73050
+ }
73051
+ function htmlToText(html, opts = {}) {
73052
+ const out = [];
73053
+ const paragraphs = opts.paragraphs === true;
73054
+ let depth = 0;
73055
+ let lastDepth = 0;
73056
+ let pendingBreak = false;
73057
+ for (const raw of toText(parseHtml(html, opts).children, { ...opts }).split(/\r?\n/)) {
73058
+ const line = raw.replace(/[ \t\u00a0]+/g, " ").trim();
73059
+ if (line === QUOTE_OPEN)
73060
+ depth++;
73061
+ else if (line === QUOTE_CLOSE)
73062
+ depth = Math.max(0, depth - 1);
73063
+ else if (line === PARA || line === BR)
73064
+ pendingBreak = paragraphs && out.length > 0;
73065
+ else {
73066
+ const text2 = line.replace(MARKS, "").trim();
73067
+ if (text2 === "")
73068
+ continue;
73069
+ if (pendingBreak)
73070
+ out.push("> ".repeat(Math.min(depth, lastDepth)).trimEnd());
73071
+ pendingBreak = false;
73072
+ out.push(`${"> ".repeat(depth)}${text2}`);
73073
+ lastDepth = depth;
73074
+ }
73075
+ }
73076
+ return out.join("\n");
73077
+ }
73078
+ var CODE_HOLE = "";
73079
+ var block = (s) => `
73080
+
73081
+ ${s}
73082
+
73083
+ `;
73084
+ function wrap(marker, inner) {
73085
+ const m = /^(\s*)([\s\S]*?)(\s*)$/.exec(inner);
73086
+ return m[2] === "" ? inner : `${m[1]}${marker}${m[2]}${marker}${m[3]}`;
73087
+ }
73088
+ function codeText(nodes) {
73089
+ return nodes.map((n) => typeof n === "string" ? decodeEntities(n) : n.tag === "br" ? "\n" : codeText(n.children)).join("");
73090
+ }
73091
+ function inlineCode(text2) {
73092
+ const longest = Math.max(0, ...[...text2.matchAll(/`+/g)].map((m) => m[0].length));
73093
+ const fence = "`".repeat(longest + 1);
73094
+ const pad = text2.startsWith("`") || text2.endsWith("`") ? " " : "";
73095
+ return `${fence}${pad}${text2}${pad}${fence}`;
73096
+ }
73097
+ var tidyInner = (s) => s.replace(/\n{3,}/g, "\n\n").trim();
73098
+ function list(el, codes, o) {
73099
+ const ordered = el.tag === "ol";
73100
+ const startAttr = Number(attr(el.attrs, "start"));
73101
+ const start = ordered && Number.isInteger(startAttr) && startAttr >= 0 ? startAttr : 1;
73102
+ const items = el.children.filter((n) => typeof n !== "string" && n.tag === "li");
73103
+ return items.map((li, i) => {
73104
+ const marker = ordered ? `${start + i}. ` : "- ";
73105
+ const lines = md(li.children, codes, o).replace(/\n{2,}/g, "\n").trim().split("\n");
73106
+ return lines.map((l, n) => n === 0 ? marker + l : l === "" ? "" : " ".repeat(marker.length) + l).join("\n");
73107
+ }).join("\n");
73108
+ }
73109
+ function rowsOf(el) {
73110
+ const out = [];
73111
+ for (const n of el.children) {
73112
+ if (typeof n === "string")
73113
+ continue;
73114
+ if (n.tag === "tr")
73115
+ out.push(n);
73116
+ else if (n.tag === "thead" || n.tag === "tbody" || n.tag === "tfoot")
73117
+ out.push(...rowsOf(n));
73118
+ }
73119
+ return out;
73120
+ }
73121
+ function table(el, codes, o) {
73122
+ const cellsOf = (tr) => tr.children.filter((n) => typeof n !== "string" && (n.tag === "td" || n.tag === "th")).map((c) => tidyInner(md(c.children, codes, o)));
73123
+ const raw = rowsOf(el).map(cellsOf).filter((cells) => cells.some((c) => c !== ""));
73124
+ if (raw.length === 0)
73125
+ return "";
73126
+ if (raw.every((cells) => cells.filter((c) => c !== "").length <= 1)) {
73127
+ return raw.map((cells) => cells.find((c) => c !== "") ?? "").join("\n\n");
73128
+ }
73129
+ if (raw.length === 1)
73130
+ return raw[0].filter((c) => c !== "").join(" ");
73131
+ const lines = raw.map((cells) => cells.map((c) => c.replace(/\s*\n\s*/g, " ").replace(/\|/g, "\\|")));
73132
+ const width = Math.max(...lines.map((cells) => cells.length));
73133
+ const row = (cells) => `| ${[...cells, ...Array(width - cells.length).fill("")].join(" | ")} |`;
73134
+ const sep = `| ${Array.from({ length: width }, () => "---").join(" | ")} |`;
73135
+ return [row(lines[0]), sep, ...lines.slice(1).map(row)].join("\n");
73136
+ }
73137
+ function md(nodes, codes, o) {
73138
+ const memo3 = o.mdMemo ??= /* @__PURE__ */ new WeakMap();
73139
+ const hit = memo3.get(nodes);
73140
+ if (hit !== void 0)
73141
+ return hit;
73142
+ let out = "";
73143
+ for (const n of nodes) {
73144
+ if (typeof n === "string") {
73145
+ out += visible(decodeEntities(n)).replace(/[\s\u00a0]+/g, " ");
73146
+ continue;
73147
+ }
73148
+ const inner = () => md(n.children, codes, o);
73149
+ if (o.element !== void 0 && n.tag !== "br") {
73150
+ const hooked = o.element(n, "markdown", n.tag === "img" ? "" : inner());
73151
+ if (hooked !== void 0) {
73152
+ out += hooked;
73153
+ continue;
73154
+ }
73155
+ }
73156
+ switch (n.tag) {
73157
+ case "br":
73158
+ out += "\n";
73159
+ break;
73160
+ case "p":
73161
+ case "div":
73162
+ out += block(inner().trim());
73163
+ break;
73164
+ case "hr":
73165
+ out += block("---");
73166
+ break;
73167
+ case "h1":
73168
+ case "h2":
73169
+ case "h3":
73170
+ case "h4":
73171
+ case "h5":
73172
+ case "h6": {
73173
+ const text2 = inner().replace(/\s+/g, " ").trim();
73174
+ out += text2 === "" ? "" : block(`${"#".repeat(Number(n.tag[1]))} ${text2}`);
73175
+ break;
73176
+ }
73177
+ case "b":
73178
+ case "strong":
73179
+ out += wrap("**", inner());
73180
+ break;
73181
+ case "i":
73182
+ case "em":
73183
+ out += wrap("_", inner());
73184
+ break;
73185
+ case "s":
73186
+ case "strike":
73187
+ case "del":
73188
+ out += wrap("~~", inner());
73189
+ break;
73190
+ case "code":
73191
+ out += inlineCode(codeText(n.children));
73192
+ break;
73193
+ case "pre": {
73194
+ codes.push("```\n" + codeText(n.children).replace(/\n+$/, "") + "\n```");
73195
+ out += block(`${CODE_HOLE}${codes.length - 1}${CODE_HOLE}`);
73196
+ break;
73197
+ }
73198
+ case "a": {
73199
+ const label = inner().trim().replace(/^\[([^\[\]]*)\]$/, "$1");
73200
+ const href = safeHref(n.attrs);
73201
+ if (href === void 0)
73202
+ out += label;
73203
+ else if (label === "" || label === href || LOOKS_LIKE_ADDRESS.test(label))
73204
+ out += href;
73205
+ else
73206
+ out += `[${label}](${href.replace(/[()\s]/g, (c) => encodeURIComponent(c))})`;
73207
+ break;
73208
+ }
73209
+ case "blockquote":
73210
+ out += block(tidyInner(inner()).split("\n").map((l) => l === "" ? ">" : `> ${l}`).join("\n"));
73211
+ break;
73212
+ case "ul":
73213
+ case "ol":
73214
+ out += block(list(n, codes, o));
73215
+ break;
73216
+ case "table":
73217
+ out += block(table(n, codes, o));
73218
+ break;
73219
+ case "img":
73220
+ out += defaultImageText(n);
73221
+ break;
73222
+ default:
73223
+ out += inner();
73224
+ }
73225
+ }
73226
+ memo3.set(nodes, out);
73227
+ return out;
73228
+ }
73229
+ function htmlToMarkdown(html, opts = {}) {
73230
+ const codes = [];
73231
+ const lines = md(parseHtml(html, opts).children, codes, { ...opts }).split("\n");
73232
+ const tidy = [];
73233
+ for (const raw of lines) {
73234
+ const line = raw.trimEnd();
73235
+ tidy.push(tidy.length === 0 || tidy[tidy.length - 1] === "" ? line.trimStart() : line);
73236
+ }
73237
+ return tidy.join("\n").replace(/\n{3,}/g, "\n\n").trim().replace(new RegExp(`${CODE_HOLE}(\\d+)${CODE_HOLE}`, "g"), (_w, i) => codes[Number(i)]);
73238
+ }
73239
+
73240
+ // node_modules/@chrischall/mcp-utils/dist/rich/markdown.js
73241
+ var HOLE_OPEN = "\uE000";
73242
+ var HOLE_CLOSE = "\uE001";
73243
+ var RESERVED2 = /[-]/g;
73244
+ var SAFE_HREF = /^(https?:\/\/|mailto:)/i;
73245
+ function renderInline(src, o) {
73246
+ const holes = [];
73247
+ const hole = (html) => `${HOLE_OPEN}${holes.push(html) - 1}${HOLE_CLOSE}`;
73248
+ const token = /(`+)([\s\S]*?[^`])\1(?!`)|\[([^\]\n]+)\]\(\s*([^\s()]+(?:\([^\s()]*\)[^\s()]*)*)\s*\)|\\([\\`*_{}[\]()#+\-.!~>|:])/g;
73249
+ let s = src.replace(token, (whole, _ticks, code2, label, href, escaped) => {
73250
+ if (code2 !== void 0)
73251
+ return hole(`<code>${escapeHtml(code2.replace(/^ (.*[^ ].*) $/s, "$1"))}</code>`);
73252
+ if (escaped !== void 0)
73253
+ return hole(escapeHtml(escaped));
73254
+ if (!SAFE_HREF.test(href))
73255
+ return whole;
73256
+ return hole(`<a href="${escapeHtml(href)}">${renderInline(label, o)}</a>`);
73257
+ });
73258
+ if (o.shortcode !== void 0) {
73259
+ const render2 = o.shortcode;
73260
+ s = s.replace(/:([a-z0-9_+-]+):/g, (whole, key) => {
73261
+ const html = render2(key);
73262
+ return html === void 0 ? whole : hole(html);
73263
+ });
73264
+ }
73265
+ s = escapeHtml(s).replace(/\*\*(?=\S)([\s\S]*?\S)\*\*/g, "<strong>$1</strong>").replace(/(^|[^\w])__(?=\S)([\s\S]*?\S)__(?!\w)/g, "$1<strong>$2</strong>").replace(/~~(?=\S)([\s\S]*?\S)~~/g, "<s>$1</s>").replace(/(^|[^\w*])\*(?=[^\s*])([\s\S]*?[^\s*])\*(?![\w*])/g, "$1<em>$2</em>").replace(/(^|[^\w])_(?=[^\s_])([\s\S]*?[^\s_])_(?!\w)/g, "$1<em>$2</em>").replace(/\n/g, "<br>");
73266
+ const fill = new RegExp(`${HOLE_OPEN}(\\d+)${HOLE_CLOSE}`, "g");
73267
+ return s.replace(fill, (_w, n) => holes[Number(n)]);
73268
+ }
73269
+ var LIST_ITEM = /^( *)([-*+]|\d{1,9}[.)])[ \t]+(.*)$/;
73270
+ var HEADING = /^ {0,3}(#{1,6})[ \t]+(.+?)[ \t]*#*[ \t]*$/;
73271
+ var QUOTE = /^ {0,3}> ?(.*)$/;
73272
+ function renderList(list2, o) {
73273
+ const tag = list2.ordered ? "ol" : "ul";
73274
+ const start = list2.ordered && list2.start !== 1 ? ` start="${list2.start}"` : "";
73275
+ const items = list2.items.map((i) => `<li>${renderInline(i.lines.join("\n"), o)}${i.children.map((c) => renderList(c, o)).join("")}</li>`);
73276
+ return `<${tag}${start}>${items.join("")}</${tag}>`;
73277
+ }
73278
+ function parseList(lines, from, o) {
73279
+ const roots = [];
73280
+ const stack = [];
73281
+ let i = from;
73282
+ for (; i < lines.length; i++) {
73283
+ const line = lines[i];
73284
+ const m = LIST_ITEM.exec(line);
73285
+ if (m === null) {
73286
+ const last = stack.at(-1)?.items.at(-1);
73287
+ if (line.trim() !== "" && /^\s/.test(line) && last !== void 0) {
73288
+ last.lines.push(line.trim());
73289
+ continue;
73290
+ }
73291
+ break;
73292
+ }
73293
+ const indent = m[1].length;
73294
+ const ordered = /\d/.test(m[2]);
73295
+ while (stack.length > 0 && indent < stack[stack.length - 1].indent)
73296
+ stack.pop();
73297
+ let top = stack.at(-1);
73298
+ if (top !== void 0 && indent >= top.indent + 2 && top.items.length > 0) {
73299
+ const nested = { indent, ordered, start: ordered ? Number.parseInt(m[2], 10) : 1, items: [] };
73300
+ top.items[top.items.length - 1].children.push(nested);
73301
+ stack.push(nested);
73302
+ top = nested;
73303
+ } else if (top === void 0 || top.ordered !== ordered) {
73304
+ const fresh = { indent, ordered, start: ordered ? Number.parseInt(m[2], 10) : 1, items: [] };
73305
+ if (top !== void 0 && stack.length > 1) {
73306
+ stack.pop();
73307
+ stack[stack.length - 1].items.at(-1).children.push(fresh);
73308
+ } else {
73309
+ roots.push(fresh);
73310
+ stack.length = 0;
73311
+ }
73312
+ stack.push(fresh);
73313
+ top = fresh;
73314
+ }
73315
+ top.items.push({ lines: [m[3]], children: [] });
73316
+ }
73317
+ return { html: roots.map((r) => renderList(r, o)).join(""), next: i };
73318
+ }
73319
+ function render(src, o, depth) {
73320
+ const lines = src.split("\n");
73321
+ const out = [];
73322
+ let para = [];
73323
+ const flush = () => {
73324
+ if (para.length > 0)
73325
+ out.push(`<p>${renderInline(para.join("\n"), o)}</p>`);
73326
+ para = [];
73327
+ };
73328
+ for (let i = 0; i < lines.length; ) {
73329
+ const line = lines[i];
73330
+ const fence = /^ {0,3}(`{3,}|~{3,})/.exec(line);
73331
+ if (fence !== null) {
73332
+ flush();
73333
+ const close = new RegExp(`^ {0,3}${fence[1][0] === "`" ? "`" : "~"}{${fence[1].length},}\\s*$`);
73334
+ const code2 = [];
73335
+ for (i++; i < lines.length && !close.test(lines[i]); i++)
73336
+ code2.push(lines[i]);
73337
+ i++;
73338
+ out.push(`<pre><code>${escapeHtml(code2.join("\n"))}</code></pre>`);
73339
+ continue;
73340
+ }
73341
+ if (LIST_ITEM.test(line)) {
73342
+ flush();
73343
+ const { html, next } = parseList(lines, i, o);
73344
+ out.push(html);
73345
+ i = next;
73346
+ continue;
73347
+ }
73348
+ const heading = o.headings === true ? HEADING.exec(line) : null;
73349
+ if (heading !== null) {
73350
+ flush();
73351
+ const level = heading[1].length;
73352
+ out.push(`<h${level}>${renderInline(heading[2], o)}</h${level}>`);
73353
+ i++;
73354
+ continue;
73355
+ }
73356
+ if (o.quotes === true && depth < 20 && QUOTE.test(line)) {
73357
+ flush();
73358
+ const quoted = [];
73359
+ for (; i < lines.length; i++) {
73360
+ const q = QUOTE.exec(lines[i]);
73361
+ if (q === null)
73362
+ break;
73363
+ quoted.push(q[1]);
73364
+ }
73365
+ out.push(`<blockquote>${render(quoted.join("\n"), o, depth + 1)}</blockquote>`);
73366
+ continue;
73367
+ }
73368
+ if (line.trim() === "")
73369
+ flush();
73370
+ else
73371
+ para.push(line);
73372
+ i++;
73373
+ }
73374
+ flush();
73375
+ return out.join("");
73376
+ }
73377
+ function renderMarkdown(src, opts = {}) {
73378
+ return render(src.replace(/\r\n?/g, "\n").replace(RESERVED2, ""), opts, 0);
73379
+ }
73380
+
72863
73381
  // src/feed.ts
72864
73382
  var MESSAGE_TYPES = /* @__PURE__ */ new Set(["feed_message", "workspace_email", "workspace_file_email"]);
72865
73383
  function str2(o, k) {
@@ -72917,9 +73435,6 @@ function itemAttachments(item) {
72917
73435
  function collapseWhitespace(s) {
72918
73436
  return s.replace(/\s+/g, " ").trim();
72919
73437
  }
72920
- function collapseHorizontalWhitespace(s) {
72921
- return s.replace(/[^\S\n]+/g, " ").replace(/ ?\n ?/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
72922
- }
72923
73438
  var PREVIEW_CHARS = 240;
72924
73439
  function summarizeMessage(item, feed, meId) {
72925
73440
  const data = obj(item, "data");
@@ -72951,22 +73466,24 @@ function summarizeMessage(item, feed, meId) {
72951
73466
  }
72952
73467
  return out;
72953
73468
  }
73469
+ var BODY_FORMATS = ["text", "markdown", "html"];
73470
+ function messageBody(data, format) {
73471
+ const html = str2(data, "html_body");
73472
+ const plain = str2(data, "plain_text");
73473
+ if (format === "html") return html ?? plain ?? "";
73474
+ if (format === "markdown") return html !== void 0 ? htmlToMarkdown(html) : plain ?? "";
73475
+ return plain ?? (html !== void 0 ? htmlToText(html, { paragraphs: true }) : "");
73476
+ }
72954
73477
  function expandMessage(item, feed, meId, format) {
72955
73478
  const data = obj(item, "data");
72956
73479
  const summary = summarizeMessage(item, feed, meId);
72957
73480
  delete summary.preview;
72958
- const body = format === "html" ? str2(data, "html_body") ?? str2(data, "plain_text") ?? "" : str2(data, "plain_text") ?? htmlToText(str2(data, "html_body") ?? "");
72959
73481
  const delivery = arr(item, "email_tracking").map((t) => ({
72960
73482
  recipient: t.recipient_name,
72961
73483
  status: t.status,
72962
73484
  date: t.date ?? null
72963
73485
  }));
72964
- return { ...summary, body_format: format, body: body.trim(), delivery };
72965
- }
72966
- function htmlToText(html) {
72967
- return collapseHorizontalWhitespace(
72968
- html.replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|div|li|h\d|tr)>/gi, "\n").replace(/<[^>]+>/g, "").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'")
72969
- );
73486
+ return { ...summary, body_format: format, body: messageBody(data, format).trim(), delivery };
72970
73487
  }
72971
73488
  function compactCalendarItem(c) {
72972
73489
  const opt = (k) => {
@@ -73249,7 +73766,7 @@ var ALLOWED_TAGS = /* @__PURE__ */ new Set([
73249
73766
  var HTML_TAG = /<\/?(p|br|div|a|b|i|u|strong|em|ul|ol|li|span|h[1-6]|table|tr|td|th|blockquote|pre|code|hr)\b[^>]*>/i;
73250
73767
  var ANY_TAG = /<(\/?)([a-zA-Z][a-zA-Z0-9]*)\b([^<>]*)>/g;
73251
73768
  var HREF_ATTR = /(?:^|\s)href\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/i;
73252
- var SAFE_HREF = /^(?:https?:\/\/|mailto:)/i;
73769
+ var SAFE_HREF2 = /^(?:https?:\/\/|mailto:)/i;
73253
73770
  var escapeAngles = (s) => s.replace(/</g, "&lt;").replace(/>/g, "&gt;");
73254
73771
  function sanitizeTag(closing, name, attrs) {
73255
73772
  const tag = name.toLowerCase();
@@ -73258,7 +73775,7 @@ function sanitizeTag(closing, name, attrs) {
73258
73775
  if (tag === "a") {
73259
73776
  const m = HREF_ATTR.exec(attrs);
73260
73777
  const href = (m?.[1] ?? m?.[2] ?? m?.[3] ?? "").trim();
73261
- if (SAFE_HREF.test(href)) return `<a href="${href.replace(/"/g, "&quot;")}">`;
73778
+ if (SAFE_HREF2.test(href)) return `<a href="${href.replace(/"/g, "&quot;")}">`;
73262
73779
  }
73263
73780
  return `<${tag}>`;
73264
73781
  }
@@ -73272,9 +73789,12 @@ function sanitizeHtml(html) {
73272
73789
  }
73273
73790
  return out + escapeAngles(html.slice(last));
73274
73791
  }
73275
- function bodyToHtml(body) {
73276
- if (HTML_TAG.test(body)) return sanitizeHtml(body);
73277
- return body.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\r?\n/g, "<br>");
73792
+ var WRITE_FORMATS = ["text", "markdown", "html"];
73793
+ function bodyToHtml(body, format) {
73794
+ const as = format ?? (HTML_TAG.test(body) ? "html" : "text");
73795
+ if (as === "markdown") return renderMarkdown(body, { headings: true, quotes: true });
73796
+ if (as === "html") return sanitizeHtml(body);
73797
+ return escapeHtml(body).replace(/\r?\n/g, "<br>");
73278
73798
  }
73279
73799
  async function sendMessage(args, ctx) {
73280
73800
  const client = await getActiveClient(args.origin);
@@ -73303,7 +73823,7 @@ async function sendMessage(args, ctx) {
73303
73823
  const taskData = {
73304
73824
  ws_id: args.workspace_id,
73305
73825
  subject,
73306
- html_body: bodyToHtml(args.body),
73826
+ html_body: bodyToHtml(args.body, args.format),
73307
73827
  force: false,
73308
73828
  general_files: [],
73309
73829
  image_files: [],
@@ -73411,11 +73931,13 @@ function registerMessageTools(server) {
73411
73931
  server.registerTool(
73412
73932
  "get_message",
73413
73933
  {
73414
- description: "Read one message in full: body (plain text by default, or the original HTML), sender, recipients, attachments and per-recipient delivery status.",
73934
+ description: "Read one message in full: body (HoneyBook's plain text by default, Markdown, or the original HTML), sender, recipients, attachments and per-recipient delivery status.",
73415
73935
  inputSchema: external_exports.object({
73416
73936
  workspace_id: external_exports.string().describe(WORKSPACE_DESC),
73417
73937
  message_id: external_exports.string().describe("The message _id from list_messages."),
73418
- format: external_exports.enum(["text", "html"]).optional().describe('Body format. Default "text".'),
73938
+ format: external_exports.enum(BODY_FORMATS).optional().describe(
73939
+ `Body format: "text" (default, HoneyBook's plain text), "markdown" (keeps lists, paragraphs and links with their address, which the plain text loses; tracking pixels and hidden text dropped) or "html" (the original, raw).`
73940
+ ),
73419
73941
  origin: schemaOrigin.describe(ORIGIN_DESC)
73420
73942
  }),
73421
73943
  annotations: { readOnlyHint: true, openWorldHint: true },
@@ -73429,7 +73951,12 @@ function registerMessageTools(server) {
73429
73951
  description: 'Send a message to the vendor (and the other members of the workspace) through the HoneyBook portal, exactly as the Activity tab composer does. HoneyBook emails it to every recipient. Pass reply_to_message_id to reply in-thread (the subject is inherited). Asks the user to confirm first: a confirmation prompt where the client supports one; otherwise the first call returns a preview and a confirmToken, and only a repeat call with that token proceeds (see MCP_CONFIRM_MODE). If HoneyBook has not finished sending within about a minute the result has status "pending" and a task_id; if it did not answer the send request at all the status is "unknown". Either way the message may still be delivered, so check list_messages before resending.',
73430
73952
  inputSchema: external_exports.object({
73431
73953
  workspace_id: external_exports.string().describe(WORKSPACE_DESC),
73432
- body: external_exports.string().describe("Message text. Plain text is sent as-is (line breaks preserved). Simple HTML formatting (p, br, b, i, a href, lists, tables) is kept; other tags are shown as text and attributes other than an http(s)/mailto link are dropped."),
73954
+ body: external_exports.string().describe(
73955
+ "Message text, written as `format` says. With no format, plain text is sent as-is (line breaks preserved) and a body using HTML tags is read as HTML."
73956
+ ),
73957
+ format: external_exports.enum(WRITE_FORMATS).optional().describe(
73958
+ '"text" (sent as typed), "markdown" (**bold**, _italic_, lists, [links](https://\u2026), `code`, # headings, > quotes \u2014 sent as formatted HTML, and cheaper to write than HTML) or "html" (simple formatting kept: p, br, b, i, a href, lists, tables; other tags shown as text, attributes other than an http(s)/mailto link dropped). Omitted: "html" if the body uses an HTML tag, else "text".'
73959
+ ),
73433
73960
  subject: external_exports.string().optional().describe("Required for a new message; optional on a reply."),
73434
73961
  reply_to_message_id: external_exports.string().optional().describe("A message _id from list_messages to reply to."),
73435
73962
  origin: schemaOrigin.describe(ORIGIN_DESC),
@@ -73818,13 +74345,13 @@ async function listAllProjects() {
73818
74345
  }
73819
74346
  return rows;
73820
74347
  }
73821
- async function loadProjectDirectory(list = listAllProjects, timeoutMs = PROJECT_LIST_TIMEOUT_MS) {
74348
+ async function loadProjectDirectory(list2 = listAllProjects, timeoutMs = PROJECT_LIST_TIMEOUT_MS) {
73822
74349
  let timer;
73823
74350
  const deadline = new Promise((resolve2) => {
73824
74351
  timer = setTimeout(() => resolve2(null), timeoutMs);
73825
74352
  timer.unref();
73826
74353
  });
73827
- const listing = Promise.resolve().then(list).then(buildProjectDirectory).catch(() => null);
74354
+ const listing = Promise.resolve().then(list2).then(buildProjectDirectory).catch(() => null);
73828
74355
  try {
73829
74356
  return await Promise.race([listing, deadline]);
73830
74357
  } finally {
package/dist/feed.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { htmlToMarkdown, htmlToText } from '@chrischall/mcp-utils/rich';
1
2
  import { apiPath } from './client.js';
2
3
  import { VENDOR_SECRET_KEYS, scrubVendorSecrets } from './vendor-secrets.js';
3
4
  /**
@@ -72,14 +73,6 @@ export function itemAttachments(item) {
72
73
  export function collapseWhitespace(s) {
73
74
  return s.replace(/\s+/g, ' ').trim();
74
75
  }
75
- /** Collapse runs of spaces but keep paragraph breaks (at most one blank line). */
76
- export function collapseHorizontalWhitespace(s) {
77
- return s
78
- .replace(/[^\S\n]+/g, ' ')
79
- .replace(/ ?\n ?/g, '\n')
80
- .replace(/\n{3,}/g, '\n\n')
81
- .trim();
82
- }
83
76
  const PREVIEW_CHARS = 240;
84
77
  /** Compact card for a message item. Bodies are deliberately left out. */
85
78
  export function summarizeMessage(item, feed, meId) {
@@ -120,35 +113,36 @@ export function summarizeMessage(item, feed, meId) {
120
113
  }
121
114
  return out;
122
115
  }
116
+ /** How get_message returns a body. */
117
+ export const BODY_FORMATS = ['text', 'markdown', 'html'];
118
+ /**
119
+ * A message body in the format asked for. "text" is HoneyBook's own
120
+ * `plain_text`, and "html" the raw `html_body`, as they always were.
121
+ * "markdown" reads the HTML (lists, paragraphs and links with their address
122
+ * survive, which `plain_text` loses; tracking pixels, styles and hidden text
123
+ * go). `str` folds '' to undefined, so an empty field falls through to the
124
+ * other rather than returning an empty body next to a real one.
125
+ */
126
+ function messageBody(data, format) {
127
+ const html = str(data, 'html_body');
128
+ const plain = str(data, 'plain_text');
129
+ if (format === 'html')
130
+ return html ?? plain ?? '';
131
+ if (format === 'markdown')
132
+ return html !== undefined ? htmlToMarkdown(html) : (plain ?? '');
133
+ return plain ?? (html !== undefined ? htmlToText(html, { paragraphs: true }) : '');
134
+ }
123
135
  /** Everything a person needs to read one message. */
124
136
  export function expandMessage(item, feed, meId, format) {
125
137
  const data = obj(item, 'data');
126
138
  const summary = summarizeMessage(item, feed, meId);
127
139
  delete summary.preview;
128
- // `str` folds '' to undefined, so an empty plain_text falls through to the
129
- // html_body rather than returning an empty body next to a real one.
130
- const body = format === 'html'
131
- ? (str(data, 'html_body') ?? str(data, 'plain_text') ?? '')
132
- : (str(data, 'plain_text') ?? htmlToText(str(data, 'html_body') ?? ''));
133
140
  const delivery = arr(item, 'email_tracking').map((t) => ({
134
141
  recipient: t.recipient_name,
135
142
  status: t.status,
136
143
  date: t.date ?? null,
137
144
  }));
138
- return { ...summary, body_format: format, body: body.trim(), delivery };
139
- }
140
- /** Last-resort text for an item whose `plain_text` is missing. Paragraph breaks survive. */
141
- export function htmlToText(html) {
142
- return collapseHorizontalWhitespace(html
143
- .replace(/<br\s*\/?>/gi, '\n')
144
- .replace(/<\/(p|div|li|h\d|tr)>/gi, '\n')
145
- .replace(/<[^>]+>/g, '')
146
- .replace(/&nbsp;/g, ' ')
147
- .replace(/&amp;/g, '&')
148
- .replace(/&lt;/g, '<')
149
- .replace(/&gt;/g, '>')
150
- .replace(/&quot;/g, '"')
151
- .replace(/&#39;/g, "'"));
145
+ return { ...summary, body_format: format, body: messageBody(data, format).trim(), delivery };
152
146
  }
153
147
  /**
154
148
  * A calendar item as the feed embeds it, minus the vendor's host link
@@ -1,7 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  import { confirmationFromEnv, confirmTokenParam, minifiedResult, requireConfirmationWithFallback, schemaOrigin, } from '@chrischall/mcp-utils';
3
+ import { escapeHtml, renderMarkdown } from '@chrischall/mcp-utils/rich';
3
4
  import { apiPath, getActiveClient } from '../client.js';
4
- import { fetchWorkspaceFeed, isMessageItem, itemDate, summarizeMessage, summarizeActivity, expandMessage, } from '../feed.js';
5
+ import { fetchWorkspaceFeed, isMessageItem, itemDate, summarizeMessage, summarizeActivity, expandMessage, BODY_FORMATS, } from '../feed.js';
5
6
  import { runClientPendingTask, isPendingTaskTimeoutError, isPendingTaskCreateTimeoutError, } from '../pending-tasks.js';
6
7
  import { moneyMeta } from './hints.js';
7
8
  export const FEED_KINDS = ['messages', 'activity', 'all'];
@@ -65,10 +66,12 @@ export async function getMessage(args) {
65
66
  return minifiedResult(expandMessage(item, feed, client.scope.userId, args.format ?? 'text'));
66
67
  }
67
68
  /**
68
- * The composer sends HTML. Plain text is escaped and line breaks become
69
- * `<br>`; a body that uses a real HTML tag is treated as authored HTML. The
70
- * tag list is an allowlist on purpose: "<Ivy>" in a sentence is prose, not
71
- * markup.
69
+ * The composer sends HTML. A body is written as `format` says: "text" is
70
+ * escaped with line breaks as `<br>`; "markdown" is rendered (raw HTML in it
71
+ * escaped, links http(s)/mailto only); "html" is sanitised. With no format, a
72
+ * body that uses a real HTML tag is treated as authored HTML and anything else
73
+ * as text. The tag list is an allowlist on purpose: "<Ivy>" in a sentence is
74
+ * prose, not markup.
72
75
  *
73
76
  * Authored HTML is SANITIZED to that allowlist, not passed through
74
77
  * (fleet-audit#506): HoneyBook emails it under the user's name, and a body the
@@ -77,6 +80,10 @@ export async function getMessage(args) {
77
80
  * text; allowlisted tags lose every attribute except an http(s)/mailto `href`
78
81
  * on `<a>`. `<img>` is deliberately not allowlisted — a remote image in an
79
82
  * email is a read receipt.
83
+ *
84
+ * This stays local rather than /rich's `sanitiseHtml`: that is a READ
85
+ * sanitiser and removes unknown tags (and everything after an unclosed
86
+ * `<script`), which on a write would silently drop the caller's own words.
80
87
  */
81
88
  const ALLOWED_TAGS = new Set([
82
89
  'p', 'br', 'div', 'a', 'b', 'i', 'u', 'strong', 'em', 'ul', 'ol', 'li', 'span',
@@ -111,14 +118,14 @@ export function sanitizeHtml(html) {
111
118
  }
112
119
  return out + escapeAngles(html.slice(last));
113
120
  }
114
- export function bodyToHtml(body) {
115
- if (HTML_TAG.test(body))
121
+ export const WRITE_FORMATS = ['text', 'markdown', 'html'];
122
+ export function bodyToHtml(body, format) {
123
+ const as = format ?? (HTML_TAG.test(body) ? 'html' : 'text');
124
+ if (as === 'markdown')
125
+ return renderMarkdown(body, { headings: true, quotes: true });
126
+ if (as === 'html')
116
127
  return sanitizeHtml(body);
117
- return body
118
- .replace(/&/g, '&amp;')
119
- .replace(/</g, '&lt;')
120
- .replace(/>/g, '&gt;')
121
- .replace(/\r?\n/g, '<br>');
128
+ return escapeHtml(body).replace(/\r?\n/g, '<br>');
122
129
  }
123
130
  export async function sendMessage(args, ctx) {
124
131
  const client = await getActiveClient(args.origin);
@@ -146,7 +153,7 @@ export async function sendMessage(args, ctx) {
146
153
  const taskData = {
147
154
  ws_id: args.workspace_id,
148
155
  subject,
149
- html_body: bodyToHtml(args.body),
156
+ html_body: bodyToHtml(args.body, args.format),
150
157
  force: false,
151
158
  general_files: [],
152
159
  image_files: [],
@@ -262,12 +269,16 @@ export function registerMessageTools(server) {
262
269
  _meta: moneyMeta('whats-new', { aliases: ['messages', 'emails', 'vendor said', 'inbox'] }),
263
270
  }, listMessages);
264
271
  server.registerTool('get_message', {
265
- description: 'Read one message in full: body (plain text by default, or the original HTML), sender, recipients, ' +
272
+ description: 'Read one message in full: body (HoneyBook\'s plain text by default, Markdown, or the original HTML), sender, recipients, ' +
266
273
  'attachments and per-recipient delivery status.',
267
274
  inputSchema: z.object({
268
275
  workspace_id: z.string().describe(WORKSPACE_DESC),
269
276
  message_id: z.string().describe('The message _id from list_messages.'),
270
- format: z.enum(['text', 'html']).optional().describe('Body format. Default "text".'),
277
+ format: z
278
+ .enum(BODY_FORMATS)
279
+ .optional()
280
+ .describe('Body format: "text" (default, HoneyBook\'s plain text), "markdown" (keeps lists, paragraphs and links ' +
281
+ 'with their address, which the plain text loses; tracking pixels and hidden text dropped) or "html" (the original, raw).'),
271
282
  origin: schemaOrigin.describe(ORIGIN_DESC),
272
283
  }),
273
284
  annotations: { readOnlyHint: true, openWorldHint: true },
@@ -285,7 +296,15 @@ export function registerMessageTools(server) {
285
296
  workspace_id: z.string().describe(WORKSPACE_DESC),
286
297
  body: z
287
298
  .string()
288
- .describe('Message text. Plain text is sent as-is (line breaks preserved). Simple HTML formatting (p, br, b, i, a href, lists, tables) is kept; other tags are shown as text and attributes other than an http(s)/mailto link are dropped.'),
299
+ .describe('Message text, written as `format` says. With no format, plain text is sent as-is (line breaks preserved) ' +
300
+ 'and a body using HTML tags is read as HTML.'),
301
+ format: z
302
+ .enum(WRITE_FORMATS)
303
+ .optional()
304
+ .describe('"text" (sent as typed), "markdown" (**bold**, _italic_, lists, [links](https://…), `code`, # headings, ' +
305
+ '> quotes — sent as formatted HTML, and cheaper to write than HTML) or "html" (simple formatting kept: ' +
306
+ 'p, br, b, i, a href, lists, tables; other tags shown as text, attributes other than an ' +
307
+ 'http(s)/mailto link dropped). Omitted: "html" if the body uses an HTML tag, else "text".'),
289
308
  subject: z.string().optional().describe('Required for a new message; optional on a reply.'),
290
309
  reply_to_message_id: z
291
310
  .string()
package/dist/version.js CHANGED
@@ -4,4 +4,4 @@
4
4
  // package.json drift. Importing these instead of package.json keeps the
5
5
  // whole manifest — npm scripts included — out of dist/bundle.js.
6
6
  export const SERVER_NAME = 'honeybook-mcp';
7
- export const VERSION = '1.3.1'; // x-release-please-version
7
+ export const VERSION = '1.4.0'; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "honeybook-mcp",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "mcpName": "io.github.chrischall/honeybook-mcp",
5
5
  "description": "HoneyBook client-portal MCP server for Claude",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -44,7 +44,7 @@
44
44
  "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json"
45
45
  },
46
46
  "dependencies": {
47
- "@chrischall/mcp-utils": "^3.1.0",
47
+ "@chrischall/mcp-utils": "^3.8.0",
48
48
  "@fetchproxy/bootstrap": "^3.6.2",
49
49
  "@fetchproxy/server": "^3.6.2",
50
50
  "@modelcontextprotocol/server": "^2.3.0",
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/chrischall/honeybook-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "1.3.1",
9
+ "version": "1.4.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "honeybook-mcp",
14
- "version": "1.3.1",
14
+ "version": "1.4.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -22,8 +22,8 @@ MCP server for HoneyBook's client portal — contracts, invoices, questionnaires
22
22
  - `list_projects` — Your projects (HoneyBook "events") with a vendor, each with the `workspace_id` the tools below take
23
23
  - `get_project(project_id, view?)` — Project details: date, time, location, guests, custom fields, people (name/email/phone/role)
24
24
  - `list_messages` — Messages in a workspace (`kind=activity` for the activity log, `kind=all` for both): compact cards, newest first; never marks anything seen
25
- - `get_message` — One message in full (`format=text` default, or `html`) with attachments and delivery status
26
- - `send_message` — Send a new message (`subject` + `body`) or reply (`reply_to_message_id`, subject inherited) through the portal; asks the user to confirm first
25
+ - `get_message` — One message in full (`format=text` default, `markdown` to keep lists and links, or the raw `html`) with attachments and delivery status
26
+ - `send_message` — Send a new message (`subject` + `body`) or reply (`reply_to_message_id`, subject inherited) through the portal; `format=markdown` sends a Markdown body as formatted HTML; asks the user to confirm first
27
27
  - `mark_messages_seen` — Mark feed items seen, the way opening the Activity tab does
28
28
  - `list_meetings` — Meetings the vendor scheduled (consultations, Zoom calls): time, join link, password; rescheduled meetings show their latest time
29
29
  - `list_tasks` — Tasks assigned to you with today/this-week/overdue counts and task groups