boondmanager-mcp-server 2.18.0 → 2.19.1

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.
@@ -0,0 +1,88 @@
1
+ /** How long a slot stays usable after creation. */
2
+ export declare const SLOT_TTL_MS: number;
3
+ export interface SlotHandle {
4
+ /** Backend-private reference to the stored object (e.g. Graph folder id). */
5
+ ref: string;
6
+ /** Pre-authenticated URL the client PUTs the file to (single request). */
7
+ uploadUrl: string;
8
+ expiresAt: Date;
9
+ }
10
+ export interface StoredObject {
11
+ size: number;
12
+ /** Short-lived, pre-authenticated URL BoondManager can download. */
13
+ downloadUrl: string;
14
+ }
15
+ export interface RelayBackend {
16
+ readonly name: string;
17
+ createSlot(slotId: string, filename: string): Promise<SlotHandle>;
18
+ /** `undefined` when nothing was uploaded to the slot yet. */
19
+ stat(ref: string, filename: string): Promise<StoredObject | undefined>;
20
+ /** First bytes of the stored object, for type sniffing. */
21
+ head(downloadUrl: string, bytes: number): Promise<Buffer>;
22
+ purge(ref: string): Promise<void>;
23
+ }
24
+ export interface SharePointConfig {
25
+ tenantId: string;
26
+ clientId: string;
27
+ clientSecret: string;
28
+ /** `host:/sites/path` (Graph site key). */
29
+ site: string;
30
+ library: string;
31
+ }
32
+ /** Accept `https://host/sites/x`, `host/sites/x` or `host:/sites/x`. */
33
+ export declare function normalizeSiteKey(raw: string): string;
34
+ /** SharePoint forbids `" * : < > ? / \ |` and `#`/`%` cause URL trouble. */
35
+ export declare function sanitizeSharePointName(filename: string): string;
36
+ export declare class SharePointRelay implements RelayBackend {
37
+ private readonly cfg;
38
+ readonly name = "sharepoint";
39
+ private token?;
40
+ private driveId?;
41
+ constructor(cfg: SharePointConfig);
42
+ private accessToken;
43
+ private graph;
44
+ private graphOk;
45
+ private drive;
46
+ createSlot(slotId: string, filename: string): Promise<SlotHandle>;
47
+ stat(ref: string, filename: string): Promise<StoredObject | undefined>;
48
+ /** `/content` answers 302 to the same pre-authenticated URL; read it without following. */
49
+ private contentRedirect;
50
+ head(downloadUrl: string, bytes: number): Promise<Buffer>;
51
+ purge(ref: string): Promise<void>;
52
+ }
53
+ /** The configured backend, or a French explanation of why there is none. */
54
+ export declare function relayFromEnv(env?: NodeJS.ProcessEnv): RelayBackend | string;
55
+ export interface CreatedSlot {
56
+ uploadSlot: string;
57
+ uploadUrl: string;
58
+ expiresAt: string;
59
+ maxBytes: number;
60
+ filename: string;
61
+ }
62
+ export interface ClaimedUpload {
63
+ downloadUrl: string;
64
+ filename: string;
65
+ contentType: string;
66
+ size: number;
67
+ /** Purge the stored object; call once BoondManager has fetched it. */
68
+ release: () => Promise<void>;
69
+ }
70
+ /**
71
+ * In-memory registry of open slots. Single-use: a claimed slot is removed
72
+ * before BoondManager is called, so a retry needs a new slot. Expired slots
73
+ * are purged opportunistically whenever a new one is created.
74
+ */
75
+ export declare class UploadRelay {
76
+ private readonly env;
77
+ private readonly slots;
78
+ private cachedBackend?;
79
+ constructor(env?: NodeJS.ProcessEnv);
80
+ private backend;
81
+ private maxBytes;
82
+ private sweep;
83
+ open(filename: string): Promise<CreatedSlot>;
84
+ claim(uploadSlot: string): Promise<ClaimedUpload>;
85
+ }
86
+ /** Process-wide registry (one server process = one set of slots). */
87
+ export declare const uploadRelay: UploadRelay;
88
+ //# sourceMappingURL=upload-relay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload-relay.d.ts","sourceRoot":"","sources":["../../src/services/upload-relay.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Upload relay for files that live neither at a public URL nor on the
3
+ * server's host — typically a conversation attachment, which sits in the MCP
4
+ * client's code-execution sandbox. The model cannot re-emit those bytes
5
+ * reliably (see `fileContent`), so they must travel outside its output:
6
+ *
7
+ * 1. `boond_documents_upload_slot` asks the relay for a single-use,
8
+ * pre-authenticated upload URL on an object store the operator controls;
9
+ * 2. the client's sandbox PUTs the file there (`curl`), bytes never passing
10
+ * through the model;
11
+ * 3. `boond_documents_create({ uploadSlot })` checks the stored file (size,
12
+ * magic bytes), hands BoondManager a short-lived pre-authenticated
13
+ * download URL through the existing `fileUrl` mechanism, then purges the
14
+ * file — permanently, bypassing the recycle bin when the store allows.
15
+ *
16
+ * Store credentials stay on the server; the client only ever sees URLs scoped
17
+ * to one file and one slot. Off unless `BOOND_MCP_UPLOAD_RELAY` is set.
18
+ *
19
+ * Backends implement {@link RelayBackend}; `sharepoint` (Microsoft Graph,
20
+ * app-only `Sites.Selected`) is the first.
21
+ */
22
+ import { randomUUID } from "node:crypto";
23
+ import { extname } from "node:path";
24
+ import { readPositiveInt, readString } from "../config/env.js";
25
+ import { DEFAULT_UPLOAD_MAX_BYTES } from "../constants.js";
26
+ import { sniffDocumentType, UploadRejectedError } from "./upload-source.js";
27
+ /** How long a slot stays usable after creation. */
28
+ export const SLOT_TTL_MS = 15 * 60 * 1000;
29
+ const GRAPH = "https://graph.microsoft.com/v1.0";
30
+ const REQUEST_TIMEOUT_MS = 30_000;
31
+ /** Accept `https://host/sites/x`, `host/sites/x` or `host:/sites/x`. */
32
+ export function normalizeSiteKey(raw) {
33
+ const s = raw
34
+ .trim()
35
+ .replace(/^https?:\/\//i, "")
36
+ .replace(/\/+$/, "");
37
+ if (s.includes(":/"))
38
+ return s;
39
+ const slash = s.indexOf("/");
40
+ return slash === -1 ? s : `${s.slice(0, slash)}:${s.slice(slash)}`;
41
+ }
42
+ /** SharePoint forbids `" * : < > ? / \ |` and `#`/`%` cause URL trouble. */
43
+ export function sanitizeSharePointName(filename) {
44
+ const cleaned = filename
45
+ .replace(/["*:<>?/\\|#%]/g, "_")
46
+ .split("")
47
+ .filter((c) => c.charCodeAt(0) >= 0x20)
48
+ .join("")
49
+ .trim()
50
+ .replace(/^\.+|\.+$/g, "");
51
+ return (cleaned.length > 0 ? cleaned : "document").slice(0, 200);
52
+ }
53
+ export class SharePointRelay {
54
+ cfg;
55
+ name = "sharepoint";
56
+ token;
57
+ driveId;
58
+ constructor(cfg) {
59
+ this.cfg = cfg;
60
+ }
61
+ async accessToken() {
62
+ if (this.token && this.token.expiresAt > Date.now() + 60_000)
63
+ return this.token.value;
64
+ const body = new URLSearchParams({
65
+ grant_type: "client_credentials",
66
+ client_id: this.cfg.clientId,
67
+ client_secret: this.cfg.clientSecret,
68
+ scope: "https://graph.microsoft.com/.default",
69
+ });
70
+ const res = await fetch(`https://login.microsoftonline.com/${encodeURIComponent(this.cfg.tenantId)}/oauth2/v2.0/token`, { method: "POST", body, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
71
+ const json = (await res.json().catch(() => ({})));
72
+ if (!res.ok || !json.access_token) {
73
+ // Never echo the request (it carries the secret); the error code is enough.
74
+ throw new Error(`Relais SharePoint : authentification Entra ID refusée (${json.error ?? res.status}).`);
75
+ }
76
+ this.token = { value: json.access_token, expiresAt: Date.now() + (json.expires_in ?? 3600) * 1000 };
77
+ return this.token.value;
78
+ }
79
+ async graph(method, path, body) {
80
+ const res = await fetch(`${GRAPH}${path}`, {
81
+ method,
82
+ headers: {
83
+ Authorization: `Bearer ${await this.accessToken()}`,
84
+ ...(body === undefined ? {} : { "Content-Type": "application/json" }),
85
+ },
86
+ body: body === undefined ? null : JSON.stringify(body),
87
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
88
+ });
89
+ const text = await res.text();
90
+ let json = {};
91
+ try {
92
+ if (text)
93
+ json = JSON.parse(text);
94
+ }
95
+ catch {
96
+ // Non-JSON error page (proxy, throttling HTML): the status says enough.
97
+ }
98
+ return { status: res.status, json };
99
+ }
100
+ async graphOk(method, path, body) {
101
+ const { status, json } = await this.graph(method, path, body);
102
+ if (status >= 400) {
103
+ const e = json.error;
104
+ throw new Error(`Relais SharePoint : ${method} ${path.split("?")[0]} → ${status} ${e?.code ?? ""} ${e?.message ?? ""}`.trim());
105
+ }
106
+ return json;
107
+ }
108
+ async drive() {
109
+ if (this.driveId)
110
+ return this.driveId;
111
+ const site = await this.graphOk("GET", `/sites/${this.cfg.site}?$select=id`);
112
+ const drives = await this.graphOk("GET", `/sites/${site.id}/drives?$select=id,name,webUrl`);
113
+ const wanted = this.cfg.library.toLowerCase();
114
+ const match = drives.value.find((d) => d.name.toLowerCase() === wanted || (d.webUrl ?? "").toLowerCase().endsWith(`/${wanted}`));
115
+ if (!match) {
116
+ const names = drives.value.map((d) => d.name).join(", ");
117
+ throw new Error(`Relais SharePoint : bibliothèque « ${this.cfg.library} » introuvable sur le site (trouvées : ${names}).`);
118
+ }
119
+ this.driveId = match.id;
120
+ return match.id;
121
+ }
122
+ async createSlot(slotId, filename) {
123
+ const driveId = await this.drive();
124
+ // One folder per slot keeps the file's own name intact — BoondManager
125
+ // names the document after the download's Content-Disposition.
126
+ const folder = await this.graphOk("POST", `/drives/${driveId}/root/children`, {
127
+ name: `mcp-${slotId}`,
128
+ folder: {},
129
+ "@microsoft.graph.conflictBehavior": "fail",
130
+ });
131
+ const name = encodeURIComponent(sanitizeSharePointName(filename));
132
+ const session = await this.graphOk("POST", `/drives/${driveId}/items/${folder.id}:/${name}:/createUploadSession`, { item: { "@microsoft.graph.conflictBehavior": "fail" } });
133
+ const graphExpiry = session.expirationDateTime ? Date.parse(session.expirationDateTime) : Infinity;
134
+ return {
135
+ ref: folder.id,
136
+ uploadUrl: session.uploadUrl,
137
+ expiresAt: new Date(Math.min(Date.now() + SLOT_TTL_MS, graphExpiry)),
138
+ };
139
+ }
140
+ async stat(ref, filename) {
141
+ const driveId = await this.drive();
142
+ // List the slot's own folder instead of addressing the file by name: the
143
+ // folder is private to the slot, so whatever file it holds is the upload,
144
+ // and a name normalised differently by SharePoint cannot cause a miss.
145
+ // No $select: SharePoint does not reliably return the download-URL
146
+ // annotation under $select with app-only tokens.
147
+ const { status, json } = await this.graph("GET", `/drives/${driveId}/items/${ref}/children`);
148
+ if (status === 404) {
149
+ throw new UploadRejectedError("Dossier de transit du slot introuvable (supprimé à la main ?). Créer un nouveau slot avec `boond_documents_upload_slot`.");
150
+ }
151
+ if (status >= 400)
152
+ throw new Error(`Relais SharePoint : lecture du dossier de transit → ${status}.`);
153
+ const files = (json.value ?? []).filter((item) => item.file !== undefined);
154
+ if (files.length === 0)
155
+ return undefined;
156
+ const wanted = sanitizeSharePointName(filename).toLowerCase();
157
+ const item = files.find((f) => (f.name ?? "").toLowerCase() === wanted) ?? files[0];
158
+ if (item.size === undefined)
159
+ return undefined;
160
+ const downloadUrl = item["@microsoft.graph.downloadUrl"] ?? (await this.contentRedirect(driveId, item.id));
161
+ if (!downloadUrl) {
162
+ throw new Error(`Relais SharePoint : fichier déposé trouvé (${item.size} octets) mais Graph ne fournit pas d'URL de lecture temporaire.`);
163
+ }
164
+ return { size: item.size, downloadUrl };
165
+ }
166
+ /** `/content` answers 302 to the same pre-authenticated URL; read it without following. */
167
+ async contentRedirect(driveId, itemId) {
168
+ const res = await fetch(`${GRAPH}/drives/${driveId}/items/${itemId}/content`, {
169
+ headers: { Authorization: `Bearer ${await this.accessToken()}` },
170
+ redirect: "manual",
171
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
172
+ });
173
+ await res.body?.cancel().catch(() => undefined);
174
+ return res.status >= 300 && res.status < 400 ? (res.headers.get("location") ?? undefined) : undefined;
175
+ }
176
+ async head(downloadUrl, bytes) {
177
+ const res = await fetch(downloadUrl, {
178
+ headers: { Range: `bytes=0-${bytes - 1}` },
179
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
180
+ });
181
+ if (!res.ok)
182
+ throw new Error(`Relais SharePoint : lecture de l'en-tête du fichier → ${res.status}.`);
183
+ return Buffer.from(await res.arrayBuffer()).subarray(0, bytes);
184
+ }
185
+ async purge(ref) {
186
+ const driveId = await this.drive();
187
+ // permanentDelete skips the recycle bin (RGPD: CVs, payslips...). Fall
188
+ // back to a regular delete if the tenant refuses it.
189
+ const { status } = await this.graph("POST", `/drives/${driveId}/items/${ref}/permanentDelete`);
190
+ if (status < 400 || status === 404)
191
+ return;
192
+ const del = await this.graph("DELETE", `/drives/${driveId}/items/${ref}`);
193
+ if (del.status >= 400 && del.status !== 404) {
194
+ throw new Error(`Relais SharePoint : suppression du dossier de transit → ${del.status}.`);
195
+ }
196
+ }
197
+ }
198
+ // ---------------------------------------------------------------------------
199
+ // Configuration and slot registry
200
+ // ---------------------------------------------------------------------------
201
+ /** The configured backend, or a French explanation of why there is none. */
202
+ export function relayFromEnv(env = process.env) {
203
+ const kind = (readString("BOOND_MCP_UPLOAD_RELAY", env) ?? "").toLowerCase();
204
+ if (kind === "") {
205
+ return "Relais d'upload désactivé : l'opérateur du serveur doit définir `BOOND_MCP_UPLOAD_RELAY` (valeur : `sharepoint`).";
206
+ }
207
+ if (kind !== "sharepoint") {
208
+ return `Relais d'upload inconnu : « ${kind} » (valeur acceptée : \`sharepoint\`).`;
209
+ }
210
+ const required = {
211
+ tenantId: "BOOND_MCP_SHAREPOINT_TENANT_ID",
212
+ clientId: "BOOND_MCP_SHAREPOINT_CLIENT_ID",
213
+ clientSecret: "BOOND_MCP_SHAREPOINT_CLIENT_SECRET",
214
+ site: "BOOND_MCP_SHAREPOINT_SITE",
215
+ };
216
+ const missing = Object.values(required).filter((name) => !readString(name, env));
217
+ if (missing.length > 0) {
218
+ return `Relais SharePoint incomplet : variable(s) manquante(s) ${missing.map((m) => `\`${m}\``).join(", ")}.`;
219
+ }
220
+ return new SharePointRelay({
221
+ tenantId: readString(required.tenantId, env),
222
+ clientId: readString(required.clientId, env),
223
+ clientSecret: readString(required.clientSecret, env),
224
+ site: normalizeSiteKey(readString(required.site, env)),
225
+ library: readString("BOOND_MCP_SHAREPOINT_LIBRARY", env) ?? "Documents",
226
+ });
227
+ }
228
+ /**
229
+ * In-memory registry of open slots. Single-use: a claimed slot is removed
230
+ * before BoondManager is called, so a retry needs a new slot. Expired slots
231
+ * are purged opportunistically whenever a new one is created.
232
+ */
233
+ export class UploadRelay {
234
+ env;
235
+ slots = new Map();
236
+ cachedBackend;
237
+ constructor(env = process.env) {
238
+ this.env = env;
239
+ }
240
+ backend() {
241
+ if (this.cachedBackend)
242
+ return this.cachedBackend;
243
+ const resolved = relayFromEnv(this.env);
244
+ if (typeof resolved === "string")
245
+ throw new UploadRejectedError(resolved);
246
+ this.cachedBackend = resolved;
247
+ return resolved;
248
+ }
249
+ maxBytes() {
250
+ return readPositiveInt("BOOND_MCP_UPLOAD_MAX_BYTES", DEFAULT_UPLOAD_MAX_BYTES, this.env);
251
+ }
252
+ async sweep() {
253
+ const now = Date.now();
254
+ for (const [id, slot] of this.slots) {
255
+ if (slot.handle.expiresAt.getTime() <= now) {
256
+ this.slots.delete(id);
257
+ await slot.backend.purge(slot.handle.ref).catch(() => undefined);
258
+ }
259
+ }
260
+ }
261
+ async open(filename) {
262
+ const backend = this.backend();
263
+ const ext = extname(filename).toLowerCase();
264
+ if (ext === "") {
265
+ throw new UploadRejectedError("`fileName` doit comporter une extension (ex. « cv.pdf »).");
266
+ }
267
+ await this.sweep();
268
+ const uploadSlot = randomUUID();
269
+ const clean = sanitizeSharePointName(filename);
270
+ const handle = await backend.createSlot(uploadSlot, clean);
271
+ this.slots.set(uploadSlot, { backend, handle, filename: clean });
272
+ return {
273
+ uploadSlot,
274
+ uploadUrl: handle.uploadUrl,
275
+ expiresAt: handle.expiresAt.toISOString(),
276
+ maxBytes: this.maxBytes(),
277
+ filename: clean,
278
+ };
279
+ }
280
+ async claim(uploadSlot) {
281
+ const slot = this.slots.get(uploadSlot);
282
+ if (!slot) {
283
+ throw new UploadRejectedError(`\`uploadSlot\` inconnu ou déjà utilisé : « ${uploadSlot} ». Créer un nouveau slot avec \`boond_documents_upload_slot\`.`);
284
+ }
285
+ const purge = () => slot.backend.purge(slot.handle.ref);
286
+ if (slot.handle.expiresAt.getTime() <= Date.now()) {
287
+ this.slots.delete(uploadSlot);
288
+ await purge().catch(() => undefined);
289
+ throw new UploadRejectedError("Slot d'upload expiré. Créer un nouveau slot avec `boond_documents_upload_slot`.");
290
+ }
291
+ const stored = await slot.backend.stat(slot.handle.ref, slot.filename);
292
+ if (!stored) {
293
+ // Not consumed: the upload may still be in flight.
294
+ throw new UploadRejectedError("Aucun fichier dans le dossier de transit de ce slot : le dépôt n'est pas terminé ou n'a pas eu lieu. " +
295
+ "Exécuter la commande d'upload (réponse HTTP 200/201 attendue), puis réessayer.");
296
+ }
297
+ this.slots.delete(uploadSlot);
298
+ try {
299
+ const max = this.maxBytes();
300
+ if (stored.size > max) {
301
+ throw new UploadRejectedError(`Fichier trop volumineux : ${(stored.size / 1024 / 1024).toFixed(1)} Mo (max ${(max / 1024 / 1024).toFixed(1)} Mo).`);
302
+ }
303
+ const head = await slot.backend.head(stored.downloadUrl, 16);
304
+ const sniffed = sniffDocumentType(head, slot.filename);
305
+ if (!sniffed || !sniffed.extensions.includes(extname(slot.filename).toLowerCase())) {
306
+ throw new UploadRejectedError(`Format non accepté pour « ${slot.filename} » : le contenu déposé ne correspond pas à un format autorisé ` +
307
+ "ou à son extension.");
308
+ }
309
+ return {
310
+ downloadUrl: stored.downloadUrl,
311
+ filename: slot.filename,
312
+ contentType: sniffed.contentType,
313
+ size: stored.size,
314
+ release: purge,
315
+ };
316
+ }
317
+ catch (error) {
318
+ await purge().catch(() => undefined);
319
+ throw error;
320
+ }
321
+ }
322
+ }
323
+ /** Process-wide registry (one server process = one set of slots). */
324
+ export const uploadRelay = new UploadRelay();
325
+ //# sourceMappingURL=upload-relay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload-relay.js","sourceRoot":"","sources":["../../src/services/upload-relay.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"documents.d.ts","sourceRoot":"","sources":["../../src/tools/documents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAiBzE,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAwN7D"}
1
+ {"version":3,"file":"documents.d.ts","sourceRoot":"","sources":["../../src/tools/documents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA6BzE,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAoS7D"}
@@ -1,6 +1,8 @@
1
1
  import { apiDownload, apiUploadForm, formatDetailResponse, DownloadTooLargeError } from "../services/boond-client.js";
2
2
  import { progressReporterFrom } from "../services/progress.js";
3
- import { DocumentGetSchema, DocumentCreateSchema } from "../schemas/index.js";
3
+ import { z } from "zod";
4
+ import { DocumentGetSchema, DocumentCreateSchema, DocumentUploadSlotSchema } from "../schemas/index.js";
5
+ import { uploadRelay } from "../services/upload-relay.js";
4
6
  import { MAX_DOCUMENT_BYTES, MAX_IMAGE_BYTES, CHARACTER_LIMIT, INLINE_UPLOAD_MAX_BYTES } from "../constants.js";
5
7
  import { decodeInlineUpload, readLocalUpload, UploadRejectedError } from "../services/upload-source.js";
6
8
  import { registerDeleteTool, MutationOutputSchema } from "./crud-factory.js";
@@ -9,6 +11,14 @@ import { extractDocxText, extractPdfText, isDocxMime, isImageMime, isPdfMime } f
9
11
  function isTextMime(mime) {
10
12
  return mime.startsWith("text/") || /[+/](json|xml)$|^application\/(json|xml|csv)$/.test(mime);
11
13
  }
14
+ const UploadSlotOutputSchema = z.object({
15
+ uploadSlot: z.string(),
16
+ uploadUrl: z.string(),
17
+ expiresAt: z.string(),
18
+ maxBytes: z.number(),
19
+ fileName: z.string(),
20
+ uploadCommand: z.string(),
21
+ });
12
22
  export function registerDocumentTools(server) {
13
23
  // Download a document (CV, justificatif, contrat...)
14
24
  server.registerTool("boond_documents_get", {
@@ -124,15 +134,70 @@ Un ID inconnu est rejeté explicitement plutôt que de renvoyer la page d'accuei
124
134
  }
125
135
  return raw();
126
136
  });
127
- // Upload a document: by URL, by local path (opt-in, stdio) or inline base64
137
+ // Open a single-use upload slot on the operator's relay (chat attachments)
138
+ server.registerTool("boond_documents_upload_slot", {
139
+ title: "Ouvrir un slot d'upload",
140
+ description: `Ouvre un slot d'upload à usage unique sur le relais de stockage configuré par l'opérateur (\`BOOND_MCP_UPLOAD_RELAY\`, ex. SharePoint), pour transmettre à BoondManager un fichier qui n'est ni à une URL publique ni sur le poste du serveur — typiquement une pièce jointe de conversation, présente dans l'environnement d'exécution de code du client.
141
+
142
+ Déroulé : (1) appeler cet outil avec le nom du fichier ; (2) déposer le fichier sur \`uploadUrl\` en **une seule requête PUT** depuis l'environnement d'exécution, avec la commande \`curl\` renvoyée — les octets ne passent pas par la réponse du modèle ; (3) appeler \`boond_documents_create\` avec \`uploadSlot\`. Le fichier est vérifié (taille, premiers octets), transmis à Boond par une URL de lecture temporaire, puis supprimé définitivement du relais.
143
+
144
+ Slot valable 15 min, un seul fichier, un seul usage. \`uploadUrl\` est pré-authentifiée : ne pas la réutiliser ni la partager.
145
+
146
+ Returns: \`uploadSlot\`, \`uploadUrl\`, \`expiresAt\`, \`maxBytes\`, \`fileName\` et la commande d'upload.`,
147
+ inputSchema: DocumentUploadSlotSchema,
148
+ outputSchema: UploadSlotOutputSchema,
149
+ annotations: {
150
+ readOnlyHint: false,
151
+ destructiveHint: false,
152
+ idempotentHint: false,
153
+ openWorldHint: true,
154
+ },
155
+ }, async (params) => {
156
+ let slot;
157
+ try {
158
+ slot = await uploadRelay.open(params.fileName);
159
+ }
160
+ catch (error) {
161
+ if (error instanceof UploadRejectedError) {
162
+ return { isError: true, content: [{ type: "text", text: `❌ ${error.message}` }] };
163
+ }
164
+ throw error;
165
+ }
166
+ const command = `F="<chemin-du-fichier>"; N=$(stat -c%s "$F"); ` +
167
+ `curl -sS --fail-with-body -X PUT -H "Content-Length: $N" -H "Content-Range: bytes 0-$((N-1))/$N" ` +
168
+ `--data-binary @"$F" "${slot.uploadUrl}"`;
169
+ return {
170
+ content: [
171
+ {
172
+ type: "text",
173
+ text: `✅ Slot d'upload ouvert (expire ${slot.expiresAt}, ${(slot.maxBytes / 1024 / 1024).toFixed(0)} Mo max).\n` +
174
+ `uploadSlot: ${slot.uploadSlot}\n\n` +
175
+ `Déposer le fichier depuis l'environnement d'exécution (remplacer <chemin-du-fichier>) :\n${command}\n\n` +
176
+ `Puis : boond_documents_create({ parentType, parentId, uploadSlot: "${slot.uploadSlot}" }).`,
177
+ },
178
+ ],
179
+ structuredContent: {
180
+ uploadSlot: slot.uploadSlot,
181
+ uploadUrl: slot.uploadUrl,
182
+ expiresAt: slot.expiresAt,
183
+ maxBytes: slot.maxBytes,
184
+ fileName: slot.filename,
185
+ uploadCommand: command,
186
+ },
187
+ };
188
+ });
189
+ // Upload a document: by URL, local path (opt-in, stdio), inline base64 or relay slot
128
190
  server.registerTool("boond_documents_create", {
129
191
  title: "Téléverser un document",
130
192
  description: `Attache un document à une entité BoondManager. Exactement une source par appel :
131
- - \`fileUrl\` : URL https que BoondManager télécharge lui-même (aucun octet ne transite par le serveur MCP) ;
132
- - \`filePath\` : chemin absolu d'un fichier local, lu par le serveur MCP — transport stdio uniquement, et seulement sous un répertoire listé dans \`BOOND_MCP_UPLOAD_DIRS\` (désactivé par défaut) ;
133
- - \`fileContent\` (base64) + \`fileName\` : petit fichier passé inline, ${Math.round(INLINE_UPLOAD_MAX_BYTES / 1024 / 1024)} Mo max.
193
+ - \`fileUrl\` : URL https que BoondManager télécharge lui-même ;
194
+ - \`filePath\` : chemin absolu d'un fichier local, lu par le serveur MCP — transport stdio uniquement, sous un répertoire de \`BOOND_MCP_UPLOAD_DIRS\` (désactivé par défaut) ;
195
+ - \`uploadSlot\` : fichier déposé sur un slot de \`boond_documents_upload_slot\` — **la voie pour une pièce jointe de conversation** ;
196
+ - \`fileContent\` (base64) + \`fileName\` : petit fichier inline, ${Math.round(INLINE_UPLOAD_MAX_BYTES / 1024 / 1024)} Mo max, réservé à un appelant qui détient déjà les octets exacts (un modèle ne retranscrit pas fiablement une pièce jointe en base64).
134
197
 
135
- Le format est vérifié sur les premiers octets du fichier (PDF, Office, ODF, RTF, images), pas sur l'extension. \`fileContent\` suppose que l'appelant détient déjà les octets exacts : un modèle ne peut pas retranscrire de façon fiable une pièce jointe de conversation en base64 — préférer \`filePath\`. Cas d'usage typiques : CV d'un candidat (parentType=candidateResume, parsing=true pour l'analyse IA Boond), justificatif de note de frais (expensesReport), bon de commande, contrat...
198
+ Le format est vérifié sur les premiers octets du fichier (PDF, Office, ODF, RTF, images), pas sur l'extension. Cas d'usage typiques : CV d'un candidat (parentType=candidateResume, parsing=true pour l'analyse IA Boond), pièce jointe d'un besoin / AO (parentType=opportunity), d'un positionnement (positioning), justificatif de note de frais (expensesReport), bon de commande, contrat...
199
+
200
+ \`parentType\` n'est pas limité à une liste fermée : il est transmis tel quel à BoondManager, qui valide le type. Un type que l'API ne gère pas est refusé sans rien écrire.
136
201
 
137
202
  Returns: Métadonnées du document créé (ID).`,
138
203
  inputSchema: DocumentCreateSchema,
@@ -145,10 +210,10 @@ Returns: Métadonnées du document créé (ID).`,
145
210
  },
146
211
  }, async (params) => {
147
212
  const reject = (text) => ({ isError: true, content: [{ type: "text", text: `❌ ${text}` }] });
148
- const sources = [params.fileUrl, params.filePath, params.fileContent].filter((v) => v !== undefined).length;
213
+ const sources = [params.fileUrl, params.filePath, params.fileContent, params.uploadSlot].filter((v) => v !== undefined).length;
149
214
  if (sources !== 1) {
150
- return reject(`Exactement une source de fichier est attendue (reçu : ${sources}). Fournir \`fileUrl\`, ou \`filePath\`, ` +
151
- "ou `fileContent` + `fileName`.");
215
+ return reject(`Exactement une source de fichier est attendue (reçu : ${sources}). Fournir \`fileUrl\`, \`filePath\`, ` +
216
+ "`uploadSlot`, ou `fileContent` + `fileName`.");
152
217
  }
153
218
  if (params.fileContent !== undefined && params.fileName === undefined) {
154
219
  return reject("`fileName` (avec extension, ex. « cv.pdf ») est requis avec `fileContent`.");
@@ -157,11 +222,14 @@ Returns: Métadonnées du document créé (ID).`,
157
222
  return reject("`fileName` ne s'utilise qu'avec `fileContent`.");
158
223
  }
159
224
  let file;
225
+ let relayed;
160
226
  try {
161
227
  if (params.filePath !== undefined)
162
228
  file = await readLocalUpload(params.filePath);
163
229
  else if (params.fileContent !== undefined && params.fileName !== undefined)
164
230
  file = decodeInlineUpload(params.fileContent, params.fileName);
231
+ else if (params.uploadSlot !== undefined)
232
+ relayed = await uploadRelay.claim(params.uploadSlot);
165
233
  }
166
234
  catch (error) {
167
235
  if (error instanceof UploadRejectedError)
@@ -174,20 +242,34 @@ Returns: Métadonnées du document créé (ID).`,
174
242
  };
175
243
  if (params.fileUrl !== undefined)
176
244
  fields.fileUrl = params.fileUrl;
245
+ // The relay hands Boond a short-lived pre-authenticated download URL.
246
+ if (relayed !== undefined)
247
+ fields.fileUrl = relayed.downloadUrl;
177
248
  if (params.parsing !== undefined)
178
249
  fields.parsing = String(params.parsing);
179
- const response = file === undefined
180
- ? await apiUploadForm("/documents", fields)
181
- : await apiUploadForm("/documents", fields, file);
250
+ let response;
251
+ try {
252
+ response =
253
+ file === undefined
254
+ ? await apiUploadForm("/documents", fields)
255
+ : await apiUploadForm("/documents", fields, file);
256
+ }
257
+ finally {
258
+ // Boond has fetched the file (or failed): purge the relay copy either way.
259
+ if (relayed !== undefined)
260
+ await relayed.release().catch(() => undefined);
261
+ }
182
262
  const entity = Array.isArray(response.data) ? response.data[0] : response.data;
183
263
  const structured = {};
184
264
  if (entity?.id !== undefined)
185
265
  structured.id = String(entity.id);
186
266
  if (entity?.type !== undefined)
187
267
  structured.type = String(entity.type);
188
- const sent = file === undefined
189
- ? ""
190
- : `\nFichier: ${file.filename} (${file.contentType}, ${(file.data.length / 1024).toFixed(0)} Ko)`;
268
+ const sent = file !== undefined
269
+ ? `\nFichier: ${file.filename} (${file.contentType}, ${(file.data.length / 1024).toFixed(0)} Ko)`
270
+ : relayed !== undefined
271
+ ? `\nFichier: ${relayed.filename} (${relayed.contentType}, ${(relayed.size / 1024).toFixed(0)} Ko, via relais — copie de transit supprimée)`
272
+ : "";
191
273
  return {
192
274
  content: [
193
275
  {