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.
- package/README.md +39 -2
- package/dist/schemas/index.d.ts +9 -26
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +32 -4
- package/dist/schemas/index.js.map +1 -1
- package/dist/services/upload-relay.d.ts +88 -0
- package/dist/services/upload-relay.d.ts.map +1 -0
- package/dist/services/upload-relay.js +325 -0
- package/dist/services/upload-relay.js.map +1 -0
- package/dist/tools/documents.d.ts.map +1 -1
- package/dist/tools/documents.js +97 -15
- package/dist/tools/documents.js.map +1 -1
- package/dist/tools/usage-guidance.d.ts.map +1 -1
- package/dist/tools/usage-guidance.js +6 -1
- package/dist/tools/usage-guidance.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -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;
|
|
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"}
|
package/dist/tools/documents.js
CHANGED
|
@@ -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 {
|
|
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
|
-
//
|
|
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
|
|
132
|
-
- \`filePath\` : chemin absolu d'un fichier local, lu par le serveur MCP — transport stdio uniquement,
|
|
133
|
-
- \`
|
|
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.
|
|
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\`,
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
189
|
-
?
|
|
190
|
-
:
|
|
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
|
{
|