fluxmail 0.13.0 → 0.14.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.
Files changed (225) hide show
  1. package/dist/accounts/registry.d.ts +1 -0
  2. package/dist/accounts/registry.d.ts.map +1 -1
  3. package/dist/accounts/registry.js +3 -2
  4. package/dist/accounts/registry.js.map +1 -1
  5. package/dist/accounts/telemetry.d.ts +6 -1
  6. package/dist/accounts/telemetry.d.ts.map +1 -1
  7. package/dist/accounts/telemetry.js +17 -0
  8. package/dist/accounts/telemetry.js.map +1 -1
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +122 -25
  11. package/dist/cli.js.map +1 -1
  12. package/dist/cliInstances.d.ts +6 -4
  13. package/dist/cliInstances.d.ts.map +1 -1
  14. package/dist/cliInstances.js +29 -9
  15. package/dist/cliInstances.js.map +1 -1
  16. package/dist/http/app.d.ts.map +1 -1
  17. package/dist/http/app.js +16 -1
  18. package/dist/http/app.js.map +1 -1
  19. package/dist/http/identity.d.ts.map +1 -1
  20. package/dist/http/identity.js +65 -10
  21. package/dist/http/identity.js.map +1 -1
  22. package/dist/http/rest.d.ts.map +1 -1
  23. package/dist/http/rest.js +30 -8
  24. package/dist/http/rest.js.map +1 -1
  25. package/dist/index.d.ts +1 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +1 -1
  28. package/dist/index.js.map +1 -1
  29. package/dist/mcp/attachmentContent.d.ts +32 -0
  30. package/dist/mcp/attachmentContent.d.ts.map +1 -0
  31. package/dist/mcp/attachmentContent.js +155 -0
  32. package/dist/mcp/attachmentContent.js.map +1 -0
  33. package/dist/mcp/buildServer.d.ts +3 -1
  34. package/dist/mcp/buildServer.d.ts.map +1 -1
  35. package/dist/mcp/buildServer.js +130 -49
  36. package/dist/mcp/buildServer.js.map +1 -1
  37. package/dist/mcp/pdfTextWorker.d.ts +2 -0
  38. package/dist/mcp/pdfTextWorker.d.ts.map +1 -0
  39. package/dist/mcp/pdfTextWorker.js +39 -0
  40. package/dist/mcp/pdfTextWorker.js.map +1 -0
  41. package/dist/permissions.d.ts +19 -0
  42. package/dist/permissions.d.ts.map +1 -1
  43. package/dist/permissions.js +42 -2
  44. package/dist/permissions.js.map +1 -1
  45. package/dist/service/emailService.d.ts +1 -0
  46. package/dist/service/emailService.d.ts.map +1 -1
  47. package/dist/service/emailService.js +4 -1
  48. package/dist/service/emailService.js.map +1 -1
  49. package/dist/service/publicErrors.d.ts +4 -0
  50. package/dist/service/publicErrors.d.ts.map +1 -1
  51. package/dist/service/publicErrors.js +10 -1
  52. package/dist/service/publicErrors.js.map +1 -1
  53. package/dist/storage/apiKeys.d.ts.map +1 -1
  54. package/dist/storage/apiKeys.js +2 -2
  55. package/dist/storage/apiKeys.js.map +1 -1
  56. package/package.json +7 -5
  57. package/pdf-cmaps/78-EUC-H.bcmap +0 -0
  58. package/pdf-cmaps/78-EUC-V.bcmap +0 -0
  59. package/pdf-cmaps/78-H.bcmap +0 -0
  60. package/pdf-cmaps/78-RKSJ-H.bcmap +0 -0
  61. package/pdf-cmaps/78-RKSJ-V.bcmap +0 -0
  62. package/pdf-cmaps/78-V.bcmap +0 -0
  63. package/pdf-cmaps/78ms-RKSJ-H.bcmap +0 -0
  64. package/pdf-cmaps/78ms-RKSJ-V.bcmap +0 -0
  65. package/pdf-cmaps/83pv-RKSJ-H.bcmap +0 -0
  66. package/pdf-cmaps/90ms-RKSJ-H.bcmap +0 -0
  67. package/pdf-cmaps/90ms-RKSJ-V.bcmap +0 -0
  68. package/pdf-cmaps/90msp-RKSJ-H.bcmap +0 -0
  69. package/pdf-cmaps/90msp-RKSJ-V.bcmap +0 -0
  70. package/pdf-cmaps/90pv-RKSJ-H.bcmap +0 -0
  71. package/pdf-cmaps/90pv-RKSJ-V.bcmap +0 -0
  72. package/pdf-cmaps/Add-H.bcmap +0 -0
  73. package/pdf-cmaps/Add-RKSJ-H.bcmap +0 -0
  74. package/pdf-cmaps/Add-RKSJ-V.bcmap +0 -0
  75. package/pdf-cmaps/Add-V.bcmap +0 -0
  76. package/pdf-cmaps/Adobe-CNS1-0.bcmap +0 -0
  77. package/pdf-cmaps/Adobe-CNS1-1.bcmap +0 -0
  78. package/pdf-cmaps/Adobe-CNS1-2.bcmap +0 -0
  79. package/pdf-cmaps/Adobe-CNS1-3.bcmap +0 -0
  80. package/pdf-cmaps/Adobe-CNS1-4.bcmap +0 -0
  81. package/pdf-cmaps/Adobe-CNS1-5.bcmap +0 -0
  82. package/pdf-cmaps/Adobe-CNS1-6.bcmap +0 -0
  83. package/pdf-cmaps/Adobe-CNS1-UCS2.bcmap +0 -0
  84. package/pdf-cmaps/Adobe-GB1-0.bcmap +0 -0
  85. package/pdf-cmaps/Adobe-GB1-1.bcmap +0 -0
  86. package/pdf-cmaps/Adobe-GB1-2.bcmap +0 -0
  87. package/pdf-cmaps/Adobe-GB1-3.bcmap +0 -0
  88. package/pdf-cmaps/Adobe-GB1-4.bcmap +0 -0
  89. package/pdf-cmaps/Adobe-GB1-5.bcmap +0 -0
  90. package/pdf-cmaps/Adobe-GB1-UCS2.bcmap +0 -0
  91. package/pdf-cmaps/Adobe-Japan1-0.bcmap +0 -0
  92. package/pdf-cmaps/Adobe-Japan1-1.bcmap +0 -0
  93. package/pdf-cmaps/Adobe-Japan1-2.bcmap +0 -0
  94. package/pdf-cmaps/Adobe-Japan1-3.bcmap +0 -0
  95. package/pdf-cmaps/Adobe-Japan1-4.bcmap +0 -0
  96. package/pdf-cmaps/Adobe-Japan1-5.bcmap +0 -0
  97. package/pdf-cmaps/Adobe-Japan1-6.bcmap +0 -0
  98. package/pdf-cmaps/Adobe-Japan1-UCS2.bcmap +0 -0
  99. package/pdf-cmaps/Adobe-Korea1-0.bcmap +0 -0
  100. package/pdf-cmaps/Adobe-Korea1-1.bcmap +0 -0
  101. package/pdf-cmaps/Adobe-Korea1-2.bcmap +0 -0
  102. package/pdf-cmaps/Adobe-Korea1-UCS2.bcmap +0 -0
  103. package/pdf-cmaps/B5-H.bcmap +0 -0
  104. package/pdf-cmaps/B5-V.bcmap +0 -0
  105. package/pdf-cmaps/B5pc-H.bcmap +0 -0
  106. package/pdf-cmaps/B5pc-V.bcmap +0 -0
  107. package/pdf-cmaps/CNS-EUC-H.bcmap +0 -0
  108. package/pdf-cmaps/CNS-EUC-V.bcmap +0 -0
  109. package/pdf-cmaps/CNS1-H.bcmap +0 -0
  110. package/pdf-cmaps/CNS1-V.bcmap +0 -0
  111. package/pdf-cmaps/CNS2-H.bcmap +0 -0
  112. package/pdf-cmaps/CNS2-V.bcmap +3 -0
  113. package/pdf-cmaps/ETHK-B5-H.bcmap +0 -0
  114. package/pdf-cmaps/ETHK-B5-V.bcmap +0 -0
  115. package/pdf-cmaps/ETen-B5-H.bcmap +0 -0
  116. package/pdf-cmaps/ETen-B5-V.bcmap +0 -0
  117. package/pdf-cmaps/ETenms-B5-H.bcmap +3 -0
  118. package/pdf-cmaps/ETenms-B5-V.bcmap +0 -0
  119. package/pdf-cmaps/EUC-H.bcmap +0 -0
  120. package/pdf-cmaps/EUC-V.bcmap +0 -0
  121. package/pdf-cmaps/Ext-H.bcmap +0 -0
  122. package/pdf-cmaps/Ext-RKSJ-H.bcmap +0 -0
  123. package/pdf-cmaps/Ext-RKSJ-V.bcmap +0 -0
  124. package/pdf-cmaps/Ext-V.bcmap +0 -0
  125. package/pdf-cmaps/GB-EUC-H.bcmap +0 -0
  126. package/pdf-cmaps/GB-EUC-V.bcmap +0 -0
  127. package/pdf-cmaps/GB-H.bcmap +4 -0
  128. package/pdf-cmaps/GB-V.bcmap +0 -0
  129. package/pdf-cmaps/GBK-EUC-H.bcmap +0 -0
  130. package/pdf-cmaps/GBK-EUC-V.bcmap +0 -0
  131. package/pdf-cmaps/GBK2K-H.bcmap +0 -0
  132. package/pdf-cmaps/GBK2K-V.bcmap +0 -0
  133. package/pdf-cmaps/GBKp-EUC-H.bcmap +0 -0
  134. package/pdf-cmaps/GBKp-EUC-V.bcmap +0 -0
  135. package/pdf-cmaps/GBT-EUC-H.bcmap +0 -0
  136. package/pdf-cmaps/GBT-EUC-V.bcmap +0 -0
  137. package/pdf-cmaps/GBT-H.bcmap +0 -0
  138. package/pdf-cmaps/GBT-V.bcmap +0 -0
  139. package/pdf-cmaps/GBTpc-EUC-H.bcmap +0 -0
  140. package/pdf-cmaps/GBTpc-EUC-V.bcmap +0 -0
  141. package/pdf-cmaps/GBpc-EUC-H.bcmap +0 -0
  142. package/pdf-cmaps/GBpc-EUC-V.bcmap +0 -0
  143. package/pdf-cmaps/H.bcmap +0 -0
  144. package/pdf-cmaps/HKdla-B5-H.bcmap +0 -0
  145. package/pdf-cmaps/HKdla-B5-V.bcmap +0 -0
  146. package/pdf-cmaps/HKdlb-B5-H.bcmap +0 -0
  147. package/pdf-cmaps/HKdlb-B5-V.bcmap +0 -0
  148. package/pdf-cmaps/HKgccs-B5-H.bcmap +0 -0
  149. package/pdf-cmaps/HKgccs-B5-V.bcmap +0 -0
  150. package/pdf-cmaps/HKm314-B5-H.bcmap +0 -0
  151. package/pdf-cmaps/HKm314-B5-V.bcmap +0 -0
  152. package/pdf-cmaps/HKm471-B5-H.bcmap +0 -0
  153. package/pdf-cmaps/HKm471-B5-V.bcmap +0 -0
  154. package/pdf-cmaps/HKscs-B5-H.bcmap +0 -0
  155. package/pdf-cmaps/HKscs-B5-V.bcmap +0 -0
  156. package/pdf-cmaps/Hankaku.bcmap +0 -0
  157. package/pdf-cmaps/Hiragana.bcmap +0 -0
  158. package/pdf-cmaps/KSC-EUC-H.bcmap +0 -0
  159. package/pdf-cmaps/KSC-EUC-V.bcmap +0 -0
  160. package/pdf-cmaps/KSC-H.bcmap +0 -0
  161. package/pdf-cmaps/KSC-Johab-H.bcmap +0 -0
  162. package/pdf-cmaps/KSC-Johab-V.bcmap +0 -0
  163. package/pdf-cmaps/KSC-V.bcmap +0 -0
  164. package/pdf-cmaps/KSCms-UHC-H.bcmap +0 -0
  165. package/pdf-cmaps/KSCms-UHC-HW-H.bcmap +0 -0
  166. package/pdf-cmaps/KSCms-UHC-HW-V.bcmap +0 -0
  167. package/pdf-cmaps/KSCms-UHC-V.bcmap +0 -0
  168. package/pdf-cmaps/KSCpc-EUC-H.bcmap +0 -0
  169. package/pdf-cmaps/KSCpc-EUC-V.bcmap +0 -0
  170. package/pdf-cmaps/Katakana.bcmap +0 -0
  171. package/pdf-cmaps/LICENSE +36 -0
  172. package/pdf-cmaps/NWP-H.bcmap +0 -0
  173. package/pdf-cmaps/NWP-V.bcmap +0 -0
  174. package/pdf-cmaps/RKSJ-H.bcmap +0 -0
  175. package/pdf-cmaps/RKSJ-V.bcmap +0 -0
  176. package/pdf-cmaps/Roman.bcmap +0 -0
  177. package/pdf-cmaps/UniCNS-UCS2-H.bcmap +0 -0
  178. package/pdf-cmaps/UniCNS-UCS2-V.bcmap +0 -0
  179. package/pdf-cmaps/UniCNS-UTF16-H.bcmap +0 -0
  180. package/pdf-cmaps/UniCNS-UTF16-V.bcmap +0 -0
  181. package/pdf-cmaps/UniCNS-UTF32-H.bcmap +0 -0
  182. package/pdf-cmaps/UniCNS-UTF32-V.bcmap +0 -0
  183. package/pdf-cmaps/UniCNS-UTF8-H.bcmap +0 -0
  184. package/pdf-cmaps/UniCNS-UTF8-V.bcmap +0 -0
  185. package/pdf-cmaps/UniGB-UCS2-H.bcmap +0 -0
  186. package/pdf-cmaps/UniGB-UCS2-V.bcmap +0 -0
  187. package/pdf-cmaps/UniGB-UTF16-H.bcmap +0 -0
  188. package/pdf-cmaps/UniGB-UTF16-V.bcmap +0 -0
  189. package/pdf-cmaps/UniGB-UTF32-H.bcmap +0 -0
  190. package/pdf-cmaps/UniGB-UTF32-V.bcmap +0 -0
  191. package/pdf-cmaps/UniGB-UTF8-H.bcmap +0 -0
  192. package/pdf-cmaps/UniGB-UTF8-V.bcmap +0 -0
  193. package/pdf-cmaps/UniJIS-UCS2-H.bcmap +0 -0
  194. package/pdf-cmaps/UniJIS-UCS2-HW-H.bcmap +0 -0
  195. package/pdf-cmaps/UniJIS-UCS2-HW-V.bcmap +0 -0
  196. package/pdf-cmaps/UniJIS-UCS2-V.bcmap +0 -0
  197. package/pdf-cmaps/UniJIS-UTF16-H.bcmap +0 -0
  198. package/pdf-cmaps/UniJIS-UTF16-V.bcmap +0 -0
  199. package/pdf-cmaps/UniJIS-UTF32-H.bcmap +0 -0
  200. package/pdf-cmaps/UniJIS-UTF32-V.bcmap +0 -0
  201. package/pdf-cmaps/UniJIS-UTF8-H.bcmap +0 -0
  202. package/pdf-cmaps/UniJIS-UTF8-V.bcmap +0 -0
  203. package/pdf-cmaps/UniJIS2004-UTF16-H.bcmap +0 -0
  204. package/pdf-cmaps/UniJIS2004-UTF16-V.bcmap +0 -0
  205. package/pdf-cmaps/UniJIS2004-UTF32-H.bcmap +0 -0
  206. package/pdf-cmaps/UniJIS2004-UTF32-V.bcmap +0 -0
  207. package/pdf-cmaps/UniJIS2004-UTF8-H.bcmap +0 -0
  208. package/pdf-cmaps/UniJIS2004-UTF8-V.bcmap +0 -0
  209. package/pdf-cmaps/UniJISPro-UCS2-HW-V.bcmap +0 -0
  210. package/pdf-cmaps/UniJISPro-UCS2-V.bcmap +0 -0
  211. package/pdf-cmaps/UniJISPro-UTF8-V.bcmap +0 -0
  212. package/pdf-cmaps/UniJISX0213-UTF32-H.bcmap +0 -0
  213. package/pdf-cmaps/UniJISX0213-UTF32-V.bcmap +0 -0
  214. package/pdf-cmaps/UniJISX02132004-UTF32-H.bcmap +0 -0
  215. package/pdf-cmaps/UniJISX02132004-UTF32-V.bcmap +0 -0
  216. package/pdf-cmaps/UniKS-UCS2-H.bcmap +0 -0
  217. package/pdf-cmaps/UniKS-UCS2-V.bcmap +0 -0
  218. package/pdf-cmaps/UniKS-UTF16-H.bcmap +0 -0
  219. package/pdf-cmaps/UniKS-UTF16-V.bcmap +0 -0
  220. package/pdf-cmaps/UniKS-UTF32-H.bcmap +0 -0
  221. package/pdf-cmaps/UniKS-UTF32-V.bcmap +0 -0
  222. package/pdf-cmaps/UniKS-UTF8-H.bcmap +0 -0
  223. package/pdf-cmaps/UniKS-UTF8-V.bcmap +0 -0
  224. package/pdf-cmaps/V.bcmap +0 -0
  225. package/pdf-cmaps/WP-Symbol.bcmap +0 -0
@@ -0,0 +1,155 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { Worker } from 'node:worker_threads';
3
+ import { z } from 'zod';
4
+ const TEXT_TYPES = /^(text\/[\w.+-]+|application\/(json|xml|csv|yaml|x-yaml|javascript|[\w.-]+\+(json|xml)))$/;
5
+ const IMAGE_TYPES = /^image\/(png|jpeg|gif|webp)$/;
6
+ // At most `workers` extractions run at once in this process, so concurrent requests cannot
7
+ // multiply the heap limit. Waiting for a worker shares the same timeout as extraction.
8
+ const PDF_TEXT_LIMITS = { maxPages: 200, maxChars: 80_000, heapMb: 128, timeoutMs: 15_000, workers: 2 };
9
+ export function mediaType(mimeType) {
10
+ return mimeType.split(';')[0].trim().toLowerCase();
11
+ }
12
+ function decodeText(content) {
13
+ try {
14
+ return new TextDecoder('utf-8', { fatal: true }).decode(content);
15
+ }
16
+ catch {
17
+ return undefined;
18
+ }
19
+ }
20
+ /**
21
+ * Some clients, including claude.ai, only pass a blob to the model through
22
+ * file storage, so UTF-8 text attachments go out as text.
23
+ */
24
+ export function attachmentContents(uri, mimeType, content) {
25
+ const text = TEXT_TYPES.test(mediaType(mimeType)) ? decodeText(content) : undefined;
26
+ return text === undefined ? { uri, mimeType, blob: content.toString('base64') } : { uri, mimeType, text };
27
+ }
28
+ export function inlineContent(uri, mimeType, content) {
29
+ if (IMAGE_TYPES.test(mediaType(mimeType)))
30
+ return { type: 'image', data: content.toString('base64'), mimeType };
31
+ return { type: 'resource', resource: attachmentContents(uri, mimeType, content) };
32
+ }
33
+ function parsePages(pages) {
34
+ const [first, last = first] = pages.split('-').map(Number);
35
+ return { first, last };
36
+ }
37
+ export const pageRange = z
38
+ .string()
39
+ .regex(/^[1-9]\d{0,5}(-[1-9]\d{0,5})?$/)
40
+ .refine((value) => {
41
+ const { first, last } = parsePages(value);
42
+ return last >= first;
43
+ }, { message: 'Use a page or an ascending range such as 201-400.' })
44
+ .describe('PDF page or page range to read as text, such as 201-400');
45
+ // The build emits the worker next to this module; tests load the source.
46
+ const PDF_WORKER_URL = new URL(import.meta.url.endsWith('.ts') ? './pdfTextWorker.ts' : './pdfTextWorker.js', import.meta.url);
47
+ // Character maps for PDFs that use predefined CJK encodings, copied from pdfjs-dist at the
48
+ // pdf.js version unpdf bundles. Refresh them when upgrading unpdf. Same path from src and dist.
49
+ export const PDF_CMAP_PDFJS_VERSION = '5.6.205';
50
+ const PDF_CMAP_DIRECTORY = fileURLToPath(new URL('../../pdf-cmaps/', import.meta.url));
51
+ let runningWorkers = 0;
52
+ const workerQueue = [];
53
+ /** Resolves true once a worker slot is held, or false when the signal aborts first. */
54
+ function acquireWorkerSlot(signal) {
55
+ if (signal.aborted)
56
+ return Promise.resolve(false);
57
+ if (runningWorkers < PDF_TEXT_LIMITS.workers) {
58
+ runningWorkers++;
59
+ return Promise.resolve(true);
60
+ }
61
+ return new Promise((resolve) => {
62
+ const grant = () => {
63
+ signal.removeEventListener('abort', cancel);
64
+ resolve(true);
65
+ };
66
+ const cancel = () => {
67
+ workerQueue.splice(workerQueue.indexOf(grant), 1);
68
+ resolve(false);
69
+ };
70
+ workerQueue.push(grant);
71
+ signal.addEventListener('abort', cancel, { once: true });
72
+ });
73
+ }
74
+ /** Hand the slot to the next waiting extraction, or free it. */
75
+ function releaseWorkerSlot() {
76
+ const next = workerQueue.shift();
77
+ if (next)
78
+ next();
79
+ else
80
+ runningWorkers--;
81
+ }
82
+ /** Extract PDF text in a memory-limited worker. Resolves undefined when extraction fails or is aborted. */
83
+ export async function pdfText(content, signal, pages = '1-999999', onError) {
84
+ const waitTimeout = AbortSignal.timeout(PDF_TEXT_LIMITS.timeoutMs);
85
+ if (!(await acquireWorkerSlot(AbortSignal.any([signal, waitTimeout])))) {
86
+ if (waitTimeout.aborted && !signal.aborted)
87
+ onError?.(new Error('PDF text extraction timed out waiting for a worker.'));
88
+ return undefined;
89
+ }
90
+ try {
91
+ return await runPdfWorker(content, signal, pages, onError);
92
+ }
93
+ finally {
94
+ releaseWorkerSlot();
95
+ }
96
+ }
97
+ function runPdfWorker(content, signal, pages, onError) {
98
+ const { first: firstPage, last: lastPage } = parsePages(pages);
99
+ return new Promise((resolve) => {
100
+ if (signal.aborted)
101
+ return resolve(undefined);
102
+ const bytes = new Uint8Array(content);
103
+ const worker = new Worker(PDF_WORKER_URL, {
104
+ workerData: {
105
+ bytes,
106
+ cMapDirectory: PDF_CMAP_DIRECTORY,
107
+ firstPage,
108
+ lastPage,
109
+ maxPages: PDF_TEXT_LIMITS.maxPages,
110
+ maxChars: PDF_TEXT_LIMITS.maxChars,
111
+ },
112
+ transferList: [bytes.buffer],
113
+ resourceLimits: { maxOldGenerationSizeMb: PDF_TEXT_LIMITS.heapMb },
114
+ });
115
+ let settled = false;
116
+ const finish = (result, error) => {
117
+ if (settled)
118
+ return;
119
+ settled = true;
120
+ clearTimeout(timer);
121
+ signal.removeEventListener('abort', abort);
122
+ if (error)
123
+ onError?.(error);
124
+ // Resolve after the thread stops so a released slot no longer holds its heap.
125
+ void worker.terminate().finally(() => resolve(result));
126
+ };
127
+ const abort = () => finish();
128
+ const timer = setTimeout(() => finish(undefined, new Error('PDF text extraction timed out.')), PDF_TEXT_LIMITS.timeoutMs);
129
+ signal.addEventListener('abort', abort, { once: true });
130
+ worker.once('message', (result) => finish(result));
131
+ worker.once('error', (error) => finish(undefined, error));
132
+ worker.once('exit', (code) => finish(undefined, new Error(`PDF text worker exited with code ${code}.`)));
133
+ });
134
+ }
135
+ export function pdfTextContent(filename, pdf) {
136
+ if (!pdf)
137
+ return [];
138
+ const { firstPage, lastPage, totalPages } = pdf;
139
+ if (lastPage < firstPage) {
140
+ return [{ type: 'text', text: `${filename} has ${totalPages} ${totalPages === 1 ? 'page' : 'pages'}.` }];
141
+ }
142
+ const whole = firstPage === 1 && lastPage === totalPages && !pdf.cut;
143
+ const pages = firstPage === lastPage ? `page ${firstPage}` : `pages ${firstPage}-${lastPage}`;
144
+ const cut = pdf.cut ? `, cut at ${PDF_TEXT_LIMITS.maxChars.toLocaleString('en-US')} characters` : '';
145
+ const scope = whole ? '' : ` (${pages} of ${totalPages}${cut})`;
146
+ return [
147
+ {
148
+ type: 'text',
149
+ text: pdf.text
150
+ ? `Text extracted from ${filename}${scope}:\n\n${pdf.text}`
151
+ : `No text found in ${filename}${scope}.`,
152
+ },
153
+ ];
154
+ }
155
+ //# sourceMappingURL=attachmentContent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachmentContent.js","sourceRoot":"","sources":["../../src/mcp/attachmentContent.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import type { EmailService, SendInput } from '../service/emailService.js';
3
- import { type PermissionPolicy } from '../permissions.js';
3
+ import { type PermissionPolicy, type PermissionSource } from '../permissions.js';
4
4
  import { type Telemetry } from '../telemetry.js';
5
5
  import { type Logger } from '../logging.js';
6
6
  type DraftArgs = {
@@ -28,6 +28,8 @@ export declare function toSendRequest(args: DraftArgs & {
28
28
  export type McpTransport = 'http' | 'stdio' | 'unknown';
29
29
  export interface BuildMcpServerOptions {
30
30
  permissions?: PermissionPolicy;
31
+ /** How the connection's permissions can be changed. Defaults to the stdio options for stdio transport. */
32
+ permissionSource?: PermissionSource;
31
33
  maxAttachmentBytes?: number;
32
34
  telemetry?: Telemetry;
33
35
  transport?: McpTransport;
@@ -1 +1 @@
1
- {"version":3,"file":"buildServer.d.ts","sourceRoot":"","sources":["../../src/mcp/buildServer.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,SAAS,EAAoB,MAAM,yCAAyC,CAAC;AAgBtF,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAG1E,OAAO,EAKL,KAAK,gBAAgB,EACtB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAoB,KAAK,SAAS,EAA4B,MAAM,iBAAiB,CAAC;AAC7F,OAAO,EAAc,KAAK,MAAM,EAAE,MAAM,eAAe,CAAC;AAyHxD,KAAK,SAAS,GAAG;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC9E,CAAC;AA0BF,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,GAAG;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAuBrG;AAoDD,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;AAExD,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,SAAS,CAAC,EAAE,YAAY,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,gBAAgB,GAAG,qBAAqB,CAAC;AAkJrD,wBAAgB,cAAc,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,GAAE,qBAA0B,GAAG,SAAS,CA0xBpG"}
1
+ {"version":3,"file":"buildServer.d.ts","sourceRoot":"","sources":["../../src/mcp/buildServer.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,SAAS,EAAoB,MAAM,yCAAyC,CAAC;AA2BtF,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAG1E,OAAO,EAQL,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACtB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAoB,KAAK,SAAS,EAA4B,MAAM,iBAAiB,CAAC;AAC7F,OAAO,EAAc,KAAK,MAAM,EAAE,MAAM,eAAe,CAAC;AAsIxD,KAAK,SAAS,GAAG;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC9E,CAAC;AA0BF,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,GAAG;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAuBrG;AAoDD,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;AAExD,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B,0GAA0G;IAC1G,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IACpC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,SAAS,CAAC,EAAE,YAAY,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,gBAAgB,GAAG,qBAAqB,CAAC;AAuKrD,wBAAgB,cAAc,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,GAAE,qBAA0B,GAAG,SAAS,CAg2BpG"}
@@ -3,15 +3,20 @@ import { randomUUID } from 'node:crypto';
3
3
  import { z } from 'zod';
4
4
  import { zodToJsonSchema } from 'zod-to-json-schema';
5
5
  import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
6
- import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
6
+ import { isInitializeRequest, LATEST_PROTOCOL_VERSION, ListToolsRequestSchema, SUPPORTED_PROTOCOL_VERSIONS, } from '@modelcontextprotocol/sdk/types.js';
7
7
  import { EmailError, isEmailError, mergeEmailQueries, parseEmailSearch, parseSingleAddress, } from '@fluxmail/core';
8
8
  import { DEFAULT_MAX_ATTACHMENT_BYTES } from '../config.js';
9
- import { FULL_PERMISSION_POLICY, hasCapability, normalizePermissionPolicy, } from '../permissions.js';
9
+ import { DEFAULT_PERMISSION_POLICY, MCP_CAPABILITIES, MCP_CAPABILITY_DESCRIPTIONS, enableCapabilitiesInstructions, hasCapability, normalizePermissionPolicy, } from '../permissions.js';
10
10
  import { captureOperation } from '../telemetry.js';
11
11
  import { logFailure } from '../logging.js';
12
- import { ClientInputError, publicError } from '../service/publicErrors.js';
12
+ import { ClientInputError, MissingCapabilityError, publicError } from '../service/publicErrors.js';
13
13
  import { outputSchemas } from './outputSchemas.js';
14
+ import { attachmentContents, inlineContent, mediaType, pageRange, pdfText, pdfTextContent, } from './attachmentContent.js';
14
15
  const MAX_BODY_CHARS = 50_000;
16
+ const BYTES_PER_MEGABYTE = 1024 * 1024;
17
+ function sizeLabel(bytes) {
18
+ return bytes % BYTES_PER_MEGABYTE === 0 ? `${bytes / BYTES_PER_MEGABYTE} MB` : `${bytes} bytes`;
19
+ }
15
20
  const TELEMETRY_ERROR = Symbol('telemetryError');
16
21
  const EMPTY_INPUT_SCHEMA = {
17
22
  type: 'object',
@@ -219,7 +224,9 @@ function toolFeatureProperties(tool, args) {
219
224
  case 'modify_emails':
220
225
  return typeof input.action === 'string' ? { action: input.action } : {};
221
226
  case 'download_attachment':
222
- return { destination: input.inline === true ? 'inline' : 'resource' };
227
+ return {
228
+ destination: input.inline === true ? 'inline' : typeof input.pages === 'string' ? 'pdf_pages' : 'resource',
229
+ };
223
230
  default:
224
231
  return {};
225
232
  }
@@ -316,8 +323,22 @@ const MODIFY_CAPABILITIES = {
316
323
  removeLabels: 'mail.organize',
317
324
  };
318
325
  const PROTECTED_MOVE_DESTINATIONS = new Set(['archive', 'trash']);
326
+ function disabledCapabilitiesInstructions(permissions, source) {
327
+ const disabled = MCP_CAPABILITIES.filter((capability) => !hasCapability(permissions, capability));
328
+ if (!disabled.length)
329
+ return '';
330
+ const actions = disabled.map((capability) => `- ${capability} (${MCP_CAPABILITY_DESCRIPTIONS[capability].replace(/\.$/, '')}). ` +
331
+ enableCapabilitiesInstructions(permissions, [capability], source));
332
+ return ('\n\nThis connection has limited permissions. Tools for the disabled actions below are not available. ' +
333
+ 'When the user asks for a disabled action, such as sending, replying to, or forwarding mail, tell them ' +
334
+ 'it is disabled for this Fluxmail connection and how to enable it. Before changing permissions, explain ' +
335
+ "the access the change grants and get the user's explicit approval for that permission change. A request " +
336
+ 'to perform an email action does not authorize changing permissions.\n' +
337
+ actions.join('\n'));
338
+ }
319
339
  export function buildMcpServer(service, options = {}) {
320
- const permissions = normalizePermissionPolicy(options.permissions ?? FULL_PERMISSION_POLICY);
340
+ const permissions = normalizePermissionPolicy(options.permissions ?? DEFAULT_PERMISSION_POLICY);
341
+ const permissionSource = options.permissionSource ?? (options.transport === 'stdio' ? { kind: 'stdio' } : undefined);
321
342
  const maxAttachmentBytes = options.maxAttachmentBytes ?? DEFAULT_MAX_ATTACHMENT_BYTES;
322
343
  const can = (capability) => hasCapability(permissions, capability);
323
344
  const canAll = (capabilities) => capabilities.every(can);
@@ -325,9 +346,20 @@ export function buildMcpServer(service, options = {}) {
325
346
  const requireCapabilities = (capabilities) => {
326
347
  const missing = capabilities.filter((capability) => !can(capability));
327
348
  if (missing.length) {
328
- throw new EmailError('permission_denied', `This MCP connection does not allow: ${missing.join(', ')}.`);
349
+ throw new MissingCapabilityError(`This MCP connection does not allow: ${missing.join(', ')}. ` +
350
+ enableCapabilitiesInstructions(permissions, missing, permissionSource), missing);
329
351
  }
330
352
  };
353
+ const downloadAttachment = async (accountId, messageId, attachmentId) => {
354
+ const attachment = await service.getAttachment(accountId, messageId, attachmentId, maxAttachmentBytes);
355
+ if (attachment.content.length > maxAttachmentBytes) {
356
+ throw new ClientInputError('invalid_request', 'Attachment is too large to return through MCP.', {
357
+ sizeBytes: attachment.content.length,
358
+ maxBytes: maxAttachmentBytes,
359
+ });
360
+ }
361
+ return attachment;
362
+ };
331
363
  // Every tool except get_status (the diagnostic way out) enforces the plan quota.
332
364
  const gated = (tool, capability, fn) => handle(tool, async (...args) => {
333
365
  requireCapabilities([capability]);
@@ -343,13 +375,41 @@ export function buildMcpServer(service, options = {}) {
343
375
  }, () => service.enforceQuota(), options);
344
376
  const server = new McpServer({ name: 'fluxmail', title: 'Fluxmail Email', version: VERSION }, {
345
377
  instructions: "Fluxmail is the user's email integration, already authenticated against their real mailboxes. " +
346
- 'Use the available Fluxmail tools for email tasks instead of browser automation or another email connector. ' +
347
378
  'Tool results are structured for you, not for display. Message ids, thread ids, draft ids, and ' +
348
379
  'account ids are internal references: keep them for chaining calls (replying, forwarding, archiving), ' +
349
380
  'but do not show them to the user unless asked. Report outcomes in plain language with details people ' +
350
381
  "care about, e.g. 'Sent \"Quarterly report\" to ann@example.com' or 'Archived the thread', rather " +
351
- 'than echoing raw payloads, ids, or field names.',
382
+ 'than echoing raw payloads, ids, or field names.' +
383
+ disabledCapabilitiesInstructions(permissions, permissionSource),
352
384
  });
385
+ // resource_link arrived in MCP 2025-06-18. Streamable HTTP sends the version on each request;
386
+ // stdio negotiates it once, so remember it. The SDK has no public accessor for the negotiated
387
+ // version, so observe the initialize request. The SDK installs onmessage before starting the
388
+ // transport, and this wrapper runs before the SDK handles each message.
389
+ let negotiatedProtocolVersion;
390
+ const connect = server.connect.bind(server);
391
+ server.connect = (transport) => {
392
+ const start = transport.start.bind(transport);
393
+ transport.start = () => {
394
+ const onmessage = transport.onmessage;
395
+ transport.onmessage = (message, extra) => {
396
+ if (isInitializeRequest(message)) {
397
+ const requested = message.params.protocolVersion;
398
+ negotiatedProtocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(requested)
399
+ ? requested
400
+ : LATEST_PROTOCOL_VERSION;
401
+ }
402
+ onmessage?.(message, extra);
403
+ };
404
+ return start();
405
+ };
406
+ return connect(transport);
407
+ };
408
+ const supportsResourceLinks = (extra) => {
409
+ const header = extra.requestInfo?.headers['mcp-protocol-version'];
410
+ const version = (typeof header === 'string' ? header : undefined) ?? negotiatedProtocolVersion ?? '2025-03-26';
411
+ return version >= '2025-06-18';
412
+ };
353
413
  const listedTools = [];
354
414
  const registerTool = ((name, config, callback) => {
355
415
  const outputSchema = outputSchemas[name];
@@ -370,46 +430,52 @@ export function buildMcpServer(service, options = {}) {
370
430
  });
371
431
  if (can('mail.read'))
372
432
  registerTool('list_accounts', {
433
+ title: 'List accounts',
373
434
  description: 'List connected email accounts (id, provider, email, status, capabilities).',
374
435
  inputSchema: {},
375
436
  annotations: { readOnlyHint: true },
376
437
  }, gated('list_accounts', 'mail.read', async () => service.listAccounts()));
377
438
  if (can('mail.read'))
378
439
  registerTool('get_status', {
379
- description: 'Account connection and scheduled-send status. Administrators also see plan details. ' +
380
- 'Call this first if other tools fail; it reports accounts that need re-authentication.',
440
+ title: 'Get status',
441
+ description: 'Account connection and scheduled-send status, including accounts that need re-authentication. ' +
442
+ 'Administrators also see plan details.',
381
443
  inputSchema: {},
382
444
  annotations: { readOnlyHint: true },
383
445
  }, allowed('get_status', 'mail.read', async () => service.status()));
384
446
  if (can('mail.read'))
385
447
  registerTool('list_folders', {
448
+ title: 'List folders',
386
449
  description: 'List navigable folders for an account, with roles (inbox, sent, drafts, trash, spam, starred).',
387
450
  inputSchema: { accountId: accountIdParam },
388
451
  annotations: { readOnlyHint: true },
389
452
  }, gated('list_folders', 'mail.read', async (args) => service.listFolders(args.accountId)));
390
453
  if (can('mail.read'))
391
454
  registerTool('list_labels', {
455
+ title: 'List labels',
392
456
  description: 'List Gmail user labels or Outlook categories for an account.',
393
457
  inputSchema: { accountId: accountIdParam },
394
458
  annotations: { readOnlyHint: true },
395
459
  }, gated('list_labels', 'mail.read', async (args) => service.listLabels(args.accountId)));
396
460
  if (can('mail.read'))
397
461
  registerTool('list_send_as', {
462
+ title: 'List sender addresses',
398
463
  description: 'List sender addresses available for an account.',
399
464
  inputSchema: { accountId: accountIdParam },
400
465
  annotations: { readOnlyHint: true },
401
466
  }, gated('list_send_as', 'mail.read', async (args) => service.listSendAs(args.accountId)));
402
467
  if (can('mail.read'))
403
468
  registerTool('list_emails', {
469
+ title: 'List emails',
404
470
  description: "List emails from the user's connected mailbox with metadata and optional previews. Filter by folder, " +
405
- 'sender, unread, dates, etc. Paginate with pageToken. Use get_email for full bodies. ' +
406
- "This is the way to check the user's email; no browser or other email integration is needed.",
471
+ 'sender, unread, dates, etc. Paginate with pageToken. Use get_email for full bodies.',
407
472
  inputSchema: { accountId: accountIdParam, ...queryShape },
408
473
  annotations: { readOnlyHint: true },
409
474
  }, gated('list_emails', 'mail.read', async (args) => service.listMessages(args.accountId, emailQuery(args), pageOpts(args))));
410
475
  const { text: _text, ...searchFilterShape } = queryShape;
411
476
  if (can('mail.read'))
412
477
  registerTool('search_emails', {
478
+ title: 'Search emails',
413
479
  description: 'Search one account with typed portable syntax. The query supports text, from:, to:, subject:, in:, ' +
414
480
  'read and starred states, attachments, and date filters.',
415
481
  inputSchema: {
@@ -442,6 +508,7 @@ export function buildMcpServer(service, options = {}) {
442
508
  const { pageToken: _batchPageToken, rawProviderQuery: _batchRawProviderQuery, ...batchSearchFilterShape } = searchFilterShape;
443
509
  if (can('mail.read'))
444
510
  registerTool('search_emails_batch', {
511
+ title: 'Search multiple accounts',
445
512
  description: 'Search up to 20 accounts with one portable query and return one result group per account.',
446
513
  inputSchema: {
447
514
  accounts: z
@@ -496,6 +563,7 @@ export function buildMcpServer(service, options = {}) {
496
563
  }));
497
564
  if (can('mail.read'))
498
565
  registerTool('get_email', {
566
+ title: 'Get email',
499
567
  description: 'Fetch one email in full: body (text and/or HTML), recipients, attachment metadata.',
500
568
  inputSchema: {
501
569
  accountId: accountIdParam,
@@ -506,6 +574,7 @@ export function buildMcpServer(service, options = {}) {
506
574
  }, gated('get_email', 'mail.read', async (args) => selectBody(await service.getMessage(args.accountId, args.messageId), args.bodyFormat ?? 'both', MAX_BODY_CHARS)));
507
575
  if (can('mail.read'))
508
576
  registerTool('get_email_body', {
577
+ title: 'Read email body',
509
578
  description: 'Read a bounded portion of one email body. Use nextOffset to continue.',
510
579
  inputSchema: {
511
580
  accountId: accountIdParam,
@@ -530,6 +599,7 @@ export function buildMcpServer(service, options = {}) {
530
599
  }));
531
600
  if (can('mail.read'))
532
601
  registerTool('get_thread', {
602
+ title: 'Get thread',
533
603
  description: 'Fetch a page of conversation messages with bounded body content.',
534
604
  inputSchema: {
535
605
  accountId: accountIdParam,
@@ -574,14 +644,17 @@ export function buildMcpServer(service, options = {}) {
574
644
  }));
575
645
  if (can('mail.drafts'))
576
646
  registerTool('get_draft', {
647
+ title: 'Get draft',
577
648
  description: 'Read an existing draft by its draft ID.',
578
649
  inputSchema: { accountId: accountIdParam, draftId: idParam },
579
650
  annotations: { readOnlyHint: true },
580
651
  }, gated('get_draft', 'mail.drafts', async (args) => service.getDraft(args.accountId, args.draftId)));
581
652
  if (can('mail.drafts'))
582
653
  registerTool('create_draft', {
654
+ title: 'Create draft',
583
655
  description: 'Create a draft. For a reply draft, pass replyToMessageId (recipients/subject are derived; replyAll for reply-all).',
584
656
  inputSchema: draftShape,
657
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
585
658
  }, gated('create_draft', 'mail.drafts', async (args) => {
586
659
  if (args.replyToMessageId)
587
660
  requireCapabilities(['mail.read']);
@@ -589,8 +662,10 @@ export function buildMcpServer(service, options = {}) {
589
662
  }));
590
663
  if (can('mail.drafts'))
591
664
  registerTool('update_draft', {
665
+ title: 'Update draft',
592
666
  description: 'Replace the content of an existing draft (full replacement, not a patch).',
593
667
  inputSchema: { draftId: idParam, ...draftShape },
668
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
594
669
  }, gated('update_draft', 'mail.drafts', async (args) => {
595
670
  if (args.replyToMessageId)
596
671
  requireCapabilities(['mail.read']);
@@ -598,15 +673,17 @@ export function buildMcpServer(service, options = {}) {
598
673
  }));
599
674
  if (can('mail.drafts'))
600
675
  registerTool('delete_draft', {
676
+ title: 'Delete draft',
601
677
  description: 'Delete a draft.',
602
678
  inputSchema: { accountId: accountIdParam, draftId: idParam },
603
- annotations: { destructiveHint: true },
679
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
604
680
  }, gated('delete_draft', 'mail.drafts', async (args) => {
605
681
  await service.deleteDraft(args.accountId, args.draftId);
606
682
  return { deleted: args.draftId };
607
683
  }));
608
684
  if (can('mail.send'))
609
685
  registerTool('preview_send', {
686
+ title: 'Preview send',
610
687
  description: 'Show the resolved sender, recipients, subject, and attachments without sending.',
611
688
  inputSchema: { draftId: idParam.optional(), ...draftShape },
612
689
  annotations: { readOnlyHint: true },
@@ -619,9 +696,9 @@ export function buildMcpServer(service, options = {}) {
619
696
  }));
620
697
  if (can('mail.send'))
621
698
  registerTool('send_email', {
622
- description: "Send an email from the user's connected account; this actually delivers mail, so prefer it over " +
623
- 'browser automation or leaving a draft when the user asked to send. Three modes: direct (to + subject ' +
624
- '+ body), sending an existing draft (draftId), or replying (replyToMessageId, optionally replyAll) ' +
699
+ title: 'Send email',
700
+ description: "Send an email from the user's connected account. Three modes: direct (to + subject + body), sending " +
701
+ 'an existing draft (draftId), or replying (replyToMessageId, optionally replyAll) ' +
625
702
  'where recipients, subject, and threading are derived from the original. Confirm with the user when ' +
626
703
  'intent is ambiguous. Add sendAt to any mode to schedule instead of sending now.',
627
704
  inputSchema: {
@@ -640,7 +717,7 @@ export function buildMcpServer(service, options = {}) {
640
717
  'for delivery by fluxmail serve or fluxmail scheduled run using the same store. Stdio does not ' +
641
718
  'deliver schedules. Overdue mail may send when a worker starts. Returns a scheduleId for list/cancel.'),
642
719
  },
643
- annotations: { destructiveHint: true },
720
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
644
721
  }, handleResult('send_email', async (args) => {
645
722
  requireCapabilities(['mail.send']);
646
723
  if (args.replyToMessageId !== undefined)
@@ -652,6 +729,7 @@ export function buildMcpServer(service, options = {}) {
652
729
  }, () => service.enforceQuota(), options));
653
730
  if (can('mail.read'))
654
731
  registerTool('list_scheduled_emails', {
732
+ title: 'List scheduled emails',
655
733
  description: 'List scheduled sends: pending ones first (with sendAt), then past ones (sent, failed, canceled). ' +
656
734
  'For failed entries, lastError says what went wrong. Pending sends only fire while the ' +
657
735
  'Fluxmail server is running.',
@@ -660,18 +738,22 @@ export function buildMcpServer(service, options = {}) {
660
738
  }, gated('list_scheduled_emails', 'mail.read', async (args) => service.listScheduled(args.accountId)));
661
739
  if (can('mail.send'))
662
740
  registerTool('get_delivery_operation', {
741
+ title: 'Get delivery status',
663
742
  description: 'Check whether a send or forward succeeded, failed, or has an uncertain outcome.',
664
743
  inputSchema: { accountId: idParam, operationId: idParam },
665
744
  annotations: { readOnlyHint: true },
666
745
  }, allowed('get_delivery_operation', 'mail.send', async (args) => service.getDelivery(args.accountId, args.operationId)));
667
746
  if (can('mail.drafts'))
668
747
  registerTool('cancel_scheduled_email', {
748
+ title: 'Cancel scheduled email',
669
749
  description: 'Cancel a pending scheduled send by scheduleId (from send_email with sendAt, or list_scheduled_emails). ' +
670
750
  'The draft stays in the Drafts folder, so the content is not lost.',
671
751
  inputSchema: { scheduleId: idParam },
752
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
672
753
  }, gated('cancel_scheduled_email', 'mail.drafts', async (args) => service.cancelScheduled(args.scheduleId)));
673
754
  if (canAll(['mail.send', 'mail.read']))
674
755
  registerTool('forward_email', {
756
+ title: 'Forward email',
675
757
  description: 'Forward an email to new recipients: quoted original body, "Fwd:" subject, original attachments included ' +
676
758
  'unless includeAttachments=false. Optional comment appears above the forwarded content.',
677
759
  inputSchema: {
@@ -690,7 +772,7 @@ export function buildMcpServer(service, options = {}) {
690
772
  .describe('Comment above the forwarded message. Keep prose paragraphs on one continuous line and separate paragraphs with blank lines.'),
691
773
  includeAttachments: z.boolean().optional().describe('Default true'),
692
774
  },
693
- annotations: { destructiveHint: true },
775
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
694
776
  }, handleResult('forward_email', async (args) => {
695
777
  requireCapabilities(['mail.send', 'mail.read']);
696
778
  const to = parseAddresses(args.to) ?? [];
@@ -708,6 +790,7 @@ export function buildMcpServer(service, options = {}) {
708
790
  if (modifyActions.length) {
709
791
  const allowedModifyActions = new Set(modifyActions);
710
792
  registerTool('modify_emails', {
793
+ title: 'Modify emails',
711
794
  description: 'Batch-modify emails using the actions allowed for this connection. Moving requires folder; labels require labels.',
712
795
  inputSchema: {
713
796
  accountId: accountIdParam,
@@ -720,7 +803,9 @@ export function buildMcpServer(service, options = {}) {
720
803
  labels: z.array(z.string().min(1)).max(100).optional().describe('Labels for addLabels/removeLabels'),
721
804
  },
722
805
  annotations: {
806
+ readOnlyHint: false,
723
807
  destructiveHint: canAny(['mail.trash', 'mail.delete']),
808
+ idempotentHint: true,
724
809
  },
725
810
  }, handleResult('modify_emails', async (args) => {
726
811
  if (!allowedModifyActions.has(args.action)) {
@@ -756,46 +841,45 @@ export function buildMcpServer(service, options = {}) {
756
841
  }
757
842
  if (can('mail.read'))
758
843
  registerTool('download_attachment', {
759
- description: 'Get attachment metadata and a fetchable resource link. Set inline to embed the bytes.',
844
+ title: 'Download attachment',
845
+ description: `Get metadata and a resource link for one attachment, up to ${sizeLabel(maxAttachmentBytes)}. ` +
846
+ 'With inline, the result also includes the content. For a PDF, inline adds extracted text, and pages ' +
847
+ 'returns the text of a page range such as 201-400.',
760
848
  inputSchema: {
761
- accountId: idParam,
849
+ accountId: accountIdParam,
762
850
  messageId: idParam,
763
851
  attachmentId: attachmentIdParam,
764
852
  inline: z.boolean().optional(),
853
+ pages: pageRange.optional(),
765
854
  },
766
855
  annotations: { readOnlyHint: true, destructiveHint: false },
767
- }, gatedResult('download_attachment', 'mail.read', async (args) => {
768
- const { meta, content } = await service.getAttachment(args.accountId, args.messageId, args.attachmentId, maxAttachmentBytes);
769
- if (content.length > maxAttachmentBytes) {
770
- throw new ClientInputError('invalid_request', 'Attachment is too large to return through MCP.', {
771
- sizeBytes: content.length,
772
- maxBytes: maxAttachmentBytes,
773
- });
856
+ }, gatedResult('download_attachment', 'mail.read', async (args, extra) => {
857
+ const { meta, content, accountId } = await downloadAttachment(args.accountId, args.messageId, args.attachmentId);
858
+ const mimeType = meta.mimeType || 'application/octet-stream';
859
+ const pdf = mediaType(mimeType) === 'application/pdf';
860
+ if (args.pages && !pdf) {
861
+ throw new ClientInputError('invalid_request', 'Pages apply only to PDF attachments.');
774
862
  }
863
+ const extracted = pdf && (args.inline || args.pages)
864
+ ? await pdfText(content, extra.signal, args.pages, (error) => options.logger?.warn('mcp.pdf_text_failed', 'PDF text extraction failed.', error, {
865
+ productSurface: 'mcp',
866
+ operation: 'download_attachment',
867
+ }))
868
+ : undefined;
775
869
  const uri = [
776
870
  'fluxmail://attachment',
777
- encodeURIComponent(args.accountId),
778
- encodeURIComponent(args.messageId),
779
- encodeURIComponent(args.attachmentId),
780
- encodeURIComponent(meta.filename),
871
+ ...[accountId, args.messageId, args.attachmentId, meta.filename].map(encodeURIComponent),
781
872
  ].join('/');
873
+ const resourceLinks = supportsResourceLinks(extra);
782
874
  return {
783
875
  structuredContent: { data: meta },
784
876
  content: [
785
- { type: 'text', text: JSON.stringify({ data: meta }, null, 2) },
786
- { type: 'resource_link', name: meta.filename, uri, mimeType: meta.mimeType },
787
- ...(args.inline
788
- ? [
789
- {
790
- type: 'resource',
791
- resource: {
792
- uri,
793
- mimeType: meta.mimeType || 'application/octet-stream',
794
- blob: content.toString('base64'),
795
- },
796
- },
797
- ]
877
+ { type: 'text', text: JSON.stringify(resourceLinks ? { data: meta } : { data: meta, uri }, null, 2) },
878
+ ...(resourceLinks
879
+ ? [{ type: 'resource_link', name: meta.filename, uri, mimeType: meta.mimeType }]
798
880
  : []),
881
+ ...(args.inline ? [inlineContent(uri, mimeType, content)] : []),
882
+ ...pdfTextContent(meta.filename, extracted),
799
883
  ],
800
884
  };
801
885
  }));
@@ -805,11 +889,8 @@ export function buildMcpServer(service, options = {}) {
805
889
  }), { description: 'Read one attachment from an authorized mailbox.' }, async (uri, variables) => {
806
890
  requireCapabilities(['mail.read']);
807
891
  service.enforceQuota();
808
- const { meta, content } = await service.getAttachment(decodeURIComponent(String(variables.accountId)), decodeURIComponent(String(variables.messageId)), decodeURIComponent(String(variables.attachmentId)), maxAttachmentBytes);
809
- if (content.length > maxAttachmentBytes) {
810
- throw new ClientInputError('invalid_request', 'Attachment is too large to return through MCP.');
811
- }
812
- return { contents: [{ uri: uri.href, mimeType: meta.mimeType, blob: content.toString('base64') }] };
892
+ const { meta, content } = await downloadAttachment(decodeURIComponent(String(variables.accountId)), decodeURIComponent(String(variables.messageId)), decodeURIComponent(String(variables.attachmentId)));
893
+ return { contents: [attachmentContents(uri.href, meta.mimeType || 'application/octet-stream', content)] };
813
894
  });
814
895
  if (listedTools.length) {
815
896
  // The SDK's list handler declares draft-07 for output schemas, which strict clients reject.