honeybook-mcp 1.1.4 → 1.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +16 -6
- package/dist/bundle.js +576 -99
- package/dist/index.js +1 -1
- package/dist/tools/contracts.js +25 -8
- package/dist/tools/invoices.js +26 -8
- package/dist/tools/messages.js +31 -12
- package/package.json +2 -2
- package/server.json +2 -2
- package/skills/honeybook/SKILL.md +6 -6
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "HoneyBook client-portal tools for Claude Code",
|
|
10
|
-
"version": "1.
|
|
10
|
+
"version": "1.2.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.
|
|
18
|
+
"version": "1.2.0",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Chall"
|
|
21
21
|
},
|
package/README.md
CHANGED
|
@@ -76,7 +76,7 @@ Add to Claude Desktop config (`~/Library/Application Support/Claude/claude_deskt
|
|
|
76
76
|
}
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
No environment variables are required.
|
|
79
|
+
No environment variables are required. The optional ones below tune how writes are confirmed — see [Confirmations](#confirmations).
|
|
80
80
|
|
|
81
81
|
## Sessions
|
|
82
82
|
|
|
@@ -134,13 +134,13 @@ Tools that touch a vendor accept an optional `origin` argument (e.g. `https://ac
|
|
|
134
134
|
| `get_workspace_file` | Full detail for one file | Auto |
|
|
135
135
|
| `get_workspace` | Workspace detail + status flags | Auto |
|
|
136
136
|
| `list_payment_methods` | Saved payment methods | Auto |
|
|
137
|
-
| `sign_contract` | Deep link to sign in portal (
|
|
138
|
-
| `pay_invoice` | Deep link to pay in portal (
|
|
137
|
+
| `sign_contract` | Deep link to sign in portal (after you confirm) | Confirm |
|
|
138
|
+
| `pay_invoice` | Deep link to pay in portal (after you confirm) | Confirm |
|
|
139
139
|
| `list_projects` | Your projects with a vendor + their workspace ids | Auto |
|
|
140
140
|
| `get_project` | Project details: date, location, people, custom fields | Auto |
|
|
141
141
|
| `list_messages` | Messages (or the activity log) in a workspace, newest first | Auto |
|
|
142
142
|
| `get_message` | One message in full: body, attachments, delivery status | Auto |
|
|
143
|
-
| `send_message` | Send or reply through the portal (
|
|
143
|
+
| `send_message` | Send or reply through the portal (after you confirm) | Confirm |
|
|
144
144
|
| `mark_messages_seen` | Mark feed items seen (reads never do this on their own) | Auto |
|
|
145
145
|
| `list_meetings` | Scheduled meetings with join links, latest time wins | Auto |
|
|
146
146
|
| `list_tasks` | Tasks the vendor assigned you, with counts | Auto |
|
|
@@ -151,7 +151,17 @@ Tools that touch a vendor accept an optional `origin` argument (e.g. `https://ac
|
|
|
151
151
|
`send_message` is the one tool that acts on your behalf: it creates the same
|
|
152
152
|
`send_workspace_message` job the portal's Activity composer creates and waits
|
|
153
153
|
for HoneyBook to finish it, so the vendor receives a normal HoneyBook email.
|
|
154
|
-
|
|
154
|
+
Until you confirm it only previews the recipients, subject and body — see [Confirmations](#confirmations).
|
|
155
|
+
|
|
156
|
+
## Confirmations
|
|
157
|
+
|
|
158
|
+
`sign_contract`, `pay_invoice` and `send_message` ask you to confirm before they act. A client that can show an MCP confirmation prompt (Claude Code) shows one. On a client that cannot (claude.ai, Claude Desktop), the first call does nothing and returns a preview plus a `confirmToken`; only a repeat call with the same arguments and that token proceeds. A token works once, and is refused if anything it covers changed between the two calls (the file's title, the message body, the workspace's recipients).
|
|
159
|
+
|
|
160
|
+
| variable | default | |
|
|
161
|
+
|---|---|---|
|
|
162
|
+
| `MCP_CONFIRM_MODE` | `ask-user` | What a write does on a client that cannot show a confirmation prompt (claude.ai, Claude Desktop). `ask-user`: two steps — the first call does nothing and returns a preview plus a token, and the model must get your approval in chat before calling again with it. `auto`: the same two steps, but the model may use the token after reviewing the preview itself. `refuse`: writes are refused on such clients. A client that can show prompts (Claude Code) always gets the real prompt. An unrecognised value is treated as `refuse`. |
|
|
163
|
+
| `MCP_CONFIRM_TTL_SECONDS` | `600` | How long a token stays valid. |
|
|
164
|
+
| `MCP_CONFIRM_SECRET` | random per process | Signing key; set it only if tokens must survive a server restart. |
|
|
155
165
|
|
|
156
166
|
## Troubleshooting
|
|
157
167
|
|
|
@@ -188,7 +198,7 @@ Nothing else changes: existing sessions in `~/.honeybook-mcp/sessions.json` keep
|
|
|
188
198
|
|
|
189
199
|
- Sessions are captured locally — auth tokens never leave your machine
|
|
190
200
|
- `~/.honeybook-mcp/sessions.json` is written with mode 0600; directory with mode 0700
|
|
191
|
-
- Write tools (`sign_contract`, `pay_invoice`)
|
|
201
|
+
- Write tools (`sign_contract`, `pay_invoice`, `send_message`) ask you to confirm first (see [Confirmations](#confirmations)); `sign_contract` and `pay_invoice` return portal deep links rather than signing/paying headlessly
|
|
192
202
|
|
|
193
203
|
## Development
|
|
194
204
|
|
package/dist/bundle.js
CHANGED
|
@@ -25547,7 +25547,7 @@ var require_websocket = __commonJS({
|
|
|
25547
25547
|
var http = __require("http");
|
|
25548
25548
|
var net = __require("net");
|
|
25549
25549
|
var tls = __require("tls");
|
|
25550
|
-
var { randomBytes:
|
|
25550
|
+
var { randomBytes: randomBytes5, createHash: createHash4 } = __require("crypto");
|
|
25551
25551
|
var { Duplex, Readable } = __require("stream");
|
|
25552
25552
|
var { URL: URL2 } = __require("url");
|
|
25553
25553
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -26085,7 +26085,7 @@ var require_websocket = __commonJS({
|
|
|
26085
26085
|
}
|
|
26086
26086
|
}
|
|
26087
26087
|
const defaultPort = isSecure ? 443 : 80;
|
|
26088
|
-
const key =
|
|
26088
|
+
const key = randomBytes5(16).toString("base64");
|
|
26089
26089
|
const request = isSecure ? https.request : http.request;
|
|
26090
26090
|
const protocolSet = /* @__PURE__ */ new Set();
|
|
26091
26091
|
let perMessageDeflate;
|
|
@@ -26215,7 +26215,7 @@ var require_websocket = __commonJS({
|
|
|
26215
26215
|
abortHandshake(websocket, socket, "Invalid Upgrade header");
|
|
26216
26216
|
return;
|
|
26217
26217
|
}
|
|
26218
|
-
const digest =
|
|
26218
|
+
const digest = createHash4("sha1").update(key + GUID).digest("base64");
|
|
26219
26219
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
26220
26220
|
abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
|
|
26221
26221
|
return;
|
|
@@ -26584,7 +26584,7 @@ var require_websocket_server = __commonJS({
|
|
|
26584
26584
|
var EventEmitter = __require("events");
|
|
26585
26585
|
var http = __require("http");
|
|
26586
26586
|
var { Duplex } = __require("stream");
|
|
26587
|
-
var { createHash } = __require("crypto");
|
|
26587
|
+
var { createHash: createHash4 } = __require("crypto");
|
|
26588
26588
|
var extension2 = require_extension();
|
|
26589
26589
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
26590
26590
|
var subprotocol2 = require_subprotocol();
|
|
@@ -26891,7 +26891,7 @@ var require_websocket_server = __commonJS({
|
|
|
26891
26891
|
);
|
|
26892
26892
|
}
|
|
26893
26893
|
if (this._state > RUNNING) return abortHandshake(socket, 503);
|
|
26894
|
-
const digest =
|
|
26894
|
+
const digest = createHash4("sha1").update(key + GUID).digest("base64");
|
|
26895
26895
|
const headers = [
|
|
26896
26896
|
"HTTP/1.1 101 Switching Protocols",
|
|
26897
26897
|
"Upgrade: websocket",
|
|
@@ -27011,6 +27011,33 @@ var storage2 = new AsyncLocalStorage2();
|
|
|
27011
27011
|
function withCallerCapabilities(capabilities, fn) {
|
|
27012
27012
|
return capabilities ? storage2.run(capabilities, fn) : fn();
|
|
27013
27013
|
}
|
|
27014
|
+
function currentCallerCapabilities() {
|
|
27015
|
+
return storage2.getStore();
|
|
27016
|
+
}
|
|
27017
|
+
var ENVELOPE_CAPABILITIES_KEY = "io.modelcontextprotocol/clientCapabilities";
|
|
27018
|
+
var ELICITATION_MODES = ["form", "url"];
|
|
27019
|
+
function isRecord(value) {
|
|
27020
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
27021
|
+
}
|
|
27022
|
+
function callerCapabilities(ctx) {
|
|
27023
|
+
const envelope = isRecord(ctx) && isRecord(ctx.mcpReq) ? ctx.mcpReq.envelope : void 0;
|
|
27024
|
+
if (isRecord(envelope)) {
|
|
27025
|
+
const declared = envelope[ENVELOPE_CAPABILITIES_KEY];
|
|
27026
|
+
if (isRecord(declared))
|
|
27027
|
+
return declared;
|
|
27028
|
+
}
|
|
27029
|
+
return currentCallerCapabilities();
|
|
27030
|
+
}
|
|
27031
|
+
function callerAcceptsFormElicitation(ctx) {
|
|
27032
|
+
const capabilities = callerCapabilities(ctx);
|
|
27033
|
+
if (!capabilities)
|
|
27034
|
+
return void 0;
|
|
27035
|
+
const elicitation = capabilities.elicitation;
|
|
27036
|
+
if (!isRecord(elicitation))
|
|
27037
|
+
return false;
|
|
27038
|
+
const namedModes = ELICITATION_MODES.filter((mode) => mode in elicitation);
|
|
27039
|
+
return namedModes.length === 0 || namedModes.includes("form");
|
|
27040
|
+
}
|
|
27014
27041
|
|
|
27015
27042
|
// node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
|
|
27016
27043
|
var __create2 = Object.create;
|
|
@@ -31409,6 +31436,37 @@ var inputRequired = Object.assign(buildInputRequired, {
|
|
|
31409
31436
|
return { method: "roots/list" };
|
|
31410
31437
|
}
|
|
31411
31438
|
});
|
|
31439
|
+
function acceptedContent(responses, key, schema) {
|
|
31440
|
+
const view = inputResponse(responses, key);
|
|
31441
|
+
if (view.kind !== "elicit" || view.action !== "accept" || view.content === void 0) return void 0;
|
|
31442
|
+
if (schema === void 0) return view.content;
|
|
31443
|
+
const outcome = schema["~standard"].validate(view.content);
|
|
31444
|
+
if (outcome instanceof Promise) throw new TypeError("acceptedContent(responses, key, schema) requires a synchronously-validating schema");
|
|
31445
|
+
return outcome.issues === void 0 ? outcome.value : void 0;
|
|
31446
|
+
}
|
|
31447
|
+
function inputResponse(responses, key) {
|
|
31448
|
+
if (responses === void 0 || typeof responses !== "object" || responses === null) return { kind: "missing" };
|
|
31449
|
+
const entry = responses[key];
|
|
31450
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return { kind: "missing" };
|
|
31451
|
+
const candidate = entry;
|
|
31452
|
+
if (candidate["action"] === "accept" || candidate["action"] === "decline" || candidate["action"] === "cancel") {
|
|
31453
|
+
const content = candidate["content"];
|
|
31454
|
+
return {
|
|
31455
|
+
kind: "elicit",
|
|
31456
|
+
action: candidate["action"],
|
|
31457
|
+
...content !== null && typeof content === "object" && !Array.isArray(content) && { content }
|
|
31458
|
+
};
|
|
31459
|
+
}
|
|
31460
|
+
if (Array.isArray(candidate["roots"])) return {
|
|
31461
|
+
kind: "roots",
|
|
31462
|
+
roots: candidate["roots"]
|
|
31463
|
+
};
|
|
31464
|
+
if (typeof candidate["role"] === "string" && candidate["content"] !== void 0) return {
|
|
31465
|
+
kind: "sampling",
|
|
31466
|
+
result: candidate
|
|
31467
|
+
};
|
|
31468
|
+
return { kind: "missing" };
|
|
31469
|
+
}
|
|
31412
31470
|
var REQUEST_STATE_ONLY_LEG_PACING_MS = 250;
|
|
31413
31471
|
function inputRequiredRoundsExceededMessage(method, maxRounds) {
|
|
31414
31472
|
return `Multi-round-trip request '${method}' still required input after ${maxRounds} rounds (inputRequired.maxRounds)`;
|
|
@@ -41456,14 +41514,14 @@ function serveStdio(factory, options = {}) {
|
|
|
41456
41514
|
return false;
|
|
41457
41515
|
};
|
|
41458
41516
|
const answerLegacyRejection = (request, reason, requestedVersion) => {
|
|
41459
|
-
const
|
|
41517
|
+
const rejection3 = modernOnlyStrictRejection({
|
|
41460
41518
|
kind: "legacy",
|
|
41461
41519
|
reason,
|
|
41462
41520
|
...requestedVersion !== void 0 && { requestedVersion }
|
|
41463
41521
|
}, SUPPORTED_MODERN_PROTOCOL_VERSIONS);
|
|
41464
|
-
if (
|
|
41465
|
-
reportError(/* @__PURE__ */ new Error(`Rejected 2025-era request on a modern-only stdio connection (${
|
|
41466
|
-
return writeErrorResponse(request.id,
|
|
41522
|
+
if (rejection3 === void 0) return Promise.resolve();
|
|
41523
|
+
reportError(/* @__PURE__ */ new Error(`Rejected 2025-era request on a modern-only stdio connection (${rejection3.cell}): ${rejection3.message}`));
|
|
41524
|
+
return writeErrorResponse(request.id, rejection3.code, rejection3.message, rejection3.data);
|
|
41467
41525
|
};
|
|
41468
41526
|
const onInstanceClosed = (channel) => {
|
|
41469
41527
|
if (closing || channel === discarding) return;
|
|
@@ -61427,6 +61485,11 @@ var MEDIA_QUALIFIER = "(?:primary|secondary|main|default|cover|hero|profile|mast
|
|
|
61427
61485
|
var MEDIA_KEY = new RegExp(`^(?:(?:${MEDIA_QUALIFIER}|${MEDIA_NOUN})[_-]?)?${MEDIA_NOUN}s?(?:[_-]?(?:link|uri|url|src)s?)?$`, "i");
|
|
61428
61486
|
|
|
61429
61487
|
// node_modules/@chrischall/mcp-utils/dist/response/index.js
|
|
61488
|
+
function textResult(data) {
|
|
61489
|
+
return {
|
|
61490
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
61491
|
+
};
|
|
61492
|
+
}
|
|
61430
61493
|
function rawTextResult(text) {
|
|
61431
61494
|
return { content: [{ type: "text", text }] };
|
|
61432
61495
|
}
|
|
@@ -61437,6 +61500,421 @@ function errorResult(message) {
|
|
|
61437
61500
|
};
|
|
61438
61501
|
}
|
|
61439
61502
|
|
|
61503
|
+
// node_modules/@chrischall/mcp-utils/dist/server/confirmation.js
|
|
61504
|
+
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
|
|
61505
|
+
|
|
61506
|
+
// node_modules/@chrischall/mcp-utils/dist/server/canonical.js
|
|
61507
|
+
function canonicalJson(value) {
|
|
61508
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
61509
|
+
return JSON.stringify(value);
|
|
61510
|
+
if (value === void 0)
|
|
61511
|
+
return "null";
|
|
61512
|
+
if (typeof value === "number")
|
|
61513
|
+
return Number.isFinite(value) ? JSON.stringify(value) : `{"$num":"${String(value)}"}`;
|
|
61514
|
+
if (typeof value === "bigint")
|
|
61515
|
+
return `{"$bigint":"${value.toString()}"}`;
|
|
61516
|
+
if (typeof value === "function" || typeof value === "symbol") {
|
|
61517
|
+
throw new TypeError(`confirmation: cannot bind a ${typeof value} argument value.`);
|
|
61518
|
+
}
|
|
61519
|
+
if (Array.isArray(value))
|
|
61520
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
61521
|
+
if (value instanceof Date) {
|
|
61522
|
+
return `{"$date":${JSON.stringify(Number.isNaN(value.getTime()) ? "Invalid Date" : value.toISOString())}}`;
|
|
61523
|
+
}
|
|
61524
|
+
if (ArrayBuffer.isView(value)) {
|
|
61525
|
+
const bytes = Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
61526
|
+
return `{"$bytes":${JSON.stringify(bytes.toString("base64"))}}`;
|
|
61527
|
+
}
|
|
61528
|
+
if (value instanceof ArrayBuffer)
|
|
61529
|
+
return `{"$bytes":${JSON.stringify(Buffer.from(value).toString("base64"))}}`;
|
|
61530
|
+
if (value instanceof Map) {
|
|
61531
|
+
const entries2 = [...value.entries()].map(([k, v]) => `[${canonicalJson(k)},${canonicalJson(v)}]`).sort();
|
|
61532
|
+
return `{"$map":[${entries2.join(",")}]}`;
|
|
61533
|
+
}
|
|
61534
|
+
if (value instanceof Set) {
|
|
61535
|
+
return `{"$set":[${[...value].map(canonicalJson).sort().join(",")}]}`;
|
|
61536
|
+
}
|
|
61537
|
+
const proto = Object.getPrototypeOf(value);
|
|
61538
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
61539
|
+
throw new TypeError("confirmation: cannot bind a non-plain object argument value (class instance).");
|
|
61540
|
+
}
|
|
61541
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
61542
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(escapeKey(k))}:${canonicalJson(v)}`).join(",")}}`;
|
|
61543
|
+
}
|
|
61544
|
+
function escapeKey(key) {
|
|
61545
|
+
return key.startsWith("$") ? `$${key}` : key;
|
|
61546
|
+
}
|
|
61547
|
+
|
|
61548
|
+
// node_modules/@chrischall/mcp-utils/dist/server/confirmation.js
|
|
61549
|
+
var DEFAULT_REQUEST_KEY = "confirmation";
|
|
61550
|
+
var DEFAULT_CONFIRMATION_LABEL = "Confirm this action should proceed.";
|
|
61551
|
+
var UNSUPPORTED_NOTE = "Nothing was done because this client cannot show a confirmation prompt (it declares no MCP elicitation capability), and this action is never taken without one";
|
|
61552
|
+
var STATE_PREFIX = "mcpu.confirm.v1.";
|
|
61553
|
+
function bindingKey(binding) {
|
|
61554
|
+
if (binding.ttlSeconds !== void 0 && !(Number.isFinite(binding.ttlSeconds) && binding.ttlSeconds > 0)) {
|
|
61555
|
+
throw new RangeError("requireConfirmation: binding.ttlSeconds must be a finite number greater than 0.");
|
|
61556
|
+
}
|
|
61557
|
+
const key = typeof binding.key === "string" ? Buffer.from(binding.key, "utf8") : Buffer.from(binding.key);
|
|
61558
|
+
if (key.length < 32) {
|
|
61559
|
+
throw new RangeError("requireConfirmation: binding.key must be at least 32 bytes.");
|
|
61560
|
+
}
|
|
61561
|
+
return key;
|
|
61562
|
+
}
|
|
61563
|
+
function commitment(action, args) {
|
|
61564
|
+
return createHash("sha256").update(`${action}\0${canonicalJson(args)}`).digest("base64url");
|
|
61565
|
+
}
|
|
61566
|
+
function mintState(key, action, binding) {
|
|
61567
|
+
const exp = Math.floor(Date.now() / 1e3) + (binding.ttlSeconds ?? 600);
|
|
61568
|
+
const body = Buffer.from(JSON.stringify({ c: commitment(action, binding.args), exp })).toString("base64url");
|
|
61569
|
+
const mac3 = createHmac("sha256", key).update(`${STATE_PREFIX}${body}`).digest("base64url");
|
|
61570
|
+
return `${STATE_PREFIX}${body}.${mac3}`;
|
|
61571
|
+
}
|
|
61572
|
+
function verifyState(key, action, binding, state) {
|
|
61573
|
+
if (typeof state !== "string" || !state.startsWith(STATE_PREFIX))
|
|
61574
|
+
return false;
|
|
61575
|
+
const rest = state.slice(STATE_PREFIX.length);
|
|
61576
|
+
const dot = rest.indexOf(".");
|
|
61577
|
+
if (dot < 0)
|
|
61578
|
+
return false;
|
|
61579
|
+
const body = rest.slice(0, dot);
|
|
61580
|
+
const mac3 = Buffer.from(rest.slice(dot + 1), "base64url");
|
|
61581
|
+
const expected = createHmac("sha256", key).update(`${STATE_PREFIX}${body}`).digest();
|
|
61582
|
+
if (mac3.length !== expected.length || !timingSafeEqual(mac3, expected))
|
|
61583
|
+
return false;
|
|
61584
|
+
let payload;
|
|
61585
|
+
try {
|
|
61586
|
+
payload = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
|
|
61587
|
+
} catch {
|
|
61588
|
+
return false;
|
|
61589
|
+
}
|
|
61590
|
+
if (typeof payload.exp !== "number" || payload.exp * 1e3 <= Date.now())
|
|
61591
|
+
return false;
|
|
61592
|
+
const want = Buffer.from(commitment(action, binding.args));
|
|
61593
|
+
const got = Buffer.from(typeof payload.c === "string" ? payload.c : "");
|
|
61594
|
+
return got.length === want.length && timingSafeEqual(got, want);
|
|
61595
|
+
}
|
|
61596
|
+
function echoedState(ctx) {
|
|
61597
|
+
const accessor = ctx.mcpReq.requestState;
|
|
61598
|
+
return typeof accessor === "function" ? accessor() : void 0;
|
|
61599
|
+
}
|
|
61600
|
+
function requireConfirmation(ctx, options) {
|
|
61601
|
+
const requestKey = options.requestKey ?? DEFAULT_REQUEST_KEY;
|
|
61602
|
+
const key = options.binding ? bindingKey(options.binding) : void 0;
|
|
61603
|
+
const confirmationSchema = external_exports.object({
|
|
61604
|
+
confirmed: external_exports.boolean().describe(options.confirmationLabel ?? DEFAULT_CONFIRMATION_LABEL)
|
|
61605
|
+
});
|
|
61606
|
+
const ask = () => {
|
|
61607
|
+
const preview = {
|
|
61608
|
+
action: options.action,
|
|
61609
|
+
...options.details === void 0 ? {} : { details: options.details }
|
|
61610
|
+
};
|
|
61611
|
+
return inputRequired({
|
|
61612
|
+
inputRequests: {
|
|
61613
|
+
[requestKey]: inputRequired.elicit({
|
|
61614
|
+
message: `${options.message}
|
|
61615
|
+
${JSON.stringify(preview, null, 2)}`,
|
|
61616
|
+
requestedSchema: confirmationSchema
|
|
61617
|
+
})
|
|
61618
|
+
},
|
|
61619
|
+
...key && options.binding ? { requestState: mintState(key, options.action, options.binding) } : {}
|
|
61620
|
+
});
|
|
61621
|
+
};
|
|
61622
|
+
const response = inputResponse(ctx.mcpReq.inputResponses, requestKey);
|
|
61623
|
+
const accepted = acceptedContent(ctx.mcpReq.inputResponses, requestKey, confirmationSchema);
|
|
61624
|
+
if (response.kind === "missing") {
|
|
61625
|
+
if (callerAcceptsFormElicitation(ctx) === false) {
|
|
61626
|
+
return textResult({
|
|
61627
|
+
confirmed: false,
|
|
61628
|
+
dispatched: false,
|
|
61629
|
+
action: options.action,
|
|
61630
|
+
reason: "confirmation-unsupported",
|
|
61631
|
+
note: options.unsupportedNote ? `${UNSUPPORTED_NOTE}. ${options.unsupportedNote}` : `${UNSUPPORTED_NOTE}.`
|
|
61632
|
+
});
|
|
61633
|
+
}
|
|
61634
|
+
return ask();
|
|
61635
|
+
}
|
|
61636
|
+
if (response.kind === "elicit" && response.action === "accept" && accepted?.confirmed === true) {
|
|
61637
|
+
if (key && options.binding) {
|
|
61638
|
+
const state = echoedState(ctx);
|
|
61639
|
+
if (state === void 0 || state === null) {
|
|
61640
|
+
return errorResult(`Confirmation for ${options.action} was accepted, but the retry carried no requestState, so it cannot be checked against the prompt that was shown. Nothing was done. The likely cause is that the MCP client or host does not round-trip requestState (it must echo the requestState from the input_required result back on the retry); asking again would loop.`);
|
|
61641
|
+
}
|
|
61642
|
+
if (!verifyState(key, options.action, options.binding, state))
|
|
61643
|
+
return ask();
|
|
61644
|
+
}
|
|
61645
|
+
return void 0;
|
|
61646
|
+
}
|
|
61647
|
+
return textResult({
|
|
61648
|
+
confirmed: false,
|
|
61649
|
+
cancelled: true,
|
|
61650
|
+
action: options.action,
|
|
61651
|
+
note: "Nothing was changed because the confirmation was declined, cancelled, or left unchecked."
|
|
61652
|
+
});
|
|
61653
|
+
}
|
|
61654
|
+
|
|
61655
|
+
// node_modules/@chrischall/mcp-utils/dist/server/confirm-token.js
|
|
61656
|
+
import { createHash as createHash2, createHmac as createHmac2, randomBytes, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
61657
|
+
function createSpentTokenStore() {
|
|
61658
|
+
const spent = /* @__PURE__ */ new Map();
|
|
61659
|
+
return {
|
|
61660
|
+
has: (nonce) => spent.has(nonce),
|
|
61661
|
+
add: (nonce, exp) => {
|
|
61662
|
+
spent.set(nonce, exp);
|
|
61663
|
+
},
|
|
61664
|
+
prune: (now) => {
|
|
61665
|
+
for (const [nonce, exp] of spent)
|
|
61666
|
+
if (exp < now)
|
|
61667
|
+
spent.delete(nonce);
|
|
61668
|
+
},
|
|
61669
|
+
clear: () => spent.clear(),
|
|
61670
|
+
get size() {
|
|
61671
|
+
return spent.size;
|
|
61672
|
+
}
|
|
61673
|
+
};
|
|
61674
|
+
}
|
|
61675
|
+
var PROCESS_SPENT = createSpentTokenStore();
|
|
61676
|
+
var PREFIX = "mcpu.token.v1.";
|
|
61677
|
+
var DEFAULT_TTL_SECONDS = 600;
|
|
61678
|
+
function tokenKey(key) {
|
|
61679
|
+
const bytes = typeof key === "string" ? Buffer.from(key, "utf8") : Buffer.from(key);
|
|
61680
|
+
if (bytes.length < 32)
|
|
61681
|
+
throw new RangeError("confirm token: key must be at least 32 bytes.");
|
|
61682
|
+
return bytes;
|
|
61683
|
+
}
|
|
61684
|
+
function ttlOf(ttlSeconds) {
|
|
61685
|
+
if (ttlSeconds === void 0)
|
|
61686
|
+
return DEFAULT_TTL_SECONDS;
|
|
61687
|
+
if (!(Number.isFinite(ttlSeconds) && ttlSeconds > 0)) {
|
|
61688
|
+
throw new RangeError("confirm token: ttlSeconds must be a finite number greater than 0.");
|
|
61689
|
+
}
|
|
61690
|
+
return ttlSeconds;
|
|
61691
|
+
}
|
|
61692
|
+
function sign(key, body) {
|
|
61693
|
+
return createHmac2("sha256", key).update(`${PREFIX}${body}`).digest();
|
|
61694
|
+
}
|
|
61695
|
+
function hashConfirmPayload(payload) {
|
|
61696
|
+
return createHash2("sha256").update(canonicalJson(payload)).digest("base64url");
|
|
61697
|
+
}
|
|
61698
|
+
function issueConfirmToken(key, binding, options = {}) {
|
|
61699
|
+
const k = tokenKey(key);
|
|
61700
|
+
const now = options.now ?? Date.now();
|
|
61701
|
+
const exp = now + ttlOf(options.ttlSeconds) * 1e3;
|
|
61702
|
+
const claims = {
|
|
61703
|
+
t: binding.tool,
|
|
61704
|
+
...binding.account === void 0 ? {} : { a: binding.account },
|
|
61705
|
+
g: binding.target,
|
|
61706
|
+
...binding.revision === void 0 ? {} : { r: binding.revision },
|
|
61707
|
+
h: binding.payloadHash,
|
|
61708
|
+
exp,
|
|
61709
|
+
n: randomBytes(16).toString("base64url")
|
|
61710
|
+
};
|
|
61711
|
+
const body = Buffer.from(JSON.stringify(claims), "utf8").toString("base64url");
|
|
61712
|
+
return { token: `${PREFIX}${body}.${sign(k, body).toString("base64url")}`, expiresAt: new Date(exp).toISOString() };
|
|
61713
|
+
}
|
|
61714
|
+
function parseClaims(body) {
|
|
61715
|
+
try {
|
|
61716
|
+
const claims = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
|
|
61717
|
+
return claims && typeof claims === "object" ? claims : void 0;
|
|
61718
|
+
} catch {
|
|
61719
|
+
return void 0;
|
|
61720
|
+
}
|
|
61721
|
+
}
|
|
61722
|
+
function verifyConfirmToken(key, token, binding, options = {}) {
|
|
61723
|
+
const k = tokenKey(key);
|
|
61724
|
+
const now = options.now ?? Date.now();
|
|
61725
|
+
const spent = options.spent ?? PROCESS_SPENT;
|
|
61726
|
+
spent.prune(now);
|
|
61727
|
+
if (!token.startsWith(PREFIX))
|
|
61728
|
+
return { ok: false, error: "TOKEN_INVALID" };
|
|
61729
|
+
const rest = token.slice(PREFIX.length);
|
|
61730
|
+
const dot = rest.indexOf(".");
|
|
61731
|
+
const body = dot < 0 ? "" : rest.slice(0, dot);
|
|
61732
|
+
const mac3 = Buffer.from(dot < 0 ? "" : rest.slice(dot + 1), "base64url");
|
|
61733
|
+
const expected = sign(k, body);
|
|
61734
|
+
if (!body || mac3.length !== expected.length || !timingSafeEqual2(mac3, expected))
|
|
61735
|
+
return { ok: false, error: "TOKEN_INVALID" };
|
|
61736
|
+
const claims = parseClaims(body);
|
|
61737
|
+
if (!claims)
|
|
61738
|
+
return { ok: false, error: "TOKEN_INVALID" };
|
|
61739
|
+
if (claims.t !== binding.tool || claims.a !== binding.account || claims.g !== binding.target) {
|
|
61740
|
+
return { ok: false, error: "TOKEN_INVALID" };
|
|
61741
|
+
}
|
|
61742
|
+
if (spent.has(claims.n))
|
|
61743
|
+
return { ok: false, error: "TOKEN_REUSED" };
|
|
61744
|
+
if (now > claims.exp)
|
|
61745
|
+
return { ok: false, error: "TOKEN_EXPIRED" };
|
|
61746
|
+
if (claims.r !== binding.revision)
|
|
61747
|
+
return { ok: false, error: "DRAFT_CHANGED", reason: "revision-changed" };
|
|
61748
|
+
if (claims.h !== binding.payloadHash)
|
|
61749
|
+
return { ok: false, error: "DRAFT_CHANGED", reason: "payload-changed" };
|
|
61750
|
+
spent.add(claims.n, claims.exp);
|
|
61751
|
+
return { ok: true };
|
|
61752
|
+
}
|
|
61753
|
+
var CONFIRM_TOKEN_INSTRUCTION = "Show this preview to the user verbatim and proceed only after they explicitly approve in chat. Then call again with confirmToken.";
|
|
61754
|
+
var confirmTokenParam = external_exports.string().optional().describe(`ONLY for the two-step confirmation fallback (a client without MCP elicitation). The confirmToken from this same tool's phase-1 "confirmation-required" response, passed back ONLY after the user has seen that preview and explicitly approved it in chat \u2014 never on the first call, never invented, never reused. Call again with the same arguments. Ignored when the client supports elicitation.`);
|
|
61755
|
+
var TOKEN_ERROR_NOTE = {
|
|
61756
|
+
TOKEN_EXPIRED: "Nothing was sent or changed: the confirmToken expired. Call again WITHOUT confirmToken for a fresh preview, and ask the user to approve it again.",
|
|
61757
|
+
TOKEN_REUSED: "Nothing was sent or changed by this call: this confirmToken was already used, and one approval acts once. If doing it again is really intended, call again WITHOUT confirmToken and get a new approval.",
|
|
61758
|
+
TOKEN_INVALID: "Nothing was sent or changed: this confirmToken was not issued by this server for this tool, account and target (or the server has restarted since). Call again WITHOUT confirmToken for a fresh preview and approval."
|
|
61759
|
+
};
|
|
61760
|
+
var CHANGED_NOTE = {
|
|
61761
|
+
"revision-changed": "Nothing was sent or changed: the target was edited since the user approved it (its version rotated), so what would happen is not what they saw.",
|
|
61762
|
+
"payload-changed": "Nothing was sent or changed: what would happen no longer matches what the user approved."
|
|
61763
|
+
};
|
|
61764
|
+
function isToolResult(value) {
|
|
61765
|
+
return Array.isArray(value.content);
|
|
61766
|
+
}
|
|
61767
|
+
function rejection2(data) {
|
|
61768
|
+
return { ...textResult({ status: "confirmation-rejected", confirmed: false, dispatched: false, ...data }), isError: true };
|
|
61769
|
+
}
|
|
61770
|
+
async function tokenConfirmation(action, fb) {
|
|
61771
|
+
const subject = await fb.subject();
|
|
61772
|
+
if (isToolResult(subject))
|
|
61773
|
+
return subject;
|
|
61774
|
+
const binding = {
|
|
61775
|
+
tool: fb.tool,
|
|
61776
|
+
...fb.account === void 0 ? {} : { account: fb.account },
|
|
61777
|
+
target: subject.target,
|
|
61778
|
+
...subject.revision === void 0 ? {} : { revision: subject.revision },
|
|
61779
|
+
payloadHash: hashConfirmPayload(subject.payload)
|
|
61780
|
+
};
|
|
61781
|
+
const phaseOne = () => {
|
|
61782
|
+
const { token, expiresAt } = issueConfirmToken(fb.key, binding, { ttlSeconds: fb.ttlSeconds });
|
|
61783
|
+
return {
|
|
61784
|
+
action,
|
|
61785
|
+
preview: subject.preview,
|
|
61786
|
+
confirmToken: token,
|
|
61787
|
+
expiresAt,
|
|
61788
|
+
ttlSeconds: ttlOf(fb.ttlSeconds),
|
|
61789
|
+
instruction: fb.instruction ?? CONFIRM_TOKEN_INSTRUCTION
|
|
61790
|
+
};
|
|
61791
|
+
};
|
|
61792
|
+
if (!fb.confirmToken) {
|
|
61793
|
+
return textResult({ status: "confirmation-required", confirmed: false, dispatched: false, ...phaseOne() });
|
|
61794
|
+
}
|
|
61795
|
+
const verdict = verifyConfirmToken(fb.key, fb.confirmToken, binding, { spent: fb.spent });
|
|
61796
|
+
if (verdict.ok)
|
|
61797
|
+
return void 0;
|
|
61798
|
+
if (verdict.error === "DRAFT_CHANGED") {
|
|
61799
|
+
return rejection2({
|
|
61800
|
+
error: "DRAFT_CHANGED",
|
|
61801
|
+
reason: verdict.reason,
|
|
61802
|
+
note: `${CHANGED_NOTE[verdict.reason]} The current preview and a fresh confirmToken are below.`,
|
|
61803
|
+
...phaseOne()
|
|
61804
|
+
});
|
|
61805
|
+
}
|
|
61806
|
+
return rejection2({ error: verdict.error, action, note: TOKEN_ERROR_NOTE[verdict.error] });
|
|
61807
|
+
}
|
|
61808
|
+
async function requireConfirmationWithFallback(ctx, options) {
|
|
61809
|
+
const { tokenFallback, ...confirmation } = options;
|
|
61810
|
+
if (tokenFallback && callerAcceptsFormElicitation(ctx) === false) {
|
|
61811
|
+
return tokenConfirmation(options.action, tokenFallback);
|
|
61812
|
+
}
|
|
61813
|
+
return requireConfirmation(ctx, confirmation);
|
|
61814
|
+
}
|
|
61815
|
+
|
|
61816
|
+
// node_modules/@chrischall/mcp-utils/dist/server/confirm-env.js
|
|
61817
|
+
import { createHash as createHash3, randomBytes as randomBytes2 } from "node:crypto";
|
|
61818
|
+
|
|
61819
|
+
// node_modules/@chrischall/mcp-utils/dist/config/index.js
|
|
61820
|
+
var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
|
|
61821
|
+
function readEnvVar(key, opts = {}) {
|
|
61822
|
+
const env = opts.env ?? process.env;
|
|
61823
|
+
const raw = env[key];
|
|
61824
|
+
if (typeof raw === "string") {
|
|
61825
|
+
const trimmed = raw.trim();
|
|
61826
|
+
if (trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null" && !PLACEHOLDER_RE.test(trimmed)) {
|
|
61827
|
+
return trimmed;
|
|
61828
|
+
}
|
|
61829
|
+
}
|
|
61830
|
+
return opts.default;
|
|
61831
|
+
}
|
|
61832
|
+
var TRUE_TOKENS = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
|
|
61833
|
+
var FALSE_TOKENS = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
|
|
61834
|
+
function parseBoolEnv(key, opts = {}) {
|
|
61835
|
+
const fallback = opts.default ?? false;
|
|
61836
|
+
const raw = readEnvVar(key, { env: opts.env });
|
|
61837
|
+
if (raw === void 0)
|
|
61838
|
+
return fallback;
|
|
61839
|
+
const token = raw.toLowerCase();
|
|
61840
|
+
if (TRUE_TOKENS.has(token))
|
|
61841
|
+
return true;
|
|
61842
|
+
if (FALSE_TOKENS.has(token))
|
|
61843
|
+
return false;
|
|
61844
|
+
return fallback;
|
|
61845
|
+
}
|
|
61846
|
+
async function loadDotenvSafely(opts = {}) {
|
|
61847
|
+
try {
|
|
61848
|
+
const mod = await import(
|
|
61849
|
+
/* @vite-ignore */
|
|
61850
|
+
"dotenv"
|
|
61851
|
+
);
|
|
61852
|
+
const result = mod.config({
|
|
61853
|
+
...opts.path !== void 0 ? { path: opts.path } : {},
|
|
61854
|
+
override: opts.override ?? false,
|
|
61855
|
+
quiet: true
|
|
61856
|
+
});
|
|
61857
|
+
return result.error === void 0;
|
|
61858
|
+
} catch {
|
|
61859
|
+
return false;
|
|
61860
|
+
}
|
|
61861
|
+
}
|
|
61862
|
+
|
|
61863
|
+
// node_modules/@chrischall/mcp-utils/dist/server/confirm-env.js
|
|
61864
|
+
var MODES = /* @__PURE__ */ new Set(["ask-user", "auto", "refuse"]);
|
|
61865
|
+
var DEFAULT_TTL_SECONDS2 = 600;
|
|
61866
|
+
var warned = /* @__PURE__ */ new Set();
|
|
61867
|
+
function readConfirmMode(env = process.env) {
|
|
61868
|
+
const raw = readEnvVar("MCP_CONFIRM_MODE", { env })?.trim().toLowerCase();
|
|
61869
|
+
if (!raw)
|
|
61870
|
+
return "ask-user";
|
|
61871
|
+
if (MODES.has(raw))
|
|
61872
|
+
return raw;
|
|
61873
|
+
if (!warned.has(raw)) {
|
|
61874
|
+
warned.add(raw);
|
|
61875
|
+
process.stderr.write(`MCP_CONFIRM_MODE="${raw}" is not one of ask-user, auto, refuse; treating it as refuse.
|
|
61876
|
+
`);
|
|
61877
|
+
}
|
|
61878
|
+
return "refuse";
|
|
61879
|
+
}
|
|
61880
|
+
function confirmTtlFromEnv(env = process.env) {
|
|
61881
|
+
const raw = readEnvVar("MCP_CONFIRM_TTL_SECONDS", { env })?.trim();
|
|
61882
|
+
return raw && /^[1-9]\d*$/.test(raw) ? Number(raw) : DEFAULT_TTL_SECONDS2;
|
|
61883
|
+
}
|
|
61884
|
+
var processKey;
|
|
61885
|
+
function confirmKeyFromEnv(env = process.env) {
|
|
61886
|
+
const secret = readEnvVar("MCP_CONFIRM_SECRET", { env });
|
|
61887
|
+
if (secret)
|
|
61888
|
+
return createHash3("sha256").update(secret, "utf8").digest();
|
|
61889
|
+
processKey ??= randomBytes2(32);
|
|
61890
|
+
return processKey;
|
|
61891
|
+
}
|
|
61892
|
+
var CONFIRM_TOKEN_AUTO_INSTRUCTION = "Nothing has been done yet. Review this preview; if it is what was intended, call again with the same arguments plus confirmToken. (This server runs with MCP_CONFIRM_MODE=auto, so the user's approval in chat is not required.)";
|
|
61893
|
+
var REFUSE_HINT = "Set MCP_CONFIRM_MODE=ask-user on the server to allow two-step confirmation instead.";
|
|
61894
|
+
function confirmationFromEnv(options) {
|
|
61895
|
+
const { tool, account, confirmToken, subject, instruction, spent, env = process.env, ...confirmation } = options;
|
|
61896
|
+
const mode = readConfirmMode(env);
|
|
61897
|
+
if (mode === "refuse") {
|
|
61898
|
+
return {
|
|
61899
|
+
...confirmation,
|
|
61900
|
+
unsupportedNote: confirmation.unsupportedNote ? `${confirmation.unsupportedNote} ${REFUSE_HINT}` : REFUSE_HINT
|
|
61901
|
+
};
|
|
61902
|
+
}
|
|
61903
|
+
return {
|
|
61904
|
+
...confirmation,
|
|
61905
|
+
tokenFallback: {
|
|
61906
|
+
key: confirmKeyFromEnv(env),
|
|
61907
|
+
tool,
|
|
61908
|
+
...account === void 0 ? {} : { account },
|
|
61909
|
+
...confirmToken === void 0 ? {} : { confirmToken },
|
|
61910
|
+
subject,
|
|
61911
|
+
ttlSeconds: confirmTtlFromEnv(env),
|
|
61912
|
+
instruction: mode === "auto" ? CONFIRM_TOKEN_AUTO_INSTRUCTION : instruction ?? CONFIRM_TOKEN_INSTRUCTION,
|
|
61913
|
+
...spent === void 0 ? {} : { spent }
|
|
61914
|
+
}
|
|
61915
|
+
};
|
|
61916
|
+
}
|
|
61917
|
+
|
|
61440
61918
|
// node_modules/@chrischall/mcp-utils/dist/server/index.js
|
|
61441
61919
|
var SERVER_PROTOCOL_VERSIONS = Object.freeze([
|
|
61442
61920
|
"2026-07-28",
|
|
@@ -61612,50 +62090,6 @@ function runMcp(opts) {
|
|
|
61612
62090
|
return handle;
|
|
61613
62091
|
}
|
|
61614
62092
|
|
|
61615
|
-
// node_modules/@chrischall/mcp-utils/dist/config/index.js
|
|
61616
|
-
var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
|
|
61617
|
-
function readEnvVar(key, opts = {}) {
|
|
61618
|
-
const env = opts.env ?? process.env;
|
|
61619
|
-
const raw = env[key];
|
|
61620
|
-
if (typeof raw === "string") {
|
|
61621
|
-
const trimmed = raw.trim();
|
|
61622
|
-
if (trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null" && !PLACEHOLDER_RE.test(trimmed)) {
|
|
61623
|
-
return trimmed;
|
|
61624
|
-
}
|
|
61625
|
-
}
|
|
61626
|
-
return opts.default;
|
|
61627
|
-
}
|
|
61628
|
-
var TRUE_TOKENS = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
|
|
61629
|
-
var FALSE_TOKENS = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
|
|
61630
|
-
function parseBoolEnv(key, opts = {}) {
|
|
61631
|
-
const fallback = opts.default ?? false;
|
|
61632
|
-
const raw = readEnvVar(key, { env: opts.env });
|
|
61633
|
-
if (raw === void 0)
|
|
61634
|
-
return fallback;
|
|
61635
|
-
const token = raw.toLowerCase();
|
|
61636
|
-
if (TRUE_TOKENS.has(token))
|
|
61637
|
-
return true;
|
|
61638
|
-
if (FALSE_TOKENS.has(token))
|
|
61639
|
-
return false;
|
|
61640
|
-
return fallback;
|
|
61641
|
-
}
|
|
61642
|
-
async function loadDotenvSafely(opts = {}) {
|
|
61643
|
-
try {
|
|
61644
|
-
const mod = await import(
|
|
61645
|
-
/* @vite-ignore */
|
|
61646
|
-
"dotenv"
|
|
61647
|
-
);
|
|
61648
|
-
const result = mod.config({
|
|
61649
|
-
...opts.path !== void 0 ? { path: opts.path } : {},
|
|
61650
|
-
override: opts.override ?? false,
|
|
61651
|
-
quiet: true
|
|
61652
|
-
});
|
|
61653
|
-
return result.error === void 0;
|
|
61654
|
-
} catch {
|
|
61655
|
-
return false;
|
|
61656
|
-
}
|
|
61657
|
-
}
|
|
61658
|
-
|
|
61659
62093
|
// node_modules/@chrischall/mcp-utils/dist/http/index.js
|
|
61660
62094
|
function formatApiError(status, method, path, errorText, opts = {}) {
|
|
61661
62095
|
const service = opts.service ?? "API";
|
|
@@ -63977,12 +64411,12 @@ async function awaitSessionReady(ready, opts) {
|
|
|
63977
64411
|
|
|
63978
64412
|
// node_modules/@fetchproxy/server/dist/extension-trust.js
|
|
63979
64413
|
import { readFile as readFile2, writeFile as writeFile2, rename as rename2, unlink as unlink2, mkdir as mkdir2, chmod as chmod2, rm as rm2 } from "node:fs/promises";
|
|
63980
|
-
import { randomBytes as
|
|
64414
|
+
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
63981
64415
|
import { isAbsolute as isAbsolute2, join as join5 } from "node:path";
|
|
63982
64416
|
|
|
63983
64417
|
// node_modules/@fetchproxy/server/dist/identity.js
|
|
63984
64418
|
import { readFile, readdir, writeFile, mkdir, chmod, rename, rm, unlink, link, lstat } from "node:fs/promises";
|
|
63985
|
-
import { randomBytes } from "node:crypto";
|
|
64419
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
63986
64420
|
import { isAbsolute, join as join4 } from "node:path";
|
|
63987
64421
|
import { homedir as homedir2 } from "node:os";
|
|
63988
64422
|
var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
|
|
@@ -64103,7 +64537,7 @@ var NO_HARD_LINKS = /* @__PURE__ */ new Set(["EPERM", "ENOTSUP", "EOPNOTSUPP", "
|
|
|
64103
64537
|
async function publishIdentityIfAbsent(dir, serverName, id) {
|
|
64104
64538
|
const path = identityFilePath(dir, serverName);
|
|
64105
64539
|
await sweepStagingFiles(dir, serverName, { olderThanMs: 6e4 });
|
|
64106
|
-
const tmp = `${path}.${
|
|
64540
|
+
const tmp = `${path}.${randomBytes3(STAGING_BYTES).toString("hex")}.tmp`;
|
|
64107
64541
|
try {
|
|
64108
64542
|
await writeFile(tmp, serializeIdentity(id), { mode: 384, flag: "wx" });
|
|
64109
64543
|
await chmod(tmp, 384);
|
|
@@ -64232,7 +64666,7 @@ async function readExtensionPin(serverName, dir = defaultTrustDir()) {
|
|
|
64232
64666
|
async function writeExtensionPin(serverName, pin, dir = defaultTrustDir()) {
|
|
64233
64667
|
const path = extensionTrustPath(serverName, dir);
|
|
64234
64668
|
await mkdir2(dir, { recursive: true, mode: 448 });
|
|
64235
|
-
const tmp = `${path}.${
|
|
64669
|
+
const tmp = `${path}.${randomBytes4(6).toString("hex")}.tmp`;
|
|
64236
64670
|
try {
|
|
64237
64671
|
await writeFile2(tmp, JSON.stringify(pin, null, 2), { mode: 384, flag: "wx" });
|
|
64238
64672
|
await chmod2(tmp, 384);
|
|
@@ -67812,7 +68246,7 @@ function bridgeErrorInfo(err) {
|
|
|
67812
68246
|
// package.json
|
|
67813
68247
|
var package_default = {
|
|
67814
68248
|
name: "honeybook-mcp",
|
|
67815
|
-
version: "1.
|
|
68249
|
+
version: "1.2.0",
|
|
67816
68250
|
mcpName: "io.github.chrischall/honeybook-mcp",
|
|
67817
68251
|
description: "HoneyBook client-portal MCP server for Claude",
|
|
67818
68252
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -67856,7 +68290,7 @@ var package_default = {
|
|
|
67856
68290
|
typecheck: "tsc -p tsconfig.json --noEmit"
|
|
67857
68291
|
},
|
|
67858
68292
|
dependencies: {
|
|
67859
|
-
"@chrischall/mcp-utils": "^2.
|
|
68293
|
+
"@chrischall/mcp-utils": "^2.6.0",
|
|
67860
68294
|
"@fetchproxy/bootstrap": "^3.2.0",
|
|
67861
68295
|
"@fetchproxy/server": "^3.2.0",
|
|
67862
68296
|
"@modelcontextprotocol/server": "^2.0.0",
|
|
@@ -68737,7 +69171,7 @@ function registerPaymentMethodTools(server) {
|
|
|
68737
69171
|
}
|
|
68738
69172
|
|
|
68739
69173
|
// src/tools/contracts.ts
|
|
68740
|
-
async function signContract(args) {
|
|
69174
|
+
async function signContract(args, ctx) {
|
|
68741
69175
|
const client = await getActiveClient(args.origin);
|
|
68742
69176
|
const file2 = await client.request(
|
|
68743
69177
|
"GET",
|
|
@@ -68751,12 +69185,28 @@ async function signContract(args) {
|
|
|
68751
69185
|
if (file2.is_file_accepted) {
|
|
68752
69186
|
throw new Error(`Contract ${args.file_id} ("${file2.file_title}") is already signed.`);
|
|
68753
69187
|
}
|
|
68754
|
-
|
|
68755
|
-
|
|
68756
|
-
|
|
68757
|
-
|
|
68758
|
-
|
|
68759
|
-
|
|
69188
|
+
const preview = {
|
|
69189
|
+
file_id: file2._id,
|
|
69190
|
+
file_title: file2.file_title,
|
|
69191
|
+
status: file2.status_name || "not signed"
|
|
69192
|
+
};
|
|
69193
|
+
const gate = await requireConfirmationWithFallback(
|
|
69194
|
+
ctx,
|
|
69195
|
+
confirmationFromEnv({
|
|
69196
|
+
action: "contract.sign",
|
|
69197
|
+
message: "Review and confirm signing this contract:",
|
|
69198
|
+
details: preview,
|
|
69199
|
+
tool: "sign_contract",
|
|
69200
|
+
account: client.scope.portalOrigin,
|
|
69201
|
+
confirmToken: args.confirmToken,
|
|
69202
|
+
subject: () => ({
|
|
69203
|
+
target: String(file2._id),
|
|
69204
|
+
payload: { file_id: file2._id, file_title: file2.file_title },
|
|
69205
|
+
preview
|
|
69206
|
+
})
|
|
69207
|
+
})
|
|
69208
|
+
);
|
|
69209
|
+
if (gate) return gate;
|
|
68760
69210
|
vendorPortalSubdomain(client.scope.portalOrigin);
|
|
68761
69211
|
const url4 = `${client.scope.portalOrigin}/app/workspace_file/${encodeURIComponent(String(file2._id))}/agreement`;
|
|
68762
69212
|
return rawTextResult(
|
|
@@ -68773,15 +69223,13 @@ function registerContractTools(server) {
|
|
|
68773
69223
|
server.registerTool(
|
|
68774
69224
|
"sign_contract",
|
|
68775
69225
|
{
|
|
68776
|
-
description: "Sign a contract you received from a vendor. In v1 this returns a deep link to the HoneyBook portal instead of signing headlessly.
|
|
69226
|
+
description: "Sign a contract you received from a vendor. In v1 this returns a deep link to the HoneyBook portal instead of signing headlessly. 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).",
|
|
68777
69227
|
inputSchema: external_exports.object({
|
|
68778
69228
|
file_id: external_exports.string().describe("The agreement file _id from list_workspace_files (file_type=agreement)."),
|
|
68779
69229
|
origin: schemaOrigin.describe(
|
|
68780
69230
|
"Portal origin (e.g. https://<vendor>.hbportal.co). Optional when only one session is active."
|
|
68781
69231
|
),
|
|
68782
|
-
|
|
68783
|
-
"Must be true to proceed. Without this, tool returns a preview."
|
|
68784
|
-
)
|
|
69232
|
+
confirmToken: confirmTokenParam
|
|
68785
69233
|
}),
|
|
68786
69234
|
annotations: { destructiveHint: true }
|
|
68787
69235
|
},
|
|
@@ -68790,7 +69238,7 @@ function registerContractTools(server) {
|
|
|
68790
69238
|
}
|
|
68791
69239
|
|
|
68792
69240
|
// src/tools/invoices.ts
|
|
68793
|
-
async function payInvoice(args) {
|
|
69241
|
+
async function payInvoice(args, ctx) {
|
|
68794
69242
|
const client = await getActiveClient(args.origin);
|
|
68795
69243
|
const file2 = await client.request("GET", apiPath`/api/v2/workspace_files/${args.file_id}`);
|
|
68796
69244
|
if (file2.file_type !== "invoice") {
|
|
@@ -68798,12 +69246,29 @@ async function payInvoice(args) {
|
|
|
68798
69246
|
`File ${args.file_id} is not an invoice (file_type=${file2.file_type}). Only invoices can be paid.`
|
|
68799
69247
|
);
|
|
68800
69248
|
}
|
|
68801
|
-
|
|
68802
|
-
|
|
68803
|
-
|
|
68804
|
-
|
|
68805
|
-
)
|
|
68806
|
-
}
|
|
69249
|
+
const preview = {
|
|
69250
|
+
file_id: file2._id,
|
|
69251
|
+
file_title: file2.file_title,
|
|
69252
|
+
status: file2.status_name || "open",
|
|
69253
|
+
has_pending_payment: Boolean(file2.has_pending_payment)
|
|
69254
|
+
};
|
|
69255
|
+
const gate = await requireConfirmationWithFallback(
|
|
69256
|
+
ctx,
|
|
69257
|
+
confirmationFromEnv({
|
|
69258
|
+
action: "invoice.pay",
|
|
69259
|
+
message: "Review and confirm paying this invoice:",
|
|
69260
|
+
details: preview,
|
|
69261
|
+
tool: "pay_invoice",
|
|
69262
|
+
account: client.scope.portalOrigin,
|
|
69263
|
+
confirmToken: args.confirmToken,
|
|
69264
|
+
subject: () => ({
|
|
69265
|
+
target: String(file2._id),
|
|
69266
|
+
payload: { file_id: file2._id, file_title: file2.file_title, has_pending_payment: preview.has_pending_payment },
|
|
69267
|
+
preview
|
|
69268
|
+
})
|
|
69269
|
+
})
|
|
69270
|
+
);
|
|
69271
|
+
if (gate) return gate;
|
|
68807
69272
|
vendorPortalSubdomain(client.scope.portalOrigin);
|
|
68808
69273
|
const url4 = `${client.scope.portalOrigin}/app/workspace_file/${encodeURIComponent(String(file2._id))}/invoice`;
|
|
68809
69274
|
const pendingNote = file2.has_pending_payment ? "\n\nNote: this invoice already has a pending payment \u2014 check the status before re-paying." : "";
|
|
@@ -68819,15 +69284,13 @@ function registerInvoiceTools(server) {
|
|
|
68819
69284
|
server.registerTool(
|
|
68820
69285
|
"pay_invoice",
|
|
68821
69286
|
{
|
|
68822
|
-
description: "Pay an invoice from a vendor. In v1 this returns a deep link to the HoneyBook portal instead of paying headlessly.
|
|
69287
|
+
description: "Pay an invoice from a vendor. In v1 this returns a deep link to the HoneyBook portal instead of paying headlessly. 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).",
|
|
68823
69288
|
inputSchema: external_exports.object({
|
|
68824
69289
|
file_id: external_exports.string().describe("The invoice file _id from list_workspace_files (file_type=invoice)."),
|
|
68825
69290
|
origin: schemaOrigin.describe(
|
|
68826
69291
|
"Portal origin (e.g. https://<vendor>.hbportal.co). Optional when only one session is active."
|
|
68827
69292
|
),
|
|
68828
|
-
|
|
68829
|
-
"Must be true to proceed. Without this, tool returns a preview."
|
|
68830
|
-
)
|
|
69293
|
+
confirmToken: confirmTokenParam
|
|
68831
69294
|
}),
|
|
68832
69295
|
annotations: { destructiveHint: true }
|
|
68833
69296
|
},
|
|
@@ -69298,7 +69761,7 @@ function bodyToHtml(body) {
|
|
|
69298
69761
|
if (HTML_TAG.test(body)) return body;
|
|
69299
69762
|
return body.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\r?\n/g, "<br>");
|
|
69300
69763
|
}
|
|
69301
|
-
async function sendMessage(args) {
|
|
69764
|
+
async function sendMessage(args, ctx) {
|
|
69302
69765
|
const client = await getActiveClient(args.origin);
|
|
69303
69766
|
const meId = client.scope.userId;
|
|
69304
69767
|
const feed = await fetchWorkspaceFeed(client, args.workspace_id);
|
|
@@ -69321,18 +69784,7 @@ async function sendMessage(args) {
|
|
|
69321
69784
|
}
|
|
69322
69785
|
if (!args.body.trim()) throw new Error("The message body is empty.");
|
|
69323
69786
|
const recipients = Object.values(feed.users).filter((u) => u._id !== meId);
|
|
69324
|
-
const
|
|
69325
|
-
if (!args.confirm) {
|
|
69326
|
-
return rawTextResult(
|
|
69327
|
-
`About to send a message in workspace ${args.workspace_id} via the HoneyBook portal.
|
|
69328
|
-
To: ${recipientLine || "(everyone in the workspace)"}
|
|
69329
|
-
Subject: ${subject}${replyTo ? ` (reply to "${String(replyTo._id)}")` : ""}
|
|
69330
|
-
|
|
69331
|
-
${args.body}
|
|
69332
|
-
|
|
69333
|
-
Re-run send_message with { confirm: true } to send it.`
|
|
69334
|
-
);
|
|
69335
|
-
}
|
|
69787
|
+
const to = recipients.map((u) => `${u.name}${u.email ? ` <${u.email}>` : ""}`);
|
|
69336
69788
|
const taskData = {
|
|
69337
69789
|
ws_id: args.workspace_id,
|
|
69338
69790
|
subject,
|
|
@@ -69343,6 +69795,33 @@ Re-run send_message with { confirm: true } to send it.`
|
|
|
69343
69795
|
flow_attachments: []
|
|
69344
69796
|
};
|
|
69345
69797
|
if (replyTo) taskData.feed_to_reply_id = replyTo._id;
|
|
69798
|
+
const preview = {
|
|
69799
|
+
workspace_id: args.workspace_id,
|
|
69800
|
+
via: "HoneyBook portal",
|
|
69801
|
+
to: to.length ? to : ["(everyone in the workspace)"],
|
|
69802
|
+
subject,
|
|
69803
|
+
reply_to: replyTo ? replyTo._id : null,
|
|
69804
|
+
body: args.body
|
|
69805
|
+
};
|
|
69806
|
+
const gate = await requireConfirmationWithFallback(
|
|
69807
|
+
ctx,
|
|
69808
|
+
confirmationFromEnv({
|
|
69809
|
+
action: "message.send",
|
|
69810
|
+
message: "Review and confirm sending this message:",
|
|
69811
|
+
details: preview,
|
|
69812
|
+
tool: "send_message",
|
|
69813
|
+
account: client.scope.portalOrigin,
|
|
69814
|
+
confirmToken: args.confirmToken,
|
|
69815
|
+
subject: () => ({
|
|
69816
|
+
target: args.workspace_id,
|
|
69817
|
+
// What goes out, and who HoneyBook emails it to: a membership change
|
|
69818
|
+
// between the two calls changes the recipients, so it must be re-approved.
|
|
69819
|
+
payload: { task: taskData, recipients: recipients.map((u) => u._id) },
|
|
69820
|
+
preview
|
|
69821
|
+
})
|
|
69822
|
+
})
|
|
69823
|
+
);
|
|
69824
|
+
if (gate) return gate;
|
|
69346
69825
|
let outcome;
|
|
69347
69826
|
try {
|
|
69348
69827
|
outcome = await runClientPendingTask(client, "send_workspace_message", taskData);
|
|
@@ -69417,16 +69896,14 @@ function registerMessageTools(server) {
|
|
|
69417
69896
|
server.registerTool(
|
|
69418
69897
|
"send_message",
|
|
69419
69898
|
{
|
|
69420
|
-
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).
|
|
69899
|
+
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: the message may still be delivered, so check list_messages before resending.',
|
|
69421
69900
|
inputSchema: external_exports.object({
|
|
69422
69901
|
workspace_id: external_exports.string().describe(WORKSPACE_DESC),
|
|
69423
69902
|
body: external_exports.string().describe("Message text. Plain text is sent as-is (line breaks preserved); HTML is passed through."),
|
|
69424
69903
|
subject: external_exports.string().optional().describe("Required for a new message; optional on a reply."),
|
|
69425
69904
|
reply_to_message_id: external_exports.string().optional().describe("A message _id from list_messages to reply to."),
|
|
69426
69905
|
origin: schemaOrigin.describe(ORIGIN_DESC),
|
|
69427
|
-
|
|
69428
|
-
"Must be true to actually send. Without it the tool returns a preview of what would go out."
|
|
69429
|
-
)
|
|
69906
|
+
confirmToken: confirmTokenParam
|
|
69430
69907
|
}),
|
|
69431
69908
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false }
|
|
69432
69909
|
},
|
|
@@ -69675,7 +70152,7 @@ var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
|
69675
70152
|
await loadDotenvSafely({ path: join6(__dirname, "..", ".env"), override: false });
|
|
69676
70153
|
await runMcp({
|
|
69677
70154
|
name: "honeybook-mcp",
|
|
69678
|
-
version: "1.
|
|
70155
|
+
version: "1.2.0",
|
|
69679
70156
|
// x-release-please-version
|
|
69680
70157
|
tools: [
|
|
69681
70158
|
registerSessionTools,
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ import { registerAttachmentTools } from './tools/attachments.js';
|
|
|
20
20
|
import { registerPaymentTools } from './tools/payments.js';
|
|
21
21
|
await runMcp({
|
|
22
22
|
name: 'honeybook-mcp',
|
|
23
|
-
version: '1.
|
|
23
|
+
version: '1.2.0', // x-release-please-version
|
|
24
24
|
tools: [
|
|
25
25
|
registerSessionTools,
|
|
26
26
|
registerFlowTools,
|
package/dist/tools/contracts.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { rawTextResult, schemaOrigin,
|
|
2
|
+
import { confirmationFromEnv, confirmTokenParam, rawTextResult, requireConfirmationWithFallback, schemaOrigin, } from '@chrischall/mcp-utils';
|
|
3
3
|
import { apiPath, getActiveClient } from '../client.js';
|
|
4
4
|
import { vendorPortalSubdomain } from '../sessions.js';
|
|
5
|
-
export async function signContract(args) {
|
|
5
|
+
export async function signContract(args, ctx) {
|
|
6
6
|
const client = await getActiveClient(args.origin);
|
|
7
7
|
const file = await client.request('GET', apiPath `/api/v2/workspace_files/${args.file_id}`);
|
|
8
8
|
if (file.file_type !== 'agreement') {
|
|
@@ -11,10 +11,26 @@ export async function signContract(args) {
|
|
|
11
11
|
if (file.is_file_accepted) {
|
|
12
12
|
throw new Error(`Contract ${args.file_id} ("${file.file_title}") is already signed.`);
|
|
13
13
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
const preview = {
|
|
15
|
+
file_id: file._id,
|
|
16
|
+
file_title: file.file_title,
|
|
17
|
+
status: file.status_name || 'not signed',
|
|
18
|
+
};
|
|
19
|
+
const gate = await requireConfirmationWithFallback(ctx, confirmationFromEnv({
|
|
20
|
+
action: 'contract.sign',
|
|
21
|
+
message: 'Review and confirm signing this contract:',
|
|
22
|
+
details: preview,
|
|
23
|
+
tool: 'sign_contract',
|
|
24
|
+
account: client.scope.portalOrigin,
|
|
25
|
+
confirmToken: args.confirmToken,
|
|
26
|
+
subject: () => ({
|
|
27
|
+
target: String(file._id),
|
|
28
|
+
payload: { file_id: file._id, file_title: file.file_title },
|
|
29
|
+
preview,
|
|
30
|
+
}),
|
|
31
|
+
}));
|
|
32
|
+
if (gate)
|
|
33
|
+
return gate;
|
|
18
34
|
// Re-checked here, not only at capture: a sessions.json written before the
|
|
19
35
|
// host rule existed can still hold a foreign origin (fleet-audit#139).
|
|
20
36
|
vendorPortalSubdomain(client.scope.portalOrigin);
|
|
@@ -25,13 +41,14 @@ export async function signContract(args) {
|
|
|
25
41
|
}
|
|
26
42
|
export function registerContractTools(server) {
|
|
27
43
|
server.registerTool('sign_contract', {
|
|
28
|
-
description: 'Sign a contract you received from a vendor. In v1 this returns a deep link to the HoneyBook portal instead of signing headlessly.
|
|
44
|
+
description: 'Sign a contract you received from a vendor. In v1 this returns a deep link to the HoneyBook portal instead of signing headlessly. ' +
|
|
45
|
+
'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).',
|
|
29
46
|
inputSchema: z.object({
|
|
30
47
|
file_id: z
|
|
31
48
|
.string()
|
|
32
49
|
.describe('The agreement file _id from list_workspace_files (file_type=agreement).'),
|
|
33
50
|
origin: schemaOrigin.describe('Portal origin (e.g. https://<vendor>.hbportal.co). Optional when only one session is active.'),
|
|
34
|
-
|
|
51
|
+
confirmToken: confirmTokenParam,
|
|
35
52
|
}),
|
|
36
53
|
annotations: { destructiveHint: true },
|
|
37
54
|
}, signContract);
|
package/dist/tools/invoices.js
CHANGED
|
@@ -1,17 +1,34 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { rawTextResult, schemaOrigin,
|
|
2
|
+
import { confirmationFromEnv, confirmTokenParam, rawTextResult, requireConfirmationWithFallback, schemaOrigin, } from '@chrischall/mcp-utils';
|
|
3
3
|
import { apiPath, getActiveClient } from '../client.js';
|
|
4
4
|
import { vendorPortalSubdomain } from '../sessions.js';
|
|
5
|
-
export async function payInvoice(args) {
|
|
5
|
+
export async function payInvoice(args, ctx) {
|
|
6
6
|
const client = await getActiveClient(args.origin);
|
|
7
7
|
const file = await client.request('GET', apiPath `/api/v2/workspace_files/${args.file_id}`);
|
|
8
8
|
if (file.file_type !== 'invoice') {
|
|
9
9
|
throw new Error(`File ${args.file_id} is not an invoice (file_type=${file.file_type}). Only invoices can be paid.`);
|
|
10
10
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
const preview = {
|
|
12
|
+
file_id: file._id,
|
|
13
|
+
file_title: file.file_title,
|
|
14
|
+
status: file.status_name || 'open',
|
|
15
|
+
has_pending_payment: Boolean(file.has_pending_payment),
|
|
16
|
+
};
|
|
17
|
+
const gate = await requireConfirmationWithFallback(ctx, confirmationFromEnv({
|
|
18
|
+
action: 'invoice.pay',
|
|
19
|
+
message: 'Review and confirm paying this invoice:',
|
|
20
|
+
details: preview,
|
|
21
|
+
tool: 'pay_invoice',
|
|
22
|
+
account: client.scope.portalOrigin,
|
|
23
|
+
confirmToken: args.confirmToken,
|
|
24
|
+
subject: () => ({
|
|
25
|
+
target: String(file._id),
|
|
26
|
+
payload: { file_id: file._id, file_title: file.file_title, has_pending_payment: preview.has_pending_payment },
|
|
27
|
+
preview,
|
|
28
|
+
}),
|
|
29
|
+
}));
|
|
30
|
+
if (gate)
|
|
31
|
+
return gate;
|
|
15
32
|
// Re-checked here, not only at capture: a sessions.json written before the
|
|
16
33
|
// host rule existed can still hold a foreign origin (fleet-audit#139).
|
|
17
34
|
vendorPortalSubdomain(client.scope.portalOrigin);
|
|
@@ -24,13 +41,14 @@ export async function payInvoice(args) {
|
|
|
24
41
|
}
|
|
25
42
|
export function registerInvoiceTools(server) {
|
|
26
43
|
server.registerTool('pay_invoice', {
|
|
27
|
-
description: 'Pay an invoice from a vendor. In v1 this returns a deep link to the HoneyBook portal instead of paying headlessly.
|
|
44
|
+
description: 'Pay an invoice from a vendor. In v1 this returns a deep link to the HoneyBook portal instead of paying headlessly. ' +
|
|
45
|
+
'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).',
|
|
28
46
|
inputSchema: z.object({
|
|
29
47
|
file_id: z
|
|
30
48
|
.string()
|
|
31
49
|
.describe('The invoice file _id from list_workspace_files (file_type=invoice).'),
|
|
32
50
|
origin: schemaOrigin.describe('Portal origin (e.g. https://<vendor>.hbportal.co). Optional when only one session is active.'),
|
|
33
|
-
|
|
51
|
+
confirmToken: confirmTokenParam,
|
|
34
52
|
}),
|
|
35
53
|
annotations: { destructiveHint: true },
|
|
36
54
|
}, payInvoice);
|
package/dist/tools/messages.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
2
|
+
import { confirmationFromEnv, confirmTokenParam, minifiedResult, requireConfirmationWithFallback, schemaOrigin, } from '@chrischall/mcp-utils';
|
|
3
3
|
import { apiPath, getActiveClient } from '../client.js';
|
|
4
4
|
import { fetchWorkspaceFeed, isMessageItem, itemDate, summarizeMessage, summarizeActivity, expandMessage, } from '../feed.js';
|
|
5
5
|
import { runClientPendingTask, isPendingTaskTimeoutError } from '../pending-tasks.js';
|
|
@@ -79,7 +79,7 @@ export function bodyToHtml(body) {
|
|
|
79
79
|
.replace(/>/g, '>')
|
|
80
80
|
.replace(/\r?\n/g, '<br>');
|
|
81
81
|
}
|
|
82
|
-
export async function sendMessage(args) {
|
|
82
|
+
export async function sendMessage(args, ctx) {
|
|
83
83
|
const client = await getActiveClient(args.origin);
|
|
84
84
|
const meId = client.scope.userId;
|
|
85
85
|
const feed = await fetchWorkspaceFeed(client, args.workspace_id);
|
|
@@ -101,14 +101,7 @@ export async function sendMessage(args) {
|
|
|
101
101
|
if (!args.body.trim())
|
|
102
102
|
throw new Error('The message body is empty.');
|
|
103
103
|
const recipients = Object.values(feed.users).filter((u) => u._id !== meId);
|
|
104
|
-
const
|
|
105
|
-
if (!args.confirm) {
|
|
106
|
-
return rawTextResult(`About to send a message in workspace ${args.workspace_id} via the HoneyBook portal.\n` +
|
|
107
|
-
`To: ${recipientLine || '(everyone in the workspace)'}\n` +
|
|
108
|
-
`Subject: ${subject}${replyTo ? ` (reply to "${String(replyTo._id)}")` : ''}\n\n` +
|
|
109
|
-
`${args.body}\n\n` +
|
|
110
|
-
`Re-run send_message with { confirm: true } to send it.`);
|
|
111
|
-
}
|
|
104
|
+
const to = recipients.map((u) => `${u.name}${u.email ? ` <${u.email}>` : ''}`);
|
|
112
105
|
const taskData = {
|
|
113
106
|
ws_id: args.workspace_id,
|
|
114
107
|
subject,
|
|
@@ -120,6 +113,31 @@ export async function sendMessage(args) {
|
|
|
120
113
|
};
|
|
121
114
|
if (replyTo)
|
|
122
115
|
taskData.feed_to_reply_id = replyTo._id;
|
|
116
|
+
const preview = {
|
|
117
|
+
workspace_id: args.workspace_id,
|
|
118
|
+
via: 'HoneyBook portal',
|
|
119
|
+
to: to.length ? to : ['(everyone in the workspace)'],
|
|
120
|
+
subject,
|
|
121
|
+
reply_to: replyTo ? replyTo._id : null,
|
|
122
|
+
body: args.body,
|
|
123
|
+
};
|
|
124
|
+
const gate = await requireConfirmationWithFallback(ctx, confirmationFromEnv({
|
|
125
|
+
action: 'message.send',
|
|
126
|
+
message: 'Review and confirm sending this message:',
|
|
127
|
+
details: preview,
|
|
128
|
+
tool: 'send_message',
|
|
129
|
+
account: client.scope.portalOrigin,
|
|
130
|
+
confirmToken: args.confirmToken,
|
|
131
|
+
subject: () => ({
|
|
132
|
+
target: args.workspace_id,
|
|
133
|
+
// What goes out, and who HoneyBook emails it to: a membership change
|
|
134
|
+
// between the two calls changes the recipients, so it must be re-approved.
|
|
135
|
+
payload: { task: taskData, recipients: recipients.map((u) => u._id) },
|
|
136
|
+
preview,
|
|
137
|
+
}),
|
|
138
|
+
}));
|
|
139
|
+
if (gate)
|
|
140
|
+
return gate;
|
|
123
141
|
let outcome;
|
|
124
142
|
try {
|
|
125
143
|
outcome = await runClientPendingTask(client, 'send_workspace_message', taskData);
|
|
@@ -196,7 +214,8 @@ export function registerMessageTools(server) {
|
|
|
196
214
|
server.registerTool('send_message', {
|
|
197
215
|
description: 'Send a message to the vendor (and the other members of the workspace) through the HoneyBook portal, ' +
|
|
198
216
|
'exactly as the Activity tab composer does. HoneyBook emails it to every recipient. ' +
|
|
199
|
-
'Pass reply_to_message_id to reply in-thread (the subject is inherited).
|
|
217
|
+
'Pass reply_to_message_id to reply in-thread (the subject is inherited). ' +
|
|
218
|
+
'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). ' +
|
|
200
219
|
'If HoneyBook has not finished sending within about a minute the result has status "pending" and a ' +
|
|
201
220
|
'task_id: the message may still be delivered, so check list_messages before resending.',
|
|
202
221
|
inputSchema: z.object({
|
|
@@ -210,7 +229,7 @@ export function registerMessageTools(server) {
|
|
|
210
229
|
.optional()
|
|
211
230
|
.describe('A message _id from list_messages to reply to.'),
|
|
212
231
|
origin: schemaOrigin.describe(ORIGIN_DESC),
|
|
213
|
-
|
|
232
|
+
confirmToken: confirmTokenParam,
|
|
214
233
|
}),
|
|
215
234
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false },
|
|
216
235
|
}, sendMessage);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "honeybook-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.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"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@chrischall/mcp-utils": "^2.
|
|
47
|
+
"@chrischall/mcp-utils": "^2.6.0",
|
|
48
48
|
"@fetchproxy/bootstrap": "^3.2.0",
|
|
49
49
|
"@fetchproxy/server": "^3.2.0",
|
|
50
50
|
"@modelcontextprotocol/server": "^2.0.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.
|
|
9
|
+
"version": "1.2.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "honeybook-mcp",
|
|
14
|
-
"version": "1.
|
|
14
|
+
"version": "1.2.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
|
@@ -17,13 +17,13 @@ MCP server for HoneyBook's client portal — contracts, invoices, questionnaires
|
|
|
17
17
|
- `get_workspace_file(file_id, section?)` — Full detail for one file. Takes `section`, **not** `view` — see below
|
|
18
18
|
- `get_workspace` — Workspace detail + status flags
|
|
19
19
|
- `list_payment_methods` — Saved payment methods
|
|
20
|
-
- `sign_contract` — Deep link to sign in portal (
|
|
21
|
-
- `pay_invoice` — Deep link to pay in portal (
|
|
20
|
+
- `sign_contract` — Deep link to sign in portal (asks the user to confirm first)
|
|
21
|
+
- `pay_invoice` — Deep link to pay in portal (asks the user to confirm first)
|
|
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
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;
|
|
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
|
|
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
|
|
@@ -96,10 +96,10 @@ shape legible.
|
|
|
96
96
|
- **"Fill in / read the vendor's questionnaire"** → user pastes the `/flow/<id>?hash=…` link → `use_flow_link` → `get_flow`
|
|
97
97
|
- **"What contracts haven't I signed?"** → `list_workspace_files` with `file_type=agreement`, filter by `is_file_accepted=false`
|
|
98
98
|
- **"Summarize my HB status with Silk Veil"** → `get_workspace` (status flags) + `list_workspace_files`
|
|
99
|
-
- **"Send me a link to sign the photographer's contract"** → `list_workspace_files` → `sign_contract`
|
|
99
|
+
- **"Send me a link to sign the photographer's contract"** → `list_workspace_files` → `sign_contract` (confirm when prompted)
|
|
100
100
|
- **"Which invoices are overdue?"** → `list_workspace_files` with `file_type=invoice`, sort by due date
|
|
101
101
|
- **"What did the planner send me?" / "Read me the latest checklist"** → `list_projects` → `list_messages` → `get_message`
|
|
102
|
-
- **"Reply to Ivy and ask about the rehearsal"** → `list_messages` (find the message) → `send_message` with `reply_to_message_id`
|
|
102
|
+
- **"Reply to Ivy and ask about the rehearsal"** → `list_messages` (find the message) → `send_message` with `reply_to_message_id` → show the user the preview → once they approve, re-run with the same arguments plus the returned `confirmToken` (on a client that shows a confirmation prompt, the prompt replaces these two steps)
|
|
103
103
|
- **"When is my next Zoom with the planner?"** → `list_meetings`; the join link and password are in the row
|
|
104
104
|
- **"What have I paid and what's left?"** → `list_payments` (`totals.paid` / `totals.unpaid`); to pay one, `pay_invoice`
|
|
105
105
|
- **"Do I have anything to do?"** → `list_tasks` (`counts.overdue`, `counts.today`)
|
|
@@ -112,5 +112,5 @@ shape legible.
|
|
|
112
112
|
- The flow storage key contains the flow id, so the fetchproxy extension asks to re-approve the scope once per new questionnaire
|
|
113
113
|
- Write tools (`sign_contract`, `pay_invoice`) return deep links in v2
|
|
114
114
|
- Session expires → re-run `use_magic_link` with a fresh URL from the vendor's email
|
|
115
|
-
- `send_message` goes out as a real HoneyBook email to everyone in the workspace (the vendor and any co-clients). Always show the preview
|
|
115
|
+
- `send_message` goes out as a real HoneyBook email to everyone in the workspace (the vendor and any co-clients). Always show the user the preview and get their explicit approval before passing the `confirmToken` back — never reuse or invent a token
|
|
116
116
|
- `list_messages` / `get_message` read the same feed the Activity tab renders and do not mark items seen; `unseen_count` counts messages from other people you have not opened
|