@suveren/gateway 0.33.0 → 0.35.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.
@@ -2,7 +2,7 @@
2
2
  "id": "mollie",
3
3
  "name": "Mollie",
4
4
  "version": "1.0.0",
5
- "description": "Payments, subscriptions, and billing",
5
+ "description": "Payment links, payments and payment customers — profile: Payments (charge@0.7). Invoices come from the ERP; Mollie never emails customers.",
6
6
  "icon": "card",
7
7
  "profile": "charge",
8
8
  "npmPackage": "mcp-remote",
@@ -29,6 +29,14 @@
29
29
  }
30
30
  },
31
31
  "oauth": null,
32
+ "values": {
33
+ "payment-link": {
34
+ "tool": "GetPaymentLink",
35
+ "args": { "paymentLinkId": "$id" },
36
+ "path": "_links.paymentLink.href",
37
+ "allowedHosts": ["payment-links.mollie.com"]
38
+ }
39
+ },
32
40
  "toolGating": {
33
41
  "default": {
34
42
  "executionMapping": {},
@@ -37,6 +45,266 @@
37
45
  }
38
46
  },
39
47
  "overrides": {
48
+ "ListProfiles": {
49
+ "category": "read",
50
+ "readGovernance": "none",
51
+ "readGovernanceReason": "Returns the organisation's own payment profile configuration (names, websites, status) — no customer or payment data."
52
+ },
53
+ "GetProfile": {
54
+ "category": "read",
55
+ "readGovernance": "none",
56
+ "readGovernanceReason": "Returns one of the organisation's own payment profile configurations — no customer or payment data."
57
+ },
58
+ "GetOrganizationDetails": {
59
+ "category": "read",
60
+ "readGovernance": "none",
61
+ "readGovernanceReason": "Returns the connected Mollie organisation's own name/registration details — no customer or payment data."
62
+ },
63
+ "GetPaymentMethod": {
64
+ "category": "read",
65
+ "readGovernance": "none",
66
+ "readGovernanceReason": "Returns the organisation's own payment-method configuration (name, image, min/max amount) — no customer or payment data."
67
+ },
68
+ "ListPaymentMethods": {
69
+ "category": "read",
70
+ "readGovernance": "none",
71
+ "readGovernanceReason": "Returns which payment methods are enabled for the organisation — no customer or payment data."
72
+ },
73
+ "ListAllPaymentMethods": {
74
+ "category": "read",
75
+ "readGovernance": "none",
76
+ "readGovernanceReason": "Returns every payment method Mollie offers — no customer or payment data."
77
+ },
78
+ "GetTerminal": {
79
+ "category": "read",
80
+ "readGovernance": "none",
81
+ "readGovernanceReason": "Returns a card-terminal's own configuration (name, status) — no customer or payment data."
82
+ },
83
+ "ListTerminals": {
84
+ "category": "read",
85
+ "readGovernance": "none",
86
+ "readGovernanceReason": "Returns the organisation's card terminals — no customer or payment data."
87
+ },
88
+ "GetClientDocs": {
89
+ "category": "read",
90
+ "readGovernance": "none",
91
+ "readGovernanceReason": "Returns Mollie's own API documentation — no customer or payment data."
92
+ },
93
+ "fetch_docs": {
94
+ "category": "read",
95
+ "readGovernance": "none",
96
+ "readGovernanceReason": "Returns Mollie's own API documentation — no customer or payment data."
97
+ },
98
+ "list_doc_sources": {
99
+ "category": "read",
100
+ "readGovernance": "none",
101
+ "readGovernanceReason": "Lists Mollie's own documentation sources — no customer or payment data."
102
+ },
103
+ "GetPayment": {
104
+ "category": "read",
105
+ "boundField": "read_access",
106
+ "requiredValue": "unlimited"
107
+ },
108
+ "ListPaymentsForGivenCustomer": {
109
+ "category": "read",
110
+ "boundField": "read_access",
111
+ "requiredValue": "unlimited"
112
+ },
113
+ "ListPaymentsForGivenPaymentLink": {
114
+ "category": "read",
115
+ "boundField": "read_access",
116
+ "requiredValue": "unlimited"
117
+ },
118
+ "ListPaymentsFromGivenIDOnwards": {
119
+ "category": "read",
120
+ "boundField": "read_access",
121
+ "requiredValue": "unlimited"
122
+ },
123
+ "ListPaymentsWithinSpecifiedTimeInterval": {
124
+ "category": "read",
125
+ "boundField": "read_access",
126
+ "requiredValue": "unlimited"
127
+ },
128
+ "SearchPaymentsByDescription": {
129
+ "category": "read",
130
+ "boundField": "read_access",
131
+ "requiredValue": "unlimited"
132
+ },
133
+ "AggregatePayments": {
134
+ "category": "read",
135
+ "boundField": "read_access",
136
+ "requiredValue": "unlimited"
137
+ },
138
+ "GetPaymentLink": {
139
+ "category": "read",
140
+ "boundField": "read_access",
141
+ "requiredValue": "unlimited"
142
+ },
143
+ "ListPaymentLinks": {
144
+ "category": "read",
145
+ "boundField": "read_access",
146
+ "requiredValue": "unlimited"
147
+ },
148
+ "ListPaymentLinksWithinSpecifiedTimeInterval": {
149
+ "category": "read",
150
+ "boundField": "read_access",
151
+ "requiredValue": "unlimited"
152
+ },
153
+ "AggregatePaymentLinks": {
154
+ "category": "read",
155
+ "boundField": "read_access",
156
+ "requiredValue": "unlimited"
157
+ },
158
+ "ListCustomers": {
159
+ "category": "read",
160
+ "boundField": "read_access",
161
+ "requiredValue": "unlimited"
162
+ },
163
+ "SearchCustomers": {
164
+ "category": "read",
165
+ "boundField": "read_access",
166
+ "requiredValue": "unlimited"
167
+ },
168
+ "GetMandate": {
169
+ "category": "read",
170
+ "boundField": "read_access",
171
+ "requiredValue": "unlimited"
172
+ },
173
+ "ListMandates": {
174
+ "category": "read",
175
+ "boundField": "read_access",
176
+ "requiredValue": "unlimited"
177
+ },
178
+ "GetCapture": {
179
+ "category": "read",
180
+ "boundField": "read_access",
181
+ "requiredValue": "unlimited"
182
+ },
183
+ "ListCaptures": {
184
+ "category": "read",
185
+ "boundField": "read_access",
186
+ "requiredValue": "unlimited"
187
+ },
188
+ "GetBalance": {
189
+ "category": "read",
190
+ "boundField": "read_access",
191
+ "requiredValue": "unlimited"
192
+ },
193
+ "GetPrimaryBalance": {
194
+ "category": "read",
195
+ "boundField": "read_access",
196
+ "requiredValue": "unlimited"
197
+ },
198
+ "ListBalances": {
199
+ "category": "read",
200
+ "boundField": "read_access",
201
+ "requiredValue": "unlimited"
202
+ },
203
+ "GetBalanceReport": {
204
+ "category": "read",
205
+ "boundField": "read_access",
206
+ "requiredValue": "unlimited"
207
+ },
208
+ "ListBalanceTransactions": {
209
+ "category": "read",
210
+ "boundField": "read_access",
211
+ "requiredValue": "unlimited"
212
+ },
213
+ "GetSettlement": {
214
+ "category": "read",
215
+ "boundField": "read_access",
216
+ "requiredValue": "unlimited"
217
+ },
218
+ "GetNextSettlement": {
219
+ "category": "read",
220
+ "boundField": "read_access",
221
+ "requiredValue": "unlimited"
222
+ },
223
+ "GetOpenSettlement": {
224
+ "category": "read",
225
+ "boundField": "read_access",
226
+ "requiredValue": "unlimited"
227
+ },
228
+ "ListSettlements": {
229
+ "category": "read",
230
+ "boundField": "read_access",
231
+ "requiredValue": "unlimited"
232
+ },
233
+ "ListSettlementCaptures": {
234
+ "category": "read",
235
+ "boundField": "read_access",
236
+ "requiredValue": "unlimited"
237
+ },
238
+ "ListSettlementChargebacks": {
239
+ "category": "read",
240
+ "boundField": "read_access",
241
+ "requiredValue": "unlimited"
242
+ },
243
+ "ListSettlementPayments": {
244
+ "category": "read",
245
+ "boundField": "read_access",
246
+ "requiredValue": "unlimited"
247
+ },
248
+ "ListSettlementRefunds": {
249
+ "category": "read",
250
+ "boundField": "read_access",
251
+ "requiredValue": "unlimited"
252
+ },
253
+ "GetSubscription": {
254
+ "category": "read",
255
+ "boundField": "read_access",
256
+ "requiredValue": "unlimited"
257
+ },
258
+ "ListAllSubscriptions": {
259
+ "category": "read",
260
+ "boundField": "read_access",
261
+ "requiredValue": "unlimited"
262
+ },
263
+ "ListCustomerSubscriptions": {
264
+ "category": "read",
265
+ "boundField": "read_access",
266
+ "requiredValue": "unlimited"
267
+ },
268
+ "ListSubscriptionPayments": {
269
+ "category": "read",
270
+ "boundField": "read_access",
271
+ "requiredValue": "unlimited"
272
+ },
273
+ "GetInvoice": {
274
+ "category": "read",
275
+ "boundField": "read_access",
276
+ "requiredValue": "unlimited"
277
+ },
278
+ "ListInvoices": {
279
+ "category": "read",
280
+ "boundField": "read_access",
281
+ "requiredValue": "unlimited"
282
+ },
283
+ "GetSalesInvoice": {
284
+ "category": "read",
285
+ "boundField": "read_access",
286
+ "requiredValue": "unlimited"
287
+ },
288
+ "ListSalesInvoices": {
289
+ "category": "read",
290
+ "boundField": "read_access",
291
+ "requiredValue": "unlimited"
292
+ },
293
+ "GetWebhook": {
294
+ "category": "read",
295
+ "boundField": "read_access",
296
+ "requiredValue": "unlimited"
297
+ },
298
+ "ListWebhooks": {
299
+ "category": "read",
300
+ "boundField": "read_access",
301
+ "requiredValue": "unlimited"
302
+ },
303
+ "GetWebhookEvent": {
304
+ "category": "read",
305
+ "boundField": "read_access",
306
+ "requiredValue": "unlimited"
307
+ },
40
308
  "CreatePayment": {
41
309
  "summary": "Payment of {amount|money}: {description|short}",
42
310
  "actionLabel": "Payment created",
@@ -48,15 +316,6 @@
48
316
  "action_type": "charge"
49
317
  }
50
318
  },
51
- "CreatePaymentForCustomer": {
52
- "executionMapping": {
53
- "amount": "amount",
54
- "currency": "currency"
55
- },
56
- "staticExecution": {
57
- "action_type": "charge"
58
- }
59
- },
60
319
  "CreatePaymentLink": {
61
320
  "summary": "Payment link[ of {amount|money}]: {description|short}",
62
321
  "actionLabel": "Payment link created",
@@ -69,35 +328,17 @@
69
328
  }
70
329
  },
71
330
  "CreateCapture": {
72
- "executionMapping": {
73
- "amount_value": "amount",
74
- "amount_currency": "currency"
331
+ "summary": "Capture of {amount_value|money} from payment {payment_id}",
332
+ "actionLabel": "Capture created",
333
+ "display": {
334
+ "currencyField": "amount_currency"
75
335
  },
76
- "staticExecution": {
77
- "action_type": "charge"
78
- }
79
- },
80
- "CreateSubscription": {
81
336
  "executionMapping": {
82
337
  "amount_value": "amount",
83
338
  "amount_currency": "currency"
84
339
  },
85
340
  "staticExecution": {
86
- "action_type": "subscribe"
87
- }
88
- },
89
- "UpdateSubscription": {
90
- "executionMapping": {},
91
- "staticExecution": {
92
- "action_type": "subscribe"
93
- }
94
- },
95
- "CancelSubscription": {
96
- "summary": "Cancel a subscription",
97
- "actionLabel": "Subscription cancelled",
98
- "executionMapping": {},
99
- "staticExecution": {
100
- "action_type": "subscribe"
341
+ "action_type": "charge"
101
342
  }
102
343
  },
103
344
  "CreateCustomer": {
@@ -105,93 +346,115 @@
105
346
  "actionLabel": "Payment customer created",
106
347
  "executionMapping": {},
107
348
  "staticExecution": {
108
- "action_type": "charge"
349
+ "action_type": "manage"
109
350
  }
110
351
  },
111
352
  "UpdateCustomer": {
353
+ "summary": "Update payment customer {customer_id}[ — {name}]",
354
+ "actionLabel": "Payment customer updated",
112
355
  "executionMapping": {},
113
356
  "staticExecution": {
114
- "action_type": "charge"
115
- }
116
- },
117
- "CreateMandate": {
118
- "executionMapping": {},
119
- "staticExecution": {
120
- "action_type": "charge"
357
+ "action_type": "manage"
121
358
  }
122
359
  },
123
360
  "RevokeMandate": {
124
- "summary": "Revoke a payment mandate",
125
- "actionLabel": "Payment mandate revoked",
361
+ "summary": "Revoke payment authorisation {mandate_id} for customer {customer_id}",
362
+ "actionLabel": "Payment authorisation revoked",
126
363
  "executionMapping": {},
127
364
  "staticExecution": {
128
- "action_type": "charge"
365
+ "action_type": "manage"
129
366
  }
130
367
  },
368
+ "CreateSalesInvoice": {
369
+ "category": "disabled",
370
+ "_reason": "Invoices come from the ERP (Invoicing profile); Mollie no longer issues invoices."
371
+ },
372
+ "UpdateSalesInvoice": {
373
+ "category": "disabled",
374
+ "_reason": "Invoices come from the ERP; Mollie no longer issues or changes invoices (also the only way to mark one paid by hand, which is excluded by decision)."
375
+ },
376
+ "CancelSubscription": {
377
+ "category": "disabled",
378
+ "_reason": "Subscriptions are read-only in Payments 0.7 — there is no action type to charge this against."
379
+ },
380
+ "UpdateSubscription": {
381
+ "category": "disabled",
382
+ "_reason": "Subscriptions are read-only in Payments 0.7; the new amount is also inside an opaque request_body and cannot be mapped to a bound."
383
+ },
131
384
  "CreateWebhook": {
132
- "executionMapping": {},
133
- "staticExecution": {
134
- "action_type": "charge"
135
- }
385
+ "category": "disabled",
386
+ "_reason": "Sends payment events to any URL the agent names — a data leak path. Webhooks are set up by a person in the dashboard."
136
387
  },
137
388
  "UpdateWebhook": {
138
- "executionMapping": {},
139
- "staticExecution": {
140
- "action_type": "charge"
141
- }
389
+ "category": "disabled",
390
+ "_reason": "Sends payment events to any URL the agent names — a data leak path. Webhooks are set up by a person in the dashboard."
142
391
  },
143
- "ListProfiles": {
144
- "category": "read",
145
- "readGovernance": "none",
146
- "readGovernanceReason": "Returns the organisation's own payment profile configuration (names, websites, status) — no customer or payment data."
392
+ "TestWebhook": {
393
+ "category": "disabled",
394
+ "_reason": "Could be pointed at any URL to exfiltrate payment data."
147
395
  },
148
- "GetOrganizationDetails": {
149
- "category": "read",
150
- "readGovernance": "none",
151
- "readGovernanceReason": "Returns the connected Mollie organisation's own name/registration details — no customer or payment data."
396
+ "CreateDraftTransfer": {
397
+ "category": "disabled",
398
+ "_reason": "Outgoing money to the organisation's own bank account — spend side, not covered by Payments 0.7."
399
+ },
400
+ "CancelDraftTransfer": {
401
+ "category": "disabled",
402
+ "_reason": "Outgoing money — spend side, not covered by Payments 0.7."
403
+ },
404
+ "GetDraftTransfer": {
405
+ "category": "disabled",
406
+ "_reason": "Spend side (payouts) — outside Payments; not covered by this profile."
407
+ },
408
+ "ListDraftTransfers": {
409
+ "category": "disabled",
410
+ "_reason": "Spend side (payouts) — outside Payments; not covered by this profile."
411
+ },
412
+ "ListBusinessAccounts": {
413
+ "category": "disabled",
414
+ "_reason": "Spend side (the organisation's own bank accounts) — outside Payments; not covered by this profile."
152
415
  }
153
416
  }
154
417
  },
155
418
  "templates": [
156
419
  {
157
- "name": "Monitor Payments",
158
- "description": "Read-only. View payments, balances, invoices, and transaction history.",
420
+ "name": "See Payments",
421
+ "description": "Read-only. Look up payments, payment links, customers, mandates, settlements and balances.",
159
422
  "risk": "low",
160
423
  "mode": "automatic",
161
424
  "bounds": {
425
+ "read_access": "unlimited",
162
426
  "amount_max": "0",
163
427
  "amount_daily_max": "0",
164
428
  "amount_monthly_max": "0",
165
- "transaction_count_daily_max": "100"
429
+ "charge_daily_max": "0",
430
+ "manage_daily_max": "0"
166
431
  },
167
432
  "context": {
168
- "currency": "EUR",
169
- "action_type": "charge"
433
+ "currency": "EUR"
170
434
  },
171
- "intent": "Allow the agent to monitor payments and view transaction history. Read-only — no payments are created or modified.",
435
+ "intent": "Allow the agent to look up payments, payment links, payment customers, settlements and balances. Read-only — nothing is created or changed.",
172
436
  "ttl": 2592000,
173
437
  "tags": [
174
- "read-only",
175
- "EUR",
176
- "100 ops/day"
438
+ "read-only"
177
439
  ]
178
440
  },
179
441
  {
180
- "name": "Small Payments",
181
- "description": "Create payment links and process charges up to €50 per transaction.",
442
+ "name": "Payment Links up to €50",
443
+ "description": "Create payment links and payments up to €50 per transaction. No read access.",
182
444
  "risk": "medium",
183
445
  "mode": "automatic",
184
446
  "bounds": {
447
+ "read_access": "none",
185
448
  "amount_max": "50",
186
449
  "amount_daily_max": "200",
187
450
  "amount_monthly_max": "2000",
188
- "transaction_count_daily_max": "20"
451
+ "charge_daily_max": "20",
452
+ "manage_daily_max": "0"
189
453
  },
190
454
  "context": {
191
- "currency": "EUR",
192
- "action_type": "charge"
455
+ "currency": "EUR"
193
456
  },
194
- "intent": "Allow the agent to create payment links and process small charges for routine invoicing. Max €50 per transaction, €200 per day.",
457
+ "intent": "Allow the agent to create payment links and payments for routine amounts. An open-amount link, or one above €50, is refused. No lookups.",
195
458
  "ttl": 2592000,
196
459
  "tags": [
197
460
  "€50/tx",
@@ -201,20 +464,21 @@
201
464
  },
202
465
  {
203
466
  "name": "Full Payment Management",
204
- "description": "Payments, refunds, and subscriptions. Every action requires your approval.",
467
+ "description": "Payments, payment links and payment customers. Every write requires your approval.",
205
468
  "risk": "high",
206
469
  "mode": "review",
207
470
  "bounds": {
471
+ "read_access": "unlimited",
208
472
  "amount_max": "1000",
209
473
  "amount_daily_max": "5000",
210
474
  "amount_monthly_max": "20000",
211
- "transaction_count_daily_max": "50"
475
+ "charge_daily_max": "50",
476
+ "manage_daily_max": "20"
212
477
  },
213
478
  "context": {
214
- "currency": "EUR",
215
- "action_type": "charge,refund,subscribe"
479
+ "currency": "EUR"
216
480
  },
217
- "intent": "Full payment management — create payments, issue refunds, manage subscriptions. Every write action requires human review before execution.",
481
+ "intent": "Full payment management — create payments and payment links, manage payment customers, and look up payments, links, customers and balances. Every write requires human review before execution.",
218
482
  "ttl": 2592000,
219
483
  "tags": [
220
484
  "€1,000/tx",
@@ -759,6 +759,25 @@ async function getToolRequestContext(data) {
759
759
  clearTimeout(timeout);
760
760
  }
761
761
  }
762
+ async function getAttachmentPreview(ref) {
763
+ const controller = new AbortController();
764
+ const timeout = setTimeout(() => controller.abort(), 12e3);
765
+ try {
766
+ const res = await fetch(`${MCP_BASE}/internal/document-preview`, {
767
+ method: "POST",
768
+ headers: internalHeaders(),
769
+ body: JSON.stringify(ref),
770
+ signal: controller.signal
771
+ });
772
+ if (!res.ok) {
773
+ const err = await res.json().catch(() => ({ error: "Unknown error" }));
774
+ throw new Error(`MCP document-preview failed: ${err.error ?? res.status}`);
775
+ }
776
+ return res.json();
777
+ } finally {
778
+ clearTimeout(timeout);
779
+ }
780
+ }
762
781
  async function getAgentContact() {
763
782
  const res = await fetch(`${MCP_BASE}/internal/agent-contact`, {
764
783
  headers: internalHeaders()
@@ -3713,6 +3732,24 @@ function createEvidenceExportRouter(spUrl, vault2, dataDir, lockAsTlsMismatch2)
3713
3732
  // src/routes/proposal-preview.ts
3714
3733
  import { Router as Router10 } from "express";
3715
3734
 
3735
+ // src/lib/attachment-preview-types.ts
3736
+ var ATTACHMENTS_SEALED_META_KEY = "_attachmentsSealed";
3737
+ function findSealedAttachment(toolArgs, sha256) {
3738
+ if (!toolArgs) return null;
3739
+ const raw = toolArgs[ATTACHMENTS_SEALED_META_KEY];
3740
+ if (raw === null || typeof raw !== "object") return null;
3741
+ const attachments = raw.attachments;
3742
+ if (!Array.isArray(attachments)) return null;
3743
+ for (const a of attachments) {
3744
+ if (a === null || typeof a !== "object") continue;
3745
+ const r = a;
3746
+ if (r.sha256 === sha256 && typeof r.connector === "string" && typeof r.kind === "string" && typeof r.id === "string") {
3747
+ return { connector: r.connector, kind: r.kind, id: r.id, sha256 };
3748
+ }
3749
+ }
3750
+ return null;
3751
+ }
3752
+
3716
3753
  // src/lib/content-crypto.ts
3717
3754
  import { createHash as createHash5 } from "crypto";
3718
3755
  import { canonicalize as canonicalize3 } from "@hap/core";
@@ -3911,6 +3948,23 @@ function createProposalStatusRouter(spUrl, vault2, dataDir, lockAsTlsMismatch2,
3911
3948
  res.status(502).json({ error: `MCP server unavailable: ${err instanceof Error ? err.message : String(err)}` });
3912
3949
  }
3913
3950
  });
3951
+ router.get("/:id/attachment-preview/:sha256", async (req, res) => {
3952
+ const proposalId = req.params.id;
3953
+ const sha256 = req.params.sha256;
3954
+ const resolved = await resolveProposalToolArgsForRoute(spUrl, vault2, dataDir, lockAsTlsMismatch2, proposalId, res);
3955
+ if (!resolved) return;
3956
+ const ref = findSealedAttachment(resolved.toolArgs, sha256);
3957
+ if (!ref) {
3958
+ res.status(404).json({ status: "error", message: "No sealed attachment with this fingerprint on this proposal." });
3959
+ return;
3960
+ }
3961
+ try {
3962
+ const result = await getAttachmentPreview(ref);
3963
+ res.json(result);
3964
+ } catch (err) {
3965
+ res.status(502).json({ status: "error", message: `MCP server unavailable: ${err instanceof Error ? err.message : String(err)}` });
3966
+ }
3967
+ });
3914
3968
  router.get("/:id/request-context", async (req, res) => {
3915
3969
  const proposalId = req.params.id;
3916
3970
  const resolved = await resolveProposalToolArgsForRoute(spUrl, vault2, dataDir, lockAsTlsMismatch2, proposalId, res);
@@ -4138,6 +4192,31 @@ function createEvidenceRouter() {
4138
4192
  });
4139
4193
  }
4140
4194
  });
4195
+ router.get("/receipt/:id/attachment-preview/:sha256", async (req, res) => {
4196
+ const id = String(req.params.id);
4197
+ const sha256 = String(req.params.sha256);
4198
+ try {
4199
+ const evidence = await getLocalEvidence();
4200
+ const entry = (evidence.receipts ?? []).find(
4201
+ (r) => r.receipt?.id === id
4202
+ );
4203
+ if (!entry) {
4204
+ res.status(404).json({ status: "error", message: "No local copy of this receipt" });
4205
+ return;
4206
+ }
4207
+ const e = entry;
4208
+ const candidate = e.sentArgs ?? e.proposal?.toolArgs ?? (e.boundContent && typeof e.boundContent === "object" && !Array.isArray(e.boundContent) ? e.boundContent : void 0);
4209
+ const ref = findSealedAttachment(candidate, sha256);
4210
+ if (!ref) {
4211
+ res.status(404).json({ status: "error", message: "No sealed attachment with this fingerprint on this ticket." });
4212
+ return;
4213
+ }
4214
+ const result = await getAttachmentPreview(ref);
4215
+ res.json(result);
4216
+ } catch (err) {
4217
+ res.status(502).json({ status: "error", message: `MCP server unavailable: ${err instanceof Error ? err.message : String(err)}` });
4218
+ }
4219
+ });
4141
4220
  router.get("/headlines", async (_req, res) => {
4142
4221
  try {
4143
4222
  res.json({ headlines: await getTicketHeadlines() });